diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 375beec1..29b60ea1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: with: node-version: ">=20.6.0" - run: npm ci - - run: cp config.json.example config.json + - run: cp config-vanilla.json config.json - run: npm run verify - run: npm run lint:ci - run: npm run prettier diff --git a/.prettierignore b/.prettierignore index 9ce8be88..e1ab4819 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,4 +2,4 @@ src/routes/api.ts static/webui/libs/ *.html *.md -config.json.example +config-vanilla.json diff --git a/README.md b/README.md index 0985b846..cd136b9f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ To get an idea of what functionality you can expect to be missing [have a look t ## config.json -SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config.json.example](config.json.example), which has most cheats disabled. +SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config-vanilla.json](config-vanilla.json), which has most cheats disabled. - `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`. - `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`. diff --git a/config.json.example b/config-vanilla.json similarity index 97% rename from config.json.example rename to config-vanilla.json index 66276fb3..12600852 100644 --- a/config.json.example +++ b/config-vanilla.json @@ -70,8 +70,9 @@ "creditBoost": false, "affinityBoost": false, "resourceBoost": false, - "starDays": true, + "tennoLiveRelay": false, "galleonOfGhouls": 0, + "starDaysOverride": null, "eidolonOverride": "", "vallisOverride": "", "duviriOverride": "", diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 703b7eaf..27abf98d 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -2,7 +2,7 @@ set -e if [ ! -f conf/config.json ]; then - jq --arg value "mongodb://openwfagent:spaceninjaserver@mongodb:27017/" '.mongodbUrl = $value' /app/config.json.example > /app/conf/config.json + jq --arg value "mongodb://openwfagent:spaceninjaserver@mongodb:27017/" '.mongodbUrl = $value' /app/config-vanilla.json > /app/conf/config.json fi exec npm run start -- --configPath conf/config.json diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 357c34cb..53c82da5 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -335,6 +335,17 @@ export const getInventoryResponse = async ( for (const uniqueName in ExportFlavour) { inventoryResponse.FlavourItems.push({ ItemType: uniqueName }); } + } else if (config.worldState?.baroTennoConRelay) { + [ + "/Lotus/Types/Items/Events/TennoConRelay2022EarlyAccess", + "/Lotus/Types/Items/Events/TennoConRelay2023EarlyAccess", + "/Lotus/Types/Items/Events/TennoConRelay2024EarlyAccess", + "/Lotus/Types/Items/Events/TennoConRelay2025EarlyAccess" + ].forEach(uniqueName => { + if (!inventoryResponse.FlavourItems.some(x => x.ItemType == uniqueName)) { + inventoryResponse.FlavourItems.push({ ItemType: uniqueName }); + } + }); } if (config.unlockAllSkins) { diff --git a/src/controllers/api/updateChallengeProgressController.ts b/src/controllers/api/updateChallengeProgressController.ts index b75e820a..e0547c7f 100644 --- a/src/controllers/api/updateChallengeProgressController.ts +++ b/src/controllers/api/updateChallengeProgressController.ts @@ -10,6 +10,7 @@ import { logger } from "@/src/utils/logger"; export const updateChallengeProgressController: RequestHandler = async (req, res) => { const challenges = getJSONfromString(String(req.body)); const account = await getAccountForRequest(req); + logger.debug(`challenge report:`, challenges); const inventory = await getInventory( account._id.toString(), @@ -17,7 +18,7 @@ export const updateChallengeProgressController: RequestHandler = async (req, res ); let affiliationMods: IAffiliationMods[] = []; if (challenges.ChallengeProgress) { - affiliationMods = addChallenges( + affiliationMods = await addChallenges( account, inventory, challenges.ChallengeProgress, diff --git a/src/index.ts b/src/index.ts index 4887db86..175ce189 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ try { if (fs.existsSync("config.json")) { console.log("Failed to load " + configPath + ": " + (e as Error).message); } else { - console.log("Failed to load " + configPath + ". You can copy config.json.example to create your config file."); + console.log("Failed to load " + configPath + ". You can copy config-vanilla.json to create your config file."); } process.exit(1); } diff --git a/src/services/configService.ts b/src/services/configService.ts index 3fd0483e..f46c747b 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -81,8 +81,10 @@ export interface IConfig { creditBoost?: boolean; affinityBoost?: boolean; resourceBoost?: boolean; - starDays?: boolean; + tennoLiveRelay?: boolean; + baroTennoConRelay?: boolean; galleonOfGhouls?: number; + starDaysOverride?: boolean; eidolonOverride?: string; vallisOverride?: string; duviriOverride?: string; diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 681ed578..68cffb4b 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -25,7 +25,8 @@ import { INemesisWeaponTargetFingerprint, INemesisPetTargetFingerprint, IDialogueDatabase, - IKubrowPetPrintClient + IKubrowPetPrintClient, + equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate, IUpdateNodeIntrosResponse } from "@/src/types/genericUpdate"; import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "@/src/types/requestTypes"; @@ -1341,7 +1342,7 @@ export const addStanding = ( // TODO: AffiliationMods support (Nightwave). export const updateGeneric = async (data: IGenericUpdate, accountId: string): Promise => { - const inventory = await getInventory(accountId, "NodeIntrosCompleted MiscItems"); + const inventory = await getInventory(accountId, "NodeIntrosCompleted MiscItems ShipDecorations"); // Make it an array for easier parsing. if (typeof data.NodeIntrosCompleted === "string") { @@ -1350,7 +1351,15 @@ export const updateGeneric = async (data: IGenericUpdate, accountId: string): Pr const inventoryChanges: IInventoryChanges = {}; for (const node of data.NodeIntrosCompleted) { - if (node == "KayaFirstVisitPack") { + if (node == "TC2025") { + inventoryChanges.ShipDecorations = [ + { + ItemType: "/Lotus/Types/Items/ShipDecos/TauGrineerLancerBobbleHead", + ItemCount: 1 + } + ]; + addShipDecorations(inventory, inventoryChanges.ShipDecorations); + } else if (node == "KayaFirstVisitPack") { inventoryChanges.MiscItems = [ { ItemType: "/Lotus/Types/Items/MiscItems/1999FixedStickersPack", @@ -1903,25 +1912,87 @@ export const addLoreFragmentScans = (inventory: TInventoryDatabaseDocument, arr: }); }; -export const addChallenges = ( +const challengeRewardsInboxMessages: Record = { + SentEvoEphemeraRankOne: { + sub: "/Lotus/Language/Inbox/EvolvingEphemeraUnlockAName", + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/Inbox/EvolvingEphemeraUnlockADesc", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + att: ["/Lotus/Upgrades/Skins/Effects/NarmerEvolvingEphemeraB"] + }, + SentEvoEphemeraRankTwo: { + sub: "/Lotus/Language/Inbox/EvolvingEphemeraUnlockBName", + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/Inbox/EvolvingEphemeraUnlockBDesc", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + att: ["/Lotus/Upgrades/Skins/Effects/NarmerEvolvingEphemeraC"] + }, + SentEvoSyandanaRankOne: { + sub: "/Lotus/Language/Inbox/EvolvingSyandanaUnlockAName", + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/Inbox/EvolvingSyandanaUnlockADesc", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + att: ["/Lotus/Upgrades/Skins/Scarves/NarmerEvolvingSyandanaBCape"] + }, + SentEvoSyandanaRankTwo: { + sub: "/Lotus/Language/Inbox/EvolvingSyandanaUnlockBName", + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/Inbox/EvolvingSyandanaUnlockBDesc", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + att: ["/Lotus/Upgrades/Skins/Scarves/NarmerEvolvingSyandanaCCape"] + }, + SentEvoSekharaRankOne: { + sub: "/Lotus/Language/Inbox/EvolvingSekharaUnlockAName", + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/Inbox/EvolvingSekharaUnlockADesc", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + att: ["/Lotus/Upgrades/Skins/Clan/ZarimanEvolvingSekharaBadgeItemB"] + }, + SentEvoSekharaRankTwo: { + sub: "/Lotus/Language/Inbox/EvolvingSekharaUnlockBName", + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/Inbox/EvolvingSekharaUnlockBDesc", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + att: ["/Lotus/Upgrades/Skins/Clan/ZarimanEvolvingSekharaBadgeItemC"] + } +}; + +export const addChallenges = async ( account: TAccountDocument, inventory: TInventoryDatabaseDocument, ChallengeProgress: IChallengeProgress[], SeasonChallengeCompletions: ISeasonChallenge[] | undefined -): IAffiliationMods[] => { - ChallengeProgress.forEach(({ Name, Progress }) => { - const itemIndex = inventory.ChallengeProgress.findIndex(i => i.Name === Name); - - if (itemIndex !== -1) { - inventory.ChallengeProgress[itemIndex].Progress = Progress; +): Promise => { + for (const { Name, Progress, Completed } of ChallengeProgress) { + let dbChallenge = inventory.ChallengeProgress.find(x => x.Name == Name); + if (dbChallenge) { + dbChallenge.Progress = Progress; } else { - inventory.ChallengeProgress.push({ Name, Progress }); + dbChallenge = { Name, Progress }; + inventory.ChallengeProgress.push(dbChallenge); } if (Name.startsWith("Calendar")) { addString(getCalendarProgress(inventory).SeasonProgress.ActivatedChallenges, Name); } - }); + + if ((Completed?.length ?? 0) > (dbChallenge.Completed?.length ?? 0)) { + dbChallenge.Completed ??= []; + for (const completion of Completed!) { + if (dbChallenge.Completed.indexOf(completion) == -1) { + if (completion == "challengeRewards") { + if (Name in challengeRewardsInboxMessages) { + await createMessage(account._id, [challengeRewardsInboxMessages[Name]]); + dbChallenge.Completed.push(completion); + // Would love to somehow let the client know about inbox or inventory changes, but there doesn't seem to anything for updateChallengeProgress. + continue; + } + } + logger.warn(`ignoring unknown challenge completion`, { challenge: Name, completion }); + } + } + } + } const affiliationMods: IAffiliationMods[] = []; if (SeasonChallengeCompletions) { @@ -2117,6 +2188,21 @@ export const cleanupInventory = (inventory: TInventoryDatabaseDocument): void => inventory.LotusCustomization.syancol = {}; } } + + { + let numFixed = 0; + for (const equipmentKey of equipmentKeys) { + for (const item of inventory[equipmentKey]) { + if (item.ModularParts?.length === 0) { + item.ModularParts = undefined; + ++numFixed; + } + } + } + if (numFixed != 0) { + logger.debug(`removed ModularParts from ${numFixed} non-modular items`); + } + } }; export const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => { diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index a7b3d9e4..4a8da370 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -292,7 +292,7 @@ export const addMissionInventoryUpdates = async ( addRecipes(inventory, value); break; case "ChallengeProgress": - addChallenges(account, inventory, value, inventoryUpdates.SeasonChallengeCompletions); + await addChallenges(account, inventory, value, inventoryUpdates.SeasonChallengeCompletions); break; case "FusionTreasures": addFusionTreasures(inventory, value); diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 7f5268c9..804be155 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -1038,13 +1038,13 @@ const pushVoidStorms = (arr: IVoidStorm[], hour: number): void => { }; interface ITimeConstraint { - //name: string; + name: string; isValidTime: (timeSecs: number) => boolean; getIdealTimeBefore: (timeSecs: number) => number; } const eidolonDayConstraint: ITimeConstraint = { - //name: "eidolon day", + name: "eidolon day", isValidTime: (timeSecs: number): boolean => { const eidolonEpoch = 1391992660; const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000); @@ -1062,7 +1062,7 @@ const eidolonDayConstraint: ITimeConstraint = { }; const eidolonNightConstraint: ITimeConstraint = { - //name: "eidolon night", + name: "eidolon night", isValidTime: (timeSecs: number): boolean => { const eidolonEpoch = 1391992660; const eidolonCycle = Math.trunc((timeSecs - eidolonEpoch) / 9000); @@ -1089,7 +1089,7 @@ const eidolonNightConstraint: ITimeConstraint = { }; const venusColdConstraint: ITimeConstraint = { - //name: "venus cold", + name: "venus cold", isValidTime: (timeSecs: number): boolean => { const vallisEpoch = 1541837628; const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600); @@ -1115,7 +1115,7 @@ const venusColdConstraint: ITimeConstraint = { }; const venusWarmConstraint: ITimeConstraint = { - //name: "venus warm", + name: "venus warm", isValidTime: (timeSecs: number): boolean => { const vallisEpoch = 1541837628; const vallisCycle = Math.trunc((timeSecs - vallisEpoch) / 1600); @@ -1157,6 +1157,25 @@ const getIdealTimeSatsifyingConstraints = (constraints: ITimeConstraint[]): numb return timeSecs; }; +const fullyStockBaro = (vt: IVoidTrader): void => { + for (const armorSet of baro.armorSets) { + if (Array.isArray(armorSet[0])) { + for (const set of armorSet as IVoidTraderOffer[][]) { + for (const item of set) { + vt.Manifest.push(item); + } + } + } else { + for (const item of armorSet as IVoidTraderOffer[]) { + vt.Manifest.push(item); + } + } + } + for (const item of baro.rest) { + vt.Manifest.push(item); + } +}; + const getVarziaRotation = (week: number): string => { const seed = new SRng(week).randomInt(0, 100_000); const rng = new SRng(seed); @@ -1321,7 +1340,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }); } else { constraints.push({ - //name: `duviri ${config.worldState.duviriOverride}`, + name: `duviri ${config.worldState.duviriOverride}`, isValidTime: (timeSecs: number): boolean => { const moodIndex = Math.trunc(timeSecs / 7200); return moodIndex % 5 == desiredMood; @@ -1336,11 +1355,20 @@ export const getWorldState = (buildLabel?: string): IWorldState => { } } const timeSecs = getIdealTimeSatsifyingConstraints(constraints); + if (constraints.length != 0) { + const delta = Math.trunc(Date.now() / 1000) - timeSecs; + if (delta > 1) { + logger.debug( + `reported time is ${delta} seconds behind real time to satisfy selected constraints (${constraints.map(x => x.name).join(", ")})` + ); + } + } const timeMs = timeSecs * 1000; const day = Math.trunc((timeMs - EPOCH) / 86400000); const week = Math.trunc(day / 7); const weekStart = EPOCH + week * 604800000; const weekEnd = weekStart + 604800000; + const date = new Date(timeMs); const worldState: IWorldState = { BuildLabel: typeof buildLabel == "string" ? buildLabel.split(" ").join("+") : buildConfig.buildLabel, @@ -1367,11 +1395,77 @@ export const getWorldState = (buildLabel?: string): IWorldState => { worldState.PVPChallengeInstances = []; } - if (config.worldState?.starDays) { + if (config.worldState?.tennoLiveRelay) { + worldState.Goals.push({ + _id: { + $oid: "687bf9400000000000000000" + }, + Activation: { + $date: { + $numberLong: "1752955200000" + } + }, + Expiry: { + $date: { + $numberLong: "2000000000000" + } + }, + Count: 0, + Goal: 0, + Success: 0, + Personal: true, + Desc: "/Lotus/Language/Locations/RelayStationTennoConB", + ToolTip: "/Lotus/Language/Locations/RelayStationTennoConDescB", + Icon: "/Lotus/Interface/Icons/Categories/IconTennoLive.png", + Tag: "TennoConRelayB", + Node: "TennoConBHUB6" + }); + } + if (config.worldState?.baroTennoConRelay) { + worldState.Goals.push({ + _id: { $oid: "687bb2f00000000000000000" }, + Activation: { $date: { $numberLong: "1752937200000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Count: 0, + Goal: 0, + Success: 0, + Personal: true, + //"Faction": "FC_GRINEER", + Desc: "/Lotus/Language/Locations/RelayStationTennoCon", + ToolTip: "/Lotus/Language/Locations/RelayStationTennoConDesc", + Icon: "/Lotus/Interface/Icons/Categories/IconTennoConSigil.png", + Tag: "TennoConRelay", + Node: "TennoConHUB2" + }); + const vt: IVoidTrader = { + _id: { $oid: "687809030379266d790495c6" }, + Activation: { $date: { $numberLong: "1752937200000" } }, + Expiry: { $date: { $numberLong: "2000000000000" } }, + Character: "Baro'Ki Teel", + Node: "TennoConHUB2", + Manifest: [] + }; + worldState.VoidTraders.push(vt); + fullyStockBaro(vt); + } + const isFebruary = date.getUTCMonth() == 1; + if (config.worldState?.starDaysOverride ?? isFebruary) { worldState.Goals.push({ _id: { $oid: "67a4dcce2a198564d62e1647" }, - Activation: { $date: { $numberLong: "1738868400000" } }, - Expiry: { $date: { $numberLong: "2000000000000" } }, + Activation: { + $date: { + $numberLong: config.worldState?.starDaysOverride + ? "1738868400000" + : Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1).toString() + } + }, + Expiry: { + $date: { + $numberLong: config.worldState?.starDaysOverride + ? "2000000000000" + : Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1).toString() + } + }, Count: 0, Goal: 0, Success: 0, @@ -1585,24 +1679,8 @@ export const getWorldState = (buildLabel?: string): IWorldState => { }; worldState.VoidTraders.push(vt); if (isBeforeNextExpectedWorldStateRefresh(timeMs, baroActualStart)) { - vt.Manifest = []; if (config.baroFullyStocked) { - for (const armorSet of baro.armorSets) { - if (Array.isArray(armorSet[0])) { - for (const set of armorSet as IVoidTraderOffer[][]) { - for (const item of set) { - vt.Manifest.push(item); - } - } - } else { - for (const item of armorSet as IVoidTraderOffer[]) { - vt.Manifest.push(item); - } - } - } - for (const item of baro.rest) { - vt.Manifest.push(item); - } + fullyStockBaro(vt); } else { const rng = new SRng(new SRng(baroIndex).randomInt(0, 100_000)); // TOVERIFY: Constraint for upgrades amount? @@ -1823,7 +1901,10 @@ export const populateFissures = async (worldState: IWorldState): Promise = _id: toOid(fissure._id), Region: meta.systemIndex + 1, Seed: 1337, - Activation: toMongoDate(fissure.Activation), + Activation: + fissure.Activation.getTime() < Date.now() // Activation is in the past? + ? { $date: { $numberLong: "1000000000000" } } // Let the client know 'explicitly' to avoid interference from time constraints. + : toMongoDate(fissure.Activation), Expiry: toMongoDate(fissure.Expiry), Node: fissure.Node, MissionType: eMissionType[meta.missionIndex].tag, diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index 8b0db0f7..a6764a35 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -74,6 +74,7 @@ export type IInventoryChanges = { InfestedFoundry?: IInfestedFoundryClient; Drones?: IDroneClient[]; MiscItems?: IMiscItem[]; + ShipDecorations?: ITypeCount[]; EmailItems?: ITypeCount[]; CrewShipRawSalvage?: ITypeCount[]; Nemesis?: Partial; diff --git a/static/fixed_responses/worldState/baro.json b/static/fixed_responses/worldState/baro.json index 352e54ef..7e11cf67 100644 --- a/static/fixed_responses/worldState/baro.json +++ b/static/fixed_responses/worldState/baro.json @@ -431,7 +431,7 @@ { "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/KuvaLichLoginSongItem", "PrimePrice": 140, "RegularPrice": 170000 }, { "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyBaro", "PrimePrice": 100, "RegularPrice": 125000 }, { "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyInaros", "PrimePrice": 120, "RegularPrice": 90000 }, - { "ItemType": "/Lotus/Upgrades/Mods/Pistol/Expert/WeaponPistolFactionDamageMurmursExpert", "PrimePrice": 375, "RegularPrice": 130000 } + { "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponPistolFactionDamageMurmursExpert", "PrimePrice": 375, "RegularPrice": 130000 } ], "allIfAny": [ [ diff --git a/static/webui/index.html b/static/webui/index.html index 0e0a6941..0c0bd3c6 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -926,8 +926,12 @@
- - + + +
+
+ +
@@ -942,6 +946,14 @@
+
+ + +