import { AccountMeta, KeyedAccountInfo, PublicKey, SystemProgram, } from '@solana/web3.js'; import { BN } from '@staratlas/anchor'; import { Account, AccountStatic, AsyncSigner, DecodedAccountData, InstructionReturn, decodeAccount, getAnchorEnum, getNumberFromEnum, isNull, staticImplements, } from '@staratlas/data-source'; import { Faction } from '@staratlas/profile-faction'; import { uniq } from 'lodash'; import { GameStateAccount, InitGameStateInput, ManageGameInput, MiscVariables, MiscVariablesInput, SageIDL, SageIDLProgram, StarbaseUpkeepLevels, UpdateGameStateInput, } from './constants'; import { FLEET_INFO_MIN_DATA_SIZE, FactionsStarbaseLevelInfo, FleetInput, Game, MISC_VARIABLES_MIN_DATA_SIZE, StarbaseLevelInfoArrayInput, StarbaseUpkeepInfoArrayInput, factionsStarbaseLevelInfoEqual, starbaseUpkeepLevelsEquals, } from './game'; import { SectorRing } from './planet'; export interface UpdateFleetDataInput { starbaseLevels?: StarbaseLevelInfoArrayInput[]; starbaseUpkeep?: StarbaseUpkeepInfoArrayInput[]; maxFleetSize?: number; } export interface StarbaseLevelInfoItem { level: number; faction: number; recipeCategoryForLevel: PublicKey; recipeForUpgrade: PublicKey; hp: BN; sp: BN; sectorRingAvailable: number; warpLaneMovementFee: BN; repairFee: BN; repairEfficiency: number; shieldRechargeRate: number; shieldBreakDelay: number; } /** * Check if two `MiscVariables` instances are equal * @param data1 - first MiscVariables * @param data2 - second MiscVariables * @returns boolean */ export function miscVariablesEquals( data1: MiscVariables, data2: MiscVariables, ): boolean { return ( data1.warpLaneFuelCostReduction === data2.warpLaneFuelCostReduction && data1.upkeepMiningEmissionsPenalty === data2.upkeepMiningEmissionsPenalty && data1.respawnFee.eq(data2.respawnFee) ); } /** * Check if two `GameStateAccount` instances are equal * @param data1 - first GameStateAccount * @param data2 - second GameStateAccount * @returns boolean */ export function gameStateDataEquals( data1: GameStateAccount, data2: GameStateAccount, ): boolean { return ( data1.version === data2.version && data1.bump === data2.bump && data1.gameId.equals(data2.gameId) && data1.updateId.eq(data2.updateId) && data1.fleet.maxFleetSize === data2.fleet.maxFleetSize && factionsStarbaseLevelInfoEqual( data1.fleet.starbaseLevels as FactionsStarbaseLevelInfo, data2.fleet.starbaseLevels as FactionsStarbaseLevelInfo, ) && starbaseUpkeepLevelsEquals( data1.fleet.upkeep as StarbaseUpkeepLevels, data2.fleet.upkeep as StarbaseUpkeepLevels, ) && miscVariablesEquals(data1.misc, data2.misc) ); } export const SCALE_DECIMALS_4 = 10_000; @staticImplements>() export class GameState implements Account { static readonly ACCOUNT_NAME: NonNullable< SageIDL['accounts'] >[number]['name'] = 'gameState'; static readonly MIN_DATA_SIZE: number = 8 + // discriminator 1 + // version 32 + // game_id 8 + // update_id FLEET_INFO_MIN_DATA_SIZE + MISC_VARIABLES_MIN_DATA_SIZE + 1; // bump constructor(private _data: GameStateAccount, private _key: PublicKey) {} get data(): Readonly { return this._data; } get key(): PublicKey { return this._key; } /** * Find the address of a `GameState` account * @param program - SAGE program * @param gameId - the SAGE game id * @param updateId - the game update id * @returns The PDA and bump respectively */ static findAddress( program: SageIDLProgram, gameId: PublicKey, updateId: BN, ): [PublicKey, number] { return PublicKey.findProgramAddressSync( [ Buffer.from('GameState'), gameId.toBuffer(), updateId.toArrayLike(Buffer, 'le', 8), ], program.programId, ); } /** * Create a new GameState account * @param program - SAGE program * @param key - the key authorized to run this instruction * @param profile - the profile with the required permissions for the instruction * @param gameId - the SAGE game id * @param gameState - the game state * @param input - input params * @returns InstructionReturn */ static initGameState( program: SageIDLProgram, key: AsyncSigner, profile: PublicKey, gameId: PublicKey, gameState: PublicKey, input: InitGameStateInput, ): InstructionReturn { return async (funder) => [ { instruction: await program.methods .initGameState(input) .accountsStrict({ gameAndProfile: { key: key.publicKey(), profile, gameId, }, funder: funder.publicKey(), gameState, systemProgram: SystemProgram.programId, }) .instruction(), signers: [key, funder], }, ]; } /** * Update `GameState` account * @param program - SAGE program * @param key - the key authorized to run this instruction * @param profile - the profile with the required permissions for the instruction * @param gameId - the SAGE game id * @param gameState - the game state * @param keyIndex - the index of the `key` in the profile permissions * @param fleet - the fleet * @param miscVariables - miscellaneous variables * @returns InstructionReturn */ static updateGameState( program: SageIDLProgram, key: AsyncSigner, profile: PublicKey, gameId: PublicKey, gameState: PublicKey, keyIndex: number, fleet?: UpdateFleetDataInput, miscVariables?: MiscVariablesInput, ): InstructionReturn { const remainingAccounts: AccountMeta[] = []; const miscVariablesInput = miscVariables || null; const fleetInput: FleetInput = { starbaseLevelInfoArray: null, upkeepInfoArray: null, maxFleetSize: null, }; const starbaseLevelsInput = fleet?.starbaseLevels || []; const data: UpdateGameStateInput = { fleet: fleetInput as never /** :( this is forced by anchor */, misc: miscVariablesInput as never, keyIndex, }; const levelsByFaction = [ starbaseLevelsInput.filter( (it) => it.faction == getNumberFromEnum(Faction, Faction.ONI), ), starbaseLevelsInput.filter((it) => it.faction == (Faction.MUD as number)), starbaseLevelsInput.filter( (it) => it.faction == getNumberFromEnum(Faction, Faction.Ustur), ), ]; for (let index = 0; index < levelsByFaction.length; index++) { const element = levelsByFaction[index]; if (element) { const uniqStarbaseLevels = uniq(element.map((it) => it.level)); if (uniqStarbaseLevels.length !== element.length) { throw new Error('Starbase levels must be unique'); } if ( Math.max(...uniqStarbaseLevels) > Game.MAX_STAR_BASE_LEVEL || Math.min(...uniqStarbaseLevels) < Game.MIN_STAR_BASE_LEVEL ) { throw new Error('Starbase levels must be between 0 and 6'); } } } starbaseLevelsInput.sort( (a, b) => a.faction - b.faction || a.level - b.level, ); for (let index = 0; index < starbaseLevelsInput.length; index++) { const element = starbaseLevelsInput[index]; if (element) { const starbaseLevelInfoElem = { faction: element.faction, level: element.level, hp: element.hp, sp: element.sp, warpLaneMovementFee: element.warpLaneMovementFee, sectorRingAvailable: getAnchorEnum( SectorRing, element.sectorRingAvailable, ), repairFee: element.repairFee, repairEfficiency: element.repairEfficiency, shieldRechargeRate: element.shieldRechargeRate, shieldBreakDelay: element.shieldBreakDelay, }; if (fleetInput.starbaseLevelInfoArray == null) { fleetInput.starbaseLevelInfoArray = [starbaseLevelInfoElem]; } else { fleetInput.starbaseLevelInfoArray.push(starbaseLevelInfoElem); } remainingAccounts.push({ isSigner: false, isWritable: false, pubkey: element.oldRecipeForUpgrade, }); remainingAccounts.push({ isSigner: false, isWritable: false, pubkey: element.newRecipeForUpgrade, }); remainingAccounts.push({ isSigner: false, isWritable: false, pubkey: element.recipeCategoryForLevel, }); } } // Set maxFleetSize if (fleet?.maxFleetSize) { fleetInput.maxFleetSize = fleet.maxFleetSize; } // Set upkeepInfoArray if (fleet?.starbaseUpkeep) { fleetInput.upkeepInfoArray = fleet.starbaseUpkeep; } data.fleet = isNull(fleetInput) ? (null as never) : (fleetInput as never); return async () => [ { instruction: await program.methods .updateGameState(data) .accountsStrict({ gameAndProfile: { key: key.publicKey(), profile, gameId, }, gameState, }) .remainingAccounts(remainingAccounts) .instruction(), signers: [key], }, ]; } /** * Sets a new `GameState` for a `Game` & increments the `update_id` * @param program - SAGE program * @param key - the key authorized to run this instruction * @param profile - the profile with the required permissions for the instruction * @param gameId - the SAGE game id * @param gameState - the game state * @param input - input params * @returns InstructionReturn */ static activateGameState( program: SageIDLProgram, key: AsyncSigner, profile: PublicKey, gameId: PublicKey, gameState: PublicKey, input: ManageGameInput, ): InstructionReturn { return async () => [ { instruction: await program.methods .activateGameState(input) .accountsStrict({ gameAndProfile: { key: key.publicKey(), profile, gameId, }, gameState, }) .instruction(), signers: [key], }, ]; } /** * Create a new `GameState` account from an older one and activates it * This is meant to be used when all we want to do is update the `update_id`, * without changing game variables. If game variables are to be changed, then we * need to use other instructions to create and activate a new `GameState` account * @param program - SAGE program * @param key - the key authorized to run this instruction * @param profile - the profile with the required permissions for the instruction * @param gameId - the SAGE game id * @param oldGameState - the old game state account * @param newGameState - the new game state account * @param input - input params * @returns InstructionReturn */ static copyGameState( program: SageIDLProgram, key: AsyncSigner, profile: PublicKey, gameId: PublicKey, oldGameState: PublicKey, newGameState: PublicKey, input: ManageGameInput, ): InstructionReturn { return async (funder) => [ { instruction: await program.methods .copyGameState(input) .accountsStrict({ gameAndProfile: { key: key.publicKey(), profile, gameId, }, funder: funder.publicKey(), oldGameState, newGameState, systemProgram: SystemProgram.programId, }) .instruction(), signers: [key, funder], }, ]; } static decodeData( account: KeyedAccountInfo, program: SageIDLProgram, ): DecodedAccountData { return decodeAccount(account, program, GameState); } }