返回提交历史
Modified
package-lock.json
+7
-0
Modified
package.json
+1
-0
Modified
src/controllers/api/guildTechController.ts
+2
-0
Modified
src/controllers/api/infestedFoundryController.ts
+1
-0
Added
src/controllers/api/nemesisController.ts
+152
-0
Modified
src/helpers/stringHelpers.ts
+3
-1
Modified
src/index.ts
+15
-0
Modified
src/models/inventoryModels/inventoryModel.ts
+53
-2
Modified
src/routes/api.ts
+2
-0
Modified
src/services/rngService.ts
+19
-0
Modified
src/types/inventoryTypes/inventoryTypes.ts
+35
-26
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
代码差异
11 个文件
+290
-29
@@ -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",
@@ -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",
@@ -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
};
@@ -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
};
@@ -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
];
@@ -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 => {
@@ -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
@@ -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
@@ -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);
@@ -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
}
@@ -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 {