返回提交历史
Modified
src/constants/timeConstants.ts
+2
-0
Modified
src/controllers/api/getPastWeeklyChallengesController.ts
+2
-2
Added
src/services/conquestService.ts
+425
-0
Modified
src/services/worldStateService.ts
+26
-13
Modified
src/types/inventoryTypes/inventoryTypes.ts
+2
-1
Modified
src/types/worldStateTypes.ts
+41
-1
XFEstudio/SpaceNinjaServer
feat: server-side conquest generation for U40 and above (#2962)
Closes #2932 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2962 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
b1c1b56d
代码差异
6 个文件
+498
-17
@@ -1,3 +1,5 @@
1
export const EPOCH = 1734307200_000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be the start of winter in 1999 iteration 0
2
1
3
const millisecondsPerSecond = 1000;
2
4
const secondsPerMinute = 60;
3
5
const minutesPerHour = 60;
@@ -1,8 +1,8 @@
1
1
import type { RequestHandler } from "express";
2
2
import { getAccountIdForRequest } from "../../services/loginService.ts";
3
3
import { getInventory } from "../../services/inventoryService.ts";
4
import { EPOCH, getSeasonChallengePools, getWorldState, pushWeeklyActs } from "../../services/worldStateService.ts";
5
import { unixTimesInMs } from "../../constants/timeConstants.ts";
4
import { getSeasonChallengePools, getWorldState, pushWeeklyActs } from "../../services/worldStateService.ts";
5
import { EPOCH, unixTimesInMs } from "../../constants/timeConstants.ts";
6
6
import type { ISeasonChallenge } from "../../types/worldStateTypes.ts";
7
7
import { ExportChallenges } from "warframe-public-export-plus";
8
8
@@ -0,0 +1,425 @@
1
import type { TFaction, TMissionType } from "warframe-public-export-plus";
2
import type { CalendarSeasonType, IConquest, IConquestMission, TConquestType } from "../types/worldStateTypes.ts";
3
import { mixSeeds, SRng } from "./rngService.ts";
4
import { EPOCH } from "../constants/timeConstants.ts";
5
6
const missionAndFactionTypes: Record<TConquestType, Partial<Record<TMissionType, TFaction[]>>> = {
7
CT_LAB: {
8
MT_EXTERMINATION: ["FC_MITW"],
9
MT_SURVIVAL: ["FC_MITW"],
10
MT_ALCHEMY: ["FC_MITW"],
11
MT_DEFENSE: ["FC_MITW"],
12
MT_ARTIFACT: ["FC_MITW"]
13
},
14
CT_HEX: {
15
MT_EXTERMINATION: ["FC_SCALDRA", "FC_TECHROT"],
16
MT_SURVIVAL: ["FC_SCALDRA", "FC_TECHROT"],
17
MT_DEFENSE: ["FC_SCALDRA"],
18
MT_ENDLESS_CAPTURE: ["FC_TECHROT"]
19
}
20
};
21
22
const assassinationFactionOptions: Record<TConquestType, TFaction[]> = {
23
CT_LAB: ["FC_MITW"],
24
CT_HEX: ["FC_SCALDRA"]
25
};
26
27
type TConquestDifficulty = "CD_NORMAL" | "CD_HARD";
28
29
interface IConquestConditional {
30
tag: string;
31
missionType?: TMissionType;
32
conquest?: TConquestType;
33
difficulty?: TConquestDifficulty;
34
season?: CalendarSeasonType;
35
}
36
37
const deviations: readonly IConquestConditional[] = [
38
{
39
tag: "AlchemicalShields",
40
missionType: "MT_ALCHEMY"
41
},
42
{
43
tag: "ContaminationZone",
44
missionType: "MT_SURVIVAL",
45
conquest: "CT_HEX"
46
},
47
{
48
tag: "DoubleTrouble",
49
missionType: "MT_ARTIFACT"
50
},
51
{
52
tag: "EscalateImmediately",
53
missionType: "MT_EXTERMINATION",
54
conquest: "CT_HEX"
55
},
56
{
57
tag: "EximusGrenadiers",
58
missionType: "MT_ALCHEMY"
59
},
60
{
61
tag: "FortifiedFoes",
62
missionType: "MT_EXTERMINATION"
63
},
64
{
65
tag: "FragileNodes",
66
missionType: "MT_ARTIFACT"
67
},
68
{
69
tag: "GrowingIncursion",
70
missionType: "MT_EXTERMINATION",
71
conquest: "CT_LAB"
72
},
73
{
74
tag: "HarshWords",
75
missionType: "MT_DEFENSE",
76
conquest: "CT_LAB"
77
},
78
{
79
tag: "HighScalingLegacyte",
80
missionType: "MT_ENDLESS_CAPTURE",
81
conquest: "CT_HEX"
82
},
83
{
84
tag: "DoubleTroubleLegacyte",
85
missionType: "MT_ENDLESS_CAPTURE",
86
conquest: "CT_HEX"
87
},
88
{
89
tag: "HostileSecurity",
90
missionType: "MT_DEFENSE",
91
conquest: "CT_LAB"
92
},
93
{
94
tag: "InfiniteTide",
95
missionType: "MT_ASSASSINATION",
96
conquest: "CT_LAB"
97
},
98
{
99
tag: "LostInTranslation",
100
missionType: "MT_DEFENSE",
101
conquest: "CT_LAB"
102
},
103
{
104
tag: "MutatedEnemies",
105
missionType: "MT_ENDLESS_CAPTURE",
106
conquest: "CT_HEX"
107
},
108
{
109
tag: "NecramechActivation",
110
missionType: "MT_SURVIVAL",
111
conquest: "CT_LAB"
112
},
113
{
114
tag: "Reinforcements",
115
missionType: "MT_ASSASSINATION",
116
conquest: "CT_LAB"
117
},
118
{
119
tag: "StickyFingers",
120
missionType: "MT_ARTIFACT",
121
conquest: "CT_LAB"
122
},
123
{
124
tag: "TankStrongArmor",
125
missionType: "MT_ASSASSINATION",
126
conquest: "CT_HEX"
127
},
128
{
129
tag: "TankReinforcements",
130
missionType: "MT_ASSASSINATION",
131
conquest: "CT_HEX"
132
},
133
{
134
tag: "TankSuperToxic",
135
missionType: "MT_ASSASSINATION",
136
conquest: "CT_HEX"
137
},
138
{
139
tag: "TechrotConjunction",
140
missionType: "MT_SURVIVAL",
141
conquest: "CT_HEX"
142
},
143
{
144
tag: "UnpoweredCapsules",
145
missionType: "MT_SURVIVAL",
146
conquest: "CT_LAB"
147
},
148
{
149
tag: "VolatileGrenades",
150
missionType: "MT_ALCHEMY"
151
},
152
{
153
tag: "GestatingTumors",
154
missionType: "MT_SURVIVAL",
155
conquest: "CT_HEX"
156
},
157
{
158
tag: "ChemicalNoise",
159
missionType: "MT_DEFENSE",
160
conquest: "CT_HEX"
161
},
162
{
163
tag: "ExplosiveEnergy",
164
missionType: "MT_DEFENSE",
165
conquest: "CT_HEX"
166
},
167
{
168
tag: "DisruptiveSounds",
169
missionType: "MT_DEFENSE",
170
conquest: "CT_HEX"
171
}
172
];
173
174
const risks: readonly IConquestConditional[] = [
175
{
176
tag: "Voidburst"
177
},
178
{
179
tag: "RegeneratingEnemies"
180
},
181
{
182
tag: "VoidAberration"
183
},
184
{
185
tag: "ShieldedFoes"
186
},
187
{
188
tag: "PointBlank"
189
},
190
{
191
tag: "Deflectors",
192
conquest: "CT_LAB"
193
},
194
{
195
tag: "AcceleratedEnemies"
196
},
197
{
198
tag: "DrainingResiduals"
199
},
200
{
201
tag: "Quicksand"
202
},
203
{
204
tag: "AntiMaterialWeapons",
205
conquest: "CT_LAB"
206
},
207
{
208
tag: "ExplosiveCrawlers",
209
conquest: "CT_LAB"
210
},
211
{
212
tag: "EMPBlackHole",
213
conquest: "CT_LAB"
214
},
215
{
216
tag: "ArtilleryBeacons",
217
conquest: "CT_HEX"
218
},
219
{
220
tag: "InfectedTechrot",
221
conquest: "CT_HEX"
222
},
223
{
224
tag: "BalloonFest",
225
conquest: "CT_HEX"
226
},
227
{
228
tag: "MiasmiteHive",
229
conquest: "CT_HEX"
230
},
231
{
232
tag: "CompetitionSpillover",
233
conquest: "CT_HEX"
234
},
235
{
236
tag: "HostileOvergrowth",
237
conquest: "CT_HEX"
238
},
239
{
240
tag: "MurmurIncursion",
241
conquest: "CT_HEX"
242
},
243
{
244
tag: "FactionSwarm_Techrot",
245
conquest: "CT_HEX",
246
difficulty: "CD_NORMAL"
247
},
248
{
249
tag: "FactionSwarm_Scaldra",
250
conquest: "CT_HEX",
251
difficulty: "CD_NORMAL"
252
},
253
{
254
tag: "HeavyWarfare",
255
conquest: "CT_HEX"
256
},
257
{
258
tag: "ArcadeAutomata",
259
conquest: "CT_HEX",
260
difficulty: "CD_NORMAL"
261
},
262
{
263
tag: "EfervonFog",
264
conquest: "CT_HEX"
265
},
266
{
267
tag: "WinterFrost",
268
conquest: "CT_HEX",
269
season: "CST_WINTER"
270
},
271
{
272
tag: "JadeSpring",
273
conquest: "CT_HEX",
274
season: "CST_SPRING"
275
},
276
{
277
tag: "ExplosiveSummer",
278
conquest: "CT_HEX",
279
season: "CST_SUMMER"
280
},
281
{
282
tag: "FallFog",
283
conquest: "CT_HEX",
284
season: "CST_FALL"
285
}
286
];
287
288
const filterConditionals = (
289
arr: readonly IConquestConditional[],
290
missionType: TMissionType | null,
291
conquest: TConquestType | null,
292
difficulty: TConquestDifficulty | null,
293
season: CalendarSeasonType | null
294
): string[] => {
295
const applicable = [];
296
for (const cond of arr) {
297
if (
298
(!cond.missionType || cond.missionType == missionType) &&
299
(!cond.conquest || cond.conquest == conquest) &&
300
(!cond.difficulty || cond.difficulty == difficulty) &&
301
(!cond.season || cond.season == season)
302
) {
303
applicable.push(cond.tag);
304
}
305
}
306
return applicable;
307
};
308
309
const buildMission = (
310
rng: SRng,
311
conquest: TConquestType,
312
missionType: TMissionType,
313
faction: TFaction,
314
season: CalendarSeasonType | null
315
): IConquestMission => {
316
const deviation = rng.randomElement(filterConditionals(deviations, missionType, conquest, null, season))!;
317
const easyRisk = rng.randomElement(filterConditionals(risks, missionType, conquest, "CD_NORMAL", season))!;
318
const hardRiskOptions = filterConditionals(risks, missionType, conquest, "CD_HARD", season);
319
{
320
const i = hardRiskOptions.indexOf(easyRisk);
321
if (i != -1) {
322
hardRiskOptions.splice(i, 1);
323
}
324
}
325
const hardRisk = rng.randomElement(hardRiskOptions)!;
326
return {
327
faction,
328
missionType,
329
difficulties: [
330
{
331
type: "CD_NORMAL",
332
deviation,
333
risks: [easyRisk]
334
},
335
{
336
type: "CD_HARD",
337
deviation,
338
risks: [easyRisk, hardRisk]
339
}
340
]
341
};
342
};
343
344
const conquestStartingDay: Record<TConquestType, number> = {
345
CT_LAB: 3703,
346
CT_HEX: 4053
347
};
348
349
// This function produces identical results to clients pre-40.0.0.
350
const getFrameVariables = (conquestType: TConquestType, time: number): [string, string, string, string] => {
351
const day = Math.floor((time - 1391990400_000) / 86400_000) - conquestStartingDay[conquestType];
352
const week = Math.floor(day / 7) + 1;
353
const frameVariables = [
354
"Framecurse",
355
"Knifestep",
356
"Exhaustion",
357
"Gearless",
358
"TimeDilation",
359
"Armorless",
360
"Starvation",
361
"ShieldDelay",
362
"Withering",
363
"ContactDamage",
364
"AbilityLockout",
365
"OperatorLockout",
366
"EnergyStarved",
367
"OverSensitive",
368
"AntiGuard",
369
"DecayingFlesh",
370
"VoidEnergyOverload",
371
"DullBlades",
372
"Undersupplied"
373
];
374
const mag = Math.floor(frameVariables.length / 4);
375
const rng = new SRng(conquestStartingDay[conquestType] + Math.floor(week / mag));
376
rng.shuffleArray(frameVariables);
377
const i = week % mag;
378
return [frameVariables[i], frameVariables[i + 1], frameVariables[i + 2], frameVariables[i + 3]];
379
};
380
381
export const getConquest = (
382
conquestType: TConquestType,
383
week: number,
384
season: CalendarSeasonType | null
385
): IConquest => {
386
const rng = new SRng(mixSeeds(conquestStartingDay[conquestType], week));
387
388
const missions: IConquestMission[] = [];
389
{
390
const missionOptions = Object.entries(missionAndFactionTypes[conquestType]);
391
{
392
const i = rng.randomInt(0, missionOptions.length - 1);
393
const [missionType, factionOptions] = missionOptions.splice(i, 1)[0];
394
missions.push(
395
buildMission(rng, conquestType, missionType as TMissionType, rng.randomElement(factionOptions)!, season)
396
);
397
}
398
{
399
const i = rng.randomInt(0, missionOptions.length - 1);
400
const [missionType, factionOptions] = missionOptions.splice(i, 1)[0];
401
missions.push(
402
buildMission(rng, conquestType, missionType as TMissionType, rng.randomElement(factionOptions)!, season)
403
);
404
}
405
missionOptions.push(["MT_ASSASSINATION", assassinationFactionOptions[conquestType]]);
406
{
407
const i = rng.randomInt(0, missionOptions.length - 1);
408
const [missionType, factionOptions] = missionOptions.splice(i, 1)[0];
409
missions.push(
410
buildMission(rng, conquestType, missionType as TMissionType, rng.randomElement(factionOptions)!, season)
411
);
412
}
413
}
414
415
const weekStart = EPOCH + week * 604800000;
416
const weekEnd = weekStart + 604800000;
417
return {
418
Activation: { $date: { $numberLong: weekStart.toString() } },
419
Expiry: { $date: { $numberLong: weekEnd.toString() } },
420
Type: conquestType,
421
Missions: missions,
422
Variables: getFrameVariables(conquestType, weekStart),
423
RandomSeed: rng.randomInt(0, 1_000_000)
424
};
425
};
@@ -10,7 +10,7 @@ import invasionNodes from "../../static/fixed_responses/worldState/invasionNodes
10
10
import invasionRewards from "../../static/fixed_responses/worldState/invasionRewards.json" with { type: "json" };
11
11
import pvpChallenges from "../../static/fixed_responses/worldState/pvpChallenges.json" with { type: "json" };
12
12
import { buildConfig } from "./buildConfigService.ts";
13
import { unixTimesInMs } from "../constants/timeConstants.ts";
13
import { EPOCH, unixTimesInMs } from "../constants/timeConstants.ts";
14
14
import { config } from "./configService.ts";
15
15
import { getRandomElement, getRandomInt, sequentiallyUniqueRandomElement, SRng } from "./rngService.ts";
16
16
import type { IMissionReward, IRegion, TFaction } from "warframe-public-export-plus";
@@ -41,6 +41,7 @@ import type {
41
41
import { toMongoDate, toOid, version_compare } from "../helpers/inventoryHelpers.ts";
42
42
import { logger } from "../utils/logger.ts";
43
43
import { DailyDeal, Fissure } from "../models/worldStateModel.ts";
44
import { getConquest } from "./conquestService.ts";
44
45
45
46
const sortieBosses = [
46
47
"SORTIE_BOSS_HYENA",
@@ -276,8 +277,6 @@ const microplanetEndlessJobs: readonly string[] = [
276
277
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessPurifyBounty"
277
278
];
278
279
279
export const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0
280
281
280
const isBeforeNextExpectedWorldStateRefresh = (nowMs: number, thenMs: number): boolean => {
282
281
return nowMs + 300_000 > thenMs;
283
282
};
@@ -3469,6 +3468,18 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
3469
3468
}
3470
3469
}
3471
3470
3471
// Void Storms
3472
const hour = Math.trunc(timeMs / unixTimesInMs.hour);
3473
const overLastHourStormExpiry = hour * unixTimesInMs.hour + 10 * unixTimesInMs.minute;
3474
const thisHourStormActivation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute;
3475
if (overLastHourStormExpiry > timeMs) {
3476
pushVoidStorms(worldState.VoidStorms, hour - 2);
3477
}
3478
pushVoidStorms(worldState.VoidStorms, hour - 1);
3479
if (isBeforeNextExpectedWorldStateRefresh(timeMs, thisHourStormActivation)) {
3480
pushVoidStorms(worldState.VoidStorms, hour);
3481
}
3482
3472
3483
// Sortie & syndicate missions cycling every day (at 16:00 or 17:00 UTC depending on if London, OT is observing DST)
3473
3484
{
3474
3485
const rollover = getSortieTime(day);
@@ -3551,16 +3562,18 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
3551
3562
worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1));
3552
3563
}
3553
3564
3554
// Void Storms
3555
const hour = Math.trunc(timeMs / unixTimesInMs.hour);
3556
const overLastHourStormExpiry = hour * unixTimesInMs.hour + 10 * unixTimesInMs.minute;
3557
const thisHourStormActivation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute;
3558
if (overLastHourStormExpiry > timeMs) {
3559
pushVoidStorms(worldState.VoidStorms, hour - 2);
3560
}
3561
pushVoidStorms(worldState.VoidStorms, hour - 1);
3562
if (isBeforeNextExpectedWorldStateRefresh(timeMs, thisHourStormActivation)) {
3563
pushVoidStorms(worldState.VoidStorms, hour);
3565
if (!buildLabel || version_compare(buildLabel, "2025.10.14.16.10") >= 0) {
3566
worldState.Conquests = [];
3567
{
3568
const season = (["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"] as const)[week % 4];
3569
worldState.Conquests.push(getConquest("CT_LAB", week, null));
3570
worldState.Conquests.push(getConquest("CT_HEX", week, season));
3571
}
3572
if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
3573
const season = (["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"] as const)[(week + 1) % 4];
3574
worldState.Conquests.push(getConquest("CT_LAB", week, null));
3575
worldState.Conquests.push(getConquest("CT_HEX", week, season));
3576
}
3564
3577
}
3565
3578
3566
3579
// Sentient Anomaly + Xtra Cheese cycles
@@ -14,6 +14,7 @@ import type { IOrbiterClient } from "../personalRoomsTypes.ts";
14
14
import type { ICountedStoreItem } from "warframe-public-export-plus";
15
15
import type { IEquipmentClient, IEquipmentDatabase, ITraits } from "../equipmentTypes.ts";
16
16
import type { ILoadOutPresets } from "../saveLoadoutTypes.ts";
17
import type { CalendarSeasonType } from "../worldStateTypes.ts";
17
18
18
19
export type InventoryDatabaseEquipment = {
19
20
[_ in TEquipmentKey]: IEquipmentDatabase[];
@@ -1180,7 +1181,7 @@ export interface IMarker {
1180
1181
}
1181
1182
1182
1183
export interface ISeasonProgress {
1183
SeasonType: "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL";
1184
SeasonType: CalendarSeasonType;
1184
1185
LastCompletedDayIdx: number;
1185
1186
LastCompletedChallengeDayIdx: number;
1186
1187
ActivatedChallenges: string[];
@@ -32,6 +32,7 @@ export interface IWorldState {
32
32
ActiveChallenges: ISeasonChallenge[];
33
33
};
34
34
KnownCalendarSeasons: ICalendarSeason[];
35
Conquests?: IConquest[];
35
36
Tmp?: string;
36
37
}
37
38
@@ -352,10 +353,11 @@ export interface ISeasonChallenge {
352
353
Challenge: string;
353
354
}
354
355
356
export type CalendarSeasonType = "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL";
355
357
export interface ICalendarSeason {
356
358
Activation: IMongoDate;
357
359
Expiry: IMongoDate;
358
Season: "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL";
360
Season: CalendarSeasonType;
359
361
Days: ICalendarDay[];
360
362
YearIteration: number;
361
363
Version: number;
@@ -416,6 +418,33 @@ export interface IGameMarketCategory {
416
418
Items?: string[];
417
419
}
418
420
421
// >= 40.0.0
422
export type TConquestType = "CT_LAB" | "CT_HEX";
423
export interface IConquest {
424
Activation: IMongoDate;
425
Expiry: IMongoDate;
426
Type: TConquestType;
427
Missions: IConquestMission[];
428
Variables: [string, string, string, string];
429
RandomSeed: number;
430
}
431
export interface IConquestMission {
432
faction: TFaction;
433
missionType: TMissionType;
434
difficulties: [
435
{
436
type: "CD_NORMAL";
437
deviation: string;
438
risks: [string];
439
},
440
{
441
type: "CD_HARD";
442
deviation: string;
443
risks: [string, string];
444
}
445
];
446
}
447
419
448
export interface ITmp {
420
449
cavabegin: string;
421
450
PurchasePlatformLockEnabled: boolean; // Seems unused
@@ -423,6 +452,8 @@ export interface ITmp {
423
452
ennnd?: boolean; // True if 1999 demo is available (no effect for >=38.6.0)
424
453
mbrt?: boolean; // Related to mobile app rating request
425
454
fbst: IFbst;
455
lqo?: IConquestOverride;
456
hqo?: IConquestOverride;
426
457
sfn: number;
427
458
edg?: TCircuitGameMode[]; // The Circuit game modes overwrite
428
459
}
@@ -451,3 +482,12 @@ interface IFbst {
451
482
e: number;
452
483
n: number;
453
484
}
485
486
// < 40.0.0
487
interface IConquestOverride {
488
mt?: string[]; // mission types but "Exterminate" instead of "MT_EXTERMINATION", etc. and "DualDefense" instead of "Defense" for hex conquest
489
mv?: string[];
490
mf?: number[]; // hex conquest only
491
c?: [string, string][];
492
fv?: string[];
493
}