From f4f1e11b3138adad7bf0f1800659f31c0f227c65 Mon Sep 17 00:00:00 2001 From: hxedcl Date: Thu, 17 Apr 2025 10:46:22 -0700 Subject: [PATCH 01/33] chore(webui): update Spanish translation (#1702) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1702 Co-authored-by: hxedcl Co-committed-by: hxedcl --- static/webui/translations/es.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 8b751375..f51f506a 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -138,7 +138,7 @@ dict = { cheats_noArgonCrystalDecay: `Sin descomposición de cristal de Argón`, cheats_noMasteryRankUpCooldown: `Sin tiempo de espera para rango de maestría`, cheats_noVendorPurchaseLimits: `Sin límite de compras de vendedores`, - cheats_noDeathMarks: `[UNTRANSLATED] No Death Marks`, + cheats_noDeathMarks: `Sin marcas de muerte`, cheats_noKimCooldowns: `Sin tiempo de espera para conversaciones KIM`, cheats_instantResourceExtractorDrones: `Drones de extracción de recursos instantáneos`, cheats_noResourceExtractorDronesDamage: `Sin daño a los drones extractores de recursos`, From f94ecbfbfc86469b687e050c7ffa1edae1b1d45a Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 17 Apr 2025 10:50:24 -0700 Subject: [PATCH 02/33] chore: validate railjack repair start (#1698) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1698 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/guildTechController.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index ebfd40ea..b2490178 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -97,6 +97,19 @@ export const guildTechController: RequestHandler = async (req, res) => { res.end(); } else { const recipe = ExportDojoRecipes.research[data.RecipeType]; + if (data.TechProductCategory) { + if ( + data.TechProductCategory != "CrewShipWeapons" && + data.TechProductCategory != "CrewShipWeaponSkins" + ) { + throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`); + } + if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId)) { + throw new Error( + `no item with id ${data.CategoryItemId} in ${getSalvageCategory(data.TechProductCategory)} array` + ); + } + } const techProject = inventory.PersonalTechProjects[ inventory.PersonalTechProjects.push({ @@ -380,6 +393,12 @@ interface IGuildTechContributeRequest { VaultMiscItems: IMiscItem[]; } +const getSalvageCategory = ( + category: "CrewShipWeapons" | "CrewShipWeaponSkins" +): "CrewShipSalvagedWeapons" | "CrewShipSalvagedWeaponSkins" => { + return category == "CrewShipWeapons" ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins"; +}; + const claimSalvagedComponent = (inventory: TInventoryDatabaseDocument, itemId: string): IInventoryChanges => { // delete personal tech project const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x => x.CategoryItemId?.equals(itemId)); @@ -387,7 +406,7 @@ const claimSalvagedComponent = (inventory: TInventoryDatabaseDocument, itemId: s inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1); const category = personalTechProject.ProductCategory! as "CrewShipWeapons" | "CrewShipWeaponSkins"; - const salvageCategory = category == "CrewShipWeapons" ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins"; + const salvageCategory = getSalvageCategory(category); // find salved part & delete it const salvageIndex = inventory[salvageCategory].findIndex(x => x._id.equals(itemId)); From 41d976d3629f7c84bd8560656605c121e2f8bbf2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 17 Apr 2025 10:58:40 -0700 Subject: [PATCH 03/33] fix: don't trigger G3 capture when LevelKeyName is present (#1699) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1699 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 3ff26b29..079f2886 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -128,11 +128,16 @@ export const addMissionInventoryUpdates = async ( ]); } } + + // Somewhat heuristically detect G3 capture: + // - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1365 + // - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1694 if ( inventoryUpdates.MissionFailed && inventoryUpdates.MissionStatus == "GS_FAILURE" && inventoryUpdates.ObjectiveReached && - !inventoryUpdates.LockedWeaponGroup + !inventoryUpdates.LockedWeaponGroup && + !inventoryUpdates.LevelKeyName ) { const loadout = (await Loadout.findById(inventory.LoadOutPresets, "NORMAL"))!; const config = loadout.NORMAL.id(inventory.CurrentLoadOutIds[0].$oid)!; From decbbdc81b127ac5ed55ad655de73d10f73c1749 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Thu, 17 Apr 2025 12:17:15 -0700 Subject: [PATCH 04/33] chore(webui): update German translation (#1704) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1704 Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 2357d455..abcae96c 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -138,7 +138,7 @@ dict = { cheats_noArgonCrystalDecay: `Argon-Kristalle verschwinden niemals`, cheats_noMasteryRankUpCooldown: `Keine Wartezeit beim Meisterschaftsrangaufstieg`, cheats_noVendorPurchaseLimits: `Keine Kaufbeschränkungen bei Händlern`, - cheats_noDeathMarks: `[UNTRANSLATED] No Death Marks`, + cheats_noDeathMarks: `Keine Todesmarkierungen`, cheats_noKimCooldowns: `Keine Wartezeit bei KIM`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_noResourceExtractorDronesDamage: `Kein Schaden für Ressourcen-Extraktor-Drohnen`, From 79492efbb4942ce7dc06a7693419a13afa9f3862 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 17 Apr 2025 13:15:50 -0700 Subject: [PATCH 05/33] chore: pass --enable-source-maps to node for npm run start (#1701) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1701 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index afcbe580..15f4597c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "WF Emulator", "main": "index.ts", "scripts": { - "start": "node --import ./build/src/pathman.js build/src/index.js", + "start": "node --enable-source-maps --import ./build/src/pathman.js build/src/index.js", "dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ", "build": "tsc --incremental --sourceMap && ncp static/webui build/static/webui", "verify": "tsgo --noEmit", From 0d8f5ee66c00990bfbd645d841debe3117faa397 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 17 Apr 2025 18:40:41 -0700 Subject: [PATCH 06/33] fix: provide proper response when unbranding a suit (#1697) Fixes #1695 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1697 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/claimCompletedRecipeController.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/claimCompletedRecipeController.ts b/src/controllers/api/claimCompletedRecipeController.ts index 880b2267..a2c8139f 100644 --- a/src/controllers/api/claimCompletedRecipeController.ts +++ b/src/controllers/api/claimCompletedRecipeController.ts @@ -18,6 +18,7 @@ import { import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; +import { toOid } from "@/src/helpers/inventoryHelpers"; interface IClaimCompletedRecipeRequest { RecipeIds: IOid[]; @@ -80,6 +81,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = } else { logger.debug("Claiming Recipe", { recipe, pendingRecipe }); + let BrandedSuits: undefined | IOid[]; if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { inventory.PendingSpectreLoadouts ??= []; inventory.SpectreLoadouts ??= []; @@ -104,9 +106,10 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)), 1 ); + BrandedSuits = [toOid(pendingRecipe.SuitToUnbrand!)]; } - let InventoryChanges = {}; + let InventoryChanges: IInventoryChanges = {}; if (recipe.consumeOnUse) { addRecipes(inventory, [ { @@ -134,6 +137,6 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) = }; } await inventory.save(); - res.json({ InventoryChanges }); + res.json({ InventoryChanges, BrandedSuits }); } }; From 379f57be2ca563d48838f209a382a03871671e1f Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 17 Apr 2025 18:40:51 -0700 Subject: [PATCH 07/33] chore: add pumpkin containers to allScans (#1703) Closes #1693 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1703 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/fixed_responses/allScans.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/static/fixed_responses/allScans.json b/static/fixed_responses/allScans.json index 716c1a3b..f4b43062 100644 --- a/static/fixed_responses/allScans.json +++ b/static/fixed_responses/allScans.json @@ -1092,5 +1092,10 @@ "/Lotus/Types/Game/CrewShip/GrineerDestroyer/GrineerDestroyerAvatar", "/Lotus/Types/LevelObjects/Zariman/ZarLootCrateUltraRare", "/Lotus/Objects/DomestikDrone/GrineerOceanDomestikDroneMover", - "/Lotus/Types/Gameplay/1999Wf/Extermination/SupplyCrate" + "/Lotus/Types/Gameplay/1999Wf/Extermination/SupplyCrate", + "/Lotus/Objects/Orokin/Props/CollectibleSeriesOne", + "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLamp", + "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLampLarge", + "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLampSmall", + "/Lotus/Types/LevelObjects/InfestedPumpkinExplosiveTotem" ] From 196182f9a8ecd166dfaf5465b3fc833dce195fe3 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:15:27 -0700 Subject: [PATCH 08/33] feat: acquisition of CrewMembers (#1705) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1705 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 58 +++++++++++++- src/services/inventoryService.ts | 84 +++++++++++++++++++- src/services/purchaseService.ts | 23 ++++-- src/types/inventoryTypes/inventoryTypes.ts | 40 ++++++---- src/types/purchaseTypes.ts | 4 +- 5 files changed, 183 insertions(+), 26 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index dcd59ff7..5faee74a 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -88,7 +88,11 @@ import { IPersonalTechProjectDatabase, IPersonalTechProjectClient, ILastSortieRewardDatabase, - ILastSortieRewardClient + ILastSortieRewardClient, + ICrewMemberSkill, + ICrewMemberSkillEfficiency, + ICrewMemberDatabase, + ICrewMemberClient } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -294,6 +298,55 @@ upgradeSchema.set("toJSON", { } }); +const crewMemberSkillSchema = new Schema( + { + Assigned: Number + }, + { _id: false } +); + +const crewMemberSkillEfficiencySchema = new Schema( + { + PILOTING: crewMemberSkillSchema, + GUNNERY: crewMemberSkillSchema, + ENGINEERING: crewMemberSkillSchema, + COMBAT: crewMemberSkillSchema, + SURVIVABILITY: crewMemberSkillSchema + }, + { _id: false } +); + +const crewMemberSchema = new Schema( + { + ItemType: { type: String, required: true }, + NemesisFingerprint: { type: BigInt, default: 0n }, + Seed: { type: BigInt, default: 0n }, + AssignedRole: Number, + SkillEfficiency: crewMemberSkillEfficiencySchema, + WeaponConfigIdx: Number, + WeaponId: { type: Schema.Types.ObjectId, default: "000000000000000000000000" }, + XP: { type: Number, default: 0 }, + PowersuitType: { type: String, required: true }, + Configs: [ItemConfigSchema], + SecondInCommand: { type: Boolean, default: false } + }, + { id: false } +); + +crewMemberSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj) { + const db = obj as ICrewMemberDatabase; + const client = obj as ICrewMemberClient; + + client.WeaponId = toOid(db.WeaponId); + client.ItemId = toOid(db._id); + + delete obj._id; + delete obj.__v; + } +}); + const slotsBinSchema = new Schema( { Slots: Number, @@ -1363,7 +1416,7 @@ const inventorySchema = new Schema( CrewShipSalvagedWeaponSkins: [upgradeSchema], //RailJack Crew - CrewMembers: [Schema.Types.Mixed], + CrewMembers: [crewMemberSchema], //Complete Mission\Quests Missions: [missionSchema], @@ -1645,6 +1698,7 @@ export type InventoryDocumentProps = { CrewShipWeaponSkins: Types.DocumentArray; CrewShipSalvagedWeaponSkins: Types.DocumentArray; PersonalTechProjects: Types.DocumentArray; + CrewMembers: Types.DocumentArray; } & { [K in TEquipmentKey]: Types.DocumentArray }; // eslint-disable-next-line @typescript-eslint/no-empty-object-type diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 2dd4c0d3..7537c8d6 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -22,7 +22,8 @@ import { IDroneClient, IUpgradeClient, TPartialStartingGear, - ILoreFragmentScan + ILoreFragmentScan, + ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes"; import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes"; @@ -713,6 +714,15 @@ export const addItem = async ( return { MiscItems: miscItemChanges }; + } else if (typeName.startsWith("/Lotus/Types/Game/CrewShip/CrewMember/")) { + if (!seed) { + throw new Error(`Expected crew member to have a seed`); + } + seed |= 0x33b81en << 32n; + return { + ...addCrewMember(inventory, typeName, seed), + ...occupySlot(inventory, InventorySlot.CREWMEMBERS, premiumPurchase) + }; } else if (typeName == "/Lotus/Types/Game/CrewShip/RailJack/DefaultHarness") { return addCrewShipHarness(inventory, typeName); } @@ -1212,6 +1222,78 @@ const addDrone = ( return inventoryChanges; }; +/*const getCrewMemberSkills = (seed: bigint, skillPointsToAssign: number): Record => { + const rng = new SRng(seed); + + const skills = ["PILOTING", "GUNNERY", "ENGINEERING", "COMBAT", "SURVIVABILITY"]; + for (let i = 1; i != 5; ++i) { + const swapIndex = rng.randomInt(0, i); + if (swapIndex != i) { + const tmp = skills[i]; + skills[i] = skills[swapIndex]; + skills[swapIndex] = tmp; + } + } + + rng.randomFloat(); // unused afaict + + const skillAssignments = [0, 0, 0, 0, 0]; + for (let skill = 0; skillPointsToAssign; skill = (skill + 1) % 5) { + const maxIncrease = Math.min(5 - skillAssignments[skill], skillPointsToAssign); + const increase = rng.randomInt(0, maxIncrease); + skillAssignments[skill] += increase; + skillPointsToAssign -= increase; + } + + skillAssignments.sort((a, b) => b - a); + + const combined: Record = {}; + for (let i = 0; i != 5; ++i) { + combined[skills[i]] = skillAssignments[i]; + } + return combined; +};*/ + +const addCrewMember = ( + inventory: TInventoryDatabaseDocument, + itemType: string, + seed: bigint, + inventoryChanges: IInventoryChanges = {} +): IInventoryChanges => { + // SkillEfficiency is additional to the base stats, so we don't need to compute this + //const skillPointsToAssign = itemType.endsWith("Strong") ? 12 : itemType.indexOf("Medium") != -1 ? 10 : 8; + //const skills = getCrewMemberSkills(seed, skillPointsToAssign); + + // Arbiters = male + // CephalonSuda = female + // NewLoka = female + // Perrin = male + // RedVeil = male + // SteelMeridian = female + const powersuitType = + itemType.indexOf("Arbiters") != -1 || itemType.indexOf("Perrin") != -1 || itemType.indexOf("RedVeil") != -1 + ? "/Lotus/Powersuits/NpcPowersuits/CrewMemberMaleSuit" + : "/Lotus/Powersuits/NpcPowersuits/CrewMemberFemaleSuit"; + + const index = + inventory.CrewMembers.push({ + ItemType: itemType, + NemesisFingerprint: 0n, + Seed: seed, + SkillEfficiency: { + PILOTING: { Assigned: 0 }, + GUNNERY: { Assigned: 0 }, + ENGINEERING: { Assigned: 0 }, + COMBAT: { Assigned: 0 }, + SURVIVABILITY: { Assigned: 0 } + }, + PowersuitType: powersuitType + }) - 1; + inventoryChanges.CrewMembers ??= []; + inventoryChanges.CrewMembers.push(inventory.CrewMembers[index].toJSON()); + return inventoryChanges; +}; + export const addEmailItem = async ( inventory: TInventoryDatabaseDocument, typeName: string, diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 32f3af38..6c3b6d8c 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -141,7 +141,8 @@ export const handlePurchase = async ( inventory, purchaseRequest.PurchaseParams.Quantity, undefined, - undefined, + false, + purchaseRequest.PurchaseParams.UsePremium, seed ); combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges); @@ -331,6 +332,7 @@ export const handleStoreItemAcquisition = async ( quantity: number = 1, durability: TRarity = "COMMON", ignorePurchaseQuantity: boolean = false, + premiumPurchase: boolean = true, seed?: bigint ): Promise => { let purchaseResponse = { @@ -352,11 +354,20 @@ export const handleStoreItemAcquisition = async ( } switch (storeCategory) { default: { - purchaseResponse = { InventoryChanges: await addItem(inventory, internalName, quantity, true, seed) }; + purchaseResponse = { + InventoryChanges: await addItem(inventory, internalName, quantity, premiumPurchase, seed) + }; break; } case "Types": - purchaseResponse = await handleTypesPurchase(internalName, inventory, quantity, ignorePurchaseQuantity); + purchaseResponse = await handleTypesPurchase( + internalName, + inventory, + quantity, + ignorePurchaseQuantity, + premiumPurchase, + seed + ); break; case "Boosters": purchaseResponse = handleBoostersPurchase(storeItemName, inventory, durability); @@ -478,13 +489,15 @@ const handleTypesPurchase = async ( typesName: string, inventory: TInventoryDatabaseDocument, quantity: number, - ignorePurchaseQuantity: boolean + ignorePurchaseQuantity: boolean, + premiumPurchase: boolean = true, + seed?: bigint ): Promise => { const typeCategory = getStoreItemTypesCategory(typesName); logger.debug(`type category ${typeCategory}`); switch (typeCategory) { default: - return { InventoryChanges: await addItem(inventory, typesName, quantity) }; + return { InventoryChanges: await addItem(inventory, typesName, quantity, premiumPurchase, seed) }; case "BoosterPacks": return handleBoosterPackPurchase(typesName, inventory, quantity); case "SlotItems": diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index ef90dbf5..d2201b73 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -49,6 +49,7 @@ export interface IInventoryDatabase | "PersonalTechProjects" | "LastSortieReward" | "LastLiteSortieReward" + | "CrewMembers" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -83,6 +84,7 @@ export interface IInventoryDatabase PersonalTechProjects: IPersonalTechProjectDatabase[]; LastSortieReward?: ILastSortieRewardDatabase[]; LastLiteSortieReward?: ILastSortieRewardDatabase[]; + CrewMembers: ICrewMemberDatabase[]; } export interface IQuestKeyDatabase { @@ -324,7 +326,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu InfestedFoundry?: IInfestedFoundryClient; BlessingCooldown?: IMongoDate; CrewShipRawSalvage: ITypeCount[]; - CrewMembers: ICrewMember[]; + CrewMembers: ICrewMemberClient[]; LotusCustomization: ILotusCustomization; UseAdultOperatorLoadout?: boolean; NemesisAbandonedRewards: string[]; @@ -461,13 +463,24 @@ export interface ICompletedJob { StageCompletions: number[]; } -export interface ICrewMember { +export interface ICrewMemberSkill { + Assigned: number; +} + +export interface ICrewMemberSkillEfficiency { + PILOTING: ICrewMemberSkill; + GUNNERY: ICrewMemberSkill; + ENGINEERING: ICrewMemberSkill; + COMBAT: ICrewMemberSkill; + SURVIVABILITY: ICrewMemberSkill; +} + +export interface ICrewMemberClient { ItemType: string; - NemesisFingerprint: number; - Seed: number; - HireDate: IMongoDate; - AssignedRole: number; - SkillEfficiency: ISkillEfficiency; + NemesisFingerprint: bigint; + Seed: bigint; + AssignedRole?: number; + SkillEfficiency: ICrewMemberSkillEfficiency; WeaponConfigIdx: number; WeaponId: IOid; XP: number; @@ -477,16 +490,9 @@ export interface ICrewMember { ItemId: IOid; } -export interface ISkillEfficiency { - PILOTING: ICombat; - GUNNERY: ICombat; - ENGINEERING: ICombat; - COMBAT: ICombat; - SURVIVABILITY: ICombat; -} - -export interface ICombat { - Assigned: number; +export interface ICrewMemberDatabase extends Omit { + WeaponId: Types.ObjectId; + _id: Types.ObjectId; } export enum InventorySlot { diff --git a/src/types/purchaseTypes.ts b/src/types/purchaseTypes.ts index 1b4c9aca..eac812a2 100644 --- a/src/types/purchaseTypes.ts +++ b/src/types/purchaseTypes.ts @@ -6,7 +6,8 @@ import { INemesisClient, ITypeCount, IRecentVendorPurchaseClient, - TEquipmentKey + TEquipmentKey, + ICrewMemberClient } from "./inventoryTypes/inventoryTypes"; export interface IPurchaseRequest { @@ -47,6 +48,7 @@ export type IInventoryChanges = { Nemesis?: Partial; NewVendorPurchase?: IRecentVendorPurchaseClient; // >= 38.5.0 RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0 + CrewMembers?: ICrewMemberClient[]; } & Record< Exclude< string, From 3baf6ad0153bb84c5fe6b807aa709d65fbb41548 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:15:50 -0700 Subject: [PATCH 09/33] feat: handle railjack armaments, crew, & customizations in saveLoadout (#1706) Closes #467 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1706 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 25 +++++++++---------- src/services/importService.ts | 16 ++++++------ src/services/saveLoadoutService.ts | 26 ++++++++++++++++++-- src/types/inventoryTypes/inventoryTypes.ts | 23 ++++++++--------- src/types/saveLoadoutTypes.ts | 19 ++++++++++++-- 5 files changed, 72 insertions(+), 37 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 5faee74a..a2963001 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -39,10 +39,9 @@ import { ILoreFragmentScan, IEvolutionProgress, IEndlessXpProgress, - ICrewShipPortGuns, ICrewShipCustomization, ICrewShipWeapon, - ICrewShipPilotWeapon, + ICrewShipWeaponEmplacements, IShipExterior, IHelminthFoodRecord, ICrewShipMembersDatabase, @@ -772,25 +771,23 @@ const endlessXpProgressSchema = new Schema( { _id: false } ); -const crewShipPilotWeaponSchema = new Schema( +const crewShipWeaponEmplacementsSchema = new Schema( { PRIMARY_A: EquipmentSelectionSchema, - SECONDARY_A: EquipmentSelectionSchema - }, - { _id: false } -); - -const crewShipPortGunsSchema = new Schema( - { - PRIMARY_A: EquipmentSelectionSchema + PRIMARY_B: EquipmentSelectionSchema, + SECONDARY_A: EquipmentSelectionSchema, + SECONDARY_B: EquipmentSelectionSchema }, { _id: false } ); const crewShipWeaponSchema = new Schema( { - PILOT: crewShipPilotWeaponSchema, - PORT_GUNS: crewShipPortGunsSchema + PILOT: crewShipWeaponEmplacementsSchema, + PORT_GUNS: crewShipWeaponEmplacementsSchema, + STARBOARD_GUNS: crewShipWeaponEmplacementsSchema, + ARTILLERY: crewShipWeaponEmplacementsSchema, + SCANNER: crewShipWeaponEmplacementsSchema }, { _id: false } ); @@ -814,7 +811,7 @@ const crewShipCustomizationSchema = new Schema( const crewShipMemberSchema = new Schema( { ItemId: { type: Schema.Types.ObjectId, required: false }, - NemesisFingerprint: { type: Number, required: false } + NemesisFingerprint: { type: BigInt, required: false } }, { _id: false } ); diff --git a/src/services/importService.ts b/src/services/importService.ts index e4f6b97c..0f5dd28d 100644 --- a/src/services/importService.ts +++ b/src/services/importService.ts @@ -104,18 +104,18 @@ const replaceSlots = (db: ISlots, client: ISlots): void => { db.Slots = client.Slots; }; -const convertCrewShipMember = (client: ICrewShipMemberClient): ICrewShipMemberDatabase => { - return { - ...client, - ItemId: client.ItemId ? new Types.ObjectId(client.ItemId.$oid) : undefined - }; +export const importCrewMemberId = (crewMemberId: ICrewShipMemberClient): ICrewShipMemberDatabase => { + if (crewMemberId.ItemId) { + return { ItemId: new Types.ObjectId(crewMemberId.ItemId.$oid) }; + } + return { NemesisFingerprint: BigInt(crewMemberId.NemesisFingerprint ?? 0) }; }; const convertCrewShipMembers = (client: ICrewShipMembersClient): ICrewShipMembersDatabase => { return { - SLOT_A: client.SLOT_A ? convertCrewShipMember(client.SLOT_A) : undefined, - SLOT_B: client.SLOT_B ? convertCrewShipMember(client.SLOT_B) : undefined, - SLOT_C: client.SLOT_C ? convertCrewShipMember(client.SLOT_C) : undefined + SLOT_A: client.SLOT_A ? importCrewMemberId(client.SLOT_A) : undefined, + SLOT_B: client.SLOT_B ? importCrewMemberId(client.SLOT_B) : undefined, + SLOT_C: client.SLOT_C ? importCrewMemberId(client.SLOT_C) : undefined }; }; diff --git a/src/services/saveLoadoutService.ts b/src/services/saveLoadoutService.ts index 472c599d..1f860433 100644 --- a/src/services/saveLoadoutService.ts +++ b/src/services/saveLoadoutService.ts @@ -13,6 +13,8 @@ import { Types } from "mongoose"; import { isEmptyObject } from "@/src/helpers/general"; import { logger } from "@/src/utils/logger"; import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; +import { IItemConfig } from "../types/inventoryTypes/commonInventoryTypes"; +import { importCrewMemberId } from "./importService"; //TODO: setup default items on account creation or like originally in giveStartingItems.php @@ -174,8 +176,8 @@ export const handleInventoryItemConfigChange = async ( } for (const [configId, config] of Object.entries(itemConfigEntries)) { - if (typeof config !== "boolean") { - inventoryItem.Configs[parseInt(configId)] = config; + if (/^[0-9]+$/.test(configId)) { + inventoryItem.Configs[parseInt(configId)] = config as IItemConfig; } } if ("Favorite" in itemConfigEntries) { @@ -184,6 +186,26 @@ export const handleInventoryItemConfigChange = async ( if ("IsNew" in itemConfigEntries) { inventoryItem.IsNew = itemConfigEntries.IsNew; } + + if ("ItemName" in itemConfigEntries) { + inventoryItem.ItemName = itemConfigEntries.ItemName; + } + if ("RailjackImage" in itemConfigEntries) { + inventoryItem.RailjackImage = itemConfigEntries.RailjackImage; + } + if ("Customization" in itemConfigEntries) { + inventoryItem.Customization = itemConfigEntries.Customization; + } + if ("Weapon" in itemConfigEntries) { + inventoryItem.Weapon = itemConfigEntries.Weapon; + } + if (itemConfigEntries.CrewMembers) { + inventoryItem.CrewMembers = { + SLOT_A: importCrewMemberId(itemConfigEntries.CrewMembers.SLOT_A ?? {}), + SLOT_B: importCrewMemberId(itemConfigEntries.CrewMembers.SLOT_B ?? {}), + SLOT_C: importCrewMemberId(itemConfigEntries.CrewMembers.SLOT_C ?? {}) + }; + } } break; } else { diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index d2201b73..38ee575d 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -538,12 +538,12 @@ export interface ICrewShipMembersDatabase { export interface ICrewShipMemberClient { ItemId?: IOid; - NemesisFingerprint?: number; + NemesisFingerprint?: number | bigint; } export interface ICrewShipMemberDatabase { ItemId?: Types.ObjectId; - NemesisFingerprint?: number; + NemesisFingerprint?: bigint; } export interface ICrewShipCustomization { @@ -568,17 +568,18 @@ export type IMiscItem = ITypeCount; // inventory.CrewShips[0].Weapon export interface ICrewShipWeapon { - PILOT: ICrewShipPilotWeapon; - PORT_GUNS: ICrewShipPortGuns; + PILOT?: ICrewShipWeaponEmplacements; + PORT_GUNS?: ICrewShipWeaponEmplacements; + STARBOARD_GUNS?: ICrewShipWeaponEmplacements; + ARTILLERY?: ICrewShipWeaponEmplacements; + SCANNER?: ICrewShipWeaponEmplacements; } -export interface ICrewShipPilotWeapon { - PRIMARY_A: IEquipmentSelection; - SECONDARY_A: IEquipmentSelection; -} - -export interface ICrewShipPortGuns { - PRIMARY_A: IEquipmentSelection; +export interface ICrewShipWeaponEmplacements { + PRIMARY_A?: IEquipmentSelection; + PRIMARY_B?: IEquipmentSelection; + SECONDARY_A?: IEquipmentSelection; + SECONDARY_B?: IEquipmentSelection; } export interface IDiscoveredMarker { diff --git a/src/types/saveLoadoutTypes.ts b/src/types/saveLoadoutTypes.ts index 61f1aba4..dcd1f1b8 100644 --- a/src/types/saveLoadoutTypes.ts +++ b/src/types/saveLoadoutTypes.ts @@ -1,7 +1,13 @@ import { IOid } from "@/src/types/commonTypes"; import { IItemConfig, IOperatorConfigClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { Types } from "mongoose"; -import { ILoadoutConfigClient } from "./inventoryTypes/inventoryTypes"; +import { + ICrewShipCustomization, + ICrewShipMembersClient, + ICrewShipWeapon, + IFlavourItem, + ILoadoutConfigClient +} from "./inventoryTypes/inventoryTypes"; export interface ISaveLoadoutRequest { LoadOuts: ILoadoutClient; @@ -51,7 +57,16 @@ export interface IItemEntry { export type IConfigEntry = { [configId in "0" | "1" | "2" | "3" | "4" | "5"]: IItemConfig; -} & { Favorite?: boolean; IsNew?: boolean }; +} & { + Favorite?: boolean; + IsNew?: boolean; + // Railjack + ItemName?: string; + RailjackImage?: IFlavourItem; + Customization?: ICrewShipCustomization; + Weapon?: ICrewShipWeapon; + CrewMembers?: ICrewShipMembersClient; +}; export type ILoadoutClient = Omit; From 0c34c87d75e644525ba7719fe82e1471d9396f78 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:16:11 -0700 Subject: [PATCH 10/33] fix: give defaultUpgrades for infested pets (#1710) Fixes #1709 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1710 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/modularWeaponCraftingController.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/modularWeaponCraftingController.ts b/src/controllers/api/modularWeaponCraftingController.ts index bab27b23..8819a2a6 100644 --- a/src/controllers/api/modularWeaponCraftingController.ts +++ b/src/controllers/api/modularWeaponCraftingController.ts @@ -17,7 +17,7 @@ import { getDefaultUpgrades } from "@/src/services/itemDataService"; import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { getRandomInt } from "@/src/services/rngService"; -import { ExportSentinels } from "warframe-public-export-plus"; +import { ExportSentinels, IDefaultUpgrade } from "warframe-public-export-plus"; import { Status } from "@/src/types/inventoryTypes/inventoryTypes"; interface IModularCraftRequest { @@ -34,10 +34,8 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) const category = modularWeaponTypes[data.WeaponType]; const inventory = await getInventory(accountId); - const defaultUpgrades = getDefaultUpgrades(data.Parts); - const defaultOverwrites: Partial = { - Configs: applyDefaultUpgrades(inventory, defaultUpgrades) - }; + let defaultUpgrades: IDefaultUpgrade[] | undefined; + const defaultOverwrites: Partial = {}; const inventoryChanges: IInventoryChanges = {}; if (category == "KubrowPets") { const traits = { @@ -129,10 +127,17 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res) // Only save mutagen & antigen in the ModularParts. defaultOverwrites.ModularParts = [data.Parts[1], data.Parts[2]]; - for (const specialItem of ExportSentinels[data.WeaponType].exalted!) { + const meta = ExportSentinels[data.WeaponType]; + + for (const specialItem of meta.exalted!) { addSpecialItem(inventory, specialItem, inventoryChanges); } + + defaultUpgrades = meta.defaultUpgrades; + } else { + defaultUpgrades = getDefaultUpgrades(data.Parts); } + defaultOverwrites.Configs = applyDefaultUpgrades(inventory, defaultUpgrades); addEquipment(inventory, category, data.WeaponType, data.Parts, inventoryChanges, defaultOverwrites); combineInventoryChanges(inventoryChanges, occupySlot(inventory, productCategoryToInventoryBin(category)!, false)); if (defaultUpgrades) { From f549b042d6ef891c23ec8bdbfa9fed0315637f3d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:16:43 -0700 Subject: [PATCH 11/33] feat: ignore list (#1711) Closes #1707 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1711 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/addIgnoredUserController.ts | 30 +++++++++++++++++++ .../api/getIgnoredUsersController.ts | 28 +++++++++-------- .../api/removeIgnoredUserController.ts | 21 +++++++++++++ .../custom/deleteAccountController.ts | 4 ++- src/models/loginModel.ts | 12 +++++++- src/routes/api.ts | 4 +++ src/types/guildTypes.ts | 4 +-- src/types/loginTypes.ts | 7 +++++ src/utils/async-utils.ts | 1 + 9 files changed, 95 insertions(+), 16 deletions(-) create mode 100644 src/controllers/api/addIgnoredUserController.ts create mode 100644 src/controllers/api/removeIgnoredUserController.ts diff --git a/src/controllers/api/addIgnoredUserController.ts b/src/controllers/api/addIgnoredUserController.ts new file mode 100644 index 00000000..99b38972 --- /dev/null +++ b/src/controllers/api/addIgnoredUserController.ts @@ -0,0 +1,30 @@ +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Account, Ignore } from "@/src/models/loginModel"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IFriendInfo } from "@/src/types/guildTypes"; +import { RequestHandler } from "express"; + +export const addIgnoredUserController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const data = getJSONfromString(String(req.body)); + const ignoreeAccount = await Account.findOne( + { DisplayName: data.playerName.substring(0, data.playerName.length - 1) }, + "_id" + ); + if (ignoreeAccount) { + await Ignore.create({ ignorer: accountId, ignoree: ignoreeAccount._id }); + res.json({ + Ignored: { + _id: toOid(ignoreeAccount._id), + DisplayName: data.playerName + } satisfies IFriendInfo + }); + } else { + res.status(400).end(); + } +}; + +interface IAddIgnoredUserRequest { + playerName: string; +} diff --git a/src/controllers/api/getIgnoredUsersController.ts b/src/controllers/api/getIgnoredUsersController.ts index 97127fba..abadb91c 100644 --- a/src/controllers/api/getIgnoredUsersController.ts +++ b/src/controllers/api/getIgnoredUsersController.ts @@ -1,16 +1,20 @@ +import { toOid } from "@/src/helpers/inventoryHelpers"; +import { Account, Ignore } from "@/src/models/loginModel"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { IFriendInfo } from "@/src/types/guildTypes"; +import { parallelForeach } from "@/src/utils/async-utils"; import { RequestHandler } from "express"; -const getIgnoredUsersController: RequestHandler = (_req, res) => { - res.writeHead(200, { - "Content-Type": "text/html", - "Content-Length": "3" +export const getIgnoredUsersController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const ignores = await Ignore.find({ ignorer: accountId }); + const ignoredUsers: IFriendInfo[] = []; + await parallelForeach(ignores, async ignore => { + const ignoreeAccount = (await Account.findById(ignore.ignoree, "DisplayName"))!; + ignoredUsers.push({ + _id: toOid(ignore.ignoree), + DisplayName: ignoreeAccount.DisplayName + "" + }); }); - res.end( - Buffer.from([ - 0x7b, 0x22, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x22, 0x3a, 0x38, 0x33, 0x30, 0x34, 0x30, 0x37, 0x37, 0x32, 0x32, - 0x34, 0x30, 0x32, 0x32, 0x32, 0x36, 0x31, 0x35, 0x30, 0x31, 0x7d - ]) - ); + res.json({ IgnoredUsers: ignoredUsers }); }; - -export { getIgnoredUsersController }; diff --git a/src/controllers/api/removeIgnoredUserController.ts b/src/controllers/api/removeIgnoredUserController.ts new file mode 100644 index 00000000..73613ce6 --- /dev/null +++ b/src/controllers/api/removeIgnoredUserController.ts @@ -0,0 +1,21 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { Account, Ignore } from "@/src/models/loginModel"; +import { getAccountForRequest } from "@/src/services/loginService"; +import { RequestHandler } from "express"; + +export const removeIgnoredUserController: RequestHandler = async (req, res) => { + const accountId = await getAccountForRequest(req); + const data = getJSONfromString(String(req.body)); + const ignoreeAccount = await Account.findOne( + { DisplayName: data.playerName.substring(0, data.playerName.length - 1) }, + "_id" + ); + if (ignoreeAccount) { + await Ignore.deleteOne({ ignorer: accountId, ignoree: ignoreeAccount._id }); + } + res.end(); +}; + +interface IRemoveIgnoredUserRequest { + playerName: string; +} diff --git a/src/controllers/custom/deleteAccountController.ts b/src/controllers/custom/deleteAccountController.ts index fad4485b..32fe4f19 100644 --- a/src/controllers/custom/deleteAccountController.ts +++ b/src/controllers/custom/deleteAccountController.ts @@ -1,6 +1,6 @@ import { RequestHandler } from "express"; import { getAccountIdForRequest } from "@/src/services/loginService"; -import { Account } from "@/src/models/loginModel"; +import { Account, Ignore } from "@/src/models/loginModel"; import { Inbox } from "@/src/models/inboxModel"; import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; @@ -23,6 +23,8 @@ export const deleteAccountController: RequestHandler = async (req, res) => { await Promise.all([ Account.deleteOne({ _id: accountId }), GuildMember.deleteMany({ accountId: accountId }), + Ignore.deleteMany({ ignorer: accountId }), + Ignore.deleteMany({ ignoree: accountId }), Inbox.deleteMany({ ownerId: accountId }), Inventory.deleteOne({ accountOwnerId: accountId }), Leaderboard.deleteMany({ ownerId: accountId }), diff --git a/src/models/loginModel.ts b/src/models/loginModel.ts index 7b12c07a..3e564aa6 100644 --- a/src/models/loginModel.ts +++ b/src/models/loginModel.ts @@ -1,4 +1,4 @@ -import { IDatabaseAccountJson } from "@/src/types/loginTypes"; +import { IDatabaseAccountJson, IIgnore } from "@/src/types/loginTypes"; import { model, Schema, SchemaOptions } from "mongoose"; const opts = { @@ -37,3 +37,13 @@ databaseAccountSchema.set("toJSON", { }); export const Account = model("Account", databaseAccountSchema); + +const ignoreSchema = new Schema({ + ignorer: Schema.Types.ObjectId, + ignoree: Schema.Types.ObjectId +}); + +ignoreSchema.index({ ignorer: 1 }); +ignoreSchema.index({ ignorer: 1, ignoree: 1 }, { unique: true }); + +export const Ignore = model("Ignore", ignoreSchema); diff --git a/src/routes/api.ts b/src/routes/api.ts index d16ea321..71b90dc4 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -4,6 +4,7 @@ import { abortDojoComponentController } from "@/src/controllers/api/abortDojoCom import { abortDojoComponentDestructionController } from "@/src/controllers/api/abortDojoComponentDestructionController"; import { activateRandomModController } from "@/src/controllers/api/activateRandomModController"; import { addFriendImageController } from "@/src/controllers/api/addFriendImageController"; +import { addIgnoredUserController } from "@/src/controllers/api/addIgnoredUserController"; import { addToAllianceController } from "@/src/controllers/api/addToAllianceController"; import { addToGuildController } from "@/src/controllers/api/addToGuildController"; import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController"; @@ -97,6 +98,7 @@ import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCode import { releasePetController } from "@/src/controllers/api/releasePetController"; import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController"; import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController"; +import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController"; import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController"; import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController"; import { saveDialogueController } from "@/src/controllers/api/saveDialogueController"; @@ -202,6 +204,7 @@ apiRouter.get("/updateSession.php", updateSessionGetController); apiRouter.post("/abortDojoComponent.php", abortDojoComponentController); apiRouter.post("/activateRandomMod.php", activateRandomModController); apiRouter.post("/addFriendImage.php", addFriendImageController); +apiRouter.post("/addIgnoredUser.php", addIgnoredUserController); apiRouter.post("/addToAlliance.php", addToAllianceController); apiRouter.post("/addToGuild.php", addToGuildController); apiRouter.post("/arcaneCommon.php", arcaneCommonController); @@ -266,6 +269,7 @@ apiRouter.post("/purchase.php", purchaseController); apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController); apiRouter.post("/releasePet.php", releasePetController); apiRouter.post("/removeFromGuild.php", removeFromGuildController); +apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController); apiRouter.post("/rerollRandomMod.php", rerollRandomModController); apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController); apiRouter.post("/saveDialogue.php", saveDialogueController); diff --git a/src/types/guildTypes.ts b/src/types/guildTypes.ts index 0d02b0db..59e7a3a0 100644 --- a/src/types/guildTypes.ts +++ b/src/types/guildTypes.ts @@ -103,12 +103,12 @@ export interface IGuildMemberDatabase { ShipDecorationsContributed?: ITypeCount[]; } -interface IFriendInfo { +export interface IFriendInfo { _id: IOid; DisplayName?: string; PlatformNames?: string[]; PlatformAccountId?: string; - Status: number; + Status?: number; ActiveAvatarImageType?: string; LastLogin?: IMongoDate; PlayerLevel?: number; diff --git a/src/types/loginTypes.ts b/src/types/loginTypes.ts index 8c443797..17128e2a 100644 --- a/src/types/loginTypes.ts +++ b/src/types/loginTypes.ts @@ -1,3 +1,5 @@ +import { Types } from "mongoose"; + export interface IAccountAndLoginResponseCommons { DisplayName: string; CountryCode: string; @@ -56,3 +58,8 @@ export interface IGroup { experiment: string; experimentGroup: string; } + +export interface IIgnore { + ignorer: Types.ObjectId; + ignoree: Types.ObjectId; +} diff --git a/src/utils/async-utils.ts b/src/utils/async-utils.ts index b2d40c0d..f8825612 100644 --- a/src/utils/async-utils.ts +++ b/src/utils/async-utils.ts @@ -1,3 +1,4 @@ +// Misnomer: We have concurrency, not parallelism - oh well! export const parallelForeach = async (data: T[], op: (datum: T) => Promise): Promise => { const promises: Promise[] = []; for (const datum of data) { From a6d4fab59551c50fb63fd42ec6018413f4ecb09d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:16:58 -0700 Subject: [PATCH 12/33] chore: rewrite gruzzling droptable to scathing/mocking whispers (#1712) Closes #1708 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1712 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 079f2886..c88e8d11 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -700,6 +700,12 @@ export const addMissionRewards = async ( if (strippedItems) { for (const si of strippedItems) { + if (si.DropTable == "/Lotus/Types/DropTables/ManInTheWall/MITWGruzzlingArcanesDropTable") { + logger.debug( + `rewriting ${si.DropTable} to /Lotus/Types/DropTables/EntratiLabDropTables/DoppelgangerDropTable` + ); + si.DropTable = "/Lotus/Types/DropTables/EntratiLabDropTables/DoppelgangerDropTable"; + } const droptables = ExportEnemies.droptables[si.DropTable] ?? []; if (si.DROP_MOD) { const modDroptable = droptables.find(x => x.type == "mod"); From de5fd5fce08e8f9847ed7d56a7fc59ab76a9f35c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:17:19 -0700 Subject: [PATCH 13/33] chore: provide a proper schema for CurrentLoadOutIds (#1714) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1714 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 4 ++-- src/models/inventoryModels/loadoutModel.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index a2963001..ac9edf1e 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -105,7 +105,7 @@ import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; -import { EquipmentSelectionSchema } from "./loadoutModel"; +import { EquipmentSelectionSchema, oidSchema } from "./loadoutModel"; export const typeCountSchema = new Schema({ ItemType: String, ItemCount: Number }, { _id: false }); @@ -1588,7 +1588,7 @@ const inventorySchema = new Schema( HasContributedToDojo: Boolean, HWIDProtectEnabled: Boolean, LoadOutPresets: { type: Schema.Types.ObjectId, ref: "Loadout" }, - CurrentLoadOutIds: [Schema.Types.Mixed], + CurrentLoadOutIds: [oidSchema], RandomUpgradesIdentified: Number, BountyScore: Number, ChallengeInstanceStates: [Schema.Types.Mixed], diff --git a/src/models/inventoryModels/loadoutModel.ts b/src/models/inventoryModels/loadoutModel.ts index 73343c8b..208b9e17 100644 --- a/src/models/inventoryModels/loadoutModel.ts +++ b/src/models/inventoryModels/loadoutModel.ts @@ -3,7 +3,7 @@ import { IEquipmentSelection } from "@/src/types/inventoryTypes/commonInventoryT import { ILoadoutConfigDatabase, ILoadoutDatabase } from "@/src/types/saveLoadoutTypes"; import { Document, Model, Schema, Types, model } from "mongoose"; -const oidSchema = new Schema( +export const oidSchema = new Schema( { $oid: String }, From bc5dc02fc9d39dffc0455b39ab4bcc02d627e301 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:17:36 -0700 Subject: [PATCH 14/33] chore: fill in guild member data asynchronously (#1715) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1715 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/guildService.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/services/guildService.ts b/src/services/guildService.ts index e490c304..d409654d 100644 --- a/src/services/guildService.ts +++ b/src/services/guildService.ts @@ -59,6 +59,7 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s const members: IGuildMemberClient[] = []; let missingEntry = true; + const dataFillInPromises: Promise[] = []; for (const guildMember of guildMembers) { const member: IGuildMemberClient = { _id: toOid(guildMember.accountId), @@ -70,8 +71,12 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s if (guildMember.accountId.equals(accountId)) { missingEntry = false; } else { - member.DisplayName = (await Account.findById(guildMember.accountId, "DisplayName"))!.DisplayName; - await fillInInventoryDataForGuildMember(member); + dataFillInPromises.push( + (async (): Promise => { + member.DisplayName = (await Account.findById(guildMember.accountId, "DisplayName"))!.DisplayName; + })() + ); + dataFillInPromises.push(fillInInventoryDataForGuildMember(member)); } members.push(member); } @@ -90,6 +95,8 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s }); } + await Promise.all(dataFillInPromises); + return { _id: toOid(guild._id), Name: guild.Name, From 6394adb0f08881eb05ad417c182431bafa5fe8f2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:17:55 -0700 Subject: [PATCH 15/33] fix(webui): handle config get request failing due to expired authz (#1716) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1716 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/webui/script.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/static/webui/script.js b/static/webui/script.js index b56e2f6e..e8e4821e 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -1315,7 +1315,7 @@ single.getRoute("/webui/cheats").on("beforeload", function () { interval = setInterval(() => { if (window.authz) { clearInterval(interval); - fetch("/custom/config?" + window.authz).then(res => { + fetch("/custom/config?" + window.authz).then(async res => { if (res.status == 200) { $("#server-settings-no-perms").addClass("d-none"); $("#server-settings").removeClass("d-none"); @@ -1335,8 +1335,16 @@ single.getRoute("/webui/cheats").on("beforeload", function () { }) ); } else { - $("#server-settings-no-perms").removeClass("d-none"); - $("#server-settings").addClass("d-none"); + if ((await res.text()) == "Log-in expired") { + revalidateAuthz(() => { + if (single.getCurrentPath() == "/webui/cheats") { + single.loadRoute("/webui/cheats"); + } + }); + } else { + $("#server-settings-no-perms").removeClass("d-none"); + $("#server-settings").addClass("d-none"); + } } }); } From a98e18d51156c61c4a42bfaa9e4b6eedd0b96f3c Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:18:11 -0700 Subject: [PATCH 16/33] feat: tenet weapon vendor rotation (#1717) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1717 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/serversideVendorsService.ts | 27 ++-- ...ubsPerrinSequenceWeaponVendorManifest.json | 133 ------------------ 2 files changed, 19 insertions(+), 141 deletions(-) delete mode 100644 static/fixed_responses/getVendorInfo/HubsPerrinSequenceWeaponVendorManifest.json diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index d1dc39e0..f6007642 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -34,7 +34,6 @@ const rawVendorManifests: IRawVendorManifest[] = [ getVendorManifestJson("EntratiLabsEntratiLabVendorManifest"), getVendorManifestJson("GuildAdvertisementVendorManifest"), // uses preprocessing getVendorManifestJson("HubsIronwakeDondaVendorManifest"), // uses preprocessing - getVendorManifestJson("HubsPerrinSequenceWeaponVendorManifest"), getVendorManifestJson("HubsRailjackCrewMemberVendorManifest"), getVendorManifestJson("MaskSalesmanManifest"), getVendorManifestJson("Nova1999ConquestShopManifest"), @@ -51,7 +50,8 @@ const rawVendorManifests: IRawVendorManifest[] = [ ]; interface IGeneratableVendorInfo extends Omit { - cycleDuration?: number; + cycleStart: number; + cycleDuration: number; } const generatableVendors: IGeneratableVendorInfo[] = [ @@ -62,6 +62,16 @@ const generatableVendors: IGeneratableVendorInfo[] = [ RandomSeedType: "VRST_WEAPON", RequiredGoalTag: "", WeaponUpgradeValueAttenuationExponent: 2.25, + cycleStart: 1740960000_000, + cycleDuration: 4 * unixTimesInMs.day + }, + { + _id: { $oid: "60ad3b6ec96976e97d227e19" }, + TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/PerrinSequenceWeaponVendorManifest", + PropertyTextHash: "34F8CF1DFF745F0D67433A5EF0A03E70", + RandomSeedType: "VRST_WEAPON", + WeaponUpgradeValueAttenuationExponent: 2.25, + cycleStart: 1744934400_000, cycleDuration: 4 * unixTimesInMs.day } // { @@ -124,7 +134,7 @@ const preprocessVendorManifest = (originalManifest: IRawVendorManifest): IVendor const refreshExpiry = (expiry: IMongoDate): number => { const period = parseInt(expiry.$date.$numberLong); if (Date.now() >= period) { - const epoch = 1734307200 * 1000; // Monday (for weekly schedules) + const epoch = 1734307200_000; // Monday (for weekly schedules) const iteration = Math.trunc((Date.now() - epoch) / period); const start = epoch + iteration * period; const end = start + period; @@ -135,11 +145,11 @@ const refreshExpiry = (expiry: IMongoDate): number => { }; const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifestPreprocessed => { - const EPOCH = 1740960000 * 1000; // Monday; aligns with coda weapons 8 day cycle. + const EPOCH = vendorInfo.cycleStart; const manifest = ExportVendors[vendorInfo.TypeName]; let binThisCycle; if (manifest.isOneBinPerCycle) { - const cycleDuration = vendorInfo.cycleDuration!; // manifest.items[0].durationHours! * 3600_000; + const cycleDuration = vendorInfo.cycleDuration; // manifest.items[0].durationHours! * 3600_000; const cycleIndex = Math.trunc((Date.now() - EPOCH) / cycleDuration); binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now. } @@ -150,7 +160,7 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani if (manifest.isOneBinPerCycle && rawItem.bin != binThisCycle) { continue; } - const cycleDuration = vendorInfo.cycleDuration!; // rawItem.durationHours! * 3600_000; + const cycleDuration = vendorInfo.cycleDuration; // rawItem.durationHours! * 3600_000; const cycleIndex = Math.trunc((Date.now() - EPOCH) / cycleDuration); const cycleStart = EPOCH + cycleIndex * cycleDuration; const cycleEnd = cycleStart + cycleDuration; @@ -181,10 +191,11 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani items.push(item); } } - delete vendorInfo.cycleDuration; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { cycleStart, cycleDuration, ...clientVendorInfo } = vendorInfo; return { VendorInfo: { - ...vendorInfo, + ...clientVendorInfo, ItemManifest: items, Expiry: { $date: { $numberLong: soonestOfferExpiry.toString() } } } diff --git a/static/fixed_responses/getVendorInfo/HubsPerrinSequenceWeaponVendorManifest.json b/static/fixed_responses/getVendorInfo/HubsPerrinSequenceWeaponVendorManifest.json deleted file mode 100644 index 1cae38e4..00000000 --- a/static/fixed_responses/getVendorInfo/HubsPerrinSequenceWeaponVendorManifest.json +++ /dev/null @@ -1,133 +0,0 @@ -{ - "VendorInfo": { - "_id": { - "$oid": "60ad3b6ec96976e97d227e19" - }, - "TypeName": "/Lotus/Types/Game/VendorManifests/Hubs/PerrinSequenceWeaponVendorManifest", - "ItemManifest": [ - { - "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/BoardExec/Primary/CrpBEFerrox/CrpBEFerrox", - "ItemPrices": [ - { - "ItemCount": 40, - "ItemType": "/Lotus/Types/Items/MiscItems/GranumBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 4383829823946960400, - "Id": { - "$oid": "66fd60b20ba592c4c95e9488" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Melee/CrpBriefcaseScythe/CrpBriefcaseScythe", - "ItemPrices": [ - { - "ItemCount": 40, - "ItemType": "/Lotus/Types/Items/MiscItems/GranumBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 7952272124248276000, - "Id": { - "$oid": "66fd60b20ba592c4c95e9489" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Melee/CrpBriefcase2HKatana/CrpBriefcase2HKatana", - "ItemPrices": [ - { - "ItemCount": 40, - "ItemType": "/Lotus/Types/Items/MiscItems/GranumBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 465952672558014140, - "Id": { - "$oid": "66fd60b20ba592c4c95e948a" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Weapons/Tenno/Melee/Swords/CrpBigSlash/CrpBigSlash", - "ItemPrices": [ - { - "ItemCount": 40, - "ItemType": "/Lotus/Types/Items/MiscItems/GranumBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 8342430883077507000, - "Id": { - "$oid": "66fd60b20ba592c4c95e948b" - } - }, - { - "StoreItem": "/Lotus/StoreItems/Weapons/Corpus/Melee/ShieldAndSword/CrpHammerShield/CrpHammerShield", - "ItemPrices": [ - { - "ItemCount": 40, - "ItemType": "/Lotus/Types/Items/MiscItems/GranumBucks", - "ProductCategory": "MiscItems" - } - ], - "Bin": "BIN_0", - "QuantityMultiplier": 1, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - }, - "PurchaseQuantityLimit": 1, - "AllowMultipurchase": false, - "LocTagRandSeed": 7441523153174502000, - "Id": { - "$oid": "66fd60b20ba592c4c95e948c" - } - } - ], - "PropertyTextHash": "34F8CF1DFF745F0D67433A5EF0A03E70", - "RandomSeedType": "VRST_WEAPON", - "WeaponUpgradeValueAttenuationExponent": 2.25, - "Expiry": { - "$date": { - "$numberLong": "9999999000000" - } - } - } -} From da6067ec43befb3788bb91aa7d964af45dcebbd6 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:18:26 -0700 Subject: [PATCH 17/33] fix: use correct drop table for phorid assassination (#1718) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1718 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 14 ++++++++++---- src/types/requestTypes.ts | 2 ++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index c88e8d11..b75522c2 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -976,10 +976,16 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | u } if (RewardInfo.node in ExportRegions) { const region = ExportRegions[RewardInfo.node]; - let rewardManifests: string[] = - RewardInfo.periodicMissionTag == "EliteAlert" || RewardInfo.periodicMissionTag == "EliteAlertB" - ? ["/Lotus/Types/Game/MissionDecks/EliteAlertMissionRewards/EliteAlertMissionRewards"] - : region.rewardManifests; + let rewardManifests: string[]; + if (RewardInfo.periodicMissionTag == "EliteAlert" || RewardInfo.periodicMissionTag == "EliteAlertB") { + rewardManifests = ["/Lotus/Types/Game/MissionDecks/EliteAlertMissionRewards/EliteAlertMissionRewards"]; + } else if (RewardInfo.invasionId && region.missionIndex == 0) { + // Invasion assassination has Phorid has the boss who should drop Nyx parts + // TODO: Check that the invasion faction is indeed FC_INFESTATION once the Invasions in worldState are more dynamic + rewardManifests = ["/Lotus/Types/Game/MissionDecks/BossMissionRewards/NyxRewards"]; + } else { + rewardManifests = region.rewardManifests; + } let rotations: number[] = []; if (RewardInfo.jobId) { diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index 9441f632..fbcc74fd 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -129,6 +129,8 @@ export type IMissionInventoryUpdateRequest = { export interface IRewardInfo { node: string; + invasionId?: string; + invasionAllyFaction?: "FC_GRINEER" | "FC_CORPUS"; sortieId?: string; sortieTag?: string; sortiePrereqs?: string[]; From cdead6fdf8210e3401a02790914f4846850c9d47 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:23:52 -0700 Subject: [PATCH 18/33] feat: archon hunt rewards (#1713) also added a check for first completion to avoid giving another reward for repeating the final mission Closes #1624 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1713 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/missionInventoryUpdateController.ts | 5 +- src/models/inventoryModels/inventoryModel.ts | 12 ++- src/services/missionInventoryUpdateService.ts | 94 +++++++++++++++---- src/types/inventoryTypes/inventoryTypes.ts | 6 ++ 4 files changed, 98 insertions(+), 19 deletions(-) diff --git a/src/controllers/api/missionInventoryUpdateController.ts b/src/controllers/api/missionInventoryUpdateController.ts index 738002fd..3eb0762c 100644 --- a/src/controllers/api/missionInventoryUpdateController.ts +++ b/src/controllers/api/missionInventoryUpdateController.ts @@ -53,6 +53,9 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) logger.debug("mission report:", missionReport); const inventory = await getInventory(accountId); + const firstCompletion = missionReport.SortieId + ? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1 + : false; const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport); if ( @@ -69,7 +72,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res) } const { MissionRewards, inventoryChanges, credits, AffiliationMods, SyndicateXPItemReward } = - await addMissionRewards(inventory, missionReport); + await addMissionRewards(inventory, missionReport, firstCompletion); await inventory.save(); const inventoryResponse = await getInventoryResponse(inventory, true); diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index ac9edf1e..63e3842e 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -91,7 +91,8 @@ import { ICrewMemberSkill, ICrewMemberSkillEfficiency, ICrewMemberDatabase, - ICrewMemberClient + ICrewMemberClient, + ISortieRewardAttenuation } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -1276,6 +1277,14 @@ lastSortieRewardSchema.set("toJSON", { } }); +const sortieRewardAttenutationSchema = new Schema( + { + Tag: String, + Atten: Number + }, + { _id: false } +); + const lockedWeaponGroupSchema = new Schema( { s: Schema.Types.ObjectId, @@ -1495,6 +1504,7 @@ const inventorySchema = new Schema( CompletedSorties: [String], LastSortieReward: { type: [lastSortieRewardSchema], default: undefined }, LastLiteSortieReward: { type: [lastSortieRewardSchema], default: undefined }, + SortieRewardAttenuation: { type: [sortieRewardAttenutationSchema], default: undefined }, // Resource Extractor Drones Drones: [droneSchema], diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index b75522c2..7a1f61c3 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -412,7 +412,9 @@ export const addMissionInventoryUpdates = async ( break; } case "SortieId": { - inventory.CompletedSorties.push(value); + if (inventory.CompletedSorties.indexOf(value) == -1) { + inventory.CompletedSorties.push(value); + } break; } case "SeasonChallengeCompletions": { @@ -569,7 +571,8 @@ export const addMissionRewards = async ( RegularCredits: creditDrops, VoidTearParticipantsCurrWave: voidTearWave, StrippedItems: strippedItems - }: IMissionInventoryUpdateRequest + }: IMissionInventoryUpdateRequest, + firstCompletion: boolean ): Promise => { if (!rewardInfo) { //TODO: if there is a case where you can have credits collected during a mission but no rewardInfo, add credits needs to be handled earlier @@ -583,22 +586,12 @@ export const addMissionRewards = async ( } //TODO: check double reward merging - const MissionRewards: IMissionReward[] = getRandomMissionDrops(rewardInfo, wagerTier); + const MissionRewards: IMissionReward[] = getRandomMissionDrops(inventory, rewardInfo, wagerTier, firstCompletion); logger.debug("random mission drops:", MissionRewards); const inventoryChanges: IInventoryChanges = {}; const AffiliationMods: IAffiliationMods[] = []; let SyndicateXPItemReward; - if (rewardInfo.sortieTag == "Final") { - inventory.LastSortieReward = [ - { - SortieId: new Types.ObjectId(rewardInfo.sortieId!.split("_")[1]), - StoreItem: MissionRewards[0].StoreItem, - Manifest: "/Lotus/Types/Game/MissionDecks/SortieRewards" - } - ]; - } - let missionCompletionCredits = 0; //inventory change is what the client has not rewarded itself, also the client needs to know the credit changes for display if (levelKeyName) { @@ -962,11 +955,78 @@ function getLevelCreditRewards(node: IRegion): number { //TODO: get dark sektor fixed credit rewards and railjack bonus } -function getRandomMissionDrops(RewardInfo: IRewardInfo, tierOverride: number | undefined): IMissionReward[] { +function getRandomMissionDrops( + inventory: TInventoryDatabaseDocument, + RewardInfo: IRewardInfo, + tierOverride: number | undefined, + firstCompletion: boolean +): IMissionReward[] { const drops: IMissionReward[] = []; - if (RewardInfo.sortieTag == "Final") { - const drop = getRandomRewardByChance(ExportRewards["/Lotus/Types/Game/MissionDecks/SortieRewards"][0])!; - drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); + if (RewardInfo.sortieTag == "Final" && firstCompletion) { + const arr = RewardInfo.sortieId!.split("_"); + let sortieId = arr[1]; + if (sortieId == "Lite") { + sortieId = arr[2]; + + // TODO: Some way to get from sortieId to reward to make this faster + more reliable at week rollover. + const boss = getWorldState().LiteSorties[0].Boss as + | "SORTIE_BOSS_AMAR" + | "SORTIE_BOSS_NIRA" + | "SORTIE_BOSS_BOREAL"; + let crystalType = { + SORTIE_BOSS_AMAR: "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalAmar", + SORTIE_BOSS_NIRA: "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalNira", + SORTIE_BOSS_BOREAL: "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalBoreal" + }[boss]; + const attenTag = { + SORTIE_BOSS_AMAR: "NarmerSortieAmarCrystalRewards", + SORTIE_BOSS_NIRA: "NarmerSortieNiraCrystalRewards", + SORTIE_BOSS_BOREAL: "NarmerSortieBorealCrystalRewards" + }[boss]; + const attenIndex = inventory.SortieRewardAttenuation?.findIndex(x => x.Tag == attenTag) ?? -1; + const mythicProbability = + 0.2 + (inventory.SortieRewardAttenuation?.find(x => x.Tag == attenTag)?.Atten ?? 0); + if (Math.random() < mythicProbability) { + crystalType += "Mythic"; + if (attenIndex != -1) { + inventory.SortieRewardAttenuation!.splice(attenIndex, 1); + } + } else { + if (attenIndex == -1) { + inventory.SortieRewardAttenuation ??= []; + inventory.SortieRewardAttenuation.push({ + Tag: attenTag, + Atten: 0.2 + }); + } else { + inventory.SortieRewardAttenuation![attenIndex].Atten += 0.2; + } + } + + drops.push({ StoreItem: crystalType, ItemCount: 1 }); + + const drop = getRandomRewardByChance( + ExportRewards["/Lotus/Types/Game/MissionDecks/ArchonSortieRewards"][0] + )!; + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); + inventory.LastLiteSortieReward = [ + { + SortieId: new Types.ObjectId(sortieId), + StoreItem: drop.type, + Manifest: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards" + } + ]; + } else { + const drop = getRandomRewardByChance(ExportRewards["/Lotus/Types/Game/MissionDecks/SortieRewards"][0])!; + drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); + inventory.LastSortieReward = [ + { + SortieId: new Types.ObjectId(sortieId), + StoreItem: drop.type, + Manifest: "/Lotus/Types/Game/MissionDecks/SortieRewards" + } + ]; + } } if (RewardInfo.periodicMissionTag?.startsWith("HardDaily")) { drops.push({ diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 38ee575d..d461ef45 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -285,6 +285,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu CompletedSorties: string[]; LastSortieReward?: ILastSortieRewardClient[]; LastLiteSortieReward?: ILastSortieRewardClient[]; + SortieRewardAttenuation?: ISortieRewardAttenuation[]; Drones: IDroneClient[]; StepSequencers: IStepSequencer[]; ActiveAvatarImageType: string; @@ -746,6 +747,11 @@ export interface ILastSortieRewardDatabase extends Omit Date: Fri, 18 Apr 2025 11:27:29 -0700 Subject: [PATCH 19/33] feat: save InvasionProgress/QualifyingInvasions (#1719) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1719 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 27 +++++++++++++++++-- src/services/missionInventoryUpdateService.ts | 20 ++++++++++++++ src/types/inventoryTypes/inventoryTypes.ts | 15 ++++++++++- src/types/requestTypes.ts | 4 ++- 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index 63e3842e..db77926e 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -92,7 +92,9 @@ import { ICrewMemberSkillEfficiency, ICrewMemberDatabase, ICrewMemberClient, - ISortieRewardAttenuation + ISortieRewardAttenuation, + IInvasionProgressDatabase, + IInvasionProgressClient } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -684,6 +686,27 @@ questKeysSchema.set("toJSON", { export const fusionTreasuresSchema = new Schema().add(typeCountSchema).add({ Sockets: Number }); +const invasionProgressSchema = new Schema( + { + invasionId: Schema.Types.ObjectId, + Delta: Number, + AttackerScore: Number, + DefenderScore: Number + }, + { _id: false } +); + +invasionProgressSchema.set("toJSON", { + transform(_doc, obj) { + const db = obj as IInvasionProgressDatabase; + const client = obj as IInvasionProgressClient; + + client._id = toOid(db.invasionId); + delete obj.invasionId; + delete obj.__v; + } +}); + const spectreLoadoutsSchema = new Schema( { ItemType: String, @@ -1482,7 +1505,7 @@ const inventorySchema = new Schema( SentientSpawnChanceBoosters: Schema.Types.Mixed, - QualifyingInvasions: [Schema.Types.Mixed], + QualifyingInvasions: [invasionProgressSchema], FactionScores: [Number], // https://warframe.fandom.com/wiki/Specter_(Tenno) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 7a1f61c3..cf0b032a 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -532,6 +532,26 @@ export const addMissionInventoryUpdates = async ( inventoryChanges.RegularCredits -= value; break; } + case "InvasionProgress": { + for (const clientProgress of value) { + const dbProgress = inventory.QualifyingInvasions.find(x => + x.invasionId.equals(clientProgress._id.$oid) + ); + if (dbProgress) { + dbProgress.Delta += clientProgress.Delta; + dbProgress.AttackerScore += clientProgress.AttackerScore; + dbProgress.DefenderScore += clientProgress.DefenderScore; + } else { + inventory.QualifyingInvasions.push({ + invasionId: new Types.ObjectId(clientProgress._id.$oid), + Delta: clientProgress.Delta, + AttackerScore: clientProgress.AttackerScore, + DefenderScore: clientProgress.DefenderScore + }); + } + } + break; + } default: // Equipment XP updates if (equipmentKeys.includes(key as TEquipmentKey)) { diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index d461ef45..03f3fda7 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -50,6 +50,7 @@ export interface IInventoryDatabase | "LastSortieReward" | "LastLiteSortieReward" | "CrewMembers" + | "QualifyingInvasions" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -85,6 +86,7 @@ export interface IInventoryDatabase LastSortieReward?: ILastSortieRewardDatabase[]; LastLiteSortieReward?: ILastSortieRewardDatabase[]; CrewMembers: ICrewMemberDatabase[]; + QualifyingInvasions: IInvasionProgressDatabase[]; } export interface IQuestKeyDatabase { @@ -272,7 +274,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu SentientSpawnChanceBoosters: ISentientSpawnChanceBoosters; SupportedSyndicate?: string; Affiliations: IAffiliation[]; - QualifyingInvasions: any[]; + QualifyingInvasions: IInvasionProgressClient[]; FactionScores: number[]; ArchwingEnabled?: boolean; PendingSpectreLoadouts?: ISpectreLoadout[]; @@ -676,6 +678,17 @@ export interface IInvasionChainProgress { count: number; } +export interface IInvasionProgressClient { + _id: IOid; + Delta: number; + AttackerScore: number; + DefenderScore: number; +} + +export interface IInvasionProgressDatabase extends Omit { + invasionId: Types.ObjectId; +} + export interface IKubrowPetEggClient { ItemType: string; ExpirationDate: IMongoDate; // seems to be set to 7 days ahead @ 0 UTC diff --git a/src/types/requestTypes.ts b/src/types/requestTypes.ts index fbcc74fd..b81fa064 100644 --- a/src/types/requestTypes.ts +++ b/src/types/requestTypes.ts @@ -19,7 +19,8 @@ import { ICollectibleEntry, IDiscoveredMarker, ILockedWeaponGroupClient, - ILoadOutPresets + ILoadOutPresets, + IInvasionProgressClient } from "./inventoryTypes/inventoryTypes"; import { IGroup } from "./loginTypes"; @@ -123,6 +124,7 @@ export type IMissionInventoryUpdateRequest = { }; wagerTier?: number; // the index creditsFee?: number; // the index + InvasionProgress?: IInvasionProgressClient[]; } & { [K in TEquipmentKey]?: IEquipmentClient[]; }; From 37ac10acd2b70f0f6be19456e72b192db3c96868 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:03:53 -0700 Subject: [PATCH 20/33] chore: use import for static vendor manifest json files again (#1725) This was changed because for VRST_WEAPON, the LocTagRandSeed is too big to be read without precision loss, but both vendors using it are now auto-generated, so we can have hot-reloading again when these files are changed. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1725 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/serversideVendorsService.ts | 96 +++++++++++++++--------- src/types/vendorTypes.ts | 2 +- 2 files changed, 60 insertions(+), 38 deletions(-) diff --git a/src/services/serversideVendorsService.ts b/src/services/serversideVendorsService.ts index f6007642..3abe56fa 100644 --- a/src/services/serversideVendorsService.ts +++ b/src/services/serversideVendorsService.ts @@ -1,6 +1,4 @@ -import fs from "fs"; -import path from "path"; -import { repoDir } from "@/src/helpers/pathHelper"; +import { unixTimesInMs } from "@/src/constants/timeConstants"; import { CRng, mixSeeds } from "@/src/services/rngService"; import { IMongoDate } from "@/src/types/commonTypes"; import { @@ -9,44 +7,68 @@ import { IVendorInfo, IVendorManifestPreprocessed } from "@/src/types/vendorTypes"; -import { JSONParse } from "json-with-bigint"; import { ExportVendors } from "warframe-public-export-plus"; -import { unixTimesInMs } from "../constants/timeConstants"; -const getVendorManifestJson = (name: string): IRawVendorManifest => { - return JSONParse(fs.readFileSync(path.join(repoDir, `static/fixed_responses/getVendorInfo/${name}.json`), "utf-8")); -}; +import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json"; +import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json"; +import DeimosFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosFishmongerVendorManifest.json"; +import DeimosHivemindCommisionsManifestFishmonger from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestFishmonger.json"; +import DeimosHivemindCommisionsManifestPetVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestPetVendor.json"; +import DeimosHivemindCommisionsManifestProspector from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestProspector.json"; +import DeimosHivemindCommisionsManifestTokenVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestTokenVendor.json"; +import DeimosHivemindCommisionsManifestWeaponsmith from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestWeaponsmith.json"; +import DeimosHivemindTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosHivemindTokenVendorManifest.json"; +import DeimosPetVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosPetVendorManifest.json"; +import DeimosProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosProspectorVendorManifest.json"; +import DuviriAcrithisVendorManifest from "@/static/fixed_responses/getVendorInfo/DuviriAcrithisVendorManifest.json"; +import EntratiLabsEntratiLabsCommisionsManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabsCommisionsManifest.json"; +import EntratiLabsEntratiLabVendorManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabVendorManifest.json"; +import GuildAdvertisementVendorManifest from "@/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.json"; +import HubsIronwakeDondaVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json"; +import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json"; +import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json"; +import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json"; +import OstronFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronFishmongerVendorManifest.json"; +import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json"; +import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json"; +import RadioLegionIntermission12VendorManifest from "@/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json"; +import SolarisDebtTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorManifest.json"; +import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json"; +import SolarisFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisFishmongerVendorManifest.json"; +import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisProspectorVendorManifest.json"; +import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json"; +import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json"; const rawVendorManifests: IRawVendorManifest[] = [ - getVendorManifestJson("ArchimedeanVendorManifest"), - getVendorManifestJson("DeimosEntratiFragmentVendorProductsManifest"), - getVendorManifestJson("DeimosFishmongerVendorManifest"), - getVendorManifestJson("DeimosHivemindCommisionsManifestFishmonger"), - getVendorManifestJson("DeimosHivemindCommisionsManifestPetVendor"), - getVendorManifestJson("DeimosHivemindCommisionsManifestProspector"), - getVendorManifestJson("DeimosHivemindCommisionsManifestTokenVendor"), - getVendorManifestJson("DeimosHivemindCommisionsManifestWeaponsmith"), - getVendorManifestJson("DeimosHivemindTokenVendorManifest"), - getVendorManifestJson("DeimosPetVendorManifest"), - getVendorManifestJson("DeimosProspectorVendorManifest"), - getVendorManifestJson("DuviriAcrithisVendorManifest"), - getVendorManifestJson("EntratiLabsEntratiLabsCommisionsManifest"), - getVendorManifestJson("EntratiLabsEntratiLabVendorManifest"), - getVendorManifestJson("GuildAdvertisementVendorManifest"), // uses preprocessing - getVendorManifestJson("HubsIronwakeDondaVendorManifest"), // uses preprocessing - getVendorManifestJson("HubsRailjackCrewMemberVendorManifest"), - getVendorManifestJson("MaskSalesmanManifest"), - getVendorManifestJson("Nova1999ConquestShopManifest"), - getVendorManifestJson("OstronFishmongerVendorManifest"), - getVendorManifestJson("OstronPetVendorManifest"), - getVendorManifestJson("OstronProspectorVendorManifest"), - getVendorManifestJson("RadioLegionIntermission12VendorManifest"), - getVendorManifestJson("SolarisDebtTokenVendorManifest"), - getVendorManifestJson("SolarisDebtTokenVendorRepossessionsManifest"), - getVendorManifestJson("SolarisFishmongerVendorManifest"), - getVendorManifestJson("SolarisProspectorVendorManifest"), - getVendorManifestJson("TeshinHardModeVendorManifest"), // uses preprocessing - getVendorManifestJson("ZarimanCommisionsManifestArchimedean") + ArchimedeanVendorManifest, + DeimosEntratiFragmentVendorProductsManifest, + DeimosFishmongerVendorManifest, + DeimosHivemindCommisionsManifestFishmonger, + DeimosHivemindCommisionsManifestPetVendor, + DeimosHivemindCommisionsManifestProspector, + DeimosHivemindCommisionsManifestTokenVendor, + DeimosHivemindCommisionsManifestWeaponsmith, + DeimosHivemindTokenVendorManifest, + DeimosPetVendorManifest, + DeimosProspectorVendorManifest, + DuviriAcrithisVendorManifest, + EntratiLabsEntratiLabsCommisionsManifest, + EntratiLabsEntratiLabVendorManifest, + GuildAdvertisementVendorManifest, // uses preprocessing + HubsIronwakeDondaVendorManifest, // uses preprocessing + HubsRailjackCrewMemberVendorManifest, + MaskSalesmanManifest, + Nova1999ConquestShopManifest, + OstronFishmongerVendorManifest, + OstronPetVendorManifest, + OstronProspectorVendorManifest, + RadioLegionIntermission12VendorManifest, + SolarisDebtTokenVendorManifest, + SolarisDebtTokenVendorRepossessionsManifest, + SolarisFishmongerVendorManifest, + SolarisProspectorVendorManifest, + TeshinHardModeVendorManifest, // uses preprocessing + ZarimanCommisionsManifestArchimedean ]; interface IGeneratableVendorInfo extends Omit { diff --git a/src/types/vendorTypes.ts b/src/types/vendorTypes.ts index 2967a1e5..f14d3f55 100644 --- a/src/types/vendorTypes.ts +++ b/src/types/vendorTypes.ts @@ -32,7 +32,7 @@ export interface IVendorInfo { TypeName: string; ItemManifest: IItemManifest[]; PropertyTextHash?: string; - RandomSeedType?: "VRST_WEAPON"; + RandomSeedType?: string; RequiredGoalTag?: string; WeaponUpgradeValueAttenuationExponent?: number; Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing. From 5eecf11b1a10d52d4d2acd9c8647c1a6cacad8f2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:04:04 -0700 Subject: [PATCH 21/33] fix: ignore assassin mission failure if recovery is still pending (#1726) Closes #1724 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1726 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index cf0b032a..a7308aa6 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -132,11 +132,13 @@ export const addMissionInventoryUpdates = async ( // Somewhat heuristically detect G3 capture: // - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1365 // - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1694 + // - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1724 if ( inventoryUpdates.MissionFailed && inventoryUpdates.MissionStatus == "GS_FAILURE" && inventoryUpdates.ObjectiveReached && !inventoryUpdates.LockedWeaponGroup && + !inventory.LockedWeaponGroup && !inventoryUpdates.LevelKeyName ) { const loadout = (await Loadout.findById(inventory.LoadOutPresets, "NORMAL"))!; From 8afb5152315a4a78c69587242b7dced3b68a6440 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:04:22 -0700 Subject: [PATCH 22/33] fix(stats): captures not being tracked for a new enemy (#1728) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1728 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/statsService.ts | 47 ++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/services/statsService.ts b/src/services/statsService.ts index 626bbe27..a074b637 100644 --- a/src/services/statsService.ts +++ b/src/services/statsService.ts @@ -1,6 +1,5 @@ import { Stats, TStatsDatabaseDocument } from "@/src/models/statsModel"; import { - IEnemy, IStatsAdd, IStatsMax, IStatsSet, @@ -137,34 +136,34 @@ export const updateStats = async (accountOwnerId: string, payload: IStatsUpdate) case "HEADSHOT": case "KILL_ASSIST": { playerStats.Enemies ??= []; - const enemyStatKey = { - KILL_ENEMY: "kills", - EXECUTE_ENEMY: "executions", - HEADSHOT: "headshots", - KILL_ASSIST: "assists" - }[category] as "kills" | "executions" | "headshots" | "assists"; + const enemyStatKey = ( + { + KILL_ENEMY: "kills", + EXECUTE_ENEMY: "executions", + HEADSHOT: "headshots", + KILL_ASSIST: "assists" + } as const + )[category]; for (const [type, count] of Object.entries(data as IUploadEntry)) { - const enemy = playerStats.Enemies.find(element => element.type === type); - if (enemy) { - if (category === "KILL_ENEMY") { - enemy.kills ??= 0; - const captureCount = (actionData as IStatsAdd)["CAPTURE_ENEMY"]?.[type]; - if (captureCount) { - enemy.kills += Math.max(count - captureCount, 0); - enemy.captures ??= 0; - enemy.captures += captureCount; - } else { - enemy.kills += count; - } + let enemy = playerStats.Enemies.find(element => element.type === type); + if (!enemy) { + enemy = { type: type }; + playerStats.Enemies.push(enemy); + } + if (category === "KILL_ENEMY") { + enemy.kills ??= 0; + const captureCount = (actionData as IStatsAdd)["CAPTURE_ENEMY"]?.[type]; + if (captureCount) { + enemy.kills += Math.max(count - captureCount, 0); + enemy.captures ??= 0; + enemy.captures += captureCount; } else { - enemy[enemyStatKey] ??= 0; - enemy[enemyStatKey] += count; + enemy.kills += count; } } else { - const newEnemy: IEnemy = { type: type }; - newEnemy[enemyStatKey] = count; - playerStats.Enemies.push(newEnemy); + enemy[enemyStatKey] ??= 0; + enemy[enemyStatKey] += count; } } break; From c1ca303310ab8eb1716fdbdd8ec245a84adfadd6 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:05:23 -0700 Subject: [PATCH 23/33] fix: handle mk1 armaments being salvaged (#1730) Fixes #1729 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1730 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../api/crewShipIdentifySalvageController.ts | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/controllers/api/crewShipIdentifySalvageController.ts b/src/controllers/api/crewShipIdentifySalvageController.ts index 6cde1cda..4ab7c466 100644 --- a/src/controllers/api/crewShipIdentifySalvageController.ts +++ b/src/controllers/api/crewShipIdentifySalvageController.ts @@ -12,6 +12,7 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { getRandomInt } from "@/src/services/rngService"; import { IFingerprintStat } from "@/src/helpers/rivenHelper"; +import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; export const crewShipIdentifySalvageController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); @@ -42,22 +43,33 @@ export const crewShipIdentifySalvageController: RequestHandler = async (req, res ); } else { const meta = ExportRailjackWeapons[payload.ItemType]; - const upgradeType = meta.defaultUpgrades![0].ItemType; - const upgradeMeta = ExportUpgrades[upgradeType]; - const buffs: IFingerprintStat[] = []; - for (const buff of upgradeMeta.upgradeEntries!) { - buffs.push({ - Tag: buff.tag, - Value: Math.trunc(Math.random() * 0x40000000) - }); + let defaultOverwrites: Partial | undefined; + if (meta.defaultUpgrades?.[0]) { + const upgradeType = meta.defaultUpgrades[0].ItemType; + const upgradeMeta = ExportUpgrades[upgradeType]; + const buffs: IFingerprintStat[] = []; + for (const buff of upgradeMeta.upgradeEntries!) { + buffs.push({ + Tag: buff.tag, + Value: Math.trunc(Math.random() * 0x40000000) + }); + } + defaultOverwrites = { + UpgradeType: upgradeType, + UpgradeFingerprint: JSON.stringify({ + compat: payload.ItemType, + buffs + } satisfies IInnateDamageFingerprint) + }; } - addEquipment(inventory, "CrewShipSalvagedWeapons", payload.ItemType, undefined, inventoryChanges, { - UpgradeType: upgradeType, - UpgradeFingerprint: JSON.stringify({ - compat: payload.ItemType, - buffs - } satisfies IInnateDamageFingerprint) - }); + addEquipment( + inventory, + "CrewShipSalvagedWeapons", + payload.ItemType, + undefined, + inventoryChanges, + defaultOverwrites + ); } inventoryChanges.CrewShipRawSalvage = [ From e59bdcdfbc25a0148129faa094b4aa05604fe909 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:05:43 -0700 Subject: [PATCH 24/33] chore(webui): assume deleting items will always succeed (#1731) instead of waiting for a response + then refreshing inventory, we can just delete the element right away and hope it works out Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1731 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/webui/script.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/static/webui/script.js b/static/webui/script.js index e8e4821e..f8fe085c 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -487,6 +487,7 @@ function updateInventory() { a.href = "#"; a.onclick = function (event) { event.preventDefault(); + document.getElementById(category + "-list").removeChild(tr); disposeOfGear(category, item.ItemId.$oid); }; a.title = loc("code_remove"); @@ -683,6 +684,7 @@ function updateInventory() { a.href = "#"; a.onclick = function (event) { event.preventDefault(); + document.getElementById("riven-list").removeChild(tr); disposeOfGear("Upgrades", item.ItemId.$oid); }; a.title = loc("code_remove"); @@ -723,6 +725,7 @@ function updateInventory() { a.href = "#"; a.onclick = function (event) { event.preventDefault(); + document.getElementById("mods-list").removeChild(tr); disposeOfGear("Upgrades", item.ItemId.$oid); }; a.title = loc("code_remove"); @@ -765,6 +768,7 @@ function updateInventory() { a.href = "#"; a.onclick = function (event) { event.preventDefault(); + document.getElementById("mods-list").removeChild(tr); disposeOfItems("Upgrades", item.ItemType, item.ItemCount); }; a.title = loc("code_remove"); @@ -1097,8 +1101,6 @@ function disposeOfGear(category, oid) { url: "/api/sell.php?" + window.authz, contentType: "text/plain", data: JSON.stringify(data) - }).done(function () { - updateInventory(); }); }); } @@ -1120,8 +1122,6 @@ function disposeOfItems(category, type, count) { url: "/api/sell.php?" + window.authz, contentType: "text/plain", data: JSON.stringify(data) - }).done(function () { - updateInventory(); }); }); } From 26f37f58e555e50b5104058c55fa8c17ff03b924 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:05:55 -0700 Subject: [PATCH 25/33] chore(webui): make add mods behave more like adding items (#1732) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1732 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/webui/script.js | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/static/webui/script.js b/static/webui/script.js index f8fe085c..ed71ed96 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -1260,21 +1260,28 @@ function doAcquireMod() { $("#mod-to-acquire").addClass("is-invalid").focus(); return; } - revalidateAuthz(() => { - $.post({ - url: "/custom/addItems?" + window.authz, - contentType: "application/json", - data: JSON.stringify([ - { - ItemType: uniqueName, - ItemCount: parseInt($("#mod-count").val()) + const count = parseInt($("#mod-count").val()); + if (count != 0) { + revalidateAuthz(() => { + $.post({ + url: "/custom/addItems?" + window.authz, + contentType: "application/json", + data: JSON.stringify([ + { + ItemType: uniqueName, + ItemCount: count + } + ]) + }).done(function () { + if (count > 0) { + toast(loc("code_succAdded")); + } else { + toast(loc("code_succRemoved")); } - ]) - }).done(function () { - document.getElementById("mod-to-acquire").value = ""; - updateInventory(); + updateInventory(); + }); }); - }); + } } const uiConfigs = [...$("#server-settings input[id]")].map(x => x.id); From ba1380ec4cd5ded6f3d9c2f15a9396b830106ac7 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:06:07 -0700 Subject: [PATCH 26/33] feat: rush repair drones (#1733) Closes #1677 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1733 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/guildTechController.ts | 28 ++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/guildTechController.ts b/src/controllers/api/guildTechController.ts index b2490178..b1612be9 100644 --- a/src/controllers/api/guildTechController.ts +++ b/src/controllers/api/guildTechController.ts @@ -360,6 +360,22 @@ export const guildTechController: RequestHandler = async (req, res) => { res.json({ inventoryChanges: inventoryChanges }); + } else if (data.Action == "InstantFinish") { + if (data.TechProductCategory != "CrewShipWeapons" && data.TechProductCategory != "CrewShipWeaponSkins") { + throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`); + } + const inventoryChanges = finishComponentRepair(inventory, data.TechProductCategory, data.CategoryItemId!); + inventoryChanges.MiscItems = [ + { + ItemType: "/Lotus/Types/Items/MiscItems/InstantSalvageRepairItem", + ItemCount: -1 + } + ]; + addMiscItems(inventory, inventoryChanges.MiscItems); + await inventory.save(); + res.json({ + inventoryChanges: inventoryChanges + }); } else { logger.debug(`data provided to ${req.path}: ${String(req.body)}`); throw new Error(`unhandled guildTech request`); @@ -372,7 +388,7 @@ type TGuildTechRequest = | IGuildTechContributeRequest; interface IGuildTechBasicRequest { - Action: "Start" | "Fabricate" | "Pause" | "Unpause" | "Cancel" | "Rush"; + Action: "Start" | "Fabricate" | "Pause" | "Unpause" | "Cancel" | "Rush" | "InstantFinish"; Mode: "Guild" | "Personal"; RecipeType: string; TechProductCategory?: string; @@ -406,11 +422,19 @@ const claimSalvagedComponent = (inventory: TInventoryDatabaseDocument, itemId: s inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1); const category = personalTechProject.ProductCategory! as "CrewShipWeapons" | "CrewShipWeaponSkins"; + return finishComponentRepair(inventory, category, itemId); +}; + +const finishComponentRepair = ( + inventory: TInventoryDatabaseDocument, + category: "CrewShipWeapons" | "CrewShipWeaponSkins", + itemId: string +): IInventoryChanges => { const salvageCategory = getSalvageCategory(category); // find salved part & delete it const salvageIndex = inventory[salvageCategory].findIndex(x => x._id.equals(itemId)); - const salvageItem = inventory[category][salvageIndex]; + const salvageItem = inventory[salvageCategory][salvageIndex]; inventory[salvageCategory].splice(salvageIndex, 1); // add final item From 7040d422a27f596dfc25902a5ae2be1c6e8a3492 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:06:20 -0700 Subject: [PATCH 27/33] feat: manage crew members (#1734) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1734 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/crewMembersController.ts | 28 ++++++++++++++++++++ src/routes/api.ts | 2 ++ src/types/inventoryTypes/inventoryTypes.ts | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/controllers/api/crewMembersController.ts diff --git a/src/controllers/api/crewMembersController.ts b/src/controllers/api/crewMembersController.ts new file mode 100644 index 00000000..15ef0fbf --- /dev/null +++ b/src/controllers/api/crewMembersController.ts @@ -0,0 +1,28 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; +import { Types } from "mongoose"; + +export const crewMembersController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "CrewMembers"); + const data = getJSONfromString(String(req.body)); + const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!; + dbCrewMember.AssignedRole = data.crewMember.AssignedRole; + dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency; + dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx; + dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid); + dbCrewMember.Configs = data.crewMember.Configs; + dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand; + await inventory.save(); + res.json({ + crewMemberId: data.crewMember.ItemId.$oid, + NemesisFingerprint: data.crewMember.NemesisFingerprint + }); +}; + +interface ICrewMembersRequest { + crewMember: ICrewMemberClient; +} diff --git a/src/routes/api.ts b/src/routes/api.ts index 71b90dc4..e6416f7a 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -28,6 +28,7 @@ import { contributeToVaultController } from "@/src/controllers/api/contributeToV import { createAllianceController } from "@/src/controllers/api/createAllianceController"; import { createGuildController } from "@/src/controllers/api/createGuildController"; import { creditsController } from "@/src/controllers/api/creditsController"; +import { crewMembersController } from "@/src/controllers/api/crewMembersController"; import { crewShipIdentifySalvageController } from "@/src/controllers/api/crewShipIdentifySalvageController"; import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController"; import { customObstacleCourseLeaderboardController } from "@/src/controllers/api/customObstacleCourseLeaderboardController"; @@ -222,6 +223,7 @@ apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentContro apiRouter.post("/contributeToVault.php", contributeToVaultController); apiRouter.post("/createAlliance.php", createAllianceController); apiRouter.post("/createGuild.php", createGuildController); +apiRouter.post("/crewMembers.php", crewMembersController); apiRouter.post("/crewShipIdentifySalvage.php", crewShipIdentifySalvageController); apiRouter.post("/customizeGuildRanks.php", customizeGuildRanksController); apiRouter.post("/customObstacleCourseLeaderboard.php", customObstacleCourseLeaderboardController); diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 03f3fda7..d5f6344a 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -489,7 +489,7 @@ export interface ICrewMemberClient { XP: number; PowersuitType: string; Configs: IItemConfig[]; - SecondInCommand: boolean; + SecondInCommand: boolean; // on call ItemId: IOid; } From c2a633b549e21b2c56c43b8f0fb4821882402152 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:06:38 -0700 Subject: [PATCH 28/33] chore: improve LiteSortie handling at week rollover (#1735) WorldState now provides the upcoming LiteSortie if relevant and the boss is derived from the sortieId so completing it at rollover should work as expected. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1735 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 8 +- src/services/worldStateService.ts | 138 ++++++++++-------- src/types/worldStateTypes.ts | 2 +- 3 files changed, 81 insertions(+), 67 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index a7308aa6..7b27faa9 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -53,7 +53,7 @@ import conservationAnimals from "@/static/fixed_responses/conservationAnimals.js import { getInfNodes } from "@/src/helpers/nemesisHelpers"; import { Loadout } from "../models/inventoryModels/loadoutModel"; import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes"; -import { getWorldState } from "./worldStateService"; +import { getLiteSortie, getWorldState, idToWeek } from "./worldStateService"; import { config } from "./configService"; const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => { @@ -990,11 +990,7 @@ function getRandomMissionDrops( if (sortieId == "Lite") { sortieId = arr[2]; - // TODO: Some way to get from sortieId to reward to make this faster + more reliable at week rollover. - const boss = getWorldState().LiteSorties[0].Boss as - | "SORTIE_BOSS_AMAR" - | "SORTIE_BOSS_NIRA" - | "SORTIE_BOSS_BOREAL"; + const boss = getLiteSortie(idToWeek(sortieId)).Boss; let crystalType = { SORTIE_BOSS_AMAR: "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalAmar", SORTIE_BOSS_NIRA: "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalNira", diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index ed65a8fc..6083dcb5 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -4,7 +4,14 @@ import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; import { CRng } from "@/src/services/rngService"; import { eMissionType, ExportNightwave, ExportRegions } from "warframe-public-export-plus"; -import { ICalendarDay, ICalendarSeason, ISeasonChallenge, ISortie, IWorldState } from "../types/worldStateTypes"; +import { + ICalendarDay, + ICalendarSeason, + ILiteSortie, + ISeasonChallenge, + ISortie, + IWorldState +} from "../types/worldStateTypes"; const sortieBosses = [ "SORTIE_BOSS_HYENA", @@ -941,65 +948,9 @@ export const getWorldState = (buildLabel?: string): IWorldState => { pushSortieIfRelevant(worldState.Sorties, day); // Archon Hunt cycling every week - // TODO: Handle imminent rollover - { - const boss = ["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"][week % 3]; - const showdownNode = ["SolNode99", "SolNode53", "SolNode24"][week % 3]; - const systemIndex = [3, 4, 2][week % 3]; // Mars, Jupiter, Earth - - const nodes: string[] = []; - for (const [key, value] of Object.entries(ExportRegions)) { - if ( - value.systemIndex === systemIndex && - value.factionIndex !== undefined && - value.factionIndex < 2 && - value.name.indexOf("Archwing") == -1 && - value.missionIndex != 0 // Exclude MT_ASSASSINATION - ) { - nodes.push(key); - } - } - - const rng = new CRng(week); - const firstNodeIndex = rng.randomInt(0, nodes.length - 1); - const firstNode = nodes[firstNodeIndex]; - nodes.splice(firstNodeIndex, 1); - worldState.LiteSorties.push({ - _id: { - $oid: Math.trunc(weekStart / 1000).toString(16) + "5e23a244740a190c" - }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards", - Seed: week, - Boss: boss, - Missions: [ - { - missionType: rng.randomElement([ - "MT_INTEL", - "MT_MOBILE_DEFENSE", - "MT_EXTERMINATION", - "MT_SABOTAGE", - "MT_RESCUE" - ]), - node: firstNode - }, - { - missionType: rng.randomElement([ - "MT_DEFENSE", - "MT_TERRITORY", - "MT_ARTIFACT", - "MT_EXCAVATE", - "MT_SURVIVAL" - ]), - node: rng.randomElement(nodes) - }, - { - missionType: "MT_ASSASSINATION", - node: showdownNode - } - ] - }); + worldState.LiteSorties.push(getLiteSortie(week)); + if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) { + worldState.LiteSorties.push(getLiteSortie(week + 1)); } // Circuit choices cycling every week @@ -1071,3 +1022,70 @@ export const getWorldState = (buildLabel?: string): IWorldState => { return worldState; }; + +export const idToWeek = (id: string): number => { + return (parseInt(id.substring(0, 8), 16) * 1000 - EPOCH) / 604800000; +}; + +export const getLiteSortie = (week: number): ILiteSortie => { + const boss = (["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"] as const)[week % 3]; + const showdownNode = ["SolNode99", "SolNode53", "SolNode24"][week % 3]; + const systemIndex = [3, 4, 2][week % 3]; // Mars, Jupiter, Earth + + const nodes: string[] = []; + for (const [key, value] of Object.entries(ExportRegions)) { + if ( + value.systemIndex === systemIndex && + value.factionIndex !== undefined && + value.factionIndex < 2 && + value.name.indexOf("Archwing") == -1 && + value.missionIndex != 0 // Exclude MT_ASSASSINATION + ) { + nodes.push(key); + } + } + + const rng = new CRng(week); + const firstNodeIndex = rng.randomInt(0, nodes.length - 1); + const firstNode = nodes[firstNodeIndex]; + nodes.splice(firstNodeIndex, 1); + + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + return { + _id: { + $oid: Math.trunc(weekStart / 1000).toString(16) + "5e23a244740a190c" + }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards", + Seed: week, + Boss: boss, + Missions: [ + { + missionType: rng.randomElement([ + "MT_INTEL", + "MT_MOBILE_DEFENSE", + "MT_EXTERMINATION", + "MT_SABOTAGE", + "MT_RESCUE" + ]), + node: firstNode + }, + { + missionType: rng.randomElement([ + "MT_DEFENSE", + "MT_TERRITORY", + "MT_ARTIFACT", + "MT_EXCAVATE", + "MT_SURVIVAL" + ]), + node: rng.randomElement(nodes) + }, + { + missionType: "MT_ASSASSINATION", + node: showdownNode + } + ] + }; +}; diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index 278f6e95..67178c73 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -103,7 +103,7 @@ export interface ILiteSortie { Expiry: IMongoDate; Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards"; Seed: number; - Boss: string; // "SORTIE_BOSS_AMAR" | "SORTIE_BOSS_NIRA" | "SORTIE_BOSS_BOREAL" + Boss: "SORTIE_BOSS_AMAR" | "SORTIE_BOSS_NIRA" | "SORTIE_BOSS_BOREAL"; Missions: { missionType: string; node: string; From 0f3d9f6c2cb1bbec0a365c9770d18cc5a6ad2688 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:06:49 -0700 Subject: [PATCH 29/33] chore: provide upcoming weekly acts before week rollover (#1736) The final piece to close #1640 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1736 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 55 ++++++++++++++++++------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 6083dcb5..d8cd0c3c 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -355,6 +355,34 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng }; }; +const pushWeeklyActs = (worldState: IWorldState, week: number): void => { + const weekStart = EPOCH + week * 604800000; + const weekEnd = weekStart + 604800000; + + worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 0)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 1)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 2)); + worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 3)); + worldState.SeasonInfo.ActiveChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12) + }); + worldState.SeasonInfo.ActiveChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12) + }); + worldState.SeasonInfo.ActiveChallenges.push({ + _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, + Activation: { $date: { $numberLong: weekStart.toString() } }, + Expiry: { $date: { $numberLong: weekEnd.toString() } }, + Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12) + }); +}; + const birthdays: number[] = [ 1, // Kaya 45, // Lettie @@ -611,29 +639,10 @@ export const getWorldState = (buildLabel?: string): IWorldState => { if (isBeforeNextExpectedWorldStateRefresh(EPOCH + (day + 1) * 86400000)) { worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(day + 1)); } - worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 0)); - worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyChallenge(week, 1)); - worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 2)); - worldState.SeasonInfo.ActiveChallenges.push(getSeasonWeeklyHardChallenge(week, 3)); - worldState.SeasonInfo.ActiveChallenges.push({ - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12) - }); - worldState.SeasonInfo.ActiveChallenges.push({ - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12) - }); - worldState.SeasonInfo.ActiveChallenges.push({ - _id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, - Activation: { $date: { $numberLong: weekStart.toString() } }, - Expiry: { $date: { $numberLong: weekEnd.toString() } }, - Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12) - }); - // TODO: Provide upcoming weekly acts if rollover is imminent + pushWeeklyActs(worldState, week); + if (isBeforeNextExpectedWorldStateRefresh(weekEnd)) { + pushWeeklyActs(worldState, week + 1); + } // Elite Sanctuary Onslaught cycling every week worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = week; // unfaithful From 8fd7152c41e71b269c880b0d4407aae1a42d7ac8 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 20 Apr 2025 07:53:11 -0700 Subject: [PATCH 30/33] fix: don't give rewards for aborted railjack missions (#1743) Fixes #1741 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1743 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index 7b27faa9..4dcb0fe4 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -71,7 +71,12 @@ const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] return [rewardInfo.rewardTier]; } - const rotationCount = rewardInfo.rewardQualifications?.length || 0; + // Aborting a railjack mission should not give any rewards (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1741) + if (rewardInfo.rewardQualifications === undefined) { + return []; + } + + const rotationCount = rewardInfo.rewardQualifications.length || 0; if (rotationCount === 0) return [0]; const rotationPattern = From 11f2ffe64ddc57fea20677671eb6a55232c1fe7f Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 20 Apr 2025 07:53:45 -0700 Subject: [PATCH 31/33] feat(import): accolades (#1750) So one is able to import e.g. `{"Staff":true}` to set that field to true without going into Compass. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1750 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/models/inventoryModels/inventoryModel.ts | 20 +++++++++++++++++++- src/services/importService.ts | 17 +++++++++++++++-- src/types/inventoryTypes/inventoryTypes.ts | 8 +++++--- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index db77926e..029bf6c0 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -94,7 +94,8 @@ import { ICrewMemberClient, ISortieRewardAttenuation, IInvasionProgressDatabase, - IInvasionProgressClient + IInvasionProgressClient, + IAccolades } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -1059,6 +1060,13 @@ pendingRecipeSchema.set("toJSON", { } }); +const accoladesSchema = new Schema( + { + Heirloom: Boolean + }, + { _id: false } +); + const infestedFoundrySchema = new Schema( { Name: String, @@ -1466,6 +1474,16 @@ const inventorySchema = new Schema( //Mastery Rank next availability TrainingDate: { type: Date, default: new Date(0) }, + //Accolades + Staff: Boolean, + Founder: Number, + Guide: Number, + Moderator: Boolean, + Partner: Boolean, + Accolades: accoladesSchema, + //Not an accolade but unlocks an extra chat + Counselor: Boolean, + //you saw last played Region when you opened the star map LastRegionPlayed: String, diff --git a/src/services/importService.ts b/src/services/importService.ts index 0f5dd28d..ea03047b 100644 --- a/src/services/importService.ts +++ b/src/services/importService.ts @@ -230,17 +230,23 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< replaceSlots(db[key], client[key]); } } + // boolean for (const key of [ "UseAdultOperatorLoadout", "HasOwnedVoidProjectionsPreviously", "ReceivedStartingGear", "ArchwingEnabled", - "PlayedParkourTutorial" + "PlayedParkourTutorial", + "Staff", + "Moderator", + "Partner", + "Counselor" ] as const) { if (client[key] !== undefined) { db[key] = client[key]; } } + // number for (const key of [ "PlayerLevel", "RegularCredits", @@ -250,12 +256,15 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< "PrimeTokens", "TradesRemaining", "GiftsRemaining", - "ChallengesFixVersion" + "ChallengesFixVersion", + "Founder", + "Guide" ] as const) { if (client[key] !== undefined) { db[key] = client[key]; } } + // string for (const key of [ "ThemeStyle", "ThemeBackground", @@ -270,6 +279,7 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< db[key] = client[key]; } } + // string[] for (const key of [ "EquippedGear", "EquippedEmotes", @@ -380,6 +390,9 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< }); }); } + if (client.Accolades !== undefined) { + db.Accolades = client.Accolades; + } }; const convertLoadOutConfig = (client: ILoadoutConfigClient): ILoadoutConfigDatabase => { diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index d5f6344a..b4dbc57c 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -250,9 +250,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu Guide?: number; Moderator?: boolean; Partner?: boolean; - Accolades?: { - Heirloom?: boolean; - }; + Accolades?: IAccolades; Counselor?: boolean; Upgrades: IUpgradeClient[]; EquippedGear: string[]; @@ -914,6 +912,10 @@ export interface IPendingRecipeClient CompletionDate: IMongoDate; } +export interface IAccolades { + Heirloom?: boolean; +} + export interface IPendingTrade { State: number; SelfReady: boolean; From 86d871537bd54ed48756641e3a6503bf63c482e3 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 20 Apr 2025 17:18:00 +0200 Subject: [PATCH 32/33] chore: add void corrupted moa to allScans.json --- static/fixed_responses/allScans.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/fixed_responses/allScans.json b/static/fixed_responses/allScans.json index f4b43062..64eebe52 100644 --- a/static/fixed_responses/allScans.json +++ b/static/fixed_responses/allScans.json @@ -1097,5 +1097,6 @@ "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLamp", "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLampLarge", "/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLampSmall", - "/Lotus/Types/LevelObjects/InfestedPumpkinExplosiveTotem" + "/Lotus/Types/LevelObjects/InfestedPumpkinExplosiveTotem", + "/Lotus/Types/Enemies/Orokin/OrokinMoaBipedAvatar" ] From 218df461e1e29e1345642fc75a205dfbeb10606a Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 20 Apr 2025 16:10:45 -0700 Subject: [PATCH 33/33] feat: send WiTW email when completing The New War or Heart of Deimos (#1749) At completion of either of the quests, check if the other has been completed, and if so, unlock WiTW. Closes #1748 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1749 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/questService.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/services/questService.ts b/src/services/questService.ts index 85cefa4b..d5ac28dc 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -216,6 +216,27 @@ const handleQuestCompletion = async ( setupKahlSyndicate(inventory); } + // Whispers in the Walls is unlocked once The New + Heart of Deimos are completed. + if ( + (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain" && + inventory.QuestKeys.find( + x => x.ItemType == "/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain" + )?.Completed) || + (questKey == "/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain" && + inventory.QuestKeys.find(x => x.ItemType == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain")?.Completed) + ) { + await createMessage(inventory.accountOwnerId, [ + { + sndr: "/Lotus/Language/Bosses/Loid", + msg: "/Lotus/Language/EntratiLab/EntratiQuest/WiTWQuestRecievedInboxBody", + att: ["/Lotus/Types/Keys/EntratiLab/EntratiQuestKeyChain"], + sub: "/Lotus/Language/EntratiLab/EntratiQuest/WiTWQuestRecievedInboxTitle", + icon: "/Lotus/Interface/Icons/Npcs/Entrati/Loid.png", + highPriority: true + } + ]); + } + const questCompletionItems = getQuestCompletionItems(questKey); logger.debug(`quest completion items`, questCompletionItems); if (questCompletionItems) {