XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

SpaceNinjaServer

A simple server for a small space ninja game

公开
关注 0 Fork 1 Star 0
返回提交历史

XFEstudio/SpaceNinjaServer

feat: start nemesis (#1227)

Closes #446 As discussed there, some support for 64-bit integers without precision loss had to be hacked in. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/1227

0e1973e2
Sainan <sainan@calamity.inc>
提交于

代码差异

11 个文件 +290 -29
Modified package-lock.json +7 -0
@@ -14,6 +14,7 @@
14 14 "copyfiles": "^2.4.1",
15 15 "crc-32": "^1.2.2",
16 16 "express": "^5",
17 "json-with-bigint": "^3.2.1",
17 18 "mongoose": "^8.11.0",
18 19 "morgan": "^1.10.0",
19 20 "typescript": ">=5.5 <5.6.0",
@@ -2346,6 +2347,12 @@
2346 2347 "dev": true,
2347 2348 "license": "MIT"
2348 2349 },
2350 "node_modules/json-with-bigint": {
2351 "version": "3.2.1",
2352 "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.2.1.tgz",
2353 "integrity": "sha512-0f8RHpU1AwBFwIPmtm71W+cFxzlXdiBmzc3JqydsNDSKSAsr0Lso6KXRbz0h2LRwTIRiHAk/UaD+xaAN5f577w==",
2354 "license": "MIT"
2355 },
2349 2356 "node_modules/json5": {
2350 2357 "version": "2.2.3",
2351 2358 "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
Modified package.json +1 -0
@@ -19,6 +19,7 @@
19 19 "copyfiles": "^2.4.1",
20 20 "crc-32": "^1.2.2",
21 21 "express": "^5",
22 "json-with-bigint": "^3.2.1",
22 23 "mongoose": "^8.11.0",
23 24 "morgan": "^1.10.0",
24 25 "typescript": ">=5.5 <5.6.0",
Modified src/controllers/api/guildTechController.ts +2 -0
@@ -23,6 +23,7 @@ import { config } from "@/src/services/configService";
23 23 import { GuildPermission, ITechProjectClient, ITechProjectDatabase } from "@/src/types/guildTypes";
24 24 import { TGuildDatabaseDocument } from "@/src/models/guildModel";
25 25 import { toMongoDate } from "@/src/helpers/inventoryHelpers";
26 import { logger } from "@/src/utils/logger";
26 27
27 28 export const guildTechController: RequestHandler = async (req, res) => {
28 29 const accountId = await getAccountIdForRequest(req);
@@ -219,6 +220,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
219 220 await guild.save();
220 221 res.end();
221 222 } else {
223 logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
222 224 throw new Error(`unknown guildTech action: ${data.Action}`);
223 225 }
224 226 };
Modified src/controllers/api/infestedFoundryController.ts +1 -0
@@ -355,6 +355,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
355 355 }
356 356
357 357 default:
358 logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
358 359 throw new Error(`unhandled infestedFoundry mode: ${String(req.query.mode)}`);
359 360 }
360 361 };
Added src/controllers/api/nemesisController.ts +152 -0
@@ -0,0 +1,152 @@
1 import { getJSONfromString } from "@/src/helpers/stringHelpers";
2 import { getInventory } from "@/src/services/inventoryService";
3 import { getAccountIdForRequest } from "@/src/services/loginService";
4 import { SRng } from "@/src/services/rngService";
5 import { IMongoDate } from "@/src/types/commonTypes";
6 import { IInfNode } from "@/src/types/inventoryTypes/inventoryTypes";
7 import { logger } from "@/src/utils/logger";
8 import { RequestHandler } from "express";
9 import { ExportRegions } from "warframe-public-export-plus";
10
11 export const nemesisController: RequestHandler = async (req, res) => {
12 if ((req.query.mode as string) == "s") {
13 const accountId = await getAccountIdForRequest(req);
14 const inventory = await getInventory(accountId, "Nemesis NemesisAbandonedRewards");
15 const body = getJSONfromString<INemesisStartRequest>(String(req.body));
16
17 const infNodes: IInfNode[] = [];
18 for (const [key, value] of Object.entries(ExportRegions)) {
19 if (
20 value.systemIndex == 2 && // earth
21 value.nodeType != 3 && // not hub
22 value.nodeType != 7 && // not junction
23 value.missionIndex && // must have a mission type and not assassination
24 value.missionIndex != 28 && // not open world
25 value.missionIndex != 32 && // not railjack
26 value.missionIndex != 41 && // not saya's visions
27 value.name.indexOf("Archwing") == -1
28 ) {
29 //console.log(dict_en[value.name]);
30 infNodes.push({ Node: key, Influence: 1 });
31 }
32 }
33
34 let weapons: readonly string[];
35 if (body.target.manifest == "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix") {
36 weapons = kuvaLichVersionSixWeapons;
37 } else if (
38 body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour" ||
39 body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree"
40 ) {
41 weapons = corpusVersionThreeWeapons;
42 } else {
43 throw new Error(`unknown nemesis manifest: ${body.target.manifest}`);
44 }
45
46 body.target.fp = BigInt(body.target.fp);
47 const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1);
48 let weaponIdx = initialWeaponIdx;
49 do {
50 const weapon = weapons[weaponIdx];
51 if (!body.target.DisallowedWeapons.find(x => x == weapon)) {
52 break;
53 }
54 weaponIdx = (weaponIdx + 1) % weapons.length;
55 } while (weaponIdx != initialWeaponIdx);
56 inventory.Nemesis = {
57 fp: body.target.fp,
58 manifest: body.target.manifest,
59 KillingSuit: body.target.KillingSuit,
60 killingDamageType: body.target.killingDamageType,
61 ShoulderHelmet: body.target.ShoulderHelmet,
62 WeaponIdx: weaponIdx,
63 AgentIdx: body.target.AgentIdx,
64 BirthNode: body.target.BirthNode,
65 Faction: body.target.Faction,
66 Rank: 0,
67 k: false,
68 Traded: false,
69 d: new Date(),
70 InfNodes: infNodes,
71 GuessHistory: [],
72 Hints: [],
73 HintProgress: 0,
74 Weakened: body.target.Weakened,
75 PrevOwners: 0,
76 HenchmenKilled: 0,
77 SecondInCommand: body.target.SecondInCommand
78 };
79 inventory.NemesisAbandonedRewards = []; // unclear if we need to do this since the client also submits this with missionInventoryUpdate
80 await inventory.save();
81
82 res.json({
83 target: inventory.toJSON().Nemesis
84 });
85 } else {
86 logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
87 throw new Error(`unknown nemesis mode: ${String(req.query.mode)}`);
88 }
89 };
90
91 export interface INemesisStartRequest {
92 target: {
93 fp: number | bigint;
94 manifest: string;
95 KillingSuit: string;
96 killingDamageType: number;
97 ShoulderHelmet: string;
98 DisallowedWeapons: string[];
99 WeaponIdx: number;
100 AgentIdx: number;
101 BirthNode: string;
102 Faction: string;
103 Rank: number;
104 k: boolean;
105 Traded: boolean;
106 d: IMongoDate;
107 InfNodes: [];
108 GuessHistory: [];
109 Hints: [];
110 HintProgress: number;
111 Weakened: boolean;
112 PrevOwners: number;
113 HenchmenKilled: number;
114 SecondInCommand: boolean;
115 };
116 }
117
118 const kuvaLichVersionSixWeapons = [
119 "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon",
120 "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak",
121 "/Lotus/Weapons/Grineer/Melee/GrnKuvaLichScythe/GrnKuvaLichScytheWeapon",
122 "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Kohm/KuvaKohm",
123 "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Ogris/KuvaOgris",
124 "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Quartakk/KuvaQuartakk",
125 "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Tonkor/KuvaTonkor",
126 "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Brakk/KuvaBrakk",
127 "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Kraken/KuvaKraken",
128 "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Seer/KuvaSeer",
129 "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Stubba/KuvaStubba",
130 "/Lotus/Weapons/Grineer/HeavyWeapons/GrnHeavyGrenadeLauncher",
131 "/Lotus/Weapons/Grineer/LongGuns/GrnKuvaLichRifle/GrnKuvaLichRifleWeapon",
132 "/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon",
133 "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind",
134 "/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor",
135 "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon",
136 "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr",
137 "/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler",
138 "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek"
139 ];
140
141 const corpusVersionThreeWeapons = [
142 "/Lotus/Weapons/Corpus/LongGuns/CrpBriefcaseLauncher/CrpBriefcaseLauncher",
143 "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEArcaPlasmor/CrpBEArcaPlasmor",
144 "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEFluxRifle/CrpBEFluxRifle",
145 "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBETetra/CrpBETetra",
146 "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBECycron/CrpBECycron",
147 "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEDetron/CrpBEDetron",
148 "/Lotus/Weapons/Corpus/Pistols/CrpIgniterPistol/CrpIgniterPistol",
149 "/Lotus/Weapons/Corpus/Pistols/CrpBriefcaseAkimbo/CrpBriefcaseAkimboPistol",
150 "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon",
151 "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion"
152 ];
Modified src/helpers/stringHelpers.ts +3 -1
@@ -1,6 +1,8 @@
1 import { JSONParse } from "json-with-bigint";
2
1 3 export const getJSONfromString = <T>(str: string): T => {
2 4 const jsonSubstring = str.substring(0, str.lastIndexOf("}") + 1);
3 return JSON.parse(jsonSubstring) as T;
5 return JSONParse<T>(jsonSubstring);
4 6 };
5 7
6 8 export const getSubstringFromKeyword = (str: string, keyword: string): string => {
Modified src/index.ts +15 -0
@@ -10,6 +10,21 @@ import { config, validateConfig } from "./services/configService";
10 10 import { registerLogFileCreationListener } from "@/src/utils/logger";
11 11 import mongoose from "mongoose";
12 12
13 // Patch JSON.stringify to work flawlessly with Bigints. Yeah, it's not pretty.
14 // TODO: Might wanna use json-with-bigint if/when possible.
15 {
16 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
17 (BigInt.prototype as any).toJSON = function (): string {
18 // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
19 return "<BIGINT>" + this.toString() + "</BIGINT>";
20 };
21 const og_stringify = JSON.stringify;
22 // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
23 (JSON as any).stringify = (obj: any): string => {
24 return og_stringify(obj).split(`"<BIGINT>`).join(``).split(`</BIGINT>"`).join(``);
25 };
26 }
27
13 28 registerLogFileCreationListener();
14 29 validateConfig();
15 30
Modified src/models/inventoryModels/inventoryModel.ts +53 -2
@@ -79,7 +79,10 @@ import {
79 79 ICrewShipWeaponDatabase,
80 80 IRecentVendorPurchaseDatabase,
81 81 IVendorPurchaseHistoryEntryDatabase,
82 IVendorPurchaseHistoryEntryClient
82 IVendorPurchaseHistoryEntryClient,
83 INemesisDatabase,
84 INemesisClient,
85 IInfNode
83 86 } from "../../types/inventoryTypes/inventoryTypes";
84 87 import { IOid } from "../../types/commonTypes";
85 88 import {
@@ -1058,6 +1061,54 @@ const libraryDailyTaskInfoSchema = new Schema<ILibraryDailyTaskInfo>(
1058 1061 { _id: false }
1059 1062 );
1060 1063
1064 const infNodeSchema = new Schema<IInfNode>(
1065 {
1066 Node: String,
1067 Influence: Number
1068 },
1069 { _id: false }
1070 );
1071
1072 const nemesisSchema = new Schema<INemesisDatabase>(
1073 {
1074 fp: BigInt,
1075 manifest: String,
1076 KillingSuit: String,
1077 killingDamageType: Number,
1078 ShoulderHelmet: String,
1079 WeaponIdx: Number,
1080 AgentIdx: Number,
1081 BirthNode: String,
1082 Faction: String,
1083 Rank: Number,
1084 k: Boolean,
1085 Traded: Boolean,
1086 d: Date,
1087 PrevOwners: Number,
1088 SecondInCommand: Boolean,
1089 Weakened: Boolean,
1090 InfNodes: [infNodeSchema],
1091 HenchmenKilled: Number,
1092 HintProgress: Number,
1093 Hints: [Number],
1094 GuessHistory: [Number]
1095 },
1096 { _id: false }
1097 );
1098
1099 nemesisSchema.set("toJSON", {
1100 virtuals: true,
1101 transform(_doc, obj) {
1102 const db = obj as INemesisDatabase;
1103 const client = obj as INemesisClient;
1104
1105 client.d = toMongoDate(db.d);
1106
1107 delete obj._id;
1108 delete obj.__v;
1109 }
1110 });
1111
1061 1112 const alignmentSchema = new Schema<IAlignment>(
1062 1113 {
1063 1114 Alignment: Number,
@@ -1341,7 +1392,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
1341 1392
1342 1393 //CorpusLich or GrineerLich
1343 1394 NemesisAbandonedRewards: { type: [String], default: [] },
1344 //CorpusLich\KuvaLich
1395 Nemesis: nemesisSchema,
1345 1396 NemesisHistory: [Schema.Types.Mixed],
1346 1397 LastNemesisAllySpawnTime: Schema.Types.Mixed,
1347 1398
Modified src/routes/api.ts +2 -0
@@ -69,6 +69,7 @@ import { missionInventoryUpdateController } from "@/src/controllers/api/missionI
69 69 import { modularWeaponCraftingController } from "@/src/controllers/api/modularWeaponCraftingController";
70 70 import { modularWeaponSaleController } from "@/src/controllers/api/modularWeaponSaleController";
71 71 import { nameWeaponController } from "@/src/controllers/api/nameWeaponController";
72 import { nemesisController } from "@/src/controllers/api/nemesisController";
72 73 import { placeDecoInComponentController } from "@/src/controllers/api/placeDecoInComponentController";
73 74 import { playerSkillsController } from "@/src/controllers/api/playerSkillsController";
74 75 import { projectionManagerController } from "@/src/controllers/api/projectionManagerController";
@@ -204,6 +205,7 @@ apiRouter.post("/missionInventoryUpdate.php", missionInventoryUpdateController);
204 205 apiRouter.post("/modularWeaponCrafting.php", modularWeaponCraftingController);
205 206 apiRouter.post("/modularWeaponSale.php", modularWeaponSaleController);
206 207 apiRouter.post("/nameWeapon.php", nameWeaponController);
208 apiRouter.post("/nemesis.php", nemesisController);
207 209 apiRouter.post("/placeDecoInComponent.php", placeDecoInComponentController);
208 210 apiRouter.post("/playerSkills.php", playerSkillsController);
209 211 apiRouter.post("/projectionManager.php", projectionManagerController);
Modified src/services/rngService.ts +19 -0
@@ -70,6 +70,7 @@ export const getRandomWeightedRewardUc = <T extends { Rarity: TRarity }>(
70 70 return getRandomReward(resultPool);
71 71 };
72 72
73 // Seeded RNG for internal usage. Based on recommendations in the ISO C standards.
73 74 export class CRng {
74 75 state: number;
75 76
@@ -92,3 +93,21 @@ export class CRng {
92 93 return arr[Math.floor(this.random() * arr.length)];
93 94 }
94 95 }
96
97 // Seeded RNG for cases where we need identical results to the game client. Based on work by Donald Knuth.
98 export class SRng {
99 state: bigint;
100
101 constructor(seed: bigint) {
102 this.state = seed;
103 }
104
105 randomInt(min: number, max: number): number {
106 const diff = max - min;
107 if (diff != 0) {
108 this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn;
109 min += (Number(this.state >> 32n) & 0x3fffffff) % (diff + 1);
110 }
111 return min;
112 }
113 }
Modified src/types/inventoryTypes/inventoryTypes.ts +35 -26
@@ -43,6 +43,7 @@ export interface IInventoryDatabase
43 43 | "Drones"
44 44 | "RecentVendorPurchases"
45 45 | "NextRefill"
46 | "Nemesis"
46 47 | TEquipmentKey
47 48 >,
48 49 InventoryDatabaseEquipment {
@@ -71,6 +72,7 @@ export interface IInventoryDatabase
71 72 Drones: IDroneDatabase[];
72 73 RecentVendorPurchases?: IRecentVendorPurchaseDatabase[];
73 74 NextRefill?: Date;
75 Nemesis?: INemesisDatabase;
74 76 }
75 77
76 78 export interface IQuestKeyDatabase {
@@ -288,7 +290,8 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
288 290 SeasonChallengeHistory: ISeasonChallenge[];
289 291 EquippedInstrument?: string;
290 292 InvasionChainProgress: IInvasionChainProgress[];
291 NemesisHistory: INemesisHistory[];
293 Nemesis?: INemesisClient;
294 NemesisHistory: INemesisBaseClient[];
292 295 LastNemesisAllySpawnTime?: IMongoDate;
293 296 Settings: ISettings;
294 297 PersonalTechProjects: IPersonalTechProject[];
@@ -782,38 +785,44 @@ export interface IMission extends IMissionDatabase {
782 785 RewardsCooldownTime?: IMongoDate;
783 786 }
784 787
785 export interface INemesisHistory {
786 fp: number;
787 manifest: Manifest;
788 export interface INemesisBaseClient {
789 fp: bigint;
790 manifest: string;
788 791 KillingSuit: string;
789 792 killingDamageType: number;
790 793 ShoulderHelmet: string;
794 WeaponIdx: number;
791 795 AgentIdx: number;
792 BirthNode: BirthNode;
796 BirthNode: string;
797 Faction: string;
793 798 Rank: number;
794 799 k: boolean;
800 Traded: boolean;
795 801 d: IMongoDate;
796 GuessHistory?: number[];
797 currentGuess?: number;
798 Traded?: boolean;
799 PrevOwners?: number;
800 SecondInCommand?: boolean;
801 Faction?: string;
802 Weakened?: boolean;
803 }
804
805 export enum BirthNode {
806 SolNode181 = "SolNode181",
807 SolNode4 = "SolNode4",
808 SolNode70 = "SolNode70",
809 SolNode76 = "SolNode76"
810 }
811
812 export enum Manifest {
813 LotusTypesEnemiesCorpusLawyersLawyerManifest = "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifest",
814 LotusTypesGameNemesisKuvaLichKuvaLichManifest = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifest",
815 LotusTypesGameNemesisKuvaLichKuvaLichManifestVersionThree = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionThree",
816 LotusTypesGameNemesisKuvaLichKuvaLichManifestVersionTwo = "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionTwo"
802 PrevOwners: number;
803 SecondInCommand: boolean;
804 Weakened: boolean;
805 }
806
807 export interface INemesisBaseDatabase extends Omit<INemesisBaseClient, "d"> {
808 d: Date;
809 }
810
811 export interface INemesisClient extends INemesisBaseClient {
812 InfNodes: IInfNode[];
813 HenchmenKilled: number;
814 HintProgress: number;
815 Hints: number[];
816 GuessHistory: number[];
817 }
818
819 export interface INemesisDatabase extends Omit<INemesisClient, "d"> {
820 d: Date;
821 }
822
823 export interface IInfNode {
824 Node: string;
825 Influence: number;
817 826 }
818 827
819 828 export interface IPendingCouponDatabase {