From 846906d39003984f864730669fdb09ee600f6144 Mon Sep 17 00:00:00 2001 From: Sainan Date: Mon, 17 Mar 2025 21:02:36 +0100 Subject: [PATCH] feat: start nemesis --- src/controllers/api/nemesisController.ts | 150 +++++++++++++++++++ src/helpers/stringHelpers.ts | 4 +- src/index.ts | 14 ++ src/json-with-bigint.ts | 53 +++++++ src/models/inventoryModels/inventoryModel.ts | 55 ++++++- src/routes/api.ts | 2 + src/services/rngService.ts | 17 +++ src/types/inventoryTypes/inventoryTypes.ts | 53 ++++--- 8 files changed, 323 insertions(+), 25 deletions(-) create mode 100644 src/controllers/api/nemesisController.ts create mode 100644 src/json-with-bigint.ts diff --git a/src/controllers/api/nemesisController.ts b/src/controllers/api/nemesisController.ts new file mode 100644 index 00000000..c501bc7f --- /dev/null +++ b/src/controllers/api/nemesisController.ts @@ -0,0 +1,150 @@ +import { getJSONfromString } from "@/src/helpers/stringHelpers"; +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { SRng } from "@/src/services/rngService"; +import { IMongoDate } from "@/src/types/commonTypes"; +import { IInfNode } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; +import { ExportRegions } from "warframe-public-export-plus"; + +export const nemesisController: RequestHandler = async (req, res) => { + if ((req.query.mode as string) == "s") { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId, "Nemesis NemesisAbandonedRewards"); + const body = getJSONfromString(String(req.body)); + + const infNodes: IInfNode[] = []; + for (const [key, value] of Object.entries(ExportRegions)) { + if ( + value.systemIndex == 2 && // earth + value.nodeType != 3 && // hub + value.nodeType != 7 && // junction + value.missionIndex && // must have a mission type and not assassination + value.missionIndex != 28 && // not open world + value.missionIndex != 32 && // not railjack + value.missionIndex != 41 && // not saya's visions + value.name.indexOf("Archwing") == -1 + ) { + //console.log(dict_en[value.name]); + infNodes.push({ Node: key, Influence: 1 }); + } + } + + let weapons: readonly string[]; + if (body.target.manifest == "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix") { + weapons = kuvaLichVersionSixWeapons; + } else if ( + body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour" || + body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree" + ) { + weapons = corpusVersionThreeWeapons; + } else { + throw new Error(`unknown nemesis manifest: ${body.target.manifest}`); + } + + body.target.fp = BigInt(body.target.fp); + const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1); + let weaponIdx = initialWeaponIdx; + do { + const weapon = weapons[weaponIdx]; + if (!body.target.DisallowedWeapons.find(x => x == weapon)) { + break; + } + weaponIdx = (weaponIdx + 1) % weapons.length; + } while (weaponIdx != initialWeaponIdx); + inventory.Nemesis = { + fp: body.target.fp, + manifest: body.target.manifest, + KillingSuit: body.target.KillingSuit, + killingDamageType: body.target.killingDamageType, + ShoulderHelmet: body.target.ShoulderHelmet, + WeaponIdx: weaponIdx, + AgentIdx: body.target.AgentIdx, + BirthNode: body.target.BirthNode, + Faction: body.target.Faction, + Rank: 0, + k: false, + Traded: false, + d: new Date(), + InfNodes: infNodes, + GuessHistory: [], + Hints: [], + HintProgress: 0, + Weakened: body.target.Weakened, + PrevOwners: 0, + HenchmenKilled: 0, + SecondInCommand: body.target.SecondInCommand + }; + inventory.NemesisAbandonedRewards = []; + await inventory.save(); + + res.json({ + target: inventory.toJSON().Nemesis + }); + } else { + throw new Error(`unknown nemesis mode: ${String(req.query.mode)}`); + } +}; + +export interface INemesisStartRequest { + target: { + fp: number | bigint; + manifest: string; + KillingSuit: string; + killingDamageType: number; + ShoulderHelmet: string; + DisallowedWeapons: string[]; + WeaponIdx: number; + AgentIdx: number; + BirthNode: string; + Faction: string; + Rank: number; + k: boolean; + Traded: boolean; + d: IMongoDate; + InfNodes: []; + GuessHistory: []; + Hints: []; + HintProgress: number; + Weakened: boolean; + PrevOwners: number; + HenchmenKilled: number; + SecondInCommand: boolean; + }; +} + +const kuvaLichVersionSixWeapons = [ + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak", + "/Lotus/Weapons/Grineer/Melee/GrnKuvaLichScythe/GrnKuvaLichScytheWeapon", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Kohm/KuvaKohm", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Ogris/KuvaOgris", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Quartakk/KuvaQuartakk", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Tonkor/KuvaTonkor", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Brakk/KuvaBrakk", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Kraken/KuvaKraken", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Seer/KuvaSeer", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Stubba/KuvaStubba", + "/Lotus/Weapons/Grineer/HeavyWeapons/GrnHeavyGrenadeLauncher", + "/Lotus/Weapons/Grineer/LongGuns/GrnKuvaLichRifle/GrnKuvaLichRifleWeapon", + "/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind", + "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr", + "/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler", + "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek" +]; + +const corpusVersionThreeWeapons = [ + "/Lotus/Weapons/Corpus/LongGuns/CrpBriefcaseLauncher/CrpBriefcaseLauncher", + "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEArcaPlasmor/CrpBEArcaPlasmor", + "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEFluxRifle/CrpBEFluxRifle", + "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBETetra/CrpBETetra", + "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBECycron/CrpBECycron", + "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEDetron/CrpBEDetron", + "/Lotus/Weapons/Corpus/Pistols/CrpIgniterPistol/CrpIgniterPistol", + "/Lotus/Weapons/Corpus/Pistols/CrpBriefcaseAkimbo/CrpBriefcaseAkimboPistol", + "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon", + "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion" +]; diff --git a/src/helpers/stringHelpers.ts b/src/helpers/stringHelpers.ts index 6ab13851..cd16fb4e 100644 --- a/src/helpers/stringHelpers.ts +++ b/src/helpers/stringHelpers.ts @@ -1,6 +1,8 @@ +import { JSONParse } from "../json-with-bigint"; + export const getJSONfromString = (str: string): T => { const jsonSubstring = str.substring(0, str.lastIndexOf("}") + 1); - return JSON.parse(jsonSubstring) as T; + return JSONParse(jsonSubstring) as T; }; export const getSubstringFromKeyword = (str: string, keyword: string): string => { diff --git a/src/index.ts b/src/index.ts index 8bf614ef..12da1e45 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,20 @@ import { config, validateConfig } from "./services/configService"; import { registerLogFileCreationListener } from "@/src/utils/logger"; import mongoose from "mongoose"; +// Patch JSON.stringify to work flawlessly with Bigints. Yeah, it's not pretty. +{ + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + (BigInt.prototype as any).toJSON = function (): string { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + return "" + this.toString() + ""; + }; + const og_stringify = JSON.stringify; + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + (JSON as any).stringify = (obj: any): string => { + return og_stringify(obj).split(`"`).join(``).split(`"`).join(``); + }; +} + registerLogFileCreationListener(); validateConfig(); diff --git a/src/json-with-bigint.ts b/src/json-with-bigint.ts new file mode 100644 index 00000000..456de87a --- /dev/null +++ b/src/json-with-bigint.ts @@ -0,0 +1,53 @@ +// Based on the json-with-bigint library: https://github.com/Ivan-Korolenko/json-with-bigint/blob/main/json-with-bigint.js +// Sadly we can't use it directly: https://github.com/Ivan-Korolenko/json-with-bigint/issues/15 + +/* eslint-disable */ +const noiseValue = /^-?\d+n+$/; // Noise - strings that match the custom format before being converted to it +export const JSONParse = (json: any) => { + if (!json) return JSON.parse(json); + + const MAX_INT = Number.MAX_SAFE_INTEGER.toString(); + const MAX_DIGITS = MAX_INT.length; + const stringsOrLargeNumbers = + /"(?:\\.|[^"])*"|-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/g; + const noiseValueWithQuotes = /^"-?\d+n+"$/; // Noise - strings that match the custom format before being converted to it + const customFormat = /^-?\d+n$/; + + // Find and mark big numbers with "n" + const serializedData = json.replace( + stringsOrLargeNumbers, + (text: any, digits: any, fractional: any, exponential: any) => { + const isString = text[0] === '"'; + const isNoise = isString && Boolean(text.match(noiseValueWithQuotes)); + + if (isNoise) return text.substring(0, text.length - 1) + 'n"'; // Mark noise values with additional "n" to offset the deletion of one "n" during the processing + + const isFractionalOrExponential = fractional || exponential; + const isLessThanMaxSafeInt = + digits && + (digits.length < MAX_DIGITS || + (digits.length === MAX_DIGITS && digits <= MAX_INT)); // With a fixed number of digits, we can correctly use lexicographical comparison to do a numeric comparison + + if (isString || isFractionalOrExponential || isLessThanMaxSafeInt) + return text; + + return '"' + text + 'n"'; + } + ); + + // Convert marked big numbers to BigInt + return JSON.parse(serializedData, (_, value) => { + const isCustomFormatBigInt = + typeof value === "string" && Boolean(value.match(customFormat)); + + if (isCustomFormatBigInt) + return BigInt(value.substring(0, value.length - 1)); + + const isNoiseValue = + typeof value === "string" && Boolean(value.match(noiseValue)); + + if (isNoiseValue) return value.substring(0, value.length - 1); // Remove one "n" off the end of the noisy string + + return value; + }); +}; diff --git a/src/models/inventoryModels/inventoryModel.ts b/src/models/inventoryModels/inventoryModel.ts index d7588a48..0f087d7e 100644 --- a/src/models/inventoryModels/inventoryModel.ts +++ b/src/models/inventoryModels/inventoryModel.ts @@ -79,7 +79,10 @@ import { ICrewShipWeaponDatabase, IRecentVendorPurchaseDatabase, IVendorPurchaseHistoryEntryDatabase, - IVendorPurchaseHistoryEntryClient + IVendorPurchaseHistoryEntryClient, + INemesisDatabase, + INemesisClient, + IInfNode } from "../../types/inventoryTypes/inventoryTypes"; import { IOid } from "../../types/commonTypes"; import { @@ -1058,6 +1061,54 @@ const libraryDailyTaskInfoSchema = new Schema( { _id: false } ); +const infNodeSchema = new Schema( + { + Node: String, + Influence: Number + }, + { _id: false } +); + +const nemesisSchema = new Schema( + { + fp: BigInt, + manifest: String, + KillingSuit: String, + killingDamageType: Number, + ShoulderHelmet: String, + WeaponIdx: Number, + AgentIdx: Number, + BirthNode: String, + Faction: String, + Rank: Number, + k: Boolean, + Traded: Boolean, + d: Date, + PrevOwners: Number, + SecondInCommand: Boolean, + Weakened: Boolean, + InfNodes: [infNodeSchema], + HenchmenKilled: Number, + HintProgress: Number, + Hints: [Number], + GuessHistory: [Number] + }, + { _id: false } +); + +nemesisSchema.set("toJSON", { + virtuals: true, + transform(_doc, obj) { + const db = obj as INemesisDatabase; + const client = obj as INemesisClient; + + client.d = toMongoDate(db.d); + + delete obj._id; + delete obj.__v; + } +}); + const alignmentSchema = new Schema( { Alignment: Number, @@ -1341,7 +1392,7 @@ const inventorySchema = new Schema( //CorpusLich or GrineerLich NemesisAbandonedRewards: { type: [String], default: [] }, - //CorpusLich\KuvaLich + Nemesis: nemesisSchema, NemesisHistory: [Schema.Types.Mixed], LastNemesisAllySpawnTime: Schema.Types.Mixed, diff --git a/src/routes/api.ts b/src/routes/api.ts index fdea25dc..6763bd0f 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -69,6 +69,7 @@ import { missionInventoryUpdateController } from "@/src/controllers/api/missionI import { modularWeaponCraftingController } from "@/src/controllers/api/modularWeaponCraftingController"; import { modularWeaponSaleController } from "@/src/controllers/api/modularWeaponSaleController"; import { nameWeaponController } from "@/src/controllers/api/nameWeaponController"; +import { nemesisController } from "@/src/controllers/api/nemesisController"; import { placeDecoInComponentController } from "@/src/controllers/api/placeDecoInComponentController"; import { playerSkillsController } from "@/src/controllers/api/playerSkillsController"; import { projectionManagerController } from "@/src/controllers/api/projectionManagerController"; @@ -203,6 +204,7 @@ apiRouter.post("/missionInventoryUpdate.php", missionInventoryUpdateController); apiRouter.post("/modularWeaponCrafting.php", modularWeaponCraftingController); apiRouter.post("/modularWeaponSale.php", modularWeaponSaleController); apiRouter.post("/nameWeapon.php", nameWeaponController); +apiRouter.post("/nemesis.php", nemesisController); apiRouter.post("/placeDecoInComponent.php", placeDecoInComponentController); apiRouter.post("/playerSkills.php", playerSkillsController); apiRouter.post("/projectionManager.php", projectionManagerController); diff --git a/src/services/rngService.ts b/src/services/rngService.ts index 3a119e6f..f93763e8 100644 --- a/src/services/rngService.ts +++ b/src/services/rngService.ts @@ -92,3 +92,20 @@ export class CRng { return arr[Math.floor(this.random() * arr.length)]; } } + +export class SRng { + state: bigint; + + constructor(seed: bigint) { + this.state = seed; + } + + randomInt(min: number, max: number): number { + const diff = max - min; + if (diff != 0) { + this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn; + min += (Number(this.state >> 32n) & 0x3fffffff) % (diff + 1); + } + return min; + } +} diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index 869cd4e7..397ee1cc 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -43,6 +43,7 @@ export interface IInventoryDatabase | "Drones" | "RecentVendorPurchases" | "NextRefill" + | "Nemesis" | TEquipmentKey >, InventoryDatabaseEquipment { @@ -71,6 +72,7 @@ export interface IInventoryDatabase Drones: IDroneDatabase[]; RecentVendorPurchases?: IRecentVendorPurchaseDatabase[]; NextRefill?: Date; + Nemesis?: INemesisDatabase; } export interface IQuestKeyDatabase { @@ -288,7 +290,8 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu SeasonChallengeHistory: ISeasonChallenge[]; EquippedInstrument?: string; InvasionChainProgress: IInvasionChainProgress[]; - NemesisHistory: INemesisHistory[]; + Nemesis?: INemesisClient; + NemesisHistory: INemesisBaseClient[]; LastNemesisAllySpawnTime?: IMongoDate; Settings: ISettings; PersonalTechProjects: IPersonalTechProject[]; @@ -782,38 +785,44 @@ export interface IMission extends IMissionDatabase { RewardsCooldownTime?: IMongoDate; } -export interface INemesisHistory { - fp: number; - manifest: Manifest; +export interface INemesisBaseClient { + fp: bigint; + manifest: string; KillingSuit: string; killingDamageType: number; ShoulderHelmet: string; + WeaponIdx: number; AgentIdx: number; - BirthNode: BirthNode; + BirthNode: string; + Faction: string; Rank: number; k: boolean; + Traded: boolean; d: IMongoDate; - GuessHistory?: number[]; - currentGuess?: number; - Traded?: boolean; - PrevOwners?: number; - SecondInCommand?: boolean; - Faction?: string; - Weakened?: boolean; + PrevOwners: number; + SecondInCommand: boolean; + Weakened: boolean; } -export enum BirthNode { - SolNode181 = "SolNode181", - SolNode4 = "SolNode4", - SolNode70 = "SolNode70", - SolNode76 = "SolNode76" +export interface INemesisBaseDatabase extends Omit { + d: Date; } -export enum Manifest { - LotusTypesEnemiesCorpusLawyersLawyerManifest = "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifest", - LotusTypesGameNemesisKuvaLichKuvaLichManifest = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifest", - LotusTypesGameNemesisKuvaLichKuvaLichManifestVersionThree = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionThree", - LotusTypesGameNemesisKuvaLichKuvaLichManifestVersionTwo = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionTwo" +export interface INemesisClient extends INemesisBaseClient { + InfNodes: IInfNode[]; + HenchmenKilled: number; + HintProgress: number; + Hints: number[]; + GuessHistory: number[]; +} + +export interface INemesisDatabase extends Omit { + d: Date; +} + +export interface IInfNode { + Node: string; + Influence: number; } export interface IPendingCouponDatabase {