返回提交历史
Modified
src/controllers/custom/deleteAccountController.ts
+3
-1
Modified
src/controllers/stats/uploadController.ts
+9
-1
Modified
src/controllers/stats/viewController.ts
+13
-11
Added
src/models/statsModel.ts
+101
-0
Modified
src/services/loginService.ts
+2
-0
Added
src/services/statsService.ts
+283
-0
Modified
src/types/statTypes.ts
+90
-5
XFEstudio/XFESpaceNinjaServer
feat: initial stats save (#884)
Closes #203 Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/884 Reviewed-by: Sainan <sainan@calamity.inc> Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
13c68a75
代码差异
7 个文件
+501
-18
@@ -6,6 +6,7 @@ import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
6
6
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
7
7
import { PersonalRooms } from "@/src/models/personalRoomsModel";
8
8
import { Ship } from "@/src/models/shipModel";
9
import { Stats } from "@/src/models/statsModel";
9
10
10
11
export const deleteAccountController: RequestHandler = async (req, res) => {
11
12
const accountId = await getAccountIdForRequest(req);
@@ -15,7 +16,8 @@ export const deleteAccountController: RequestHandler = async (req, res) => {
15
16
Inventory.deleteOne({ accountOwnerId: accountId }),
16
17
Loadout.deleteOne({ loadoutOwnerId: accountId }),
17
18
PersonalRooms.deleteOne({ personalRoomsOwnerId: accountId }),
18
Ship.deleteOne({ ShipOwnerId: accountId })
19
Ship.deleteOne({ ShipOwnerId: accountId }),
20
Stats.deleteOne({ accountOwnerId: accountId })
19
21
]);
20
22
res.end();
21
23
};
@@ -1,6 +1,14 @@
1
import { getJSONfromString } from "@/src/helpers/stringHelpers";
2
import { getAccountIdForRequest } from "@/src/services/loginService";
3
import { getStats, uploadStats } from "@/src/services/statsService";
4
import { IStatsUpload } from "@/src/types/statTypes";
1
5
import { RequestHandler } from "express";
2
6
3
const uploadController: RequestHandler = (_req, res) => {
7
const uploadController: RequestHandler = async (req, res) => {
8
const payload = getJSONfromString<IStatsUpload>(String(req.body));
9
const accountId = await getAccountIdForRequest(req);
10
const playerStats = await getStats(accountId);
11
await uploadStats(playerStats, payload);
4
12
res.status(200).end();
5
13
};
6
14
@@ -1,31 +1,33 @@
1
1
import { RequestHandler } from "express";
2
2
import { getAccountIdForRequest } from "@/src/services/loginService";
3
import { IStatsView } from "@/src/types/statTypes";
4
3
import { config } from "@/src/services/configService";
5
4
import allScans from "@/static/fixed_responses/allScans.json";
6
5
import { ExportEnemies } from "warframe-public-export-plus";
7
6
import { getInventory } from "@/src/services/inventoryService";
7
import { getStats } from "@/src/services/statsService";
8
import { IStatsView } from "@/src/types/statTypes";
8
9
9
10
const viewController: RequestHandler = async (req, res) => {
10
11
const accountId = await getAccountIdForRequest(req);
11
12
const inventory = await getInventory(accountId, "XPInfo");
13
const playerStats = await getStats(accountId);
12
14
13
const responseJson: IStatsView = {};
14
responseJson.Weapons = [];
15
const responseJson: IStatsView = playerStats.toJSON();
16
responseJson.Weapons ??= [];
15
17
for (const item of inventory.XPInfo) {
16
responseJson.Weapons.push({
17
type: item.ItemType,
18
xp: item.XP
19
});
18
const weaponIndex = responseJson.Weapons.findIndex(element => element.type == item.ItemType);
19
if (weaponIndex !== -1) {
20
responseJson.Weapons[weaponIndex].xp == item.XP;
21
} else {
22
responseJson.Weapons.push({ type: item.ItemType, xp: item.XP });
23
}
20
24
}
21
25
if (config.unlockAllScans) {
22
26
const scans = new Set(allScans);
23
27
for (const type of Object.keys(ExportEnemies.avatars)) {
24
if (!scans.has(type)) {
25
scans.add(type);
26
}
28
if (!scans.has(type)) scans.add(type);
27
29
}
28
responseJson.Scans = [];
30
responseJson.Scans ??= [];
29
31
for (const type of scans) {
30
32
responseJson.Scans.push({ type: type, scans: 9999 });
31
33
}
@@ -0,0 +1,101 @@
1
import { Document, Schema, Types, model } from "mongoose";
2
import { IEnemy, IMission, IScan, ITutorial, IAbility, IWeapon, IStatsDatabase } from "@/src/types/statTypes";
3
4
const abilitySchema = new Schema<IAbility>(
5
{
6
type: { type: String, required: true },
7
used: Number
8
},
9
{ _id: false }
10
);
11
12
const enemySchema = new Schema<IEnemy>(
13
{
14
type: { type: String, required: true },
15
executions: Number,
16
headshots: Number,
17
kills: Number,
18
assists: Number,
19
deaths: Number
20
},
21
{ _id: false }
22
);
23
24
const missionSchema = new Schema<IMission>(
25
{
26
type: { type: String, required: true },
27
highScore: Number
28
},
29
{ _id: false }
30
);
31
32
const scanSchema = new Schema<IScan>(
33
{
34
type: { type: String, required: true },
35
scans: Number
36
},
37
{ _id: false }
38
);
39
40
const tutorialSchema = new Schema<ITutorial>(
41
{
42
stage: Number
43
},
44
{ _id: false }
45
);
46
47
const weaponSchema = new Schema<IWeapon>(
48
{
49
type: { type: String, required: true },
50
equipTime: Number,
51
hits: Number,
52
kills: Number,
53
xp: Number,
54
assists: Number,
55
headshots: Number,
56
fired: Number
57
},
58
{ _id: false }
59
);
60
61
const statsSchema = new Schema<IStatsDatabase>({
62
accountOwnerId: { type: Schema.Types.ObjectId, required: true },
63
CiphersSolved: Number,
64
CiphersFailed: Number,
65
CipherTime: Number,
66
Weapons: { type: [weaponSchema], default: [] },
67
Enemies: { type: [enemySchema], default: [] },
68
MeleeKills: Number,
69
MissionsCompleted: Number,
70
MissionsQuit: Number,
71
MissionsFailed: Number,
72
TimePlayedSec: Number,
73
PickupCount: Number,
74
Tutorial: { type: Map, of: tutorialSchema, default: {} },
75
Abilities: { type: [abilitySchema], default: [] },
76
Rating: Number,
77
Income: Number,
78
Rank: Number,
79
PlayerLevel: Number,
80
Scans: { type: [scanSchema], default: [] },
81
Missions: { type: [missionSchema], default: [] },
82
Deaths: Number,
83
HealCount: Number,
84
ReviveCount: Number
85
});
86
87
statsSchema.set("toJSON", {
88
transform(_document, returnedObject) {
89
delete returnedObject._id;
90
delete returnedObject.__v;
91
delete returnedObject.accountOwnerId;
92
}
93
});
94
95
export const Stats = model<IStatsDatabase>("Stats", statsSchema);
96
97
// eslint-disable-next-line @typescript-eslint/ban-types
98
export type TStatsDatabaseDocument = Document<unknown, {}, IStatsDatabase> & {
99
_id: Types.ObjectId;
100
__v: number;
101
} & IStatsDatabase;
@@ -7,6 +7,7 @@ import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
7
7
import { PersonalRooms } from "@/src/models/personalRoomsModel";
8
8
import { Request } from "express";
9
9
import { config } from "@/src/services/configService";
10
import { createStats } from "@/src/services/statsService";
10
11
11
12
export const isCorrectPassword = (requestPassword: string, databasePassword: string): boolean => {
12
13
return requestPassword === databasePassword;
@@ -24,6 +25,7 @@ export const createAccount = async (accountData: IDatabaseAccount): Promise<IDat
24
25
const shipId = await createShip(account._id);
25
26
await createInventory(account._id, { loadOutPresetId: loadoutId, ship: shipId });
26
27
await createPersonalRooms(account._id, shipId);
28
await createStats(account._id.toString());
27
29
return account.toJSON();
28
30
} catch (error) {
29
31
if (error instanceof Error) {
@@ -0,0 +1,283 @@
1
import { Stats, TStatsDatabaseDocument } from "@/src/models/statsModel";
2
import { IStatsUpload } from "@/src/types/statTypes";
3
4
export const createStats = async (accountId: string): Promise<TStatsDatabaseDocument> => {
5
const stats = new Stats({ accountOwnerId: accountId });
6
await stats.save();
7
return stats;
8
};
9
10
export const getStats = async (accountOwnerId: string): Promise<TStatsDatabaseDocument> => {
11
let stats = await Stats.findOne({ accountOwnerId: accountOwnerId });
12
13
if (!stats) stats = await createStats(accountOwnerId);
14
15
return stats;
16
};
17
18
export const uploadStats = async (playerStats: TStatsDatabaseDocument, payload: IStatsUpload): Promise<void> => {
19
if (payload.add) {
20
const {
21
MISSION_COMPLETE,
22
PICKUP_ITEM,
23
SCAN,
24
USE_ABILITY,
25
FIRE_WEAPON,
26
HIT_ENTITY_ITEM,
27
HEADSHOT_ITEM,
28
KILL_ENEMY_ITEM,
29
KILL_ENEMY,
30
EXECUTE_ENEMY,
31
HEADSHOT,
32
DIE,
33
MELEE_KILL,
34
INCOME,
35
CIPHER
36
} = payload.add;
37
38
if (MISSION_COMPLETE) {
39
for (const [key, value] of Object.entries(MISSION_COMPLETE)) {
40
switch (key) {
41
case "GS_SUCCESS":
42
playerStats.MissionsCompleted ??= 0;
43
playerStats.MissionsCompleted += value;
44
break;
45
case "GS_QUIT":
46
playerStats.MissionsQuit ??= 0;
47
playerStats.MissionsQuit += value;
48
break;
49
case "GS_FAILURE":
50
playerStats.MissionsFailed ??= 0;
51
playerStats.MissionsFailed += value;
52
break;
53
}
54
}
55
}
56
57
if (PICKUP_ITEM) {
58
for (const value of Object.values(PICKUP_ITEM)) {
59
playerStats.PickupCount ??= 0;
60
playerStats.PickupCount += value;
61
}
62
}
63
64
if (SCAN) {
65
playerStats.Scans ??= [];
66
for (const [key, scans] of Object.entries(SCAN)) {
67
const scan = playerStats.Scans.find(element => element.type === key);
68
if (scan) {
69
scan.scans ??= 0;
70
scan.scans += scans;
71
} else {
72
playerStats.Scans.push({ type: key, scans });
73
}
74
}
75
}
76
77
if (USE_ABILITY) {
78
playerStats.Abilities ??= [];
79
for (const [key, used] of Object.entries(USE_ABILITY)) {
80
const ability = playerStats.Abilities.find(element => element.type === key);
81
if (ability) {
82
ability.used ??= 0;
83
ability.used += used;
84
} else {
85
playerStats.Abilities.push({ type: key, used });
86
}
87
}
88
}
89
90
if (FIRE_WEAPON) {
91
playerStats.Weapons ??= [];
92
for (const [key, fired] of Object.entries(FIRE_WEAPON)) {
93
const weapon = playerStats.Weapons.find(element => element.type === key);
94
if (weapon) {
95
weapon.fired ??= 0;
96
weapon.fired += fired;
97
} else {
98
playerStats.Weapons.push({ type: key, fired });
99
}
100
}
101
}
102
103
if (HIT_ENTITY_ITEM) {
104
playerStats.Weapons ??= [];
105
for (const [key, hits] of Object.entries(HIT_ENTITY_ITEM)) {
106
const weapon = playerStats.Weapons.find(element => element.type === key);
107
if (weapon) {
108
weapon.hits ??= 0;
109
weapon.hits += hits;
110
} else {
111
playerStats.Weapons.push({ type: key, hits });
112
}
113
}
114
}
115
116
if (HEADSHOT_ITEM) {
117
playerStats.Weapons ??= [];
118
for (const [key, headshots] of Object.entries(HEADSHOT_ITEM)) {
119
const weapon = playerStats.Weapons.find(element => element.type === key);
120
if (weapon) {
121
weapon.headshots ??= 0;
122
weapon.headshots += headshots;
123
} else {
124
playerStats.Weapons.push({ type: key, headshots });
125
}
126
}
127
}
128
129
if (KILL_ENEMY_ITEM) {
130
playerStats.Weapons ??= [];
131
for (const [key, kills] of Object.entries(KILL_ENEMY_ITEM)) {
132
const weapon = playerStats.Weapons.find(element => element.type === key);
133
if (weapon) {
134
weapon.kills ??= 0;
135
weapon.kills += kills;
136
} else {
137
playerStats.Weapons.push({ type: key, kills });
138
}
139
}
140
}
141
142
if (KILL_ENEMY) {
143
playerStats.Enemies ??= [];
144
for (const [key, kills] of Object.entries(KILL_ENEMY)) {
145
const enemy = playerStats.Enemies.find(element => element.type === key);
146
if (enemy) {
147
enemy.kills ??= 0;
148
enemy.kills += kills;
149
} else {
150
playerStats.Enemies.push({ type: key, kills });
151
}
152
}
153
}
154
155
if (EXECUTE_ENEMY) {
156
playerStats.Enemies ??= [];
157
for (const [key, executions] of Object.entries(EXECUTE_ENEMY)) {
158
const enemy = playerStats.Enemies.find(element => element.type === key);
159
if (enemy) {
160
enemy.executions ??= 0;
161
enemy.executions += executions;
162
} else {
163
playerStats.Enemies.push({ type: key, executions });
164
}
165
}
166
}
167
168
if (HEADSHOT) {
169
playerStats.Enemies ??= [];
170
for (const [key, headshots] of Object.entries(HEADSHOT)) {
171
const enemy = playerStats.Enemies.find(element => element.type === key);
172
if (enemy) {
173
enemy.headshots ??= 0;
174
enemy.headshots += headshots;
175
} else {
176
playerStats.Enemies.push({ type: key, headshots });
177
}
178
}
179
}
180
181
if (DIE) {
182
playerStats.Enemies ??= [];
183
for (const [key, deaths] of Object.entries(DIE)) {
184
playerStats.Deaths ??= 0;
185
playerStats.Deaths += deaths;
186
const enemy = playerStats.Enemies.find(element => element.type === key);
187
if (enemy) {
188
enemy.deaths ??= 0;
189
enemy.deaths += deaths;
190
} else {
191
playerStats.Enemies.push({ type: key, deaths });
192
}
193
}
194
}
195
196
if (MELEE_KILL) {
197
playerStats.MeleeKills ??= 0;
198
for (const kills of Object.values(MELEE_KILL)) {
199
playerStats.MeleeKills += kills;
200
}
201
}
202
203
if (INCOME) {
204
playerStats.Income ??= 0;
205
playerStats.Income += INCOME;
206
}
207
208
if (CIPHER) {
209
if (CIPHER["0"] > 0) {
210
playerStats.CiphersFailed ??= 0;
211
playerStats.CiphersFailed += CIPHER["0"];
212
}
213
if (CIPHER["1"] > 0) {
214
playerStats.CiphersSolved ??= 0;
215
playerStats.CiphersSolved += CIPHER["1"];
216
}
217
}
218
}
219
220
if (payload.timers) {
221
const { EQUIP_WEAPON, CURRENT_MISSION_TIME, CIPHER_TIME } = payload.timers;
222
223
if (EQUIP_WEAPON) {
224
playerStats.Weapons ??= [];
225
for (const [key, equipTime] of Object.entries(EQUIP_WEAPON)) {
226
const weapon = playerStats.Weapons.find(element => element.type === key);
227
if (weapon) {
228
weapon.equipTime ??= 0;
229
weapon.equipTime += equipTime;
230
} else {
231
playerStats.Weapons.push({ type: key, equipTime });
232
}
233
}
234
}
235
236
if (CURRENT_MISSION_TIME) {
237
playerStats.TimePlayedSec ??= 0;
238
playerStats.TimePlayedSec += CURRENT_MISSION_TIME;
239
}
240
241
if (CIPHER_TIME) {
242
playerStats.CipherTime ??= 0;
243
playerStats.CipherTime += CIPHER_TIME;
244
}
245
}
246
247
if (payload.max) {
248
const { WEAPON_XP, MISSION_SCORE } = payload.max;
249
250
if (WEAPON_XP) {
251
playerStats.Weapons ??= [];
252
for (const [key, xp] of Object.entries(WEAPON_XP)) {
253
const weapon = playerStats.Weapons.find(element => element.type === key);
254
if (weapon) {
255
weapon.xp = xp;
256
} else {
257
playerStats.Weapons.push({ type: key, xp });
258
}
259
}
260
}
261
262
if (MISSION_SCORE) {
263
playerStats.Missions ??= [];
264
for (const [key, highScore] of Object.entries(MISSION_SCORE)) {
265
const mission = playerStats.Missions.find(element => element.type === key);
266
if (mission) {
267
mission.highScore = highScore;
268
} else {
269
playerStats.Missions.push({ type: key, highScore });
270
}
271
}
272
}
273
}
274
275
if (payload.set) {
276
const { ELO_RATING, RANK, PLAYER_LEVEL } = payload.set;
277
if (ELO_RATING) playerStats.Rating = ELO_RATING;
278
if (RANK) playerStats.Rank = RANK;
279
if (PLAYER_LEVEL) playerStats.PlayerLevel = PLAYER_LEVEL;
280
}
281
282
await playerStats.save();
283
};
@@ -1,3 +1,5 @@
1
import { Types } from "mongoose";
2
1
3
export interface IStatsView {
2
4
CiphersSolved?: number;
3
5
CiphersFailed?: number;
@@ -23,28 +25,32 @@ export interface IStatsView {
23
25
ReviveCount?: number;
24
26
}
25
27
28
export interface IStatsDatabase extends IStatsView {
29
accountOwnerId: Types.ObjectId;
30
}
31
26
32
export interface IAbility {
27
used: number;
28
33
type: string;
34
used: number;
29
35
}
30
36
31
37
export interface IEnemy {
38
type: string;
32
39
executions?: number;
33
40
headshots?: number;
34
41
kills?: number;
35
type: string;
36
42
assists?: number;
37
43
deaths?: number;
38
44
}
39
45
40
46
export interface IMission {
41
highScore: number;
42
47
type: string;
48
highScore: number;
43
49
}
44
50
45
51
export interface IScan {
46
scans: number;
47
52
type: string;
53
scans: number;
48
54
}
49
55
50
56
export interface ITutorial {
@@ -52,12 +58,91 @@ export interface ITutorial {
52
58
}
53
59
54
60
export interface IWeapon {
61
type: string;
55
62
equipTime?: number;
56
63
hits?: number;
57
64
kills?: number;
58
65
xp?: number;
59
66
assists?: number;
60
type: string;
61
67
headshots?: number;
62
68
fired?: number;
63
69
}
70
71
export interface IStatsUpload {
72
displayName: string;
73
guildId?: string;
74
PS?: string;
75
add?: IStatsAdd;
76
set?: IStatsSet;
77
max?: IStatsMax;
78
timers?: IStatsTimers;
79
}
80
81
export interface IStatsAdd {
82
GEAR_USED?: IUploadEntry;
83
SCAN?: IUploadEntry;
84
MISSION_COMPLETE?: IUploadEntry;
85
HEADSHOT_ITEM?: IUploadEntry;
86
HEADSHOT?: IUploadEntry;
87
PLAYER_COUNT?: IUploadEntry;
88
HOST_MIGRATION?: IUploadEntry;
89
PICKUP_ITEM?: IUploadEntry;
90
FIRE_WEAPON?: IUploadEntry;
91
HIT_ENTITY_ITEM?: IUploadEntry;
92
DESTROY_DECORATION?: IUploadEntry;
93
KILL_ENEMY?: IUploadEntry;
94
TAKE_DAMAGE?: IUploadEntry;
95
SQUAD_KILL_ENEMY?: IUploadEntry;
96
RECEIVE_UPGRADE?: IUploadEntry;
97
USE_ABILITY?: IUploadEntry;
98
SQUAD_VIP_KILL?: IUploadEntry;
99
HEAL_BUDDY?: IUploadEntry;
100
INCOME?: number;
101
CIPHER?: IUploadEntry;
102
EQUIP_COSMETIC?: IUploadEntry;
103
EQUIP_UPGRADE?: IUploadEntry;
104
KILL_BOSS?: IUploadEntry;
105
MISSION_TYPE?: IUploadEntry;
106
MISSION_FACTION?: IUploadEntry;
107
MISSION_PLAYED?: IUploadEntry;
108
MISSION_PLAYED_TIME?: IUploadEntry;
109
MEDALS_TOP?: IUploadEntry;
110
INPUT_ACTIVITY_TIME?: IUploadEntry;
111
KILL_ENEMY_ITEM?: IUploadEntry;
112
TAKE_DAMAGE_ITEM?: IUploadEntry;
113
SQUAD_KILL_ENEMY_ITEM?: IUploadEntry;
114
MELEE_KILL?: IUploadEntry;
115
SQUAD_MELEE_KILL?: IUploadEntry;
116
MELEE_KILL_ITEM?: IUploadEntry;
117
SQUAD_MELEE_KILL_ITEM?: IUploadEntry;
118
DIE?: IUploadEntry;
119
DIE_ITEM?: IUploadEntry;
120
EXECUTE_ENEMY?: IUploadEntry;
121
EXECUTE_ENEMY_ITEM?: IUploadEntry;
122
}
123
124
export interface IUploadEntry {
125
[key: string]: number;
126
}
127
128
export interface IStatsMax {
129
WEAPON_XP?: IUploadEntry;
130
MISSION_SCORE?: IUploadEntry;
131
}
132
133
export interface IStatsSet {
134
ELO_RATING?: number;
135
RANK?: number;
136
PLAYER_LEVEL?: number;
137
}
138
139
export interface IStatsTimers {
140
IN_SHIP_TIME?: number;
141
IN_SHIP_VIEW_TIME?: IUploadEntry;
142
EQUIP_WEAPON?: IUploadEntry;
143
MISSION_TIME?: IUploadEntry;
144
REGION_TIME?: IUploadEntry;
145
PLATFORM_TIME?: IUploadEntry;
146
CURRENT_MISSION_TIME?: number;
147
CIPHER_TIME?: number;
148
}