feat: start nemesis #1227

Merged
Sainan merged 7 commits from nemesis into main 2025-03-20 05:36:10 -07:00
8 changed files with 323 additions and 25 deletions
Showing only changes of commit 846906d390 - Show all commits

View File

@ -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<INemesisStartRequest>(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"
];

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(jsonSubstring) as T;
}; };
export const getSubstringFromKeyword = (str: string, keyword: string): string => { export const getSubstringFromKeyword = (str: string, keyword: string): string => {

View File

@ -10,6 +10,20 @@ 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.
{
// 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 "<JS_SUCKS>" + this.toString() + "</JS_SUCKS>";
};
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(`"<JS_SUCKS>`).join(``).split(`</JS_SUCKS>"`).join(``);
};
}
registerLogFileCreationListener(); registerLogFileCreationListener();
validateConfig(); validateConfig();

53
src/json-with-bigint.ts Normal file
View File

@ -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;
});
};

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

@ -92,3 +92,20 @@ export class CRng {
return arr[Math.floor(this.random() * arr.length)]; 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;
}
}

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 {