import { MPL_BUBBLEGUM_PROGRAM_ID, MetadataArgsArgs, SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, SPL_NOOP_PROGRAM_ID, fetchMerkleTree, getCurrentRoot, getMerkleProof, hashLeaf, hashMetadataCreators, hashMetadataData, setTreeDelegate, } from '@metaplex-foundation/mpl-bubblegum'; import { Umi, Keypair as UmiKeypair, PublicKey as UmiPublicKey, createSignerFromKeypair, publicKey as getUmiPublicKey, } from '@metaplex-foundation/umi'; import { fromWeb3JsKeypair, fromWeb3JsPublicKey, toWeb3JsPublicKey, } from '@metaplex-foundation/umi-web3js-adapters'; import { TOKEN_PROGRAM_ID, Account as TokenAccount, getAssociatedTokenAddress, } from '@solana/spl-token'; import { AddressLookupTableAccount, ComputeBudgetProgram, Connection, Finality, Keypair, PublicKey, SystemProgram, TransactionConfirmationStatus, TransactionSignature, } from '@solana/web3.js'; import { AnchorProvider, BN, Program, Wallet } from '@staratlas/anchor'; import { CARGO_IDL, CargoIDLProgram, CargoPod, CargoStatsDefinition, CargoType, InitCargoTypeInput, } from '@staratlas/cargo'; import { CRAFTING_IDL, CraftableItem, CraftingDomain, CraftingFacility, CraftingFacilityLocationType, CraftingIDLProgram, Recipe, RecipeCategory, RecipeStatus, RegisterRecipeInput, } from '@staratlas/crafting'; import { CREW_IDL, CrewConfig, CrewIDLProgram, registerCrewConfig, } from '@staratlas/crew'; import { AsyncSigner, FixedSizeArray, InstructionReturn, buildDynamicTransactions, buildSendAndCheck, createAndExtendAddressLookupTable, createAssociatedTokenAccount, createAssociatedTokenAccountIdempotent, createMint, getCurrentTimestampOnChain, getParsedTokenAccountsByOwner, ixToIxReturn, keypairToAsyncSigner, mintToTokenAccount, readFromRPCNullable, readFromRPCOrError, sendTransaction, stringToByteArray, } from '@staratlas/data-source'; import { PLAYER_PROFILE_IDL, PermissionType, PlayerProfile, PlayerProfileIDL, ProfilePermissions, } from '@staratlas/player-profile'; import { POINTS_IDL, PointsCategory, PointsIDLProgram, PointsLevelLicenseType, UserPoints, } from '@staratlas/points'; import { POINTS_STORE_IDL, PointsStoreIDLProgram, RedemptionConfig, getDayIndex, } from '@staratlas/points-store'; import { Points } from '@staratlas/points/dist/src/idl/points'; import { Faction, PROFILE_FACTION_IDL, ProfileFactionAccount, ProfileFactionIDL, } from '@staratlas/profile-faction'; import assert from 'assert'; import { chunk, groupBy } from 'lodash'; import { CargoStats, CombatStats, CrewTransferInput, DisbandedFleet, Fleet, FleetShips, GLOBAL_SCALE_DECIMALS_4, Game, GameState, LocationType, MIN_CSS_LEVEL, MineItem, MineItemAccount, MiscStats, MiscVariablesInput, MovementStats, Planet, PlanetType, PointsCategoryType, PointsLimitInput, ProgressionConfig, ProgressionItemInput, ProgressionItemType, RegisterCombatConfigInput, RegisterSurveyDataUnitTrackerParams, Resource, ResourceAccount, RiskZonesData, SAGE_IDL, SageCrewConfig, SageIDLProgram, SagePlayerProfile, Sector, SectorRing, Ship, ShipStats, SizeClass, Starbase, StarbasePlayer, StarbaseUpkeepLevels, UpdateFleetDataInput, cleanCargoPodsByStarbasePlayer, divideIntoParts, getCargoPodsByAuthority, getSizeClassAnchorEnum, normalizePublicKey, } from '../src'; import { Sage } from '../src/idl/sage'; import { createTree, createUmi, mint as mintCNFT, } from './compressed-nft-helpers'; import { CombatConfig, UpdateCombatConfigInput } from '../src/combatConfig'; export const DEFAULT_MUD_STAR_BASE_LEVELS = [...Array(7).keys()].map((it) => { return { level: it, faction: Faction.MUD as number, oldRecipeForUpgrade: PublicKey.default, newRecipeForUpgrade: PublicKey.default, recipeCategoryForLevel: PublicKey.default, hp: new BN(0), sp: new BN(0), sectorRingAvailable: SectorRing.Inner, warpLaneMovementFee: new BN(0), repairFee: new BN(0), repairEfficiency: 0, shieldRechargeRate: 0, shieldBreakDelay: 0, }; }); export const DEFAULT_ONI_STAR_BASE_LEVELS = [...Array(7).keys()].map((it) => { return { level: it, faction: Faction.ONI, oldRecipeForUpgrade: PublicKey.default, newRecipeForUpgrade: PublicKey.default, recipeCategoryForLevel: PublicKey.default, hp: new BN(0), sp: new BN(0), sectorRingAvailable: SectorRing.Inner, warpLaneMovementFee: new BN(0), repairFee: new BN(0), repairEfficiency: 0, shieldRechargeRate: 0, shieldBreakDelay: 0, }; }); export const DEFAULT_USTUR_STAR_BASE_LEVELS = [...Array(7).keys()].map((it) => { return { level: it, faction: Faction.Ustur, oldRecipeForUpgrade: PublicKey.default, newRecipeForUpgrade: PublicKey.default, recipeCategoryForLevel: PublicKey.default, hp: new BN(0), sp: new BN(0), sectorRingAvailable: SectorRing.Inner, warpLaneMovementFee: new BN(0), repairFee: new BN(0), repairEfficiency: 0, shieldRechargeRate: 0, shieldBreakDelay: 0, }; }); export const BALANCED_STAR_BASE_LEVELS = [ ...DEFAULT_MUD_STAR_BASE_LEVELS, ...DEFAULT_ONI_STAR_BASE_LEVELS, ...DEFAULT_USTUR_STAR_BASE_LEVELS, ]; /** * Asserts that the element exists or raises an error * @param p - any value * @returns the input value or an error */ export const existOrError = (p: T | undefined | null): T => { if (p == null) { throw new Error(`element of type ${typeof p} is undefined or null`); } return p; }; /** * Get random int * @param max - the max value of the random number * @returns number */ export function getRandomInt(max: number) { return Math.floor(Math.random() * max); } /** * Get random int within range * @param min - the min value of the random number * @param max - the max value of the random number * @returns number */ export const randomWithinRange = (min: number, max: number) => Math.floor(Math.random() * (max - min)) + min; /** * Converts a number to a little endian byte array * @param num - number to convert * @param length - number of bytes in the array. Defaults to 16 bytes (u128) * @returns Uint8Array */ export const numberToLEByteArray = (num: number, length = 16): Uint8Array => { const byteArray = new Uint8Array(length); for (let i = 0; i < length; i++) { byteArray[i] = num & 0xff; num = num >>> 8; } return byteArray; }; export const removeDuplicateKeys = (keys: PublicKey[]): PublicKey[] => { const uniqueKeys = new Map(); keys.forEach((key) => { uniqueKeys.set(key.toString(), key); // Map will overwrite if duplicate found }); return Array.from(uniqueKeys.values()); // Convert map values to array }; export type CreateProfileKeysInput = { key: PublicKey | AsyncSigner; expireTime: BN | null; permissions: PermissionType; scope: PublicKey; }[]; export const requestAirDrops = async ( connection: Connection, recipients: PublicKey[], lamports = 100_000_000_000, ) => { await Promise.all( recipients.map((it) => connection.requestAirdrop(it, lamports)), ); }; export const checkTokenBalance = async ( tokenKey: PublicKey, expectedAmount: BN, connection: Connection, ) => { const tokenAccount = await connection.getTokenAccountBalance( tokenKey, 'confirmed', ); if (tokenAccount.value.uiAmount == null) { throw 'token account should be defined'; } else { assert(new BN(tokenAccount.value.amount).eq(expectedAmount)); } }; /** * Creates and funds associated token accounts for the owner and mints * @param mints - array of mints * @param owner - the owner of the token account created * @param funder - the funder/payer * @param walletSigner - the signer * @param connection - Solana connection object */ export const createTokenAccounts = async ( mints: PublicKey[], owner: PublicKey, funder: AsyncSigner, walletSigner: AsyncSigner, connection: Connection, ) => { // create token accounts await buildSendAndCheck( mints.map( (it) => createAssociatedTokenAccount(it, owner, true).instructions, ), walletSigner, connection, ); }; export const getParsedTokenAccountsByManyOwners = async ( connection: Connection, owners: PublicKey[], programId = TOKEN_PROGRAM_ID, ): Promise => { return ( await Promise.all( owners.map((owner) => getParsedTokenAccountsByOwner(connection, owner, programId), ), ) ).flat(); }; export const createManyMints = async ( keypairs: Keypair[], mintAuthority: PublicKey, walletSigner: AsyncSigner, connection: Connection, ) => { const ixs = keypairs.map((it) => { return createMint(keypairToAsyncSigner(it), 9, mintAuthority, null); }); await sendManyToChain(ixs, walletSigner, connection); }; /** * Creates and funds associated token accounts for the owner and mints * @param mintAuthority - the mint authority * @param mints - array of mints indicating how many tokens to fund for each * @param owner - the owner of the token accounts created * @param funder - the funder/payer * @param walletSigner - the signer * @param connection - Solana connection object */ export const createAndFundTokenAccounts = async ( mintAuthority: AsyncSigner, mints: Array<[PublicKey, number]>, owner: PublicKey, funder: AsyncSigner, walletSigner: AsyncSigner, connection: Connection, ) => { // create token accounts await buildSendAndCheck( mints.map( (it) => createAssociatedTokenAccount(it[0], owner, true).instructions, ), walletSigner, connection, ); // fund token accounts await buildSendAndCheck( ( await Promise.all( mints.map(async (it) => mintToTokenAccount( mintAuthority, it[0], await getAssociatedTokenAddress(it[0], owner, true), it[1], ), ), ) ).flat(), walletSigner, connection, ); }; export const createProfile = async ( program: Program, profileFactionProgram: Program, profileId: AsyncSigner, funder: AsyncSigner, keys: CreateProfileKeysInput, connection: Connection, authAuthority?: AsyncSigner, walletSigner?: AsyncSigner, faction: Exclude = Faction.MUD, keyThreshold = 1, ) => { if (authAuthority) { keys.unshift({ key: authAuthority, expireTime: null, permissions: ProfilePermissions.all(), scope: program.programId, }); } const createProfileIx = PlayerProfile.createProfile( program, profileId, keys, keyThreshold, ); await buildSendAndCheck(createProfileIx, walletSigner || funder, connection); const profileOwner = 'sign' in keys[0].key ? keys[0].key : authAuthority; if (profileOwner) { const { instructions: chooseFactionIx } = ProfileFactionAccount.chooseFactionBareBones( profileFactionProgram, profileOwner, profileId.publicKey(), faction, 0, ); await buildSendAndCheck( chooseFactionIx, walletSigner || funder, connection, ); } }; /** * Create cargo types from an array of Keypairs * Note that the mint for each Keypair must already exist * @param keyPairs - the array of Key pairs * @param statsDefinition - the cargo stats definition * @param managerKey - the key authorized to create cargo types * @param profile - the profile with the required permissions * @param walletSigner - the signer * @param statsDefinitionSeqId - the seq id field's value from the cargo stats definition * @param input - inut params * @param connection - Solana connection object * @param program - cargo program * @returns array of the created cargo type addresses */ export const createCargoTypes = async ( keyPairs: PublicKey[], statsDefinition: PublicKey, managerKey: AsyncSigner, profile: PublicKey, walletSigner: AsyncSigner, statsDefinitionSeqId: number, input: InitCargoTypeInput, connection: Connection, program: CargoIDLProgram, ) => { await buildSendAndCheck( ( await Promise.all( keyPairs.map((item) => { return CargoType.initCargoType({ program, managerKey, profile, statsDefinition, mint: item, input, statsDefinitionSeqId, }); }), ) ).flat(), walletSigner, connection, ); return keyPairs.map((it) => { return CargoType.findAddress( program, statsDefinition, it, statsDefinitionSeqId, ); }); }; /** * Create mine item from an array of [mint, resourceHardness] * Note that each mint must already exist * @param items - input items * @param profileKey - the key with the required permissions to create mine items * @param profile - the profile that contains the permissions * @param gameId - the game id * @param keyIndex - the index of the key in the profile permissions * @param walletSigner - the signer * @param connection - Solana connection object * @param program - SAGE program * @returns array of the created mine item addresses */ export const createMineItems = async ( items: Array<[PublicKey, string, number]>, profileKey: AsyncSigner, profile: PublicKey, gameId: PublicKey, keyIndex: number, walletSigner: AsyncSigner, connection: Connection, program: SageIDLProgram, ) => { const results = items.map((it) => { return MineItem.registerMineItem( program, profileKey, profile, it[0], gameId, { keyIndex, name: stringToByteArray(it[1], 64), resourceHardness: it[2], }, ); }); await buildSendAndCheck( results.map((it) => it.instructions).flat(), walletSigner, connection, ); return results.map((it) => it.mineItemKey); }; /** * Set up crafting domain and crafting facility * @param domain - the crafting domain * @param facility - the crafting facility * @param walletSigner - the signer * @param profileSigners - array the keys with the required permissions * @param profile - the profile that contains the required permissions * @param profileKeyIndices - array of key indices in the profile for each key provided * @param facilityLocation - the location of the crafting facility * @param connection - Solana connection object * @param program - crafting program * @param domainName - the crafting domain name */ export const setupBaseCraftingAccounts = async ( domain: AsyncSigner, facility: AsyncSigner, walletSigner: AsyncSigner, profileSigners: AsyncSigner[], profile: PublicKey, profileKeyIndices: number[], facilityLocation: PublicKey, connection: Connection, program: CraftingIDLProgram, domainName = '🎰', ) => { const domainSigner = profileSigners[1]; const facilitySigner = profileSigners[0]; const facilityKeyIndex = profileKeyIndices[0]; if (facilityKeyIndex && facilitySigner && domainSigner) { await buildSendAndCheck( [ CraftingDomain.initializeDomain( program, domain, domainSigner, profile, stringToByteArray(domainName, 32), ), CraftingFacility.registerCraftingFacility( program, facility, facilitySigner, profile, domain.publicKey(), facilityLocation, { locationType: CraftingFacilityLocationType.Starbase, efficiency: 10000, maxConcurrentProcesses: 1000, keyIndex: facilityKeyIndex, }, ), ], walletSigner, connection, ); } }; /** * Set up crafting facility * @param craftingDomain - the crafting domain * @param facility - the crafting facility * @param walletSigner - the signer * @param facilitySigner - key with the required permissions * @param profile - the profile that contains the required permissions * @param facilityKeyIndex - key index in the profile permissions * @param facilityLocation - the location of the crafting facility * @param connection - Solana connection object * @param program - crafting program */ export const setupCraftingFacility = async ( craftingDomain: PublicKey, facility: AsyncSigner, walletSigner: AsyncSigner, facilitySigner: AsyncSigner, profile: PublicKey, facilityKeyIndex: number, facilityLocation: PublicKey, connection: Connection, program: CraftingIDLProgram, ) => { await buildSendAndCheck( CraftingFacility.registerCraftingFacility( program, facility, facilitySigner, profile, craftingDomain, facilityLocation, { locationType: CraftingFacilityLocationType.Starbase, efficiency: 10000, maxConcurrentProcesses: 1000, keyIndex: facilityKeyIndex, }, ), walletSigner, connection, ); }; /** * Create a new recipe and recipe category * @param facility - crafting facility * @param recipe - new crafting recipe * @param recipeCategory - new recipe category * @param walletSigner - the signer * @param duration - the duration of the recipe * @param minDuration - the minimum duration of the recipe * @param domain - the crafting domain * @param profileSigners - array the keys with the required permissions * @param profile - the profile that contains the required permissions * @param profileKeyIndices - array of key indices in the profile for each key provided * @param connection - Solana connection object * @param program - the crafting program * @param recipeName - the recipe name * @param recipeCategoryName - the recipe category name */ export const createRecipeAndRecipeCategory = async ( facility: PublicKey, recipe: AsyncSigner, recipeCategory: AsyncSigner, walletSigner: AsyncSigner, duration: BN, minDuration: BN, domain: PublicKey, profileSigners: AsyncSigner[], profile: PublicKey, profileKeyIndices: number[], connection: Connection, program: CraftingIDLProgram, recipeName = '🎰', recipeCategoryName = '🐸', ) => { const recipeSigner = profileSigners[2]; const recipeKeyIndex = profileKeyIndices[2]; const recipeCatSigner = profileSigners[1]; const recipeCatKeyIndex = profileKeyIndices[1]; const facilitySigner = profileSigners[0]; const facilityKeyIndex = profileKeyIndices[0]; if ( facilityKeyIndex && facilitySigner && recipeCatKeyIndex && recipeCatSigner && recipeKeyIndex && recipeSigner ) { await buildSendAndCheck( ( await Promise.all( [ RecipeCategory.registerRecipeCategory( program, recipeCategory, recipeCatSigner, profile, domain, { namespace: stringToByteArray(recipeCategoryName, 32), keyIndex: recipeCatKeyIndex, }, ), Recipe.registerRecipe( program, recipe, recipeSigner, profile, domain, recipeCategory.publicKey(), { namespace: stringToByteArray(recipeName, 32), duration, minDuration, keyIndex: recipeKeyIndex, }, ), CraftingFacility.addCraftingFacilityRecipeCategory( program, facility, facilitySigner, profile, domain, recipeCategory.publicKey(), { keyIndex: facilityKeyIndex }, ), ].flat(), ) ).flat(), walletSigner, connection, ); } }; /** * Register craftable items using mints that are already valid cargo types * @param inputs - array of mint and mint authority pairs * @param walletSigner - the signer * @param domain - crafting domain * @param profileSigner - the key with the required permissions to create mine items * @param profile - the profile that contains the permissions * @param profileKeyIndex - the index of the key in the profile permissions * @param connection - Solana connection object * @param program - crafting program * @returns array of created craftable items */ export const registerCraftableItems = async ( inputs: Array<{ mint: PublicKey; mintAuthority: AsyncSigner }>, walletSigner: AsyncSigner, domain: PublicKey, profileSigner: AsyncSigner, profile: PublicKey, profileKeyIndex: number, connection: Connection, program: CraftingIDLProgram, ) => { await buildSendAndCheck( inputs.map((it, index) => CraftableItem.registerCraftableItem( program, profileSigner, profile, domain, it.mint, { namespace: stringToByteArray(`CraftableItem-${index}`, 32), keyIndex: profileKeyIndex, }, ), ), walletSigner, connection, ); return inputs.map( (it) => CraftableItem.findAddress(program, domain, it.mint)[0], ); }; /** * Get instructions for Register Recipe Input/Outputs * Note that all the inputs/outputs must be valid Cargo Types * @param recipe - the recipe * @param consumableInputs - array of pairs of consumable inputs and their quantities * @param nonConsumableInputs - array of pairs of non-consumable inputs and their quantities * @param outputs - array of pairs of outputs and their quantities * @param domain - crafting domain * @param profileSigner - the key with the required permissions to create mine items * @param profile - the profile that contains the permissions * @param profileKeyIndex - the index of the key in the profile permissions * @param program - crafting program * @returns array of instructions */ export const registerRecipeInputOutputsInstructions = ( recipe: PublicKey, consumableInputs: Array<[PublicKey, number]>, nonConsumableInputs: Array<[PublicKey, number]>, outputs: Array<[PublicKey, number]>, domain: PublicKey, profileSigner: AsyncSigner, profile: PublicKey, profileKeyIndex: number, program: CraftingIDLProgram, ) => { return [ consumableInputs .map((input) => Recipe.addConsumableInputToRecipe( program, recipe, profileSigner, profile, domain, input[0], { amount: new BN(input[1]), mint: input[0], keyIndex: profileKeyIndex, }, ), ) .flat(), nonConsumableInputs .map((input) => Recipe.addNonConsumableInputToRecipe( program, recipe, profileSigner, profile, domain, input[0], { amount: new BN(input[1]), mint: input[0], keyIndex: profileKeyIndex, }, ), ) .flat(), outputs .map((output) => Recipe.addOutputToRecipe( program, recipe, profileSigner, profile, domain, CraftableItem.findAddress(program, domain, output[0])[0], { amount: new BN(output[1]), mint: output[0], keyIndex: profileKeyIndex, }, ), ) .flat(), ].flat(); }; /** * Register Recipe Input/Outputs * Note that all the inputs/outputs must be valid Cargo Types * @param recipe - the recipe * @param consumableInputs - array of pairs of consumable inputs and their quantities * @param nonConsumableInputs - array of pairs of non-consumable inputs and their quantities * @param outputs - array of pairs of outputs and their quantities * @param walletSigner - the signer * @param domain - crafting domain * @param profileSigner - the key with the required permissions to create mine items * @param profile - the profile that contains the permissions * @param profileKeyIndex - the index of the key in the profile permissions * @param connection - Solana connection * @param program - crafting program */ export const registerRecipeInputOutputs = async ( recipe: PublicKey, consumableInputs: Array<[PublicKey, number]>, nonConsumableInputs: Array<[PublicKey, number]>, outputs: Array<[PublicKey, number]>, walletSigner: AsyncSigner, domain: PublicKey, profileSigner: AsyncSigner, profile: PublicKey, profileKeyIndex: number, connection: Connection, program: CraftingIDLProgram, ) => { await buildSendAndCheck( registerRecipeInputOutputsInstructions( recipe, consumableInputs, nonConsumableInputs, outputs, domain, profileSigner, profile, profileKeyIndex, program, ), walletSigner, connection, ); }; /** * Calculate mining results i.e. duration and amount of resource extracted * @param fleetAccount - the fleet account loaded after harvesting * @param fleetFoodToken - fleet food token account loaded before harvesting * @param fleetAmmoToken - fleet ammo token account loaded before harvesting * @param mineItem - the mine item account in question * @param resource - the resource account in question * @param penalty - the mining rate penalty * @param startTime - the mining start time if known (time since last harvest) * @returns mining results */ export const calculateMiningResults = ( fleetAccount: Fleet /** after harvesting */, fleetFoodToken: TokenAccount /** before harvesting */, fleetAmmoToken: TokenAccount /** before harvesting */, mineItem: MineItemAccount, resource: ResourceAccount, penalty: number, startTime?: BN, ) => { if (!fleetAccount.state.MineAsteroid) { throw 'Fleet not mining'; } const miningStart = startTime ?? fleetAccount.state.MineAsteroid.start; const miningDuration = fleetAccount.state.MineAsteroid.lastUpdate .sub(miningStart) .toNumber(); const maxFoodDuration = Fleet.calculateAsteroidMiningFoodDuration( fleetAccount.data.stats, Number(fleetFoodToken.delegatedAmount), ); const maxAmmoDuration = Fleet.calculateAsteroidMiningAmmoDuration( fleetAccount.data.stats, Number(fleetAmmoToken.delegatedAmount), ); const foodConsumed = Fleet.calculateAsteroidMiningFoodToConsume( fleetAccount.data.stats, Number(fleetFoodToken.delegatedAmount), Math.min(miningDuration, Math.floor(maxFoodDuration)), ); const ammoConsumed = Fleet.calculateAsteroidMiningAmmoToConsume( fleetAccount.data.stats, Number(fleetAmmoToken.delegatedAmount), Math.min( miningDuration, Math.floor(maxFoodDuration), Math.floor(maxAmmoDuration), ), ); const resourceExtracted = Fleet.calculateAsteroidMiningResourceToExtract( fleetAccount.data.stats, mineItem, resource, Math.min( miningDuration, Math.floor(maxFoodDuration), Math.floor(maxAmmoDuration), ), 1_000_000 /** arbitrary large number */, penalty, ); return { ammoConsumed, foodConsumed, maxAmmoDuration, maxFoodDuration, miningDuration, resourceExtracted, }; }; /** * Send many transactions to the chain * @param instructions - instruction or instructions * @param signer - the transaction signer * @param connection - Solana connection object * @param commitment - the Solana commitement level * @param skipPreflight - whether to skip preflight checks or not * @param lookupTables - Optional list of lookup tables to try to build transactions with. * @returns array of `TransactionSignature` */ export const sendManyToChain = async ( instructions: InstructionReturn | InstructionReturn[], signer: AsyncSigner, connection: Connection, commitment: Finality = 'confirmed', skipPreflight = false, lookupTables: AddressLookupTableAccount[] = [], ): Promise => { const txs = await buildDynamicTransactions( instructions, signer, { connection, }, [], // beforeIxs [], // afterIxs lookupTables, ); if (txs.isErr()) { throw txs.error; } const txSignatures: TransactionSignature[] = []; for (const tx of txs.value) { const result = await sendTransaction(tx, connection, { commitment, sendOptions: { skipPreflight, }, }); if (result.value.isErr()) { throw result.value.error; } txSignatures.push(result.value.value); } return txSignatures; }; export const setupPointsAccounts = async ( adminKey: AsyncSigner, funder: AsyncSigner, adminProfile: PublicKey, playerProfile: PublicKey, pointsProgram: Program, connection: Connection, pointLimit = 100_000_000, maxLevels = 10, pointsPerLevel = 1000, ) => { const lpCategory = keypairToAsyncSigner(Keypair.generate()); const craftingXpCategory = keypairToAsyncSigner(Keypair.generate()); const combatXpCategory = keypairToAsyncSigner(Keypair.generate()); const miningXpCategory = keypairToAsyncSigner(Keypair.generate()); const pilotXpCategory = keypairToAsyncSigner(Keypair.generate()); const councilRankXpCategory = keypairToAsyncSigner(Keypair.generate()); const pointCategories = [ miningXpCategory, pilotXpCategory, councilRankXpCategory, craftingXpCategory, combatXpCategory, lpCategory, ]; const instructions: InstructionReturn[] = []; for (let index = 0; index < pointCategories.length; index++) { const category = pointCategories[index]; instructions.push( PointsCategory.registerPointCategory( pointsProgram, adminProfile, category, { licenseType: { type: 'none' }, pointLimit: new BN(pointLimit), isSpendable: true, }, ), ); for (let index2 = 0; index2 < maxLevels; index2++) { const level = index + 1; instructions.push( PointsCategory.addPointCategoryLevelBareBones( pointsProgram, adminKey, adminProfile, category.publicKey(), { level, points: new BN(pointsPerLevel * level), licenseType: PointsLevelLicenseType.None, keyIndex: 0, }, ), ); } instructions.push( UserPoints.createUserPointAccount( pointsProgram, playerProfile, category.publicKey(), ).instructions, ); } await sendManyToChain(instructions, funder, connection); return { craftingXpCategory: craftingXpCategory.publicKey(), combatXpCategory: combatXpCategory.publicKey(), miningXpCategory: miningXpCategory.publicKey(), pilotXpCategory: pilotXpCategory.publicKey(), councilRankXpCategory: councilRankXpCategory.publicKey(), lpCategory: lpCategory.publicKey(), }; }; export const setupPointModifierAccounts = async ( categories: { miningXpCategory?: PublicKey; pilotXpCategory: PublicKey; councilRankXpCategory: PublicKey; lpCategory?: PublicKey; craftingXpCategory?: PublicKey; combatXpCategory?: PublicKey; }, adminKey: AsyncSigner, funder: AsyncSigner, adminProfile: PublicKey, gameId: PublicKey, sageProgram: Program, pointsProgram: Program, connection: Connection, ) => { const instructions = Object.entries(categories).map(([key, category]) => { const categoryType = key === 'miningXpCategory' ? PointsCategoryType.MXP : key === 'pilotXpCategory' ? PointsCategoryType.PXP : key === 'dataRunningXpCategory' ? PointsCategoryType.DRXP : key === 'craftingXpCategory' ? PointsCategoryType.CXP : key === 'lpCategory' ? PointsCategoryType.LP : key === 'combatXpCategory' ? PointsCategoryType.COXP : PointsCategoryType.CRXP; return Game.registerSagePointModifier( sageProgram, pointsProgram, adminKey, adminProfile, gameId, category, categoryType, 0, ).instructions; }); await sendManyToChain(instructions, funder, connection, 'confirmed', true); }; export const checkUserPoints = async ( userPointsKey: PublicKey, expectedPoints: number, pointsProgram: PointsIDLProgram, connection: Connection, ) => { const data = await readFromRPCOrError( connection, pointsProgram, userPointsKey, UserPoints, ); if (!data.data.earnedPoints.eq(new BN(expectedPoints))) { console.log('earnedPoints', data.data.earnedPoints.toNumber()); console.log('expectedPoints', expectedPoints); } assert(data.data.earnedPoints.eq(new BN(expectedPoints))); }; export const getDefaultProgressionInputs = () => Object.keys(ProgressionItemType) .filter((key) => isNaN(Number(key))) .map((key) => { const itemType = ProgressionItemType[key as keyof typeof ProgressionItemType]; const out: ProgressionItemInput = { item: { value: (itemType + 1) * GLOBAL_SCALE_DECIMALS_4 }, itemType, }; return out; }); export const setupProgression = async ( adminKey: AsyncSigner, funder: AsyncSigner, adminProfile: PublicKey, gameId: PublicKey, sageProgram: SageIDLProgram, connection: Connection, progressionItems?: ProgressionItemInput[], pointsLimits?: PointsLimitInput, ) => { const items = progressionItems ?? getDefaultProgressionInputs(); const instructions: InstructionReturn[] = [ ProgressionConfig.registerProgressionConfig( sageProgram, adminKey, adminProfile, gameId, { keyIndex: 0 }, ).instructions, ProgressionConfig.updateProgressionConfig( sageProgram, adminKey, adminProfile, gameId, { keyIndex: 0, items, }, ), ]; if (pointsLimits) { instructions.push( ProgressionConfig.updateProgressionConfig( sageProgram, adminKey, adminProfile, gameId, { ...pointsLimits, keyIndex: 0 }, ), ); } await sendManyToChain(instructions, funder, connection, 'confirmed', true); }; export const setupTokenRedemptionInstructions = async ( redeemableTokenMint: PublicKey, lpCategory: PublicKey, adminKey: AsyncSigner, adminProfile: PublicKey, adminKeyIndex: number, pointsStoreProgram: PointsStoreIDLProgram, connection: Connection, redemptionConfigInput?: AsyncSigner, userRedemptionInput?: AsyncSigner, faction = Faction.MUD, numEpochs = 5, redeemableTokens = 1_000_000, allowOnlyCurrentEpoch = true, ) => { const redemptionConfig = redemptionConfigInput ?? keypairToAsyncSigner(Keypair.generate()); const userRedemption = userRedemptionInput ?? keypairToAsyncSigner(Keypair.generate()); const configResult = RedemptionConfig.createRedemptionConfig( pointsStoreProgram, adminProfile, redemptionConfig, lpCategory, { mint: redeemableTokenMint }, { faction, allowOnlyCurrentEpoch }, ); const currentTime = await getCurrentTimestampOnChain(connection); const currentDayIndex = getDayIndex(new BN(currentTime.toString())); const epochIxs = [...Array(numEpochs).keys()].map((_, index) => RedemptionConfig.addRedemptionEpoch( pointsStoreProgram, adminKey, adminProfile, redemptionConfig.publicKey(), { totalTokens: new BN(redeemableTokens), dayIndex: new BN(currentDayIndex + index), keyIndex: adminKeyIndex, }, ), ); return { bankAccount: configResult.bankAccount, configSigner: configResult.configSigner, instructions: [...configResult.instructions, ...epochIxs], redemptionConfig, userRedemption, }; }; export const chunkShipAmounts = (amount: number, max = 254) => { const chunks = [...Array(Math.floor(amount / max)).keys()].map(() => max); if (amount % max > 0) { chunks.push(amount % max); } return chunks; }; export const setupUmi = async ( funder: AsyncSigner, connection: Connection, otherSigners: AsyncSigner[] = [], ) => { if (!funder.inner) { throw 'Keypair not found'; } const signers: Array<[Keypair, boolean]> = [ [funder.inner() as Keypair, true], ]; for (let index = 0; index < otherSigners.length; index++) { const element = otherSigners[index]; if (!element.inner) { throw 'other signer Keypair not found'; } signers.push([element.inner() as Keypair, true]); } const umi = await createUmi(signers, connection); return umi; }; export const CREW_TREE_MAX_DEPTH = 5; export const CREW_TREE_CANOPY_DEPTH = 5; export const CREW_TREE_MAX_BUFFER_SIZE = 8; export const setupCrewConfigInstructions = async ( adminSigner: AsyncSigner, adminProfile: PublicKey, adminKeyIndex: number, gameId: PublicKey, sageProgram: SageIDLProgram, crewProgram: CrewIDLProgram, umi: Umi, connection: Connection, maxDepth = CREW_TREE_MAX_DEPTH, maxBufferSize = CREW_TREE_MAX_BUFFER_SIZE, canopyDepth = CREW_TREE_CANOPY_DEPTH, ) => { if (!adminSigner.inner) { throw 'adminSigner Keypair not found'; } const crewCollectionKeypair = Keypair.generate(); const crewCreatorKeypair = Keypair.generate(); const [crewConfigPubkey, _crewConfigBump] = CrewConfig.findAddress( crewProgram, gameId, ); const crewConfigAccount = await readFromRPCNullable( connection, crewProgram, crewConfigPubkey, CrewConfig, ); const instructions = []; let merkleTree: PublicKey; if (crewConfigAccount) { // use the last tree in the array to ensure freshness incase it is created in another package's tests merkleTree = crewConfigAccount.merkleTrees[crewConfigAccount.merkleTrees.length - 1]; } else { const merkleTreeCreated = await createTree(umi, { public: false, maxDepth, maxBufferSize, canopyDepth, treeCreator: createSignerFromKeypair( umi, fromWeb3JsKeypair(adminSigner.inner() as Keypair), ), }); await setTreeDelegate(umi, { merkleTree: merkleTreeCreated, newTreeDelegate: fromWeb3JsPublicKey(crewConfigPubkey), }).sendAndConfirm(umi, { send: { skipPreflight: true }, }); merkleTree = toWeb3JsPublicKey(merkleTreeCreated); instructions.push( registerCrewConfig( crewProgram, adminProfile, { namePrefix: 'Test', symbol: 'Test', uriPrefix: 'Test', sellerFeeBasisPoints: 20, collection: crewCollectionKeypair.publicKey, creators: [{ key: crewCreatorKeypair.publicKey, share: 100 }], }, [merkleTree], gameId, // seedPubkey ), ); } const sageCrewConfigAddress = SageCrewConfig.findAddress(sageProgram, gameId); const sageCrewConfigAccount = await readFromRPCNullable( connection, sageProgram, sageCrewConfigAddress[0], SageCrewConfig, ); if (!sageCrewConfigAccount) { instructions.push( SageCrewConfig.registerSageCrewConfig({ program: sageProgram, key: adminSigner, profile: adminProfile, crewProgramConfig: crewConfigPubkey, gameId, input: { keyIndex: adminKeyIndex }, }), ); } return { crewProgramConfig: crewConfigPubkey, sageCrewConfig: SageCrewConfig.findAddress(sageProgram, gameId), instructions, merkleTree, }; }; export const mintAndPrepareCrew = async ( crewMerkleTree: PublicKey, crewOwner: PublicKey, umi: Umi, knownLeaves: UmiPublicKey[] = [], numCrew = 5, startingLeafIndex?: number, treeCreatorOrDelegate?: AsyncSigner, ) => { const umiCrewMerkleTree = fromWeb3JsPublicKey(crewMerkleTree); const merkleTreeAccount = await fetchMerkleTree(umi, umiCrewMerkleTree); const localKnownLeaves = [...knownLeaves]; const leafIndex = startingLeafIndex ?? localKnownLeaves.length; let treeAdmin: UmiKeypair | undefined = undefined; if (treeCreatorOrDelegate && treeCreatorOrDelegate.inner) { treeAdmin = fromWeb3JsKeypair(treeCreatorOrDelegate.inner() as Keypair); } const items: (CrewTransferInput & { metadata: MetadataArgsArgs })[] = []; for (let index = 0; index < numCrew; index++) { const { metadata, leaf, leafIndex: newLeafIndex, } = await mintCNFT(umi, { merkleTree: umiCrewMerkleTree, leafOwner: fromWeb3JsPublicKey(crewOwner), leafIndex: leafIndex + index, treeCreatorOrDelegate: treeAdmin && createSignerFromKeypair(umi, treeAdmin), }); const newProof = getMerkleProof( [...localKnownLeaves, leaf], CREW_TREE_MAX_DEPTH, leaf, newLeafIndex, ); localKnownLeaves.push(leaf); items.push({ merkleTree: toWeb3JsPublicKey(umiCrewMerkleTree), root: Array.from(getCurrentRoot(merkleTreeAccount.tree)), dataHash: Array.from(hashMetadataData(metadata)), leafIndex: newLeafIndex, metadata, creatorHash: new PublicKey(hashMetadataCreators(metadata.creators)), proof: newProof.map((it) => toWeb3JsPublicKey(it)), }); } return { items, updatedLeaves: localKnownLeaves, }; }; const createNewLookupTable = async ( addresses: PublicKey[], authority: AsyncSigner, feePayer: AsyncSigner, connection: Connection, recentSlot?: number, awaitNewSlot = true, ) => { const newLookup = ( await createAndExtendAddressLookupTable( connection, authority, feePayer, addresses, recentSlot, // recentSlot?: number, { awaitNewSlot }, // options?: ExtendTransactionOptions ) )._unsafeUnwrap(); const table = await connection.getAddressLookupTable(newLookup); if (table.value === null) { throw 'Address lookup table not found'; } return table.value; }; const divideNumber = (n: number, x: number): number[] => { const groups: number[] = []; while (n > 0) { if (n >= x) { groups.push(x); n -= x; } else { groups.push(n); n = 0; } } return groups; }; export const mintAndImportCrewToGame = async ( crewMerkleTree: PublicKey, crewOwner: AsyncSigner, crewProgramConfig: PublicKey, umi: Umi, sageProgram: SageIDLProgram, profile: PublicKey, profileFaction: PublicKey, starbasePlayer: PublicKey, starbase: PublicKey, gameId: PublicKey, funder: AsyncSigner, connection: Connection, knownLeaves: UmiPublicKey[] = [], numCrew = 15, startingLeafIndex?: number, treeCreatorOrDelegate?: AsyncSigner, ) => { const crewChunks = divideNumber(numCrew, 5); for (let index = 0; index < crewChunks.length; index++) { const crewChunk = crewChunks[index]; const mintPrepareResult = await mintAndPrepareCrew( crewMerkleTree, crewOwner.publicKey(), umi, knownLeaves, crewChunk, startingLeafIndex ?? knownLeaves.length, treeCreatorOrDelegate || crewOwner, ); const lookupTable = await createNewLookupTable( removeDuplicateKeys([ sageProgram.programId, profile, profileFaction, crewOwner.publicKey(), starbasePlayer, starbase, crewMerkleTree, crewProgramConfig, gameId, funder.publicKey(), SagePlayerProfile.findAddress(sageProgram, profile, gameId)[0], SageCrewConfig.findAddress(sageProgram, gameId)[0], PublicKey.findProgramAddressSync( [crewMerkleTree.toBuffer()], toWeb3JsPublicKey(MPL_BUBBLEGUM_PROGRAM_ID), )[0], toWeb3JsPublicKey(SPL_ACCOUNT_COMPRESSION_PROGRAM_ID), toWeb3JsPublicKey(MPL_BUBBLEGUM_PROGRAM_ID), toWeb3JsPublicKey(SPL_NOOP_PROGRAM_ID), SystemProgram.programId, normalizePublicKey(mintPrepareResult.items[0].creatorHash), ...mintPrepareResult.items.map((it) => it.proof).flat(), ]), funder, funder, connection, undefined, // recentSlot true, // awaitNewSlot ); await sendManyToChain( [ ixToIxReturn( ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 426 }), ), ixToIxReturn( ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }), ), SagePlayerProfile.addCrewToGame( sageProgram, profile, profileFaction, crewOwner, starbasePlayer, starbase, crewProgramConfig, gameId, { items: [...mintPrepareResult.items], }, ), ], funder, connection, 'confirmed', true, [lookupTable], ); const leavesUpdatedAfterAdd = mintPrepareResult.items.map((it) => hashLeaf(umi, { merkleTree: fromWeb3JsPublicKey(crewMerkleTree), owner: fromWeb3JsPublicKey( SagePlayerProfile.findAddress(sageProgram, profile, gameId)[0], ), leafIndex: it.leafIndex, metadata: it.metadata, }), ); knownLeaves.push(...leavesUpdatedAfterAdd.map((it) => getUmiPublicKey(it))); } }; export interface SageGameTestProfile { key: AsyncSigner; profile: PublicKey; index: number; } export interface SageGameTestProfiles { superuser: SageGameTestProfile; player1: SageGameTestProfile; others: Array; } export interface SageGameTestShip { [key: string]: PublicKey[]; } export interface SageGameTestSector { sector: PublicKey; starbases: PublicKey[]; planets: PublicKey[]; starbasePlayers: SageGameTestStarbasePlayer[]; } export interface SageGameTestRecipes { category: PublicKey; recipes: PublicKey[]; } export interface SageGameTestStarbasePlayer { starbasePlayer: PublicKey; cargoPod: PublicKey; } export interface SageGameTestCargoTypeInput { mint: PublicKey; size: number; } export interface PointsCategories { miningXpCategory: PublicKey; pilotXpCategory: PublicKey; dataRunningXpCategory: PublicKey; craftingXpCategory: PublicKey; combatXpCategory: PublicKey; lpCategory: PublicKey; councilRankXpCategory: PublicKey; } export interface SageGameTestCrewConfig { crewProgramConfig: PublicKey; sageCrewConfig: PublicKey; crewMerkleTree: PublicKey; knownLeaves: UmiPublicKey[]; } export interface SageGameTestData { profiles: SageGameTestProfiles; mints?: { [key: string]: PublicKey }; game: PublicKey; gameState?: PublicKey; cargoStatsDefinition?: PublicKey; craftingDomain?: PublicKey; sectors?: SageGameTestSector[]; ships?: SageGameTestShip[]; fleets?: PublicKey[]; recipes?: SageGameTestRecipes[]; pointCategories?: PointsCategories; crew?: SageGameTestCrewConfig; } export class SageGameTest { static readonly SAGE_PROGRAM_ID = 'SAgEeT8u14TE69JXtanGSgNkEdoPUcLabeyZD2uw8x9'; static readonly PLAYER_PROFILE_PROGRAM_ID = 'PprofUW1pURCnMW2si88GWPXEEK3Bvh9Tksy8WtnoYJ'; static readonly PROFILE_FACTION_PROGRAM_ID = 'pFACzkX2eSpAjDyEohD6i3VRJvREtH9ynbtM1DwVFsj'; static readonly CARGO_PROGRAM_ID = 'CArGoi989iv3VL3xArrJXmYYDNhjwCX5ey5sY5KKwMG'; static readonly CRAFTING_PROGRAM_ID = 'CRAFtUSjCW74gQtCS6LyJH33rhhVhdPhZxbPegE4Qwfq'; static readonly CREW_PROGRAM_ID = 'CrewCfY4Na9HBj7UAJ9Yi9AteoEYPhEs8sg6YnjK4qBL'; static readonly POINTS_PROGRAM_ID = 'PointJfvuHi8DgGsPCy97EaZkQ6NvpghAAVkuquLf3w'; static readonly POINTS_STORE_PROGRAM_ID = 'PsTRqmfqtLcXcGQoguTLAppxq1rnuTqqyUMRoY62XL1'; static readonly MARKET_HOOK_PROGRAM_ID = 'hooKwBRKyzBqxVZFQVpLMKGexhmc6ZNaRAbwWi8uMok'; static readonly ATLAS = 'ATLAS'; static readonly POLIS = 'POLIS'; static readonly SDU = 'SDU'; static readonly FOOD = 'FOOD'; static readonly AMMO = 'AMMO'; static readonly FUEL = 'FUEL'; static readonly REPAIR_KIT = 'REPAIR_KIT'; static readonly RESOURCES = [ this.FOOD, this.AMMO, this.FUEL, this.REPAIR_KIT, ]; static readonly INGREDIENT1 = 'INGREDIENT1'; static readonly INGREDIENT2 = 'INGREDIENT2'; static readonly INGREDIENT3 = 'INGREDIENT3'; static readonly INGREDIENTS = [ this.INGREDIENT1, this.INGREDIENT2, this.INGREDIENT3, ]; static readonly OUTPUT1 = 'OUTPUT1'; static readonly OUTPUT2 = 'OUTPUT2'; static readonly OUTPUTS = [this.OUTPUT1, this.OUTPUT2]; static readonly defaultShipsInFleet = 1; static readonly defaultCargoSize = 1; static readonly defaultSduSize = 2; static readonly defaultStarbaseLevel = MIN_CSS_LEVEL; static readonly defaultNumShips = 1000; static readonly starbaseCargoAmt = 100_000_000_000; static readonly defaultEmptyCargoSpace = 0.2; static readonly defaultFaction = Faction.MUD; static readonly defaultSector: [BN, BN] = [new BN(0), new BN(0)]; static readonly defaultNumCategories = 8; static readonly defaultNumRecipes = 24; static readonly defaultCraftableItemAmount = 100_000; static readonly defaultConsumableAmount = 2; static readonly defaultNonConsumableAmount = 1; static readonly defaultOutputAmount = 1; static readonly defaultUpgradeMultiplier = 10; static readonly default_ships_in_game = 100; static readonly default_ships_in_fleet = 1; static readonly default_cargo_size = 1; static readonly default_sdu_size = 2; static readonly default_starbase_level = 6; static readonly default_num_ships = 1000; static readonly starbase_cargo_amt = 100_000_000_000; static readonly default_empty_cargo_space = 0.2; static readonly default_faction = Faction.MUD; static readonly default_sector: [BN, BN] = [new BN(0), new BN(0)]; ready: Promise; program: SageIDLProgram; playerProfileProgram: Program; profileFactionProgram: Program; cargoProgram: CargoIDLProgram; craftingProgram: CraftingIDLProgram; crewProgram: CrewIDLProgram; pointsProgram: PointsIDLProgram; pointsStoreProgram: PointsStoreIDLProgram; connection: Connection; provider: AnchorProvider; profiles: SageGameTestProfiles; funder: AsyncSigner; gameId: PublicKey; gameState?: PublicKey; cargoStatsDefinition?: PublicKey; craftingDomain?: PublicKey; sagePlayerProfile?: PublicKey; ships?: SageGameTestShip; sectors?: SageGameTestSector[]; fleets?: PublicKey[]; recipes?: SageGameTestRecipes[]; mints?: { [key: string]: PublicKey }; pointCategories?: PointsCategories; crew?: SageGameTestCrewConfig; lookupTables?: AddressLookupTableAccount[]; constructor(funder: Keypair, connection: Connection) { this.connection = connection; this.provider = new AnchorProvider( connection, new Wallet(funder), AnchorProvider.defaultOptions(), ); this.program = new Program( SAGE_IDL, new PublicKey(SageGameTest.SAGE_PROGRAM_ID), this.provider, ); this.cargoProgram = new Program( CARGO_IDL, new PublicKey(SageGameTest.CARGO_PROGRAM_ID), this.provider, ); this.craftingProgram = new Program( CRAFTING_IDL, new PublicKey(SageGameTest.CRAFTING_PROGRAM_ID), this.provider, ); this.crewProgram = new Program( CREW_IDL, new PublicKey(SageGameTest.CREW_PROGRAM_ID), this.provider, ); this.playerProfileProgram = new Program( PLAYER_PROFILE_IDL, new PublicKey(SageGameTest.PLAYER_PROFILE_PROGRAM_ID), this.provider, ); this.profileFactionProgram = new Program( PROFILE_FACTION_IDL, new PublicKey(SageGameTest.PROFILE_FACTION_PROGRAM_ID), this.provider, ); this.pointsProgram = new Program( POINTS_IDL, new PublicKey(SageGameTest.POINTS_PROGRAM_ID), this.provider, ); this.pointsStoreProgram = new Program( POINTS_STORE_IDL, new PublicKey(SageGameTest.POINTS_STORE_PROGRAM_ID), this.provider, ); this.funder = keypairToAsyncSigner(funder); const gameAuthority = keypairToAsyncSigner(Keypair.generate()); const gameKeypair = keypairToAsyncSigner(Keypair.generate()); this.gameId = gameKeypair.publicKey(); const adminProfile = keypairToAsyncSigner(Keypair.generate()); const player1 = keypairToAsyncSigner(Keypair.generate()); const player1Profile = keypairToAsyncSigner(Keypair.generate()); const player2 = keypairToAsyncSigner(Keypair.generate()); const player2Profile = keypairToAsyncSigner(Keypair.generate()); const player3 = keypairToAsyncSigner(Keypair.generate()); const player3Profile = keypairToAsyncSigner(Keypair.generate()); this.profiles = { superuser: { key: gameAuthority, index: 0, profile: adminProfile.publicKey(), }, player1: { key: player1, index: 0, profile: player1Profile.publicKey(), }, others: [ { key: player2, index: 0, profile: player2Profile.publicKey(), }, { key: player3, index: 0, profile: player3Profile.publicKey(), }, ], }; this.ready = Promise.all([ createProfile( this.playerProfileProgram, this.profileFactionProgram, adminProfile, this.funder, [], connection, this.profiles.superuser.key, ), createProfile( this.playerProfileProgram, this.profileFactionProgram, player1Profile, this.funder, [], connection, this.profiles.player1.key, undefined, SageGameTest.defaultFaction, ), createProfile( this.playerProfileProgram, this.profileFactionProgram, player2Profile, this.funder, [], connection, this.profiles.others[0].key, undefined, SageGameTest.default_faction, ), createProfile( this.playerProfileProgram, this.profileFactionProgram, player3Profile, this.funder, [], connection, this.profiles.others[1].key, undefined, SageGameTest.default_faction, ), ]).then(() => Promise.resolve( buildSendAndCheck( Game.initGame( this.program, this.profiles.superuser.key, this.profiles.superuser.profile, gameKeypair, ), this.funder, connection, ), ), ); } getResourceMints() { if (!this.mints) { throw 'this.mints not set'; } return Object.entries(this.mints) .filter(([key, _val]) => SageGameTest.RESOURCES.includes(key)) .map(([_key, val]) => val); } getCraftingMints() { if (!this.mints) { throw 'this.mints not set'; } return Object.entries(this.mints) .filter( ([key, _val]) => SageGameTest.INGREDIENTS.includes(key) || SageGameTest.OUTPUTS.includes(key), ) .map(([_key, val]) => val); } getShips() { if (!this.ships) { throw 'this.ships not set'; } return Object.values(this.ships).flat(); } getShipMint(ship: PublicKey) { if (!this.ships) { throw 'this.ships not set'; } for (let index = 0; index < Object.entries(this.ships).length; index++) { const [mint, shipArray] = Object.entries(this.ships)[index]; if (shipArray.map((it) => it.toBase58()).includes(ship.toBase58())) { return new PublicKey(mint); } } throw 'ship not found'; } getStarbasePlayer(starbasePlayer: PublicKey) { if (!this.sectors) { throw 'this.sectors not set'; } const thisSBPData = this.sectors .map((it) => it.starbasePlayers) .flat() .find((it) => it.starbasePlayer.equals(starbasePlayer)); if (!thisSBPData) { throw 'starbasePlayer not found'; } return thisSBPData; } getCargoTypeAddress(mint: PublicKey) { if (!this.cargoStatsDefinition) { throw 'this.cargoStatsDefinition not set'; } return CargoType.findAddress( this.cargoProgram, this.cargoStatsDefinition, mint, 0, )[0]; } getMineItemAddress(mint: PublicKey) { return MineItem.findAddress(this.program, this.gameId, mint)[0]; } getResourceAddress(mineItem: PublicKey, location: PublicKey) { return Resource.findAddress(this.program, mineItem, location)[0]; } getUserPointsAccountAddress(category: PublicKey, profileKey?: PublicKey) { if (!this.pointCategories) { throw 'this.pointCategories not set'; } return UserPoints.findAddress( this.pointsProgram, category, profileKey ?? this.profiles.player1.profile, )[0]; } getPointsModifierAddress(category: PublicKey) { if (!this.pointCategories) { throw 'this.pointCategories not set'; } return Game.findPointsModifierAddress( this.program, this.gameId, category, )[0]; } static getDefaultGameVars() { // risk zones const riskZones = { mudSecurityZone: { center: [new BN(-5), new BN(-3)], radius: new BN(2), } as never, oniSecurityZone: { center: [new BN(5), new BN(3)], radius: new BN(2), } as never, usturSecurityZone: { center: [new BN(10), new BN(10)], radius: new BN(2), } as never, highRiskZone: { center: [new BN(0), new BN(0)], radius: new BN(2), } as never, mediumRiskZone: { center: [new BN(0), new BN(0)], radius: new BN(20), } as never, }; return { riskZones }; } static getDefaultGameStateVars() { // fleet const starbaseLevels = BALANCED_STAR_BASE_LEVELS.map((it, index) => { const baseWarpLaneMovementFee = 64; const baseHP = 1337; const baseSP = 13337; const baseRepairFee = 72; const baseRepairEfficiency = 75; const baseShieldRechargeRate = 25; const baseShieldBreakDelay = 1; return { ...it, hp: new BN(index * baseHP), sp: new BN(index * baseSP), warpLaneMovementFee: new BN(index * baseWarpLaneMovementFee), repairFee: new BN(baseRepairFee * index), repairEfficiency: baseRepairEfficiency * index, shieldRechargeRate: baseShieldRechargeRate * index, shieldBreakDelay: baseShieldBreakDelay * index, }; }); const fleet = { maxFleetSize: 64, starbaseLevels, starbaseUpkeep: SageGameTest.getUpkeepInput( SageGameTest.getDefaultUpkeep(), ), }; // miscVariables const miscVariables = { warpLaneFuelCostReduction: 75, upkeepMiningEmissionsPenalty: 50, respawnFee: new BN(125), }; return { fleet, miscVariables }; } static getDefaultShipStats( crew?: number, subwarpSpeed?: number, ammoConsumptionRate?: number, foodConsumptionRate?: number, ) { const movementStats: MovementStats = { warpSpeed: 1000000, subwarpSpeed: subwarpSpeed == null ? 400000 : subwarpSpeed, maxWarpDistance: 10000, warpCoolDown: 2, subwarpFuelConsumptionRate: randomWithinRange(100, 950) * 100, warpFuelConsumptionRate: randomWithinRange(400, 1500) * 100, planetExitFuelAmount: 750, }; const cargoStats: CargoStats = { cargoCapacity: randomWithinRange(70000, 110000), fuelCapacity: randomWithinRange(10000, 60000), ammoCapacity: randomWithinRange(5000, 50000), ammoConsumptionRate: ammoConsumptionRate == null ? 200000 : ammoConsumptionRate, foodConsumptionRate: foodConsumptionRate == null ? 250000 : foodConsumptionRate, miningRate: 450000, upgradeRate: 5337, cargoTransferRate: 0, tractorBeamGatherRate: 0, }; const combatStats: CombatStats = { ammoConsumptionRate: 2000, ap: 5000, hp: 4500, sp: 1500, apRegenRate: 9000, shieldRechargeRate: 1500, repairRate: 8000, lootRate: 200, shieldBreakDelay: 2, warpSpoolDuration: 0, repairAbility: randomWithinRange(1, 121), repairEfficiency: randomWithinRange(30, 174), }; const miscStats: MiscStats = { requiredCrew: crew == null ? 1 : crew, crewCount: 0, passengerCapacity: 10, rentedCrew: 0, respawnTime: 2, scanCoolDown: 2, sduPerScan: 10, scanCost: 1, placeholder: 0, placeholder2: 0, placeholder3: 0, }; const stats: ShipStats = { movementStats: movementStats, cargoStats: cargoStats, combatStats, miscStats: miscStats, }; return stats; } static getDefaultUpkeep() { const upkeep: StarbaseUpkeepLevels = { level0: { ammoReserve: new BN(0), ammoDepletionRate: 0, foodReserve: new BN(0), foodDepletionRate: 0, toolkitReserve: new BN(0), toolkitDepletionRate: 0, }, level1: { ammoReserve: new BN(1000), ammoDepletionRate: 1000, foodReserve: new BN(1010), foodDepletionRate: 1100, toolkitReserve: new BN(1020), toolkitDepletionRate: 50000, }, level2: { ammoReserve: new BN(2_000_000), ammoDepletionRate: 2000, foodReserve: new BN(2_000_000), foodDepletionRate: 2000, toolkitReserve: new BN(2_000_000), toolkitDepletionRate: 2000, }, level3: { ammoReserve: new BN(3_000_000), ammoDepletionRate: 3000, foodReserve: new BN(3_000_000), foodDepletionRate: 3000, toolkitReserve: new BN(3_000_000), toolkitDepletionRate: 3000, }, level4: { ammoReserve: new BN(4_000_000), ammoDepletionRate: 4000, foodReserve: new BN(4_000_000), foodDepletionRate: 4000, toolkitReserve: new BN(4_000_000), toolkitDepletionRate: 4000, }, level5: { ammoReserve: new BN(5_000_000), ammoDepletionRate: 5000, foodReserve: new BN(5_000_000), foodDepletionRate: 5000, toolkitReserve: new BN(5_000_000), toolkitDepletionRate: 5000, }, level6: { ammoReserve: new BN(6_000_000), ammoDepletionRate: 6000, foodReserve: new BN(6_000_000), foodDepletionRate: 6000, toolkitReserve: new BN(6_000_000), toolkitDepletionRate: 6000, }, }; return upkeep; } static getDefaultSDUTrackerStats() { const stats: Omit = { coordinatesRange: [ new BN(-50), new BN(50), ] /** this gives valid range of [-50, -50] to [50, 50] */, cssCoordinates: [ [new BN(-40), new BN(30)], [new BN(40), new BN(30)], [new BN(0), new BN(-39)], ] /** coordinates of the three faction CSS */, originCoordinates: [new BN(0), new BN(7)], cssMaxDistance: 1000 /** translates to 10.0 */, originMaxDistance: 7000 /** translates to 70.0 */, distanceWeighting: 100 /** translates to 1.0 */, tMax: new BN(120) /** two minutes */, xMul: 3000 /** translates to 0.3 */, yMul: 3000 /** translates to 0.3 */, zMul: 10 /** translates to 0.0001 */, sduMaxPerSector: 1000 /** max SDUs found per sector */, scanChanceRegenPeriod: 600 /** sector scan chance regen time period in seconds */, }; return stats; } static getUpkeepInput(upkeep: StarbaseUpkeepLevels) { const upkeepArray = Object.keys(upkeep).map((key) => { const level = parseInt(key.replace('level', '')); return { level: level, info: upkeep[key as keyof StarbaseUpkeepLevels], }; }); return upkeepArray; } getProfileFactionAddress(profile: PublicKey) { return ProfileFactionAccount.findAddress( this.profileFactionProgram, profile, )[0]; } async loadGame() { return await readFromRPCOrError( this.connection, this.program, this.gameId, Game, 'confirmed', ); } async loadGameState() { if (!this.gameState) { throw 'this.gameState not set'; } return await readFromRPCOrError( this.connection, this.program, this.gameState, GameState, 'confirmed', ); } async loadCargoStatsDefinition() { if (!this.cargoStatsDefinition) { throw 'this.cargoStatsDefinition not set'; } return await readFromRPCOrError( this.connection, this.cargoProgram, this.cargoStatsDefinition, CargoStatsDefinition, 'confirmed', ); } async loadCraftingDomain() { if (!this.craftingDomain) { throw 'this.craftingDomain not set'; } return await readFromRPCOrError( this.connection, this.craftingProgram, this.craftingDomain, CraftingDomain, 'confirmed', ); } async loadStarbasePlayer( starbasePlayer: PublicKey, commitment: TransactionConfirmationStatus = 'confirmed', ) { if (!this.cargoStatsDefinition) { throw 'this.cargoStatsDefinition not set'; } return await readFromRPCOrError( this.connection, this.program, starbasePlayer, StarbasePlayer, commitment, ); } async loadShip(ship: PublicKey) { return await readFromRPCOrError( this.connection, this.program, ship, Ship, 'confirmed', ); } async loadFleet(fleet: PublicKey) { return await readFromRPCOrError( this.connection, this.program, fleet, Fleet, 'confirmed', ); } async loadFleetShips(fleetShips: PublicKey) { return await readFromRPCOrError( this.connection, this.program, fleetShips, FleetShips, 'confirmed', ); } async loadRecipe(recipe: PublicKey) { return await readFromRPCOrError( this.connection, this.craftingProgram, recipe, Recipe, 'confirmed', ); } async loadMineItem(address: PublicKey) { return await readFromRPCOrError( this.connection, this.program, address, MineItem, 'confirmed', ); } async loadMineItemByMint(mint: PublicKey) { return await this.loadMineItem(this.getMineItemAddress(mint)); } async loadResource(address: PublicKey) { return await readFromRPCOrError( this.connection, this.program, address, Resource, 'confirmed', ); } async loadResourceByMintAndLocation(mint: PublicKey, location: PublicKey) { return await this.loadResource( this.getResourceAddress(this.getMineItemAddress(mint), location), ); } async loadUserPoints(category: PublicKey) { const address = this.getUserPointsAccountAddress(category); return await readFromRPCOrError( this.connection, this.pointsProgram, address, UserPoints, 'confirmed', ); } async sendToChain(instructions: InstructionReturn | InstructionReturn[]) { return ( await sendManyToChain(instructions, this.funder, this.connection) )[0]; } async sendManyToChain( instructions: InstructionReturn | InstructionReturn[], skipPreflight = false, lookupTables: AddressLookupTableAccount[] = [], commitment: Finality = 'confirmed', ) { return await sendManyToChain( instructions, this.funder, this.connection, commitment, skipPreflight, lookupTables, ); } async newLookupTable( addresses: PublicKey[], recentSlot?: number, awaitNewSlot = true, ) { const result = await createNewLookupTable( addresses, this.profiles.superuser.key, this.funder, this.connection, recentSlot, awaitNewSlot, ); if (!result) { throw 'Address lookup table not found'; } if (this.lookupTables) { this.lookupTables.push(result); } else { this.lookupTables = [result]; } return result; } static async createGame({ funder, connection, numberOfShips, setupCraftingAccounts = false, // Default value assigned here riskZones, sector, fleet, miscVariables, numberOfShipsInFleet, addCargoToFleet = true, fleetToIdle = true, configureCrew = true, numCrewForPlayer = undefined, // number of crew to add to game chunkCrewInstructions = undefined, shipStats, }: { funder: Keypair; connection: Connection; numberOfShips?: number; setupCraftingAccounts?: boolean; riskZones?: RiskZonesData; sector?: [BN, BN]; fleet?: UpdateFleetDataInput; miscVariables?: MiscVariablesInput; numberOfShipsInFleet?: number | 'False'; addCargoToFleet?: boolean; fleetToIdle?: boolean; configureCrew?: boolean; numCrewForPlayer?: number; chunkCrewInstructions?: number; shipStats?: ShipStats; }) { // Set default values if undefined numberOfShips = numberOfShips ?? SageGameTest.defaultShipsInFleet; numberOfShipsInFleet = numberOfShipsInFleet ?? SageGameTest.defaultShipsInFleet; const game = new SageGameTest(funder, connection); await game.ready; await game.createCargoStatsDefinition(); await game.createCraftingDomain(); await game.createMints(SageGameTest.RESOURCES, 0); if (!game.mints) { throw 'mint not created'; } await game.createMints([SageGameTest.ATLAS, SageGameTest.POLIS], 8); await game.createMints( [SageGameTest.SDU, ...SageGameTest.INGREDIENTS, ...SageGameTest.OUTPUTS], 0, ); const atlastVault = await game.getOrCreateAssociatedTokenAccount( game.mints[SageGameTest.ATLAS], game.profiles.superuser.key.publicKey(), true, ); const polisVault = await game.getOrCreateAssociatedTokenAccount( game.mints[SageGameTest.POLIS], game.profiles.superuser.key.publicKey(), true, ); await game.updateGame( undefined /** points */, { atlas: game.mints[SageGameTest.ATLAS], polis: game.mints[SageGameTest.POLIS], ammo: game.mints[SageGameTest.AMMO], food: game.mints[SageGameTest.FOOD], fuel: game.mints[SageGameTest.FUEL], repairKit: game.mints[SageGameTest.REPAIR_KIT], } /** mints */, { atlas: atlastVault, polis: polisVault, } /** vaults */, { domain: game.craftingDomain, } /** crafting */, { statsDefinition: game.cargoStatsDefinition, } /** cargo */, riskZones || SageGameTest.getDefaultGameVars().riskZones /** risk zones */, ); await game.setupPoints(); const defaultGameStateVars = SageGameTest.getDefaultGameStateVars(); const fleetInput: UpdateFleetDataInput = { ...defaultGameStateVars.fleet, ...(fleet?.maxFleetSize != null && { maxFleetSize: fleet?.maxFleetSize }), ...(fleet?.starbaseLevels != null && { starbaseLevels: fleet?.starbaseLevels, }), ...(fleet?.starbaseUpkeep != null && { starbaseUpkeep: fleet?.starbaseUpkeep, }), }; const gameStateVars: { fleet?: UpdateFleetDataInput; miscVariables?: MiscVariablesInput; } = { fleet: fleetInput, miscVariables: miscVariables || defaultGameStateVars.miscVariables, }; await game.createOrUpdateGameState( gameStateVars.fleet, gameStateVars.miscVariables, ); if (configureCrew) { await game.configureCrew(); } const resourceMints = game.getResourceMints(); const craftingMints = game.getCraftingMints(); const newCargoTypeMints = [ resourceMints, game.mints[SageGameTest.SDU], craftingMints, ].flat(); await game.createCargoTypes( newCargoTypeMints.map((mint) => { return { mint, size: SageGameTest.defaultCargoSize, }; }), ); await game.createMineItems(resourceMints); if (setupCraftingAccounts) { await game.createCraftingAccounts(); await game.configStarbaseLevelsInfo(); } await game.createShips(numberOfShips, shipStats); const initialSector = sector || SageGameTest.defaultSector; await game.createSector({ coordinates: initialSector, faction: SageGameTest.defaultFaction, addResourcesToStarbaseCargoPod: true, setUpPlayer: numberOfShipsInFleet !== 'False', shipsInFleet: numberOfShipsInFleet === 'False' ? numberOfShips : numberOfShipsInFleet, addCargoToFleet, fleetToIdle, numCrewForPlayer, chunkCrewInstructions, }); await game.setupPlayerProgression(); return game; } async updateGame( pointCategories?: { lpCategory?: PublicKey; councilRankXpCategory?: PublicKey; pilotXpCategory?: PublicKey; dataRunningXpCategory?: PublicKey; miningXpCategory?: PublicKey; combatXpCategory?: PublicKey; craftingXpCategory?: PublicKey; }, mints?: { atlas?: PublicKey; polis?: PublicKey; ammo?: PublicKey; food?: PublicKey; fuel?: PublicKey; repairKit?: PublicKey; }, vaults?: { atlas?: PublicKey; polis?: PublicKey; }, crafting?: { domain?: PublicKey; }, cargo?: { statsDefinition?: PublicKey; }, riskZones?: RiskZonesData, ) { const gameAccount = await this.loadGame(); if (gameAccount.data.updateId.gt(new BN(0))) { throw 'game is already active'; } return this.sendManyToChain( Game.updateGame( this.gameId, this.program, this.profiles.superuser.key, this.profiles.superuser.profile, this.profiles.superuser.index, pointCategories /** points */, mints /** mints */, vaults /** vaults */, crafting /** crafting */, cargo /** cargo */, riskZones /** risk zones*/, ), ); } async createOrUpdateGameState( fleet?: UpdateFleetDataInput, miscVariables?: MiscVariablesInput, ) { const gameAccount = await this.loadGame(); const gameStateId = GameState.findAddress( this.program, this.gameId, gameAccount.data.updateId.add(new BN(1)), ); let firstIx: InstructionReturn; if (gameAccount.data.updateId.eq(new BN(0))) { firstIx = GameState.initGameState( this.program, this.profiles.superuser.key, this.profiles.superuser.profile, this.gameId, gameStateId[0], { keyIndex: this.profiles.superuser.index, }, ); } else { const oldGameStateId = GameState.findAddress( this.program, this.gameId, gameAccount.data.updateId, ); firstIx = GameState.copyGameState( this.program, this.profiles.superuser.key, this.profiles.superuser.profile, this.gameId, oldGameStateId[0], gameStateId[0], { keyIndex: this.profiles.superuser.index, }, ); } const instructions: InstructionReturn[] = [firstIx]; if (fleet || miscVariables) { instructions.push( ...[ GameState.updateGameState( this.program, this.profiles.superuser.key, this.profiles.superuser.profile, this.gameId, gameStateId[0], this.profiles.superuser.index, undefined /** fleet */, miscVariables /** miscVariables */, ), ], ); if (fleet?.maxFleetSize) { instructions.push( GameState.updateGameState( this.program, this.profiles.superuser.key, this.profiles.superuser.profile, this.gameId, gameStateId[0], this.profiles.superuser.index, { maxFleetSize: fleet.maxFleetSize, } /** fleet with just maxFleetSize */, undefined /** miscVariables */, ), ); } if (fleet?.starbaseLevels) { const groupedStarbaseLevels = groupBy(fleet.starbaseLevels, 'faction'); instructions.push( ...Object.values(groupedStarbaseLevels).map( (starbaseLevelsByFaction) => GameState.updateGameState( this.program, this.profiles.superuser.key, this.profiles.superuser.profile, this.gameId, gameStateId[0], this.profiles.superuser.index, { starbaseLevels: starbaseLevelsByFaction, } /** fleet with just starbaseLevels grouped by faction so that ix is not too large */, undefined /** miscVariables */, ), ), ); } if (fleet?.starbaseUpkeep) { instructions.push( GameState.updateGameState( this.program, this.profiles.superuser.key, this.profiles.superuser.profile, this.gameId, gameStateId[0], this.profiles.superuser.index, { starbaseUpkeep: fleet.starbaseUpkeep, } /** fleet with just starbaseUpkeep */, undefined /** miscVariables */, ), ); } instructions.push( ...[ GameState.activateGameState( this.program, this.profiles.superuser.key, this.profiles.superuser.profile, this.gameId, gameStateId[0], { keyIndex: this.profiles.superuser.index, }, ), ], ); await this.sendManyToChain(instructions, true); this.gameState = gameStateId[0]; } if (!this.sagePlayerProfile) { this.createSagePlayerProfile(); } } async createMints(names: string[], decimals = 9) { const unprocessed = !this.mints ? names : names .map((it) => it.toUpperCase()) .filter( (name) => this.mints && !Object.keys(this.mints).includes(name), ); const ixs: InstructionReturn[] = []; for (let index = 0; index < unprocessed.length; index++) { const element = unprocessed[index]; const mintSigner = keypairToAsyncSigner(Keypair.generate()); ixs.push( createMint( mintSigner, decimals, this.profiles.superuser.key.publicKey(), null, ), ); if (this.mints) { this.mints[element] = mintSigner.publicKey(); } else { this.mints = { [element]: mintSigner.publicKey() }; } } if (ixs.length > 0) { await this.sendManyToChain(ixs); } else { console.info('no mints needed to be created'); } } async getOrCreateAssociatedTokenAccount( mint: PublicKey, owner: PublicKey, allowOwnerOffCurve?: boolean, tokenProgramId = TOKEN_PROGRAM_ID, ) { const ata = createAssociatedTokenAccount( mint, owner, allowOwnerOffCurve, tokenProgramId, ); const info = await this.connection.getAccountInfo(ata.address, 'processed'); if (info == null) { await this.sendManyToChain(ata.instructions); } return ata.address; } async createCargoStatsDefinition() { const statsDefinitionSigner = keypairToAsyncSigner(Keypair.generate()); await this.sendManyToChain( CargoStatsDefinition.initDefinition({ program: this.cargoProgram, profile: this.profiles.superuser.profile, statsDefinition: statsDefinitionSigner, input: { cargoStats: 1, }, }), ); await this.updateGame(undefined, undefined, undefined, undefined, { statsDefinition: statsDefinitionSigner.publicKey(), }); this.cargoStatsDefinition = statsDefinitionSigner.publicKey(); } async createCraftingDomain() { const craftingDomain = keypairToAsyncSigner(Keypair.generate()); await this.sendManyToChain( CraftingDomain.initializeDomain( this.craftingProgram, craftingDomain, craftingDomain, this.profiles.superuser.profile, stringToByteArray('Domain', 32), ), ); await this.updateGame(undefined, undefined, undefined, { domain: craftingDomain.publicKey(), }); this.craftingDomain = craftingDomain.publicKey(); } async configureCrew() { const crewConfigResult = await setupCrewConfigInstructions( this.profiles.superuser.key, this.profiles.superuser.profile, this.profiles.superuser.index, this.gameId, this.program, this.crewProgram, await setupUmi(this.funder, this.connection, [ this.profiles.superuser.key, ]), this.connection, ); const { instructions, ...rest } = crewConfigResult; if (instructions.length > 0) { await this.sendManyToChain(instructions, true); } this.crew = { crewMerkleTree: rest.merkleTree, sageCrewConfig: rest.sageCrewConfig[0], crewProgramConfig: rest.crewProgramConfig, knownLeaves: [], }; return rest; } registerRecipeInstructions( recipe: AsyncSigner, category: PublicKey, input: Omit, feeRecipient?: PublicKey, ) { if (!this.craftingDomain) { throw 'this.craftingDomain not set'; } return Recipe.registerRecipe( this.craftingProgram, recipe, this.profiles.superuser.key, this.profiles.superuser.profile, this.craftingDomain, category, { ...input, keyIndex: this.profiles.superuser.index, }, feeRecipient, ); } setupRecipeInstructions( recipe: AsyncSigner, category: PublicKey, recipeConfig: Omit, consumableInputs: Array<[PublicKey, number]>, nonConsumableInputs: Array<[PublicKey, number]>, outputs: Array<[PublicKey, number]>, feeRecipient?: PublicKey, activateRecipe = true, ) { if (!this.craftingDomain) { throw 'this.craftingDomain not set'; } const instructions = [ this.registerRecipeInstructions( recipe, category, recipeConfig, feeRecipient, ), ]; const recipeIOIx = registerRecipeInputOutputsInstructions( recipe.publicKey(), consumableInputs, nonConsumableInputs, outputs, this.craftingDomain, this.profiles.superuser.key, this.profiles.superuser.profile, this.profiles.superuser.index, this.craftingProgram, ); instructions.push(...recipeIOIx); if (activateRecipe) { instructions.push( Recipe.updateRecipe( this.craftingProgram, recipe.publicKey(), this.profiles.superuser.key, this.profiles.superuser.profile, this.craftingDomain, { status: RecipeStatus.Active, keyIndex: this.profiles.superuser.index, }, ), ); } return instructions; } async setupRecipe( recipe: AsyncSigner, category: PublicKey, recipeConfig: RegisterRecipeInput, consumableInputs: Array<[PublicKey, number]>, nonConsumableInputs: Array<[PublicKey, number]>, outputs: Array<[PublicKey, number]>, recipeFeeRecipient?: PublicKey, activateRecipe = true, ) { await this.sendManyToChain( this.setupRecipeInstructions( recipe, category, recipeConfig, consumableInputs, nonConsumableInputs, outputs, recipeFeeRecipient, activateRecipe, ), ); } async setupResourceRecipes( items: Array<{ name: string; recipeValue: number; createCraftableItem?: boolean; consumableInputs?: Array<[PublicKey, number]>; nonConsumableInputs?: Array<[PublicKey, number]>; outputs?: Array<[PublicKey, number]>; }>, category: PublicKey | null = null, duration = 2, minDuration = 1, defaultOutputAmount = 1, ) { if (!this.mints || !this.recipes || !this.craftingDomain) { throw 'game incorrectly set up'; } const recipeKeys: PublicKey[] = []; const instructions: InstructionReturn[] = []; for (let index = 0; index < items.length; index++) { const element = items[index]; const elementRecipe = keypairToAsyncSigner(Keypair.generate()); const elementName = stringToByteArray(element.name, 32); const elementMint = this.mints[element.name]; recipeKeys.push(elementRecipe.publicKey()); if (!elementMint) { throw `mint ${element.name} not found`; } const createCraftableItem = element.createCraftableItem ?? true; if (createCraftableItem) { instructions.push( CraftableItem.registerCraftableItem( this.craftingProgram, this.profiles.superuser.key, this.profiles.superuser.profile, this.craftingDomain, elementMint, { namespace: elementName, keyIndex: this.profiles.superuser.index, }, ), ); } instructions.push( ...this.setupRecipeInstructions( elementRecipe, category ?? this.recipes[1].category, { duration: new BN(duration), minDuration: new BN(minDuration), namespace: elementName, value: new BN(element.recipeValue * GLOBAL_SCALE_DECIMALS_4), }, element.consumableInputs ?? [], element.nonConsumableInputs ?? [], element.outputs ?? [[elementMint, defaultOutputAmount]], ), ); } await this.sendManyToChain(instructions, true); return recipeKeys; } async createCraftingAccounts( numCategories = SageGameTest.defaultNumCategories, numRecipes = SageGameTest.defaultNumRecipes, craftableItemAmount = SageGameTest.defaultCraftableItemAmount, consumableAmount = SageGameTest.defaultConsumableAmount, nonConsumableAmount = SageGameTest.defaultNonConsumableAmount, outputAmount = SageGameTest.defaultOutputAmount, upgradeMultiplier = SageGameTest.defaultUpgradeMultiplier, ) { if (!this.craftingDomain) { throw 'this.craftingDomain not set'; } if (!this.mints) { throw 'this.mints not set'; } const thisCraftingDomain = this.craftingDomain; const allMints = this.mints; const outputs = SageGameTest.OUTPUTS.map((it) => allMints[it]); const craftableItemIxs = outputs.map((mint, index) => { const craftableItem = CraftableItem.findAddress( this.craftingProgram, thisCraftingDomain, mint, )[0]; const bankATA = createAssociatedTokenAccountIdempotent( mint, craftableItem, true, ); return [ CraftableItem.registerCraftableItem( this.craftingProgram, this.profiles.superuser.key, this.profiles.superuser.profile, thisCraftingDomain, mint, { namespace: stringToByteArray(`craftableItem${index}`, 32), keyIndex: this.profiles.superuser.index, }, ), bankATA.instructions, mintToTokenAccount( this.profiles.superuser.key, mint, bankATA.address, craftableItemAmount, ), ]; }); const sageRecipes: SageGameTestRecipes[] = []; const craftingCategories = [...Array(numCategories).keys()].map(() => keypairToAsyncSigner(Keypair.generate()), ); const craftingCategoriesIx = craftingCategories.map((it, index) => { return RecipeCategory.registerRecipeCategory( this.craftingProgram, it, this.profiles.superuser.key, this.profiles.superuser.profile, thisCraftingDomain, { namespace: stringToByteArray(`Category ${index}`, 32), keyIndex: this.profiles.superuser.index, }, ); }); const recipes = [...Array(numRecipes).keys()].map(() => keypairToAsyncSigner(Keypair.generate()), ); const recipesByCategory = chunk( recipes, Math.ceil(recipes.length / craftingCategories.length), ); const recipeIx: InstructionReturn[] = []; for (let index = 0; index < recipesByCategory.length; index++) { const category = craftingCategories[index]; const recipesForCategory = recipesByCategory[index]; sageRecipes.push({ category: category.publicKey(), recipes: recipesForCategory.map((it) => it.publicKey()), }); const registerRecipeIx = recipesForCategory.map((it, index2) => { return this.registerRecipeInstructions(it, category.publicKey(), { namespace: stringToByteArray(`Recipe ${index} ${index2}`, 32), duration: index === 1 ? new BN(0) : new BN(1), minDuration: new BN(0), value: new BN((index2 + 1) * GLOBAL_SCALE_DECIMALS_4), }); }); recipeIx.push(...registerRecipeIx); // first category is for upgrades if (index === 0) { for (let index3 = 0; index3 < recipesForCategory.length; index3++) { const recipe = recipesForCategory[index3]; if (index3 % 2 === 0) { // just non consumables, no consumables or outputs recipeIx.push( ...registerRecipeInputOutputsInstructions( recipe.publicKey(), [], [ [allMints[SageGameTest.INGREDIENT1], consumableAmount], [allMints[SageGameTest.INGREDIENT2], nonConsumableAmount], ], [], thisCraftingDomain, this.profiles.superuser.key, this.profiles.superuser.profile, this.profiles.superuser.index, this.craftingProgram, ), ); } else { // just consumables, no non-consumables or outputs recipeIx.push( ...registerRecipeInputOutputsInstructions( recipe.publicKey(), [ [ allMints[SageGameTest.INGREDIENT1], consumableAmount * upgradeMultiplier, ], [ allMints[SageGameTest.INGREDIENT2], nonConsumableAmount * upgradeMultiplier, ], ], [], [], thisCraftingDomain, this.profiles.superuser.key, this.profiles.superuser.profile, this.profiles.superuser.index, this.craftingProgram, ), ); } } } else { // all other categories are for regular crafting const recipeIOIx = recipesForCategory.map((it) => registerRecipeInputOutputsInstructions( it.publicKey(), [[allMints[SageGameTest.INGREDIENT1], consumableAmount]], [[allMints[SageGameTest.INGREDIENT2], nonConsumableAmount]], [[allMints[SageGameTest.OUTPUT1], outputAmount]], thisCraftingDomain, this.profiles.superuser.key, this.profiles.superuser.profile, this.profiles.superuser.index, this.craftingProgram, ), ); recipeIx.push(...recipeIOIx.flat()); } const activateRecipeIx = recipesForCategory.map((it) => { return Recipe.updateRecipe( this.craftingProgram, it.publicKey(), this.profiles.superuser.key, this.profiles.superuser.profile, thisCraftingDomain, { status: RecipeStatus.Active, keyIndex: this.profiles.superuser.index, }, ); }); recipeIx.push(...activateRecipeIx); } await this.sendManyToChain([ ...craftableItemIxs.flat(), ...craftingCategoriesIx, ...recipeIx, ]); this.recipes = sageRecipes; } setupPlayerProgressionInstructions(itemInput?: ProgressionItemInput[]) { if (!this.mints) { throw 'this.mints not set'; } const items = itemInput ?? getDefaultProgressionInputs(); const instructions: InstructionReturn[] = [ ProgressionConfig.registerProgressionConfig( this.program, this.profiles.superuser.key, this.profiles.superuser.profile, this.gameId, { keyIndex: this.profiles.superuser.index }, ).instructions, ProgressionConfig.updateProgressionConfig( this.program, this.profiles.superuser.key, this.profiles.superuser.profile, this.gameId, { keyIndex: this.profiles.superuser.index, items, }, ), ]; return instructions; } setupCombatConfigInstructions( input: Omit, ) { const instructions: InstructionReturn[] = [ CombatConfig.registerCombatConfig( this.program, this.profiles.superuser.key, this.profiles.superuser.profile, this.gameId, { ...input, keyIndex: this.profiles.superuser.index, }, ).instructions, ]; return instructions; } updateCombatConfigInstructions( input: Omit, ) { const instructions: InstructionReturn[] = [ CombatConfig.updateCombatConfig( this.program, this.profiles.superuser.key, this.profiles.superuser.profile, this.gameId, { ...input, keyIndex: this.profiles.superuser.index, }, ), ]; return instructions; } getRegisterSagePointModifierInstructions(categories: { miningXpCategory: PublicKey; pilotXpCategory: PublicKey; dataRunningXpCategory: PublicKey; craftingXpCategory: PublicKey; lpCategory: PublicKey; combatXpCategory: PublicKey; councilRankXpCategory: PublicKey; }) { const instructions = Object.entries(categories).map(([key, category]) => { const categoryType = key === 'miningXpCategory' ? PointsCategoryType.MXP : key === 'pilotXpCategory' ? PointsCategoryType.PXP : key === 'dataRunningXpCategory' ? PointsCategoryType.DRXP : key === 'craftingXpCategory' ? PointsCategoryType.CXP : key === 'lpCategory' ? PointsCategoryType.LP : key === 'combatXpCategory' ? PointsCategoryType.COXP : PointsCategoryType.CRXP; return Game.registerSagePointModifier( this.program, this.pointsProgram, this.profiles.superuser.key, this.profiles.superuser.profile, this.gameId, category, categoryType, 0, ).instructions; }); return instructions; } async setupPlayerProgression(itemInput?: ProgressionItemInput[]) { await this.sendManyToChain( this.setupPlayerProgressionInstructions(itemInput), ); } async setupCombatConfig(input: Omit) { await this.sendManyToChain(this.setupCombatConfigInstructions(input)); } async updateCombatConfig(input: Omit) { await this.sendManyToChain(this.updateCombatConfigInstructions(input)); } async setupPoints( pointLimit = 100_000_000, maxLevels = 30, pointsPerLevel = 1000, ) { const miningXpCategory = keypairToAsyncSigner(Keypair.generate()); const pilotXpCategory = keypairToAsyncSigner(Keypair.generate()); const dataRunningXpCategory = keypairToAsyncSigner(Keypair.generate()); const craftingXpCategory = keypairToAsyncSigner(Keypair.generate()); const combatXpCategory = keypairToAsyncSigner(Keypair.generate()); const lpCategory = keypairToAsyncSigner(Keypair.generate()); const councilRankXpCategory = keypairToAsyncSigner(Keypair.generate()); const pointCategories = [ miningXpCategory, pilotXpCategory, dataRunningXpCategory, craftingXpCategory, lpCategory, councilRankXpCategory, combatXpCategory, ]; const instructions: InstructionReturn[] = []; for (let index = 0; index < pointCategories.length; index++) { const category = pointCategories[index]; instructions.push( PointsCategory.registerPointCategory( this.pointsProgram, this.profiles.superuser.profile, category, { licenseType: { type: 'none' }, pointLimit: new BN(pointLimit), isSpendable: true, }, ), ); for (let index2 = 0; index2 < maxLevels; index2++) { const level = index + 1; instructions.push( PointsCategory.addPointCategoryLevelBareBones( this.pointsProgram, this.profiles.superuser.key, this.profiles.superuser.profile, category.publicKey(), { level, points: new BN(pointsPerLevel * level), licenseType: PointsLevelLicenseType.None, keyIndex: this.profiles.superuser.index, }, ), ); } instructions.push( UserPoints.createUserPointAccount( this.pointsProgram, this.profiles.player1.profile, category.publicKey(), ).instructions, ); } const pointsAccountAddresses = { miningXpCategory: miningXpCategory.publicKey(), pilotXpCategory: pilotXpCategory.publicKey(), dataRunningXpCategory: dataRunningXpCategory.publicKey(), craftingXpCategory: craftingXpCategory.publicKey(), combatXpCategory: combatXpCategory.publicKey(), lpCategory: lpCategory.publicKey(), councilRankXpCategory: councilRankXpCategory.publicKey(), }; await this.sendManyToChain(instructions); await this.updateGame(pointsAccountAddresses); const modifierIxs = this.getRegisterSagePointModifierInstructions( pointsAccountAddresses, ); const moreInstructions = [...modifierIxs]; await this.sendManyToChain(moreInstructions); this.pointCategories = pointsAccountAddresses; } async setupPlayerProgressionForPlayer(profileKey: PublicKey) { if (!this.pointCategories) { throw 'this.pointCategories not set'; } const instructions: InstructionReturn[] = []; for (const key in this.pointCategories) { const category = this.pointCategories[key as keyof PointsCategories]; instructions.push( UserPoints.createUserPointAccount( this.pointsProgram, profileKey, category, ).instructions, ); } await this.sendManyToChain(instructions); } async setupAtlasRedemption( redemptionConfigInput?: AsyncSigner, userRedemptionInput?: AsyncSigner, faction = Faction.MUD, numEpochs = 5, redeemableTokens = 1_000_000, allowOnlyCurrentEpoch = true, ) { if (!this.mints || !this.pointCategories) { throw 'this.pointCategories not set'; } const result = await setupTokenRedemptionInstructions( this.mints[SageGameTest.ATLAS], this.pointCategories.lpCategory, this.profiles.superuser.key, this.profiles.superuser.profile, this.profiles.superuser.index, this.pointsStoreProgram, this.connection, redemptionConfigInput, userRedemptionInput, faction, numEpochs, redeemableTokens, allowOnlyCurrentEpoch, ); await this.sendManyToChain(result.instructions, true); return { bankAccount: result.bankAccount, configSigner: result.configSigner, instructions: result.instructions, redemptionConfig: result.redemptionConfig, userRedemption: result.userRedemption, }; } async configStarbaseLevelsInfo() { if ( !this.recipes || this.recipes.length < SageGameTest.defaultNumCategories ) { throw 'this.recipes not set'; } const upgradeRecipes = this.recipes[0]; const allRecipes = this.recipes; const { starbaseLevels } = SageGameTest.getDefaultGameStateVars().fleet; const newStarbaseLevels = starbaseLevels.map((starbaseLevelInfo) => { const recipeInfo = allRecipes[starbaseLevelInfo.level + 1]; return { ...starbaseLevelInfo, ...(starbaseLevelInfo.level !== MIN_CSS_LEVEL && { newRecipeForUpgrade: upgradeRecipes.recipes[1], sectorRingAvailable: starbaseLevelInfo.level < 3 ? SectorRing.Inner : starbaseLevelInfo.level >= 3 && starbaseLevelInfo.level < 5 ? SectorRing.Mid : SectorRing.Outer, }), recipeCategoryForLevel: recipeInfo.category, }; }); await this.createOrUpdateGameState({ starbaseLevels: newStarbaseLevels }); } async createCargoTypes(input: SageGameTestCargoTypeInput[]) { const statsDefinitionData = await this.loadCargoStatsDefinition(); await this.sendManyToChain( input.map((it) => CargoType.initCargoType({ program: this.cargoProgram, managerKey: this.profiles.superuser.key, profile: this.profiles.superuser.profile, statsDefinition: statsDefinitionData.key, mint: it.mint, input: { keyIndex: this.profiles.superuser.index, values: [new BN(it.size)], }, statsDefinitionSeqId: statsDefinitionData.data.seqId, }), ), ); } createMineItemInstructions( mints: PublicKey[], resourceHardness = 100, availableAmount = 1_000_000, ) { if (!this.mints) { throw 'this.mints not set'; } const allMints = this.mints; const mineItemIxs = mints.map((mint, index) => { const name = Object.keys(allMints).find((key) => allMints[key].equals(mint)) || `MINT-${index}`; const mineItemResult = MineItem.registerMineItem( this.program, this.profiles.superuser.key, this.profiles.superuser.profile, mint, this.gameId, { keyIndex: this.profiles.superuser.index, name: stringToByteArray(name, 64), resourceHardness, }, ); const bankATA = createAssociatedTokenAccountIdempotent( mint, mineItemResult.mineItemKey[0], true, ); return [ mineItemResult.instructions, bankATA.instructions, mintToTokenAccount( this.profiles.superuser.key, mint, bankATA.address, availableAmount, ), ]; }); return mineItemIxs.flat(); } async createMineItems( mints: PublicKey[], resourceHardness = 100, availableAmount = 1_000_000, ) { const instructions = this.createMineItemInstructions( mints, resourceHardness, availableAmount, ); return await this.sendManyToChain(instructions); } async createSagePlayerProfile(profile?: PublicKey) { if (!this.gameState) { throw 'this.gameState not set'; } if (!profile && this.sagePlayerProfile != null) { throw 'this.sagePlayerProfile already set'; } await this.sendManyToChain( SagePlayerProfile.registerSagePlayerProfile( this.program, profile || this.profiles.player1.profile, this.gameId, this.gameState, ), ); if (this.sagePlayerProfile == null && !profile) { this.sagePlayerProfile = SagePlayerProfile.findAddress( this.program, profile || this.profiles.player1.profile, this.gameId, )[0]; } } async registerShip( mint: PublicKey, stats: ShipStats, name = '💀❤️🤖', activate = true, amount?: number, sizeClass: SizeClass = SizeClass.Small, ) { if (!this.mints) { throw 'mints not defined'; } if ( !Object.values(this.mints) .map((it) => it.toBase58()) .includes(mint.toBase58()) ) { throw 'mint not created'; } const shipKey = keypairToAsyncSigner(Keypair.generate()); const ixs = [ Ship.registerShip( this.program, this.profiles.superuser.key, this.profiles.superuser.profile, shipKey, mint, this.gameId, { name: stringToByteArray(name, 64) as FixedSizeArray, sizeClass: getSizeClassAnchorEnum(sizeClass), stats, isActive: activate, keyIndex: this.profiles.superuser.index, }, ), ]; await this.sendManyToChain(ixs); const mintAsString = mint.toBase58(); if (!this.ships) { this.ships = { [mintAsString]: [shipKey.publicKey()] }; } else if (!this.ships[mintAsString]) { this.ships[mintAsString] = [shipKey.publicKey()]; } else { this.ships[mintAsString].push(shipKey.publicKey()); } if (amount) { const ata = await this.getOrCreateAssociatedTokenAccount( mint, this.profiles.player1.key.publicKey(), true, ); this.sendManyToChain([ mintToTokenAccount(this.profiles.superuser.key, mint, ata, amount), ]); } } async createShips( num = 3, stats: ShipStats = SageGameTest.getDefaultShipStats(), ) { const names = [...Array(num).keys()].map((it) => `SHIP${it}`); await this.createMints(names, 0); if (!this.mints) { throw 'mints not defined'; } for (let index = 0; index < names.length; index++) { const name = names[index]; await this.registerShip( this.mints[name], stats, name, true, SageGameTest.defaultNumShips, ); } } async createSector({ coordinates, faction, addResourcesToStarbaseCargoPod = true, setUpPlayer = true, starbaseLevel, shipsInFleet, addCargoToFleet = true, fleetToIdle = true, createPlanet = true, createAsteroid = false, numCrewForPlayer = undefined, chunkCrewInstructions = undefined, }: { coordinates: [BN, BN]; faction?: Faction; addResourcesToStarbaseCargoPod?: boolean; setUpPlayer?: boolean; starbaseLevel?: number; shipsInFleet?: number; addCargoToFleet?: boolean; fleetToIdle?: boolean; createPlanet?: boolean; createAsteroid?: boolean; numCrewForPlayer?: number; chunkCrewInstructions?: number; }) { // Set default values using nullish coalescing for non-literals faction = faction ?? SageGameTest.defaultFaction; starbaseLevel = starbaseLevel ?? SageGameTest.defaultStarbaseLevel; shipsInFleet = shipsInFleet ?? SageGameTest.defaultShipsInFleet; if (!this.gameState) { throw 'this.gameState not set'; } if (!this.cargoStatsDefinition) { throw 'this.cargoStatsDefinition not set'; } if (!this.sagePlayerProfile) { throw 'this.sagePlayerProfile not set'; } if (!this.mints) { throw 'this.mints not set'; } const sectorName = coordinates.toString(); const sectorIx = Sector.registerSector( this.program, this.profiles.superuser.key, this.profiles.superuser.profile, this.profiles.superuser.profile /** discoverer */, this.gameId, coordinates, sectorName, this.profiles.superuser.index, ); const starbaseIx = Starbase.registerStarbase( this.program, this.profiles.superuser.key, this.profiles.superuser.profile, sectorIx.sectorKey[0], this.gameId, this.gameState, { name: stringToByteArray(`${sectorName} Starbase`, 64), subCoordinates: [new BN(0), new BN(0)], sectorCoordinates: coordinates, starbaseLevelIndex: starbaseLevel, faction, keyIndex: this.profiles.superuser.index, }, ); const starbaseKey = Starbase.findAddress( this.program, this.gameId, coordinates, )[0]; const starbaseSeqId = 0; const starbasePlayerIx = StarbasePlayer.registerStarbasePlayer( this.program, this.getProfileFactionAddress(this.profiles.player1.profile), this.sagePlayerProfile, starbaseKey, this.gameId, this.gameState, starbaseSeqId, ); const starbasePlayerKey = StarbasePlayer.findAddress( this.program, starbaseKey, this.sagePlayerProfile, starbaseSeqId, )[0]; const sbpPodSeeds = Keypair.generate().publicKey.toBuffer(); const cargoIx = StarbasePlayer.createCargoPod( this.program, this.cargoProgram, starbasePlayerKey, this.profiles.player1.key, this.profiles.player1.profile, this.getProfileFactionAddress(this.profiles.player1.profile), starbaseKey, this.cargoStatsDefinition, this.gameId, this.gameState, { keyIndex: this.profiles.player1.index, podSeeds: Array.from(sbpPodSeeds), }, ); const allSectorInstructions = [ sectorIx.instructions, starbaseIx, starbasePlayerIx, cargoIx, ]; const planets: PublicKey[] = []; if (createPlanet) { const planetResult = this.registerPlanetInstructions( sectorIx.sectorKey[0], PlanetType.Terrestrial, ); allSectorInstructions.push(planetResult.instructions); planets.push(planetResult.planetKey); } if (createAsteroid) { const asteroidBeltResult = this.registerPlanetInstructions( sectorIx.sectorKey[0], PlanetType.AsteroidBelt, ); const mineItem = this.getMineItemAddress( this.mints[SageGameTest.REPAIR_KIT], ); const resourceResult = this.registerResourceInstructions( asteroidBeltResult.planetKey, mineItem, ); allSectorInstructions.push(asteroidBeltResult.instructions); allSectorInstructions.push(resourceResult.instructions); planets.push(asteroidBeltResult.planetKey); } await this.sendManyToChain(allSectorInstructions); const sbpData = { starbasePlayer: starbasePlayerKey, cargoPod: CargoPod.findAddress(this.cargoProgram, sbpPodSeeds)[0], }; const sectorData = { sector: sectorIx.sectorKey[0], planets, starbases: [starbaseKey], starbasePlayers: [sbpData], }; if (!this.sectors) { this.sectors = [sectorData]; } else { this.sectors.push(sectorData); } if ( (addResourcesToStarbaseCargoPod || setUpPlayer) && starbaseLevel === MIN_CSS_LEVEL ) { const resourceMints = this.getResourceMints(); for (let index = 0; index < resourceMints.length; index++) { const mint = resourceMints[index]; await this.addCargoToGame( starbaseKey, starbasePlayerKey, mint, SageGameTest.starbaseCargoAmt, ); } } if (setUpPlayer && starbaseLevel === MIN_CSS_LEVEL) { await this.playerSetup({ starbase: starbaseKey, starbasePlayer: starbasePlayerKey, numShips: shipsInFleet, addCargoToFleet, fleetToIdle, numCrew: numCrewForPlayer, chunkCrewInstructions, }); } } addCargoToGameInstructions( starbase: PublicKey, starbasePlayer: PublicKey, mint: PublicKey, amount: number, starbasePlayerCargoPod?: PublicKey, ) { if (!this.gameState) { throw 'this.gameState not set'; } if (!this.sagePlayerProfile) { throw 'this.sagePlayerProfile not set'; } if (!this.cargoStatsDefinition) { throw 'this.cargoStatsDefinition not set'; } const thisSBPData = this.getStarbasePlayer(starbasePlayer); const tokenFrom = createAssociatedTokenAccountIdempotent( mint, this.profiles.player1.key.publicKey(), true, ); const destinationPod = starbasePlayerCargoPod || thisSBPData.cargoPod; const tokenTo = createAssociatedTokenAccountIdempotent( mint, destinationPod, true, ); const instructions: InstructionReturn[] = [ tokenFrom.instructions, tokenTo.instructions, ]; instructions.push( mintToTokenAccount( this.profiles.superuser.key, mint, tokenFrom.address, amount, ), ); instructions.push( StarbasePlayer.depositCargoToGame( this.program, this.cargoProgram, starbasePlayer, this.profiles.player1.key, this.profiles.player1.profile, this.getProfileFactionAddress(this.profiles.player1.profile), starbase, destinationPod, this.getCargoTypeAddress(mint), this.cargoStatsDefinition, tokenFrom.address, tokenTo.address, this.gameId, this.gameState, { amount: new BN(amount), keyIndex: this.profiles.player1.index, }, ), ); return instructions; } async addCargoToGame( starbase: PublicKey, starbasePlayer: PublicKey, mint: PublicKey, amount: number, starbasePlayerCargoPod?: PublicKey, ) { await this.sendManyToChain( this.addCargoToGameInstructions( starbase, starbasePlayer, mint, amount, starbasePlayerCargoPod, ), ); } async addShipToGame( ship: PublicKey, starbase: PublicKey, starbasePlayer: PublicKey, amount: number, ) { if (!this.gameState) { throw 'this.gameState not set'; } if (!this.sagePlayerProfile) { throw 'this.sagePlayerProfile not set'; } const thisGameState = this.gameState; const thisSagePlayerProfile = this.sagePlayerProfile; const shipMint = this.getShipMint(ship); const escrowAta = createAssociatedTokenAccountIdempotent( shipMint, this.sagePlayerProfile, true, ); const shipWalletAta = createAssociatedTokenAccountIdempotent( shipMint, this.profiles.player1.key.publicKey(), true, ); const sbpData = await this.loadStarbasePlayer(starbasePlayer); const shipEscrowIndex = sbpData.wrappedShipEscrows.findIndex((it) => it.ship.equals(ship), ); const chunks = chunkShipAmounts(amount); const addShipIx = chunks.map((it, index) => SagePlayerProfile.addShipEscrow( this.program, this.profiles.player1.profile, this.getProfileFactionAddress(this.profiles.player1.profile), thisSagePlayerProfile, // escrow authority this.profiles.player1.key, // origin token account owner shipWalletAta.address, ship, escrowAta.address, starbasePlayer, starbase, this.gameId, thisGameState, { shipAmount: new BN(it), index: shipEscrowIndex < 0 ? (index === 0 ? null : 0) : shipEscrowIndex, }, ), ); await this.sendManyToChain( [ shipWalletAta.instructions, escrowAta.instructions, mintToTokenAccount( this.profiles.superuser.key, shipMint, shipWalletAta.address, amount, ), ...addShipIx, ], true, ); } async removeShipFromGame( ship: PublicKey, starbase: PublicKey, starbasePlayer: PublicKey, amount: number, ) { if (!this.gameState) { throw 'this.gameState not set'; } if (!this.sagePlayerProfile) { throw 'this.sagePlayerProfile not set'; } const thisGameState = this.gameState; const thisSagePlayerProfile = this.sagePlayerProfile; const shipMint = this.getShipMint(ship); const escrowAta = createAssociatedTokenAccountIdempotent( shipMint, this.sagePlayerProfile, true, ); const shipWalletAta = createAssociatedTokenAccountIdempotent( shipMint, this.profiles.player1.key.publicKey(), true, ); const sbpData = await this.loadStarbasePlayer(starbasePlayer); const shipEscrowIndex = sbpData.wrappedShipEscrows.findIndex((it) => it.ship.equals(ship), ); const chunks = chunkShipAmounts(amount); const addShipIx = chunks.map((it, index) => SagePlayerProfile.removeShipEscrow( this.program, this.profiles.player1.key, this.profiles.player1.profile, this.getProfileFactionAddress(this.profiles.player1.profile), thisSagePlayerProfile, // escrow authority shipWalletAta.address, ship, escrowAta.address, starbasePlayer, starbase, this.gameId, thisGameState, { shipAmount: new BN(it), permissionKeyIndex: this.profiles.player1.index, shipEscrowIndex, }, ), ); await this.sendManyToChain( [ shipWalletAta.instructions, escrowAta.instructions, mintToTokenAccount( this.profiles.superuser.key, shipMint, shipWalletAta.address, amount, ), ...addShipIx, ], true, ); } async createFleet({ ship, shipAmount, shipEscrowIndex, starbase, starbasePlayer, fleetName = 'fleet', fuelAmtToDeposit, foodAmtToDeposit, ammoAmtToDeposit, repairKitAmtToDeposit, toIdle = false, }: { ship: PublicKey; shipAmount: number; shipEscrowIndex: number; starbase: PublicKey; starbasePlayer: PublicKey; fleetName?: string; fuelAmtToDeposit?: number; foodAmtToDeposit?: number; ammoAmtToDeposit?: number; repairKitAmtToDeposit?: number; toIdle?: boolean; }) { if (!this.gameState) { throw 'this.gameState not set'; } if (!this.sagePlayerProfile) { throw 'this.sagePlayerProfile not set'; } if (!this.cargoStatsDefinition) { throw 'this.cargoStatsDefinition not set'; } if (!this.mints) { throw 'this.mints not set'; } const shipAmountChunks = chunkShipAmounts(shipAmount); const fleetLabel = stringToByteArray(fleetName, 32); const createFleetIxResult = Fleet.createFleet( this.program, this.cargoProgram, this.profiles.player1.key, this.profiles.player1.profile, this.getProfileFactionAddress(this.profiles.player1.profile), ship, starbasePlayer, starbase, this.gameId, this.gameState, this.cargoStatsDefinition, { shipAmount: shipAmountChunks[0], shipEscrowIndex: shipEscrowIndex, fleetLabel, keyIndex: this.profiles.player1.index, }, ); const addShipsToFleetIx = shipAmountChunks.slice(1).map((it) => Fleet.addShipToFleet( this.program, this.profiles.player1.key, this.profiles.player1.profile, this.getProfileFactionAddress(this.profiles.player1.profile), createFleetIxResult.fleetKey[0], ship, starbasePlayer, starbase, this.gameId, this.gameState as PublicKey, { shipAmount: it, shipEscrowIndex: shipEscrowIndex, fleetShipInfoIndex: 0, keyIndex: this.profiles.player1.index, }, ), ); const instructions = [ createFleetIxResult.instructions, ...addShipsToFleetIx, ]; if (this.fleets == null) { this.fleets = [createFleetIxResult.fleetKey[0]]; } else { this.fleets.push(createFleetIxResult.fleetKey[0]); } if (fuelAmtToDeposit) { const fuelMint = this.mints[SageGameTest.FUEL]; instructions.push( ...this.depositCargoToFleetInstructions( createFleetIxResult.fleetKey[0], createFleetIxResult.fuelTankKey[0], starbase, starbasePlayer, fuelMint, fuelAmtToDeposit, ), ); } if (foodAmtToDeposit) { const foodMint = this.mints[SageGameTest.FOOD]; instructions.push( ...this.depositCargoToFleetInstructions( createFleetIxResult.fleetKey[0], createFleetIxResult.cargoHoldKey[0], starbase, starbasePlayer, foodMint, foodAmtToDeposit, ), ); } if (ammoAmtToDeposit) { const ammoMint = this.mints[SageGameTest.AMMO]; instructions.push( ...this.depositCargoToFleetInstructions( createFleetIxResult.fleetKey[0], createFleetIxResult.ammoBankKey[0], starbase, starbasePlayer, ammoMint, ammoAmtToDeposit, ), ); } if (repairKitAmtToDeposit) { const repairKitMint = this.mints[SageGameTest.REPAIR_KIT]; instructions.push( ...this.depositCargoToFleetInstructions( createFleetIxResult.fleetKey[0], createFleetIxResult.cargoHoldKey[0], starbase, starbasePlayer, repairKitMint, repairKitAmtToDeposit, ), ); } if (toIdle) { instructions.push( Fleet.loadingBayToIdle( this.program, this.profiles.player1.key, this.profiles.player1.profile, this.getProfileFactionAddress(this.profiles.player1.profile), createFleetIxResult.fleetKey[0], starbase, starbasePlayer, this.gameId, this.gameState, this.profiles.player1.index, ), ); } await this.sendManyToChain(instructions); return { fleetKey: createFleetIxResult.fleetKey[0], cargoHoldKey: createFleetIxResult.cargoHoldKey[0], fuelTankKey: createFleetIxResult.fuelTankKey[0], ammoBankKey: createFleetIxResult.ammoBankKey[0], }; } async quickCreateFleet( starbase: PublicKey, starbasePlayer: PublicKey, numShips = SageGameTest.default_ships_in_fleet, fuelAmtToDeposit: number | undefined = undefined, foodAmtToDeposit: number | undefined = undefined, ammoAmtToDeposit: number | undefined = undefined, repairKitAmtToDeposit: number | undefined = undefined, toIdle = true, addShipsToGame = true, ) { if (!this.gameState) { throw 'this.gameState not set'; } if (!this.sagePlayerProfile) { throw 'this.sagePlayerProfile not set'; } if (!this.cargoStatsDefinition) { throw 'this.cargoStatsDefinition not set'; } if (!this.mints) { throw 'this.mints not set'; } const allShips = this.getShips(); const shipToUse = allShips[0]; if (addShipsToGame) { await this.addShipToGame(shipToUse, starbase, starbasePlayer, numShips); } const sbpData = await this.loadStarbasePlayer(starbasePlayer); const shipEscrowIndex = sbpData.wrappedShipEscrows.findIndex( (it) => it.ship.equals(shipToUse) && it.amount.gte(new BN(numShips)), ); if (shipEscrowIndex < 0) { throw 'ship not added to game'; } return await this.createFleet({ ship: shipToUse, shipAmount: numShips, shipEscrowIndex: shipEscrowIndex, starbase: starbase, starbasePlayer: starbasePlayer, fleetName: `fleet-${new Date().valueOf()}`, fuelAmtToDeposit: fuelAmtToDeposit, foodAmtToDeposit: foodAmtToDeposit, ammoAmtToDeposit: ammoAmtToDeposit, repairKitAmtToDeposit: repairKitAmtToDeposit, toIdle: toIdle, }); } async createManyFleetsInstructions({ starbase, starbasePlayer, numFleets, numShips, fleetsIndicesToStayDocked, fleetsIndicesToCargo, }: { starbase: PublicKey; starbasePlayer: PublicKey; numFleets: number; numShips?: number; fleetsIndicesToStayDocked?: number[]; fleetsIndicesToCargo?: { [key: number]: { [key: string]: number; }; }; }) { if (!this.gameState) { throw 'this.gameState not set'; } if (!this.sagePlayerProfile) { throw 'this.sagePlayerProfile not set'; } if (!this.cargoStatsDefinition) { throw 'this.cargoStatsDefinition not set'; } if (!this.mints) { throw 'this.mints not set'; } numShips = numShips ?? SageGameTest.default_ships_in_fleet; fleetsIndicesToStayDocked = fleetsIndicesToStayDocked ?? []; const instructions: InstructionReturn[] = []; const sbpData = await this.loadStarbasePlayer(starbasePlayer); const shipEscrowIndex = 0; const shipInEscrow = sbpData.wrappedShipEscrows[shipEscrowIndex]; const ship = shipInEscrow ? shipInEscrow.ship : this.getShips()[0]; const shipMint = this.getShipMint(ship); const shipEscrowAta = createAssociatedTokenAccountIdempotent( shipMint, this.sagePlayerProfile, true, ); const shipWalletAta = createAssociatedTokenAccountIdempotent( shipMint, this.profiles.player1.key.publicKey(), true, ); const totalShips = numFleets * numShips; const addShipIx = SagePlayerProfile.addShipEscrow( this.program, this.profiles.player1.profile, this.getProfileFactionAddress(this.profiles.player1.profile), this.sagePlayerProfile, // escrow authority this.profiles.player1.key, // origin token account owner shipWalletAta.address, ship, shipEscrowAta.address, starbasePlayer, starbase, this.gameId, this.gameState, { shipAmount: new BN(totalShips), index: shipInEscrow ? shipEscrowIndex : null, }, ); instructions.push( ...[ shipWalletAta.instructions, shipEscrowAta.instructions, mintToTokenAccount( this.profiles.superuser.key, shipMint, shipWalletAta.address, totalShips, ), addShipIx, ], ); const fleetResults: Array<{ fleetKey: PublicKey; cargoHoldKey: PublicKey; fuelTankKey: PublicKey; ammoBankKey: PublicKey; }> = []; for (let index = 0; index < numFleets; index++) { const createFleetIxResult = Fleet.createFleet( this.program, this.cargoProgram, this.profiles.player1.key, this.profiles.player1.profile, this.getProfileFactionAddress(this.profiles.player1.profile), ship, starbasePlayer, starbase, this.gameId, this.gameState, this.cargoStatsDefinition, { shipAmount: numShips, shipEscrowIndex: shipEscrowIndex, fleetLabel: stringToByteArray( `Fleet-${new Date().valueOf()} ${index}`, 32, ), keyIndex: this.profiles.player1.index, }, ); instructions.push(createFleetIxResult.instructions); fleetResults.push({ fleetKey: createFleetIxResult.fleetKey[0], cargoHoldKey: createFleetIxResult.cargoHoldKey[0], fuelTankKey: createFleetIxResult.fuelTankKey[0], ammoBankKey: createFleetIxResult.ammoBankKey[0], }); } for (let index = 0; index < fleetResults.length; index++) { const fleetResult = fleetResults[index]; if (fleetsIndicesToCargo != null) { const fleetCargoData = fleetsIndicesToCargo[index]; if (fleetCargoData) { for (const key in fleetCargoData) { const cargoMint = this.mints[key]; const cargoAmount = fleetCargoData[key]; if (cargoMint && cargoAmount) { const fleetCargoPodKey = key === SageGameTest.AMMO ? fleetResult.ammoBankKey : key === SageGameTest.FUEL ? fleetResult.fuelTankKey : fleetResult.cargoHoldKey; const depositCargoIx = this.depositCargoToFleetInstructions( fleetResult.fleetKey, fleetCargoPodKey, starbase, starbasePlayer, cargoMint, cargoAmount, ); instructions.push(...depositCargoIx); } } } } if (fleetsIndicesToStayDocked.length > 0) { if (!fleetsIndicesToStayDocked.includes(index)) { instructions.push( Fleet.loadingBayToIdle( this.program, this.profiles.player1.key, this.profiles.player1.profile, this.getProfileFactionAddress(this.profiles.player1.profile), fleetResult.fleetKey, starbase, starbasePlayer, this.gameId, this.gameState, this.profiles.player1.index, ), ); } } } return { fleetResults, instructions, }; } async createManyFleets({ starbase, starbasePlayer, numFleets, numShips, fleetsIndicesToStayDocked, fleetsIndicesToCargo, }: { starbase: PublicKey; starbasePlayer: PublicKey; numFleets: number; numShips?: number; fleetsIndicesToStayDocked?: number[]; fleetsIndicesToCargo?: { [key: number]: { [key: string]: number; }; }; }) { const { fleetResults, instructions } = await this.createManyFleetsInstructions({ starbase, starbasePlayer, numFleets, numShips, fleetsIndicesToStayDocked, fleetsIndicesToCargo, }); await this.sendManyToChain(instructions, true, undefined, 'confirmed'); return fleetResults; } async disbandFleetInstructions( fleet: PublicKey, starbase: PublicKey, starbasePlayer: PublicKey, ) { if (!this.gameState) { throw 'this.gameState not set'; } if (!this.cargoStatsDefinition) { throw 'this.cargoStatsDefinition not set'; } const thisGameState = this.gameState; const fleetAccount = await this.loadFleet(fleet); if (!fleetAccount.state.StarbaseLoadingBay) { throw 'Fleet is not docked'; } if (!fleetAccount.state.StarbaseLoadingBay.starbase.equals(starbase)) { throw 'Fleet is not docked at the provided starbase'; } const fleetShipsAddress = FleetShips.findAddress(this.program, fleet)[0]; const fleetShipsAccount = await this.loadFleetShips(fleetShipsAddress); const sbpData = await this.loadStarbasePlayer(starbasePlayer); const disbandFleetResult = Fleet.disbandFleet( this.program, this.cargoProgram, this.profiles.player1.key, this.profiles.player1.profile, this.getProfileFactionAddress(this.profiles.player1.profile), fleetAccount, starbasePlayer, starbase, this.gameId, thisGameState, { keyIndex: this.profiles.player1.index }, ); const instructions: InstructionReturn[] = [disbandFleetResult.instructions]; for ( let index = fleetShipsAccount.fleetShips.length - 1; index >= 0; index-- ) { const fleetShipsInfo = fleetShipsAccount.fleetShips[index]; const shipAmountChunks = chunkShipAmounts( fleetShipsInfo.amount.toNumber(), 65_000, ); const shipEscrowIndex = sbpData.wrappedShipEscrows.findIndex((it) => it.ship.equals(fleetShipsInfo.ship), ); const removeShipIx = shipAmountChunks.map((it, index2) => DisbandedFleet.disbandedFleetToEscrow( this.program, this.profiles.player1.key, this.profiles.player1.profile, this.getProfileFactionAddress(this.profiles.player1.profile), disbandFleetResult.disbandedFleetKey[0], fleetShipsAddress, fleetShipsInfo.ship, starbasePlayer, starbase, this.gameId, thisGameState, { fleetShipInfoIndex: index, shipAmount: it, shipEscrowIndex: shipEscrowIndex < 0 ? (index2 === 0 ? null : 0) : shipEscrowIndex, keyIndex: this.profiles.player1.index, }, ), ); instructions.push(...removeShipIx); } instructions.push( DisbandedFleet.closeDisbandedFleet( this.program, this.profiles.player1.key, this.profiles.player1.profile, 'funder', disbandFleetResult.disbandedFleetKey[0], fleetShipsAddress, { keyIndex: this.profiles.player1.index, }, ), ); return instructions; } async disbandFleet( fleet: PublicKey, starbase: PublicKey, starbasePlayer: PublicKey, ) { if (!this.sectors) { throw 'this.sectors not set'; } if (!this.gameState) { throw 'this.gameState not set'; } if (!this.cargoStatsDefinition) { throw 'this.cargoStatsDefinition not set'; } let txIds = await this.sendManyToChain( await this.disbandFleetInstructions(fleet, starbase, starbasePlayer), ); // clean up pods txIds = txIds.concat( await this.sendManyToChain( await cleanCargoPodsByStarbasePlayer( this.connection, this.program, this.cargoProgram, starbasePlayer, starbase, this.profiles.player1.profile, this.getProfileFactionAddress(this.profiles.player1.profile), this.cargoStatsDefinition, this.gameId, this.gameState, this.profiles.player1.key, this.profiles.player1.index, ), ), ); const cargoPods = await getCargoPodsByAuthority( this.connection, this.cargoProgram, starbasePlayer, ); if (cargoPods.length < 1) { throw 'No starbase cargo pods'; } let found = false; for (let index = 0; index < this.sectors.length; index++) { if (found) { break; } const thisSectors = this.sectors[index]; for ( let index2 = 0; index2 < thisSectors.starbasePlayers.length; index2++ ) { const thisStarbasePlayer = thisSectors.starbasePlayers[index2]; if (thisStarbasePlayer.starbasePlayer.equals(starbasePlayer)) { thisStarbasePlayer.cargoPod = cargoPods[0].key; found = true; break; } } } return txIds; } depositCargoToFleetInstructions( fleet: PublicKey, fleetCargoPod: PublicKey, starbase: PublicKey, starbasePlayer: PublicKey, mint: PublicKey, amount: number, ) { if (!this.gameState) { throw 'this.gameState not set'; } if (!this.sagePlayerProfile) { throw 'this.sagePlayerProfile not set'; } if (!this.cargoStatsDefinition) { throw 'this.cargoStatsDefinition not set'; } const thisSBPData = this.getStarbasePlayer(starbasePlayer); const tokenTo = createAssociatedTokenAccountIdempotent( mint, fleetCargoPod, true, ); const tokenFrom = createAssociatedTokenAccountIdempotent( mint, thisSBPData.cargoPod, true, ); return [ tokenTo.instructions, tokenFrom.instructions, Fleet.depositCargoToFleet( this.program, this.cargoProgram, this.profiles.player1.key, this.profiles.player1.profile, this.getProfileFactionAddress(this.profiles.player1.profile), this.funder.publicKey(), starbase, starbasePlayer, fleet, thisSBPData.cargoPod, fleetCargoPod, this.getCargoTypeAddress(mint), this.cargoStatsDefinition, tokenFrom.address, tokenTo.address, mint, this.gameId, this.gameState, { amount: new BN(amount), keyIndex: this.profiles.player1.index, }, ), ]; } async depositCargoToFleet( fleet: PublicKey, fleetCargoPod: PublicKey, starbase: PublicKey, starbasePlayer: PublicKey, mint: PublicKey, amount: number, ) { const instructions = this.depositCargoToFleetInstructions( fleet, fleetCargoPod, starbase, starbasePlayer, mint, amount, ); return await this.sendManyToChain(instructions); } withdrawCargoFromFleetInstructions( fleet: PublicKey, fleetCargoPod: PublicKey, starbase: PublicKey, starbasePlayer: PublicKey, mint: PublicKey, amount: number, ) { if (!this.gameState) { throw 'this.gameState not set'; } if (!this.sagePlayerProfile) { throw 'this.sagePlayerProfile not set'; } if (!this.cargoStatsDefinition) { throw 'this.cargoStatsDefinition not set'; } const thisSBPData = this.getStarbasePlayer(starbasePlayer); const tokenFrom = createAssociatedTokenAccountIdempotent( mint, fleetCargoPod, true, ); const tokenTo = createAssociatedTokenAccountIdempotent( mint, thisSBPData.cargoPod, true, ); return [ tokenTo.instructions, tokenFrom.instructions, Fleet.withdrawCargoFromFleet( this.program, this.cargoProgram, this.profiles.player1.key, 'funder', this.profiles.player1.profile, this.getProfileFactionAddress(this.profiles.player1.profile), starbase, starbasePlayer, fleet, fleetCargoPod, thisSBPData.cargoPod, this.getCargoTypeAddress(mint), this.cargoStatsDefinition, tokenFrom.address, tokenTo.address, mint, this.gameId, this.gameState, { amount: new BN(amount), keyIndex: this.profiles.player1.index, }, ), ]; } warpToCoordinateInstructions( fleet: PublicKey, fleetFuelTank: PublicKey, toSector: [BN, BN], ) { if (!this.gameState) { throw 'this.gameState not set'; } if (!this.mints) { throw 'this.mints not set'; } if (!this.cargoStatsDefinition) { throw 'this.cargoStatsDefinition not set'; } const fleetFuelToken = createAssociatedTokenAccountIdempotent( this.mints[SageGameTest.FUEL], fleetFuelTank, true, ); return [ fleetFuelToken.instructions, Fleet.warpToCoordinate( this.program, this.profiles.player1.key, this.profiles.player1.profile, this.getProfileFactionAddress(this.profiles.player1.profile), fleet, fleetFuelTank, this.getCargoTypeAddress(this.mints[SageGameTest.FUEL]), this.cargoStatsDefinition, fleetFuelToken.address, this.mints[SageGameTest.FUEL], this.gameState, this.gameId, this.cargoProgram, { keyIndex: this.profiles.player1.index, toSector, }, ), ]; } async warpToCoordinate( fleet: PublicKey, fleetFuelTank: PublicKey, toSector: [BN, BN], ) { await this.sendManyToChain( this.warpToCoordinateInstructions(fleet, fleetFuelTank, toSector), ); } moveWarpHandlerInstructions(fleet: PublicKey) { if (!this.pointCategories) { throw 'this.pointCategories not set'; } return [ Fleet.moveWarpHandler( this.program, this.pointsProgram, this.profiles.player1.profile, fleet, this.getUserPointsAccountAddress(this.pointCategories.pilotXpCategory), this.pointCategories.pilotXpCategory, this.getPointsModifierAddress(this.pointCategories.pilotXpCategory), this.getUserPointsAccountAddress( this.pointCategories.councilRankXpCategory, ), this.pointCategories.councilRankXpCategory, this.getPointsModifierAddress( this.pointCategories.councilRankXpCategory, ), this.gameId, ), ]; } async moveWarpHandler(fleet: PublicKey) { await this.sendManyToChain(this.moveWarpHandlerInstructions(fleet)); } startMiningAsteroidInstructions( fleet: PublicKey, starbase: PublicKey, starbasePlayer: PublicKey, planet: PublicKey, mint: PublicKey, fleetFuelTank: PublicKey, ) { if (!this.gameState) { throw 'this.gameState not set'; } if (!this.mints) { throw 'this.mints not set'; } const mineItemKey = this.getMineItemAddress(mint); const resourceKey = this.getResourceAddress(mineItemKey, planet); const fleetFuelToken = createAssociatedTokenAccountIdempotent( this.mints[SageGameTest.FUEL], fleetFuelTank, true, ); return [ fleetFuelToken.instructions, Fleet.startMiningAsteroid( this.program, this.profiles.player1.key, this.profiles.player1.profile, this.getProfileFactionAddress(this.profiles.player1.profile), fleet, starbase, starbasePlayer, mineItemKey, resourceKey, planet, this.gameState, this.gameId, fleetFuelToken.address, { keyIndex: this.profiles.player1.index, }, ), ]; } asteroidMiningHandlerInstructions( fleet: PublicKey, starbase: PublicKey, planet: PublicKey, mint: PublicKey, fleetCargoHold: PublicKey, fleetAmmoBank: PublicKey, ) { if (!this.gameState) { throw 'this.gameState not set'; } if (!this.mints) { throw 'this.mints not set'; } if (!this.cargoStatsDefinition) { throw 'this.cargoStatsDefinition not set'; } const mineItemKey = this.getMineItemAddress(mint); const resourceKey = this.getResourceAddress(mineItemKey, planet); const fleetFoodToken = createAssociatedTokenAccountIdempotent( this.mints[SageGameTest.FOOD], fleetCargoHold, true, ); const fleetAmmoToken = createAssociatedTokenAccountIdempotent( this.mints[SageGameTest.AMMO], fleetAmmoBank, true, ); const resourceTokenFrom = createAssociatedTokenAccountIdempotent( mint, mineItemKey, true, ); const resourceTokenTo = createAssociatedTokenAccountIdempotent( mint, fleetCargoHold, true, ); return [ fleetFoodToken.instructions, fleetAmmoToken.instructions, resourceTokenFrom.instructions, resourceTokenTo.instructions, Fleet.asteroidMiningHandler( this.program, this.cargoProgram, fleet, starbase, mineItemKey, resourceKey, planet, fleetCargoHold, fleetAmmoBank, this.getCargoTypeAddress(this.mints[SageGameTest.FOOD]), this.getCargoTypeAddress(this.mints[SageGameTest.AMMO]), this.getCargoTypeAddress(mint), this.cargoStatsDefinition, this.gameState, this.gameId, fleetFoodToken.address, fleetAmmoToken.address, resourceTokenFrom.address, resourceTokenTo.address, this.mints[SageGameTest.FOOD], this.mints[SageGameTest.AMMO], ), ]; } stopMiningAsteroidInstructions( fleet: PublicKey, planet: PublicKey, mint: PublicKey, fleetFuelTank: PublicKey, ) { if (!this.gameState) { throw 'this.gameState not set'; } if (!this.mints) { throw 'this.mints not set'; } if (!this.cargoStatsDefinition) { throw 'this.cargoStatsDefinition not set'; } if (!this.pointCategories) { throw 'this.pointCategories not set'; } const mineItemKey = this.getMineItemAddress(mint); const resourceKey = this.getResourceAddress(mineItemKey, planet); const fleetFuelToken = createAssociatedTokenAccountIdempotent( this.mints[SageGameTest.FUEL], fleetFuelTank, true, ); return [ fleetFuelToken.instructions, Fleet.stopMiningAsteroid( this.program, this.cargoProgram, this.pointsProgram, this.profiles.player1.key, this.profiles.player1.profile, this.getProfileFactionAddress(this.profiles.player1.profile), fleet, mineItemKey, resourceKey, planet, fleetFuelTank, this.getCargoTypeAddress(this.mints[SageGameTest.FUEL]), this.cargoStatsDefinition, this.getUserPointsAccountAddress(this.pointCategories.miningXpCategory), this.pointCategories.miningXpCategory, this.getPointsModifierAddress(this.pointCategories.miningXpCategory), this.getUserPointsAccountAddress(this.pointCategories.pilotXpCategory), this.pointCategories.pilotXpCategory, this.getPointsModifierAddress(this.pointCategories.pilotXpCategory), this.getUserPointsAccountAddress( this.pointCategories.councilRankXpCategory, ), this.pointCategories.councilRankXpCategory, this.getPointsModifierAddress( this.pointCategories.councilRankXpCategory, ), this.gameState, this.gameId, fleetFuelToken.address, this.mints[SageGameTest.FUEL], { keyIndex: this.profiles.player1.index }, ), ]; } async withdrawCargoFromFleet( fleet: PublicKey, fleetCargoPod: PublicKey, starbase: PublicKey, starbasePlayer: PublicKey, mint: PublicKey, amount: number, ) { const instructions = this.withdrawCargoFromFleetInstructions( fleet, fleetCargoPod, starbase, starbasePlayer, mint, amount, ); return await this.sendManyToChain(instructions); } registerPlanetInstructions( sector: PublicKey, planetType = PlanetType.Terrestrial, size = 1337, position = SectorRing.Inner, subCoordinates?: FixedSizeArray, ) { const input = { name: `${planetType} ${size}`, size: new BN(size), maxHp: new BN(size), subCoordinates: subCoordinates ?? ([new BN(2), new BN(2)] as FixedSizeArray), planetType, position, keyIndex: this.profiles.superuser.index, }; const planet = keypairToAsyncSigner(Keypair.generate()); return { instructions: Planet.registerPlanet( this.program, this.profiles.superuser.key, this.profiles.superuser.profile, planet, sector, this.gameId, input, ), planetKey: planet.publicKey(), }; } registerResourceInstructions( planet: PublicKey, mineItem: PublicKey, systemRichness = 100, ) { return Resource.registerResource( this.program, this.profiles.superuser.key, this.profiles.superuser.profile, planet, mineItem, this.gameId, { systemRichness, keyIndex: this.profiles.superuser.index, locationType: LocationType.Planet, }, ); } async syncStarbasePlayer(starbasePlayer: PublicKey) { if (!this.gameState) { throw 'this.gameState not set'; } const thisGameState = this.gameState; const sbp = await this.loadStarbasePlayer(starbasePlayer); if (sbp.wrappedShipEscrows.length == 0) { await this.sendManyToChain( StarbasePlayer.syncStarbasePlayer( this.program, starbasePlayer, sbp.data.starbase, this.gameId, thisGameState, ), true, ); } else { await this.sendManyToChain( ( await Promise.all( sbp.wrappedShipEscrows.map(async (it, index) => { const thisShip = await this.loadShip(it.ship); return SagePlayerProfile.updateShipEscrow( this.program, it.ship, thisShip.data.next.key, starbasePlayer, sbp.data.starbase, this.gameId, thisGameState, { shipEscrowIndex: index, }, ); }), ) ).flat(), true, ); } } async mintAndAddCrewToGame( crewMerkleTree: PublicKey, crewOwner: PublicKey, starbase: PublicKey, starbasePlayer: PublicKey, numCrew = 5, startingLeafIndex?: number, knownLeaves: UmiPublicKey[] = [], selectedProfile?: SageGameTestProfile, ) { const MAX_CREW_PER_IX = 7; if (!this.crew) { throw 'crew not setup correctly'; } const theProfile = selectedProfile || this.profiles.player1; const thisSagePlayerProfile = SagePlayerProfile.findAddress( this.program, theProfile.profile, this.gameId, )[0]; const umi = await setupUmi(this.funder, this.connection, [ this.profiles.superuser.key, ]); const crewChunks = divideNumber(numCrew, MAX_CREW_PER_IX); const mintPrepareResults = []; const merkleTreeAccountKey = fromWeb3JsPublicKey(this.crew.crewMerkleTree); const sagePlayerProfileKey = fromWeb3JsPublicKey(thisSagePlayerProfile); for (let index = 0; index < crewChunks.length; index++) { const crewChunk = crewChunks[index]; const mintPrepareResult = await mintAndPrepareCrew( crewMerkleTree, crewOwner, umi, [...this.crew.knownLeaves, ...knownLeaves], crewChunk, startingLeafIndex, ); const lookupTable = await this.newLookupTable( removeDuplicateKeys([ this.program.programId, theProfile.profile, theProfile.key.publicKey(), this.getProfileFactionAddress(theProfile.profile), crewOwner, starbasePlayer, starbase, this.crew.crewMerkleTree, this.crew.crewProgramConfig, this.gameId, this.funder.publicKey(), thisSagePlayerProfile, SageCrewConfig.findAddress(this.program, this.gameId)[0], PublicKey.findProgramAddressSync( [crewMerkleTree.toBuffer()], toWeb3JsPublicKey(MPL_BUBBLEGUM_PROGRAM_ID), )[0], toWeb3JsPublicKey(SPL_ACCOUNT_COMPRESSION_PROGRAM_ID), toWeb3JsPublicKey(MPL_BUBBLEGUM_PROGRAM_ID), toWeb3JsPublicKey(SPL_NOOP_PROGRAM_ID), SystemProgram.programId, normalizePublicKey(mintPrepareResult.items[0].creatorHash), ...mintPrepareResult.items.map((it) => it.proof).flat(), ]), ); // call `addCrewToGame` await this.sendManyToChain( [ ixToIxReturn( ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 426 }), ), ixToIxReturn( ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }), ), SagePlayerProfile.addCrewToGame( this.program, theProfile.profile, this.getProfileFactionAddress(theProfile.profile), theProfile.key, starbasePlayer, starbase, this.crew.crewProgramConfig, this.gameId, { items: mintPrepareResult.items, }, ), ], true, [lookupTable], ); this.crew.knownLeaves.push( ...mintPrepareResult.items.map((it) => getUmiPublicKey( hashLeaf(umi, { merkleTree: merkleTreeAccountKey, owner: sagePlayerProfileKey, leafIndex: it.leafIndex, metadata: it.metadata, }), ), ), ); mintPrepareResults.push(mintPrepareResult); } return mintPrepareResults; } async playerSetup({ starbase, starbasePlayer, numShips, ship, addCargoToFleet = true, fleetToIdle, numCrew = 7, chunkCrewInstructions = 1, }: { starbase: PublicKey; starbasePlayer: PublicKey; numShips?: number; numCrew?: number; chunkCrewInstructions?: number; ship?: PublicKey; addCargoToFleet?: boolean; fleetToIdle?: boolean; }) { // Set default value in the function body because it is not a literal numShips = numShips ?? SageGameTest.defaultShipsInFleet; if (!this.gameState) { throw 'this.gameState not set'; } if (!this.sagePlayerProfile) { throw 'this.sagePlayerProfile not set'; } if (!this.cargoStatsDefinition) { throw 'this.cargoStatsDefinition not set'; } if (!this.mints) { throw 'this.mints not set'; } if (!this.ships) { throw 'this.ships not set'; } // crew if (!this.crew) { throw 'this.crew not set'; } const crewChunkNum = chunkCrewInstructions == null ? 0 : chunkCrewInstructions; const crewChunks = divideIntoParts(numCrew, crewChunkNum); for (const chunk of crewChunks) { if (chunk) { await this.mintAndAddCrewToGame( this.crew.crewMerkleTree, this.profiles.player1.key.publicKey(), starbase, starbasePlayer, chunk, ); } } // ships const allShips = this.getShips(); const shipToUse = ship || allShips[0]; await this.addShipToGame(shipToUse, starbase, starbasePlayer, numShips); const sbpData = await this.loadStarbasePlayer(starbasePlayer); const shipEscrowIndex = sbpData.wrappedShipEscrows.findIndex( (it) => it.ship.equals(shipToUse) && it.amount.gte(new BN(numShips as number)), ); if (shipEscrowIndex < 0) { throw 'ship not added to game'; } const shipData = await this.loadShip(shipToUse); const shipCargoStats = shipData.data.stats.cargoStats as CargoStats; const ammoCapacity = shipCargoStats.ammoCapacity * numShips; const fuelCapacity = shipCargoStats.fuelCapacity * numShips; const cargoCapacity = shipCargoStats.cargoCapacity * numShips * SageGameTest.defaultEmptyCargoSpace; await this.createFleet({ ship: shipToUse, shipAmount: numShips, shipEscrowIndex: shipEscrowIndex, starbase: starbase, starbasePlayer: starbasePlayer, fleetName: `fleet-${new Date().valueOf()}`, fuelAmtToDeposit: addCargoToFleet ? Math.floor(fuelCapacity / SageGameTest.defaultCargoSize) : undefined, foodAmtToDeposit: addCargoToFleet ? Math.floor(cargoCapacity / (SageGameTest.defaultCargoSize * 2)) : undefined, ammoAmtToDeposit: addCargoToFleet ? Math.floor(ammoCapacity / SageGameTest.defaultCargoSize) : undefined, repairKitAmtToDeposit: addCargoToFleet ? Math.floor(cargoCapacity / (SageGameTest.defaultCargoSize * 2)) : undefined, toIdle: fleetToIdle, }); } } export const SAGE_PROGRAM_ID = new PublicKey(SageGameTest.SAGE_PROGRAM_ID); export const CREW_PROGRAM_ID = new PublicKey(SageGameTest.CREW_PROGRAM_ID);