feat: start nemesis #1227

Merged
Sainan merged 7 commits from nemesis into main 2025-03-20 05:36:10 -07:00
11 changed files with 286 additions and 25 deletions

7
package-lock.json generated
View File

@ -14,6 +14,7 @@
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
"express": "^5", "express": "^5",
"json-with-bigint": "^3.2.1",
"mongoose": "^8.11.0", "mongoose": "^8.11.0",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"typescript": ">=5.5 <5.6.0", "typescript": ">=5.5 <5.6.0",
@ -2346,6 +2347,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-with-bigint": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.2.1.tgz",
"integrity": "sha512-0f8RHpU1AwBFwIPmtm71W+cFxzlXdiBmzc3JqydsNDSKSAsr0Lso6KXRbz0h2LRwTIRiHAk/UaD+xaAN5f577w==",
"license": "MIT"
},
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",

View File

@ -19,6 +19,7 @@
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
"express": "^5", "express": "^5",
"json-with-bigint": "^3.2.1",
"mongoose": "^8.11.0", "mongoose": "^8.11.0",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"typescript": ">=5.5 <5.6.0", "typescript": ">=5.5 <5.6.0",

View File

@ -23,6 +23,7 @@ import { config } from "@/src/services/configService";
import { GuildPermission, ITechProjectClient, ITechProjectDatabase } from "@/src/types/guildTypes"; import { GuildPermission, ITechProjectClient, ITechProjectDatabase } from "@/src/types/guildTypes";
import { TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TGuildDatabaseDocument } from "@/src/models/guildModel";
import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { logger } from "@/src/utils/logger";
export const guildTechController: RequestHandler = async (req, res) => { export const guildTechController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -219,6 +220,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
await guild.save(); await guild.save();
res.end(); res.end();
} else { } else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unknown guildTech action: ${data.Action}`); throw new Error(`unknown guildTech action: ${data.Action}`);
} }
}; };

View File

@ -357,6 +357,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
} }
default: default:
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unhandled infestedFoundry mode: ${String(req.query.mode)}`); throw new Error(`unhandled infestedFoundry mode: ${String(req.query.mode)}`);
} }
}; };

View File

@ -0,0 +1,152 @@
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 { logger } from "@/src/utils/logger";
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<INemesisStartRequest>(String(req.body));
const infNodes: IInfNode[] = [];
for (const [key, value] of Object.entries(ExportRegions)) {
if (
value.systemIndex == 2 && // earth
value.nodeType != 3 && // not hub
value.nodeType != 7 && // not 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 = []; // unclear if we need to do this since the client also submits this with missionInventoryUpdate
await inventory.save();
res.json({
target: inventory.toJSON().Nemesis
});
} else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
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"
];

View File

@ -1,6 +1,8 @@
import { JSONParse } from "json-with-bigint";
export const getJSONfromString = <T>(str: string): T => { export const getJSONfromString = <T>(str: string): T => {
const jsonSubstring = str.substring(0, str.lastIndexOf("}") + 1); const jsonSubstring = str.substring(0, str.lastIndexOf("}") + 1);
return JSON.parse(jsonSubstring) as T; return JSONParse<T>(jsonSubstring);
}; };
export const getSubstringFromKeyword = (str: string, keyword: string): string => { export const getSubstringFromKeyword = (str: string, keyword: string): string => {

View File

@ -10,6 +10,21 @@ import { config, validateConfig } from "./services/configService";
import { registerLogFileCreationListener } from "@/src/utils/logger"; import { registerLogFileCreationListener } from "@/src/utils/logger";
import mongoose from "mongoose"; import mongoose from "mongoose";
// Patch JSON.stringify to work flawlessly with Bigints. Yeah, it's not pretty.
// TODO: Might wanna use json-with-bigint if/when possible.
{
// 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 "<BIGINT>" + this.toString() + "</BIGINT>";
Sainan marked this conversation as resolved Outdated

lets keep this out tho, the lack of support there is uncomfortable, but lets keep our code.. noice

lets keep this out tho, the lack of support there is uncomfortable, but lets keep our code.. noice
};
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(`"<BIGINT>`).join(``).split(`</BIGINT>"`).join(``);
};
}
registerLogFileCreationListener(); registerLogFileCreationListener();
validateConfig(); validateConfig();

View File

@ -79,7 +79,10 @@ import {
ICrewShipWeaponDatabase, ICrewShipWeaponDatabase,
IRecentVendorPurchaseDatabase, IRecentVendorPurchaseDatabase,
IVendorPurchaseHistoryEntryDatabase, IVendorPurchaseHistoryEntryDatabase,
IVendorPurchaseHistoryEntryClient IVendorPurchaseHistoryEntryClient,
INemesisDatabase,
INemesisClient,
IInfNode
} from "../../types/inventoryTypes/inventoryTypes"; } from "../../types/inventoryTypes/inventoryTypes";
import { IOid } from "../../types/commonTypes"; import { IOid } from "../../types/commonTypes";
import { import {
@ -1058,6 +1061,54 @@ const libraryDailyTaskInfoSchema = new Schema<ILibraryDailyTaskInfo>(
{ _id: false } { _id: false }
); );
const infNodeSchema = new Schema<IInfNode>(
{
Node: String,
Influence: Number
},
{ _id: false }
);
const nemesisSchema = new Schema<INemesisDatabase>(
{
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<IAlignment>( const alignmentSchema = new Schema<IAlignment>(
{ {
Alignment: Number, Alignment: Number,
@ -1341,7 +1392,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//CorpusLich or GrineerLich //CorpusLich or GrineerLich
NemesisAbandonedRewards: { type: [String], default: [] }, NemesisAbandonedRewards: { type: [String], default: [] },
//CorpusLich\KuvaLich Nemesis: nemesisSchema,
NemesisHistory: [Schema.Types.Mixed], NemesisHistory: [Schema.Types.Mixed],
LastNemesisAllySpawnTime: Schema.Types.Mixed, LastNemesisAllySpawnTime: Schema.Types.Mixed,

View File

@ -69,6 +69,7 @@ import { missionInventoryUpdateController } from "@/src/controllers/api/missionI
import { modularWeaponCraftingController } from "@/src/controllers/api/modularWeaponCraftingController"; import { modularWeaponCraftingController } from "@/src/controllers/api/modularWeaponCraftingController";
import { modularWeaponSaleController } from "@/src/controllers/api/modularWeaponSaleController"; import { modularWeaponSaleController } from "@/src/controllers/api/modularWeaponSaleController";
import { nameWeaponController } from "@/src/controllers/api/nameWeaponController"; import { nameWeaponController } from "@/src/controllers/api/nameWeaponController";
import { nemesisController } from "@/src/controllers/api/nemesisController";
import { placeDecoInComponentController } from "@/src/controllers/api/placeDecoInComponentController"; import { placeDecoInComponentController } from "@/src/controllers/api/placeDecoInComponentController";
import { playerSkillsController } from "@/src/controllers/api/playerSkillsController"; import { playerSkillsController } from "@/src/controllers/api/playerSkillsController";
import { projectionManagerController } from "@/src/controllers/api/projectionManagerController"; import { projectionManagerController } from "@/src/controllers/api/projectionManagerController";
@ -203,6 +204,7 @@ apiRouter.post("/missionInventoryUpdate.php", missionInventoryUpdateController);
apiRouter.post("/modularWeaponCrafting.php", modularWeaponCraftingController); apiRouter.post("/modularWeaponCrafting.php", modularWeaponCraftingController);
apiRouter.post("/modularWeaponSale.php", modularWeaponSaleController); apiRouter.post("/modularWeaponSale.php", modularWeaponSaleController);
apiRouter.post("/nameWeapon.php", nameWeaponController); apiRouter.post("/nameWeapon.php", nameWeaponController);
apiRouter.post("/nemesis.php", nemesisController);
apiRouter.post("/placeDecoInComponent.php", placeDecoInComponentController); apiRouter.post("/placeDecoInComponent.php", placeDecoInComponentController);
apiRouter.post("/playerSkills.php", playerSkillsController); apiRouter.post("/playerSkills.php", playerSkillsController);
apiRouter.post("/projectionManager.php", projectionManagerController); apiRouter.post("/projectionManager.php", projectionManagerController);

View File

@ -70,6 +70,7 @@ export const getRandomWeightedRewardUc = <T extends { Rarity: TRarity }>(
return getRandomReward(resultPool); return getRandomReward(resultPool);
}; };
// Seeded RNG for internal usage. Based on recommendations in the ISO C standards.
export class CRng { export class CRng {
state: number; state: number;
@ -92,3 +93,21 @@ export class CRng {
return arr[Math.floor(this.random() * arr.length)]; return arr[Math.floor(this.random() * arr.length)];
} }
} }
// Seeded RNG for cases where we need identical results to the game client. Based on work by Donald Knuth.
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;
}
}

View File

@ -43,6 +43,7 @@ export interface IInventoryDatabase
| "Drones" | "Drones"
| "RecentVendorPurchases" | "RecentVendorPurchases"
| "NextRefill" | "NextRefill"
| "Nemesis"
| TEquipmentKey | TEquipmentKey
>, >,
InventoryDatabaseEquipment { InventoryDatabaseEquipment {
@ -71,6 +72,7 @@ export interface IInventoryDatabase
Drones: IDroneDatabase[]; Drones: IDroneDatabase[];
RecentVendorPurchases?: IRecentVendorPurchaseDatabase[]; RecentVendorPurchases?: IRecentVendorPurchaseDatabase[];
NextRefill?: Date; NextRefill?: Date;
Nemesis?: INemesisDatabase;
} }
export interface IQuestKeyDatabase { export interface IQuestKeyDatabase {
@ -288,7 +290,8 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
SeasonChallengeHistory: ISeasonChallenge[]; SeasonChallengeHistory: ISeasonChallenge[];
EquippedInstrument?: string; EquippedInstrument?: string;
InvasionChainProgress: IInvasionChainProgress[]; InvasionChainProgress: IInvasionChainProgress[];
NemesisHistory: INemesisHistory[]; Nemesis?: INemesisClient;
NemesisHistory: INemesisBaseClient[];
LastNemesisAllySpawnTime?: IMongoDate; LastNemesisAllySpawnTime?: IMongoDate;
Settings: ISettings; Settings: ISettings;
PersonalTechProjects: IPersonalTechProject[]; PersonalTechProjects: IPersonalTechProject[];
@ -782,38 +785,44 @@ export interface IMission extends IMissionDatabase {
RewardsCooldownTime?: IMongoDate; RewardsCooldownTime?: IMongoDate;
} }
export interface INemesisHistory { export interface INemesisBaseClient {
fp: number; fp: bigint;
manifest: Manifest; manifest: string;
KillingSuit: string; KillingSuit: string;
killingDamageType: number; killingDamageType: number;
ShoulderHelmet: string; ShoulderHelmet: string;
WeaponIdx: number;
AgentIdx: number; AgentIdx: number;
BirthNode: BirthNode; BirthNode: string;
Faction: string;
Rank: number; Rank: number;
k: boolean; k: boolean;
Traded: boolean;
d: IMongoDate; d: IMongoDate;
GuessHistory?: number[]; PrevOwners: number;
currentGuess?: number; SecondInCommand: boolean;
Traded?: boolean; Weakened: boolean;
PrevOwners?: number;
SecondInCommand?: boolean;
Faction?: string;
Weakened?: boolean;
} }
export enum BirthNode { export interface INemesisBaseDatabase extends Omit<INemesisBaseClient, "d"> {
SolNode181 = "SolNode181", d: Date;
SolNode4 = "SolNode4",
SolNode70 = "SolNode70",
SolNode76 = "SolNode76"
} }
export enum Manifest { export interface INemesisClient extends INemesisBaseClient {
LotusTypesEnemiesCorpusLawyersLawyerManifest = "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifest", InfNodes: IInfNode[];
LotusTypesGameNemesisKuvaLichKuvaLichManifest = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifest", HenchmenKilled: number;
LotusTypesGameNemesisKuvaLichKuvaLichManifestVersionThree = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionThree", HintProgress: number;
LotusTypesGameNemesisKuvaLichKuvaLichManifestVersionTwo = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionTwo" Hints: number[];
GuessHistory: number[];
}
export interface INemesisDatabase extends Omit<INemesisClient, "d"> {
d: Date;
}
export interface IInfNode {
Node: string;
Influence: number;
} }
export interface IPendingCouponDatabase { export interface IPendingCouponDatabase {