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

XFESpaceNinjaServer

A simple server for a small space ninja game

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

XFEstudio/XFESpaceNinjaServer

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
Sainan <63328889+Sainan@users.noreply.github.com>
提交于

代码差异

6 个文件 +498 -17
Modified src/constants/timeConstants.ts +2 -0
@@ -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;
Modified src/controllers/api/getPastWeeklyChallengesController.ts +2 -2
@@ -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
Added src/services/conquestService.ts +425 -0
@@ -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 };
Modified src/services/worldStateService.ts +26 -13
@@ -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
Modified src/types/inventoryTypes/inventoryTypes.ts +2 -1
@@ -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[];
Modified src/types/worldStateTypes.ts +41 -1
@@ -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 }