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: galleon of ghouls (#2280)

Re #1103 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2280 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>

731ce6c2
Sainan <63328889+Sainan@users.noreply.github.com>
提交于

代码差异

15 个文件 +263 -30
Modified config.json.example +1 -0
@@ -69,6 +69,7 @@
69 69 "affinityBoost": false,
70 70 "resourceBoost": false,
71 71 "starDays": true,
72 "galleonOfGhouls": 0,
72 73 "eidolonOverride": "",
73 74 "vallisOverride": "",
74 75 "duviriOverride": "",
Modified package-lock.json +4 -4
@@ -21,7 +21,7 @@
21 21 "ncp": "^2.0.0",
22 22 "typescript": "^5.5",
23 23 "undici": "^7.10.0",
24 "warframe-public-export-plus": "^0.5.69",
24 "warframe-public-export-plus": "^0.5.70",
25 25 "warframe-riven-info": "^0.1.2",
26 26 "winston": "^3.17.0",
27 27 "winston-daily-rotate-file": "^5.0.0",
@@ -3396,9 +3396,9 @@
3396 3396 }
3397 3397 },
3398 3398 "node_modules/warframe-public-export-plus": {
3399 "version": "0.5.69",
3400 "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.69.tgz",
3401 "integrity": "sha512-vTU1tUzqpihzpseUSJMrM82pYbCDZCfW40jXIi+Ol9B3a3Acz0DccfP7i4eoXf7Abahu4H/sjRt/nSHLNBvLHA=="
3399 "version": "0.5.70",
3400 "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.70.tgz",
3401 "integrity": "sha512-d5dQ/a0rakQnW9tl1HitST8439jDvEgMhkkntQIw7HmdM7s7mvIxvaYSl5wjlYawpUVfGyvGBdZVoAJ7kkQRWw=="
3402 3402 },
3403 3403 "node_modules/warframe-riven-info": {
3404 3404 "version": "0.1.2",
Modified package.json +1 -1
@@ -33,7 +33,7 @@
33 33 "ncp": "^2.0.0",
34 34 "typescript": "^5.5",
35 35 "undici": "^7.10.0",
36 "warframe-public-export-plus": "^0.5.69",
36 "warframe-public-export-plus": "^0.5.70",
37 37 "warframe-riven-info": "^0.1.2",
38 38 "winston": "^3.17.0",
39 39 "winston-daily-rotate-file": "^5.0.0",
Modified src/controllers/api/inventoryController.ts +12 -3
@@ -22,7 +22,8 @@ import { getNemesisManifest } from "@/src/helpers/nemesisHelpers";
22 22 import { getPersonalRooms } from "@/src/services/personalRoomsService";
23 23 import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
24 24 import { Ship } from "@/src/models/shipModel";
25 import { toLegacyOid, version_compare } from "@/src/helpers/inventoryHelpers";
25 import { toLegacyOid, toOid, version_compare } from "@/src/helpers/inventoryHelpers";
26 import { Inbox } from "@/src/models/inboxModel";
26 27
27 28 export const inventoryController: RequestHandler = async (request, response) => {
28 29 const account = await getAccountForRequest(request);
@@ -128,13 +129,21 @@ export const getInventoryResponse = async (
128 129 xpBasedLevelCapDisabled: boolean,
129 130 buildLabel: string | undefined
130 131 ): Promise<IInventoryClient> => {
131 const [inventoryWithLoadOutPresets, ships] = await Promise.all([
132 const [inventoryWithLoadOutPresets, ships, latestMessage] = await Promise.all([
132 133 inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>("LoadOutPresets"),
133 Ship.find({ ShipOwnerId: inventory.accountOwnerId })
134 Ship.find({ ShipOwnerId: inventory.accountOwnerId }),
135 Inbox.findOne({ ownerId: inventory.accountOwnerId }, "_id").sort({ date: -1 })
134 136 ]);
135 137 const inventoryResponse = inventoryWithLoadOutPresets.toJSON<IInventoryClient>();
136 138 inventoryResponse.Ships = ships.map(x => x.toJSON<IShipInventory>());
137 139
140 // In case mission inventory update added an inbox message, we need to send the Mailbox part so the client knows to refresh it.
141 if (latestMessage) {
142 inventoryResponse.Mailbox = {
143 LastInboxId: toOid(latestMessage._id)
144 };
145 }
146
138 147 if (config.infiniteCredits) {
139 148 inventoryResponse.RegularCredits = 999999999;
140 149 }
Modified src/index.ts +2 -1
@@ -21,13 +21,14 @@ import mongoose from "mongoose";
21 21 import { JSONStringify } from "json-with-bigint";
22 22 import { startWebServer } from "./services/webService";
23 23
24 import { validateConfig } from "@/src/services/configWatcherService";
24 import { syncConfigWithDatabase, validateConfig } from "@/src/services/configWatcherService";
25 25 import { updateWorldStateCollections } from "./services/worldStateService";
26 26
27 27 // Patch JSON.stringify to work flawlessly with Bigints.
28 28 JSON.stringify = JSONStringify;
29 29
30 30 validateConfig();
31 syncConfigWithDatabase();
31 32
32 33 mongoose
33 34 .connect(config.mongodbUrl)
Modified src/models/inboxModel.ts +5 -3
@@ -27,11 +27,12 @@ export interface IMessage {
27 27 icon?: string;
28 28 highPriority?: boolean;
29 29 lowPrioNewPlayers?: boolean;
30 startDate?: Date;
31 endDate?: Date;
30 transmission?: string;
32 31 att?: string[];
33 32 countedAtt?: ITypeCount[];
34 transmission?: string;
33 startDate?: Date;
34 endDate?: Date;
35 goalTag?: string;
35 36 CrossPlatform?: boolean;
36 37 arg?: Arg[];
37 38 gifts?: IGift[];
@@ -107,6 +108,7 @@ const messageSchema = new Schema<IMessageDatabase>(
107 108 lowPrioNewPlayers: Boolean,
108 109 startDate: Date,
109 110 endDate: Date,
111 goalTag: String,
110 112 date: { type: Date, required: true },
111 113 r: Boolean,
112 114 CrossPlatform: Boolean,
Modified src/models/inventoryModels/inventoryModel.ts +31 -8
@@ -1,4 +1,4 @@
1 import { Document, HydratedDocument, Model, Schema, Types, model } from "mongoose";
1 import { Document, Model, Schema, Types, model } from "mongoose";
2 2 import {
3 3 IFlavourItem,
4 4 IRawUpgrade,
@@ -7,7 +7,6 @@ import {
7 7 IBooster,
8 8 IInventoryClient,
9 9 ISlots,
10 IMailboxDatabase,
11 10 IDuviriInfo,
12 11 IPendingRecipeDatabase,
13 12 IPendingRecipeClient,
@@ -54,7 +53,6 @@ import {
54 53 IUpgradeDatabase,
55 54 ICrewShipMemberDatabase,
56 55 ICrewShipMemberClient,
57 IMailboxClient,
58 56 TEquipmentKey,
59 57 equipmentKeys,
60 58 IKubrowPetDetailsDatabase,
@@ -99,7 +97,9 @@ import {
99 97 IAccolades,
100 98 IHubNpcCustomization,
101 99 ILotusCustomization,
102 IEndlessXpReward
100 IEndlessXpReward,
101 IPersonalGoalProgressDatabase,
102 IPersonalGoalProgressClient
103 103 } from "../../types/inventoryTypes/inventoryTypes";
104 104 import { IOid } from "../../types/commonTypes";
105 105 import {
@@ -371,7 +371,7 @@ FlavourItemSchema.set("toJSON", {
371 371 }
372 372 });
373 373
374 const MailboxSchema = new Schema<IMailboxDatabase>(
374 /*const MailboxSchema = new Schema<IMailboxDatabase>(
375 375 {
376 376 LastInboxId: Schema.Types.ObjectId
377 377 },
@@ -384,7 +384,7 @@ MailboxSchema.set("toJSON", {
384 384 delete mailboxDatabase.__v;
385 385 (returnedObject as IMailboxClient).LastInboxId = toOid(mailboxDatabase.LastInboxId);
386 386 }
387 });
387 });*/
388 388
389 389 const DuviriInfoSchema = new Schema<IDuviriInfo>(
390 390 {
@@ -457,6 +457,29 @@ const discoveredMarkerSchema = new Schema<IDiscoveredMarker>(
457 457 { _id: false }
458 458 );
459 459
460 const personalGoalProgressSchema = new Schema<IPersonalGoalProgressDatabase>(
461 {
462 Best: Number,
463 Count: Number,
464 Tag: String,
465 goalId: Types.ObjectId
466 },
467 { _id: false }
468 );
469
470 personalGoalProgressSchema.set("toJSON", {
471 virtuals: true,
472 transform(_doc, obj) {
473 const db = obj as IPersonalGoalProgressDatabase;
474 const client = obj as IPersonalGoalProgressClient;
475
476 client._id = toOid(db.goalId);
477
478 delete obj.goalId;
479 delete obj.__v;
480 }
481 });
482
460 483 const challengeProgressSchema = new Schema<IChallengeProgress>(
461 484 {
462 485 Progress: Number,
@@ -1630,7 +1653,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
1630 1653 //CompletedJobs: [Schema.Types.Mixed],
1631 1654
1632 1655 //Game mission\ivent score example "Tag": "WaterFight", "Best": 170, "Count": 1258,
1633 //PersonalGoalProgress: [Schema.Types.Mixed],
1656 PersonalGoalProgress: { type: [personalGoalProgressSchema], default: undefined },
1634 1657
1635 1658 //Setting interface Style
1636 1659 ThemeStyle: String,
@@ -1701,7 +1724,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
1701 1724 //Unknown and system
1702 1725 DuviriInfo: DuviriInfoSchema,
1703 1726 LastInventorySync: Schema.Types.ObjectId,
1704 Mailbox: MailboxSchema,
1727 //Mailbox: MailboxSchema,
1705 1728 HandlerPoints: Number,
1706 1729 ChallengesFixVersion: Number,
1707 1730 PlayedParkourTutorial: Boolean,
Modified src/services/configService.ts +1 -0
@@ -76,6 +76,7 @@ export interface IConfig {
76 76 affinityBoost?: boolean;
77 77 resourceBoost?: boolean;
78 78 starDays?: boolean;
79 galleonOfGhouls?: number;
79 80 eidolonOverride?: string;
80 81 vallisOverride?: string;
81 82 duviriOverride?: string;
Modified src/services/configWatcherService.ts +18 -0
@@ -3,6 +3,7 @@ import fsPromises from "fs/promises";
3 3 import { logger } from "../utils/logger";
4 4 import { config, configPath, loadConfig } from "./configService";
5 5 import { getWebPorts, sendWsBroadcast, startWebServer, stopWebServer } from "./webService";
6 import { Inbox } from "../models/inboxModel";
6 7
7 8 let amnesia = false;
8 9 fs.watchFile(configPath, (now, then) => {
@@ -22,6 +23,7 @@ fs.watchFile(configPath, (now, then) => {
22 23 process.exit(1);
23 24 }
24 25 validateConfig();
26 syncConfigWithDatabase();
25 27
26 28 const webPorts = getWebPorts();
27 29 if (config.httpPort != webPorts.http || config.httpsPort != webPorts.https) {
@@ -51,6 +53,15 @@ export const validateConfig = (): void => {
51 53 }
52 54 }
53 55 }
56 if (
57 config.worldState?.galleonOfGhouls &&
58 config.worldState.galleonOfGhouls != 1 &&
59 config.worldState.galleonOfGhouls != 2 &&
60 config.worldState.galleonOfGhouls != 3
61 ) {
62 config.worldState.galleonOfGhouls = 0;
63 modified = true;
64 }
54 65 if (modified) {
55 66 logger.info(`Updating config file to fix some issues with it.`);
56 67 void saveConfig();
@@ -61,3 +72,10 @@ export const saveConfig = async (): Promise<void> => {
61 72 amnesia = true;
62 73 await fsPromises.writeFile(configPath, JSON.stringify(config, null, 2));
63 74 };
75
76 export const syncConfigWithDatabase = (): void => {
77 // Event messages are deleted after endDate. Since we don't use beginDate/endDate and instead have config toggles, we need to delete the messages once those bools are false.
78 if (!config.worldState?.galleonOfGhouls) {
79 void Inbox.deleteMany({ goalTag: "GalleonRobbery" }).then(() => {}); // For some reason, I can't just do `Inbox.deleteMany(...)`; it needs this whole circus.
80 }
81 };
Modified src/services/inboxService.ts +16 -0
@@ -54,6 +54,22 @@ export const createNewEventMessages = async (req: Request): Promise<void> => {
54 54 });
55 55 }
56 56
57 // BUG: Deleting the inbox message manually means it'll just be automatically re-created. This is because we don't use startDate/endDate for these config-toggled events.
58 if (config.worldState?.galleonOfGhouls) {
59 if (!(await Inbox.exists({ ownerId: account._id, goalTag: "GalleonRobbery" }))) {
60 newEventMessages.push({
61 sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek",
62 sub: "/Lotus/Language/Events/GalleonRobberyIntroMsgTitle",
63 msg: "/Lotus/Language/Events/GalleonRobberyIntroMsgDesc",
64 icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png",
65 transmission: "/Lotus/Sounds/Dialog/GalleonOfGhouls/DGhoulsWeekOneInbox0010VayHek",
66 att: ["/Lotus/Upgrades/Skins/Events/OgrisOldSchool"],
67 startDate: new Date(),
68 goalTag: "GalleonRobbery"
69 });
70 }
71 }
72
57 73 if (newEventMessages.length === 0) {
58 74 return;
59 75 }
Modified src/services/missionInventoryUpdateService.ts +71 -1
@@ -46,7 +46,7 @@ import {
46 46 import { updateQuestKey } from "@/src/services/questService";
47 47 import { Types } from "mongoose";
48 48 import { IAffiliationMods, IInventoryChanges } from "@/src/types/purchaseTypes";
49 import { fromStoreItem, getLevelKeyRewards, toStoreItem } from "@/src/services/itemDataService";
49 import { fromStoreItem, getLevelKeyRewards, isStoreItem, toStoreItem } from "@/src/services/itemDataService";
50 50 import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
51 51 import { getEntriesUnsafe } from "@/src/utils/ts-utils";
52 52 import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
@@ -609,6 +609,47 @@ export const addMissionInventoryUpdates = async (
609 609 inventoryChanges.RegularCredits -= value;
610 610 break;
611 611 }
612 case "GoalProgress": {
613 for (const uploadProgress of value) {
614 const goal = getWorldState().Goals.find(x => x._id.$oid == uploadProgress._id.$oid);
615 if (goal && goal.Personal) {
616 inventory.PersonalGoalProgress ??= [];
617 const goalProgress = inventory.PersonalGoalProgress.find(x => x.goalId.equals(goal._id.$oid));
618 if (goalProgress) {
619 goalProgress.Best = Math.max(goalProgress.Best, uploadProgress.Best);
620 goalProgress.Count += uploadProgress.Count;
621 } else {
622 inventory.PersonalGoalProgress.push({
623 Best: uploadProgress.Best,
624 Count: uploadProgress.Count,
625 Tag: goal.Tag,
626 goalId: new Types.ObjectId(goal._id.$oid)
627 });
628
629 if (
630 goal.Reward &&
631 goal.Reward.items &&
632 goal.MissionKeyName &&
633 goal.MissionKeyName in goalMessagesByKey
634 ) {
635 // Send reward via inbox
636 const info = goalMessagesByKey[goal.MissionKeyName];
637 await createMessage(inventory.accountOwnerId, [
638 {
639 sndr: info.sndr,
640 msg: info.msg,
641 att: goal.Reward.items.map(x => (isStoreItem(x) ? fromStoreItem(x) : x)),
642 sub: info.sub,
643 icon: info.icon,
644 highPriority: true
645 }
646 ]);
647 }
648 }
649 }
650 }
651 break;
652 }
612 653 case "InvasionProgress": {
613 654 for (const clientProgress of value) {
614 655 const dbProgress = inventory.QualifyingInvasions.find(x =>
@@ -962,6 +1003,14 @@ export const addMissionRewards = async (
962 1003
963 1004 let missionCompletionCredits = 0;
964 1005 //inventory change is what the client has not rewarded itself, also the client needs to know the credit changes for display
1006
1007 if (rewardInfo.goalId) {
1008 const goal = getWorldState().Goals.find(x => x._id.$oid == rewardInfo.goalId);
1009 if (goal?.MissionKeyName) {
1010 levelKeyName = goal.MissionKeyName;
1011 }
1012 }
1013
965 1014 if (levelKeyName) {
966 1015 const fixedLevelRewards = getLevelKeyRewards(levelKeyName);
967 1016 //logger.debug(`fixedLevelRewards ${fixedLevelRewards}`);
@@ -1978,3 +2027,24 @@ const getHexBounties = (seed: number): { nodes: string[]; buddies: string[] } =>
1978 2027 }
1979 2028 return { nodes, buddies };
1980 2029 };*/
2030
2031 const goalMessagesByKey: Record<string, { sndr: string; msg: string; sub: string; icon: string }> = {
2032 "/Lotus/Types/Keys/GalleonRobberyAlert": {
2033 sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek",
2034 msg: "/Lotus/Language/Messages/GalleonRobbery2025RewardMsgA",
2035 sub: "/Lotus/Language/Messages/GalleonRobbery2025MissionTitleA",
2036 icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png"
2037 },
2038 "/Lotus/Types/Keys/GalleonRobberyAlertB": {
2039 sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek",
2040 msg: "/Lotus/Language/Messages/GalleonRobbery2025RewardMsgB",
2041 sub: "/Lotus/Language/Messages/GalleonRobbery2025MissionTitleB",
2042 icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png"
2043 },
2044 "/Lotus/Types/Keys/GalleonRobberyAlertC": {
2045 sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek",
2046 msg: "/Lotus/Language/Messages/GalleonRobbery2025RewardMsgC",
2047 sub: "/Lotus/Language/Messages/GalleonRobbery2025MissionTitleC",
2048 icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png"
2049 }
2050 };
Modified src/services/worldStateService.ts +71 -0
@@ -1149,6 +1149,77 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
1149 1149 Node: "SolarisUnitedHub1"
1150 1150 });
1151 1151 }
1152 // The client gets kinda confused when multiple goals have the same tag, so considering these mutually exclusive.
1153 if (config.worldState?.galleonOfGhouls == 1) {
1154 worldState.Goals.push({
1155 _id: { $oid: "6814ddf00000000000000000" },
1156 Activation: { $date: { $numberLong: "1746198000000" } },
1157 Expiry: { $date: { $numberLong: "2000000000000" } },
1158 Count: 0,
1159 Goal: 1,
1160 Success: 0,
1161 Personal: true,
1162 Bounty: true,
1163 ClampNodeScores: true,
1164 Node: "EventNode19",
1165 MissionKeyName: "/Lotus/Types/Keys/GalleonRobberyAlert",
1166 Desc: "/Lotus/Language/Events/GalleonRobberyEventMissionTitle",
1167 Icon: "/Lotus/Interface/Icons/Player/GalleonRobberiesEvent.png",
1168 Tag: "GalleonRobbery",
1169 Reward: {
1170 items: [
1171 "/Lotus/StoreItems/Types/Recipes/Weapons/GrnChainSawTonfaBlueprint",
1172 "/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
1173 ]
1174 }
1175 });
1176 } else if (config.worldState?.galleonOfGhouls == 2) {
1177 worldState.Goals.push({
1178 _id: { $oid: "681e18700000000000000000" },
1179 Activation: { $date: { $numberLong: "1746802800000" } },
1180 Expiry: { $date: { $numberLong: "2000000000000" } },
1181 Count: 0,
1182 Goal: 1,
1183 Success: 0,
1184 Personal: true,
1185 Bounty: true,
1186 ClampNodeScores: true,
1187 Node: "EventNode28",
1188 MissionKeyName: "/Lotus/Types/Keys/GalleonRobberyAlertB",
1189 Desc: "/Lotus/Language/Events/GalleonRobberyEventMissionTitle",
1190 Icon: "/Lotus/Interface/Icons/Player/GalleonRobberiesEvent.png",
1191 Tag: "GalleonRobbery",
1192 Reward: {
1193 items: [
1194 "/Lotus/StoreItems/Types/Recipes/Weapons/MortiforShieldAndSwordBlueprint",
1195 "/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
1196 ]
1197 }
1198 });
1199 } else if (config.worldState?.galleonOfGhouls == 3) {
1200 worldState.Goals.push({
1201 _id: { $oid: "682752f00000000000000000" },
1202 Activation: { $date: { $numberLong: "1747407600000" } },
1203 Expiry: { $date: { $numberLong: "2000000000000" } },
1204 Count: 0,
1205 Goal: 1,
1206 Success: 0,
1207 Personal: true,
1208 Bounty: true,
1209 ClampNodeScores: true,
1210 Node: "EventNode19",
1211 MissionKeyName: "/Lotus/Types/Keys/GalleonRobberyAlertC",
1212 Desc: "/Lotus/Language/Events/GalleonRobberyEventMissionTitle",
1213 Icon: "/Lotus/Interface/Icons/Player/GalleonRobberiesEvent.png",
1214 Tag: "GalleonRobbery",
1215 Reward: {
1216 items: [
1217 "/Lotus/Types/StoreItems/Packages/EventCatalystReactorBundle",
1218 "/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"
1219 ]
1220 }
1221 });
1222 }
1152 1223
1153 1224 // Nightwave Challenges
1154 1225 const nightwaveSyndicateTag = getNightwaveSyndicateTag(buildLabel);
Modified src/types/inventoryTypes/inventoryTypes.ts +14 -8
Modified src/types/requestTypes.ts +10 -0
Modified src/types/worldStateTypes.ts +6 -1