返回提交历史
Modified
config.json.example
+1
-0
Modified
package-lock.json
+4
-4
Modified
package.json
+1
-1
Modified
src/controllers/api/inventoryController.ts
+12
-3
Modified
src/index.ts
+2
-1
Modified
src/models/inboxModel.ts
+5
-3
Modified
src/models/inventoryModels/inventoryModel.ts
+31
-8
Modified
src/services/configService.ts
+1
-0
Modified
src/services/configWatcherService.ts
+18
-0
Modified
src/services/inboxService.ts
+16
-0
Modified
src/services/missionInventoryUpdateService.ts
+71
-1
Modified
src/services/worldStateService.ts
+71
-0
Modified
src/types/inventoryTypes/inventoryTypes.ts
+14
-8
Modified
src/types/requestTypes.ts
+10
-0
Modified
src/types/worldStateTypes.ts
+6
-1
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
代码差异
15 个文件
+263
-30
@@ -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": "",
@@ -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",
@@ -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",
@@ -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
}
@@ -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)
@@ -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,
@@ -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,
@@ -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;
@@ -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
};
@@ -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
}
@@ -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
};
@@ -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);