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: retroactive clan cheats (#3694)

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

08af0c2f
Sainan <63328889+Sainan@users.noreply.github.com>
提交于

代码差异

9 个文件 +188 -14
Added src/controllers/custom/retroactivelyApplyGuildCheatController.ts +25 -0
@@ -0,0 +1,25 @@
1 import type { RequestHandler } from "express";
2 import { getAccountIdForRequest } from "../../services/loginService.ts";
3 import { clanLockCheats } from "../../services/clanCheatsService.ts";
4 import type { IGuildCheats } from "../../types/guildTypes.ts";
5 import { getGuildForRequest } from "../../services/guildService.ts";
6 import { GuildMember } from "../../models/guildModel.ts";
7 import { broadcastControlMessages } from "../../services/arbiterService.ts";
8
9 export const retroactivelyApplyGuildCheatController: RequestHandler = async (req, res) => {
10 const accountId = await getAccountIdForRequest(req);
11 const guild = await getGuildForRequest(req, accountId);
12 const member = await GuildMember.findOne({ accountId: accountId, guildId: guild._id });
13 if (member && member.rank <= 1) {
14 const meta = clanLockCheats[req.query.cheat as string as keyof IGuildCheats]!;
15 const msgs = meta.cleanupGuild(guild);
16 if (!meta.isGuildInIdealState(guild)) {
17 throw new Error(`cleanupGuild for ${req.query.cheat as string} does not satsify its isGuildInIdealState`);
18 }
19 await guild.save();
20 if (msgs.length) {
21 broadcastControlMessages(`${guild._id.toString().toUpperCase()}_0`, msgs);
22 }
23 }
24 res.end();
25 };
Modified src/controllers/custom/setGuildCheatController.ts +9 -6
@@ -1,4 +1,5 @@
1 1 import { GuildMember } from "../../models/guildModel.ts";
2 import { clanLockCheats } from "../../services/clanCheatsService.ts";
2 3 import { getGuildForRequest } from "../../services/guildService.ts";
3 4 import { getAccountIdForRequest } from "../../services/loginService.ts";
4 5 import type { IGuildCheats } from "../../types/guildTypes.ts";
@@ -6,17 +7,19 @@ import type { RequestHandler } from "express";
6 7
7 8 export const setGuildCheatController: RequestHandler = async (req, res) => {
8 9 const accountId = await getAccountIdForRequest(req);
9 const payload = req.body as ISetGuildCheatRequest;
10 10 const guild = await getGuildForRequest(req, accountId);
11 11 const member = await GuildMember.findOne({ accountId: accountId, guildId: guild._id });
12 12
13 if (member) {
14 if (member.rank > 1) {
15 res.end();
16 return;
17 }
13 if (member && member.rank <= 1) {
14 const payload = req.body as ISetGuildCheatRequest;
15 const meta = payload.value ? clanLockCheats[payload.key] : undefined;
16
18 17 guild[payload.key] = payload.value;
19 18 await guild.save();
19
20 if (meta && !meta.isGuildInIdealState(guild)) {
21 res.send("retroactivable");
22 }
20 23 }
21 24 res.end();
22 25 };
Modified src/helpers/udp.ts +15 -2
@@ -18,9 +18,22 @@ export const sendUdpMessage = (host: string, port: number, message: Buffer | str
18 18
19 19 export const sendCustomMessageToNrs = async (message: string): Promise<void> => {
20 20 const msgbuf = Buffer.from("\x00" + message, "utf8");
21 const crc = crc32(Buffer.from("b471e49539930dc9b5a131e6247c7387A", "utf8"), crc32(msgbuf));
22 const msg = Buffer.concat([Buffer.from([0x00, crc >> 24, crc >> 16, crc >> 8, crc]), msgbuf]);
21 const msg = addCompressionAndIntegrityToDatagram(msgbuf);
23 22 for (const [nrsAddr, nrsPort] of getNrsAddresses()) {
24 23 await sendUdpMessage(nrsAddr, nrsPort, msg);
25 24 }
26 25 };
26
27 const SALT_BUF = Buffer.from("b471e49539930dc9b5a131e6247c7387A", "utf8");
28
29 const addCompressionAndIntegrityToDatagram = (msg: Buffer): Buffer => {
30 const crc = crc32(SALT_BUF, crc32(msg));
31 return Buffer.concat([Buffer.from([0x00, crc >> 24, crc >> 16, crc >> 8, crc]), msg]);
32 };
33
34 export const packHubDatagram = (msg: Buffer): Buffer => {
35 const header = Buffer.alloc(6);
36 header.writeUInt32LE(0x80000000, 0);
37 header.writeUInt16LE(msg.length, 4);
38 return addCompressionAndIntegrityToDatagram(Buffer.concat([header, msg]));
39 };
Modified src/routes/custom.ts +2 -0
@@ -29,6 +29,7 @@ import { removeCustomizationController } from "../controllers/custom/removeCusto
29 29 import { removeIsNewController } from "../controllers/custom/removeIsNewController.ts";
30 30 import { removeItemsController } from "../controllers/custom/removeItemsController.ts";
31 31 import { retroactivelyApplyCheatController } from "../controllers/api/retroactivelyApplyCheatController.ts";
32 import { retroactivelyApplyGuildCheatController } from "../controllers/custom/retroactivelyApplyGuildCheatController.ts";
32 33
33 34 import { abilityOverrideController } from "../controllers/custom/abilityOverrideController.ts";
34 35 import { createAccountController } from "../controllers/custom/createAccountController.ts";
@@ -86,6 +87,7 @@ customRouter.get("/removeCustomization", removeCustomizationController);
86 87 customRouter.get("/removeIsNew", removeIsNewController);
87 88 customRouter.get("/removeItems", removeItemsController);
88 89 customRouter.get("/retroactivelyApplyCheat", retroactivelyApplyCheatController);
90 customRouter.get("/retroactivelyApplyGuildCheat", retroactivelyApplyGuildCheatController);
89 91
90 92 customRouter.post("/abilityOverride", abilityOverrideController);
91 93 customRouter.post("/createAccount", createAccountController);
Modified src/services/arbiterService.ts +40 -0
@@ -1,4 +1,6 @@
1 import { packHubDatagram } from "../helpers/udp.ts";
1 2 import { config, type IHubServer } from "./configService.ts";
3 import dgram from "node:dgram";
2 4
3 5 export type TRegionId = "ASIA" | "OCEANIA" | "EUROPE" | "RUSSIA" | "NORTH_AMERICA" | "SOUTH_AMERICA";
4 6
@@ -38,3 +40,41 @@ export const pickHubServer = (regionId: TRegionId): IHubServer => {
38 40 }
39 41 return bestServer;
40 42 };
43
44 export const broadcastControlMessages = (level: string, msgs: string[]): void => {
45 const instance = hubInstances[level] as IHubInstance | undefined;
46 if (instance) {
47 instance.Players += 1; // to account for the hubDropped that we will cause here
48
49 const [host, port] = instance.Hub.split("%THIS_MACHINE%").join("127.0.0.1").split(":");
50 const socket = dgram.createSocket("udp4");
51
52 socket.send(
53 packHubDatagram(
54 Buffer.concat([
55 Buffer.from([0xb4, 0x03, 0x18, 0x00, 0x00, 0x00]),
56 Buffer.from("000000000000000000000000", "utf8"),
57 Buffer.alloc(8),
58 Buffer.from([0x03, 0x00, 0x00, 0x00]),
59 Buffer.from("SNS", "utf8"),
60 Buffer.alloc(4),
61 Buffer.from([level.length, 0x00, 0x00, 0x00]),
62 Buffer.from(level, "utf8")
63 ])
64 ),
65 parseInt(port),
66 host
67 );
68 socket.once("message", msg => {
69 const peerId = msg.readUInt16LE(13);
70 for (const msg of msgs) {
71 const header = Buffer.alloc(8);
72 header.writeUInt8(0xb4, 0);
73 header.writeUInt8(0x07, 1);
74 header.writeUInt16LE(peerId, 2);
75 header.writeUInt32LE(msg.length, 4);
76 socket.send(packHubDatagram(Buffer.concat([header, Buffer.from(msg, "utf8")])), parseInt(port), host);
77 }
78 });
79 }
80 };
Added src/services/clanCheatsService.ts +70 -0
@@ -0,0 +1,70 @@
1 import type { TGuildDatabaseDocument } from "../models/guildModel.ts";
2 import type { IGuildCheats } from "../types/guildTypes.ts";
3
4 interface IClanLockCheat {
5 isGuildInIdealState: (guild: TGuildDatabaseDocument) => boolean;
6 cleanupGuild: (guild: TGuildDatabaseDocument) => string[];
7 }
8
9 export const clanLockCheats: Partial<Record<keyof IGuildCheats, IClanLockCheat>> = {
10 noDojoRoomBuildStage: {
11 isGuildInIdealState: (guild: TGuildDatabaseDocument) =>
12 guild.DojoComponents.every(
13 component => component.CompletionTime && component.CompletionTime.getTime() <= Date.now()
14 ),
15 cleanupGuild: (guild: TGuildDatabaseDocument) => {
16 for (const component of guild.DojoComponents) {
17 if (!component.CompletionTime || component.CompletionTime.getTime() > Date.now()) {
18 component.CompletionTime = new Date();
19 }
20 }
21 return [
22 `{"from":"000000000000000000000000","to":"dojo","msg":{"dojoMsgType":1,"n":1,"a":false,"f":false,"u":""}}`
23 ];
24 }
25 },
26 fastDojoRoomDestruction: {
27 isGuildInIdealState: (guild: TGuildDatabaseDocument) =>
28 guild.DojoComponents.every(
29 component => !component.DestructionTime || component.DestructionTime.getTime() <= Date.now()
30 ),
31 cleanupGuild: (guild: TGuildDatabaseDocument) => {
32 for (const component of guild.DojoComponents) {
33 if (component.DestructionTime && component.DestructionTime.getTime() > Date.now()) {
34 component.DestructionTime = new Date();
35 }
36 }
37 return [
38 `{"from":"000000000000000000000000","to":"dojo","msg":{"dojoMsgType":1,"n":1,"a":false,"f":false,"u":""}}`
39 ];
40 }
41 },
42 noDojoDecoBuildStage: {
43 isGuildInIdealState: (guild: TGuildDatabaseDocument) =>
44 guild.DojoComponents.every(
45 component =>
46 !component.Decos ||
47 component.Decos.every(deco => deco.CompletionTime && deco.CompletionTime.getTime() <= Date.now())
48 ),
49 cleanupGuild: (guild: TGuildDatabaseDocument) => {
50 const msgs: string[] = [];
51 for (const component of guild.DojoComponents) {
52 if (component.Decos) {
53 let anyChanges = false;
54 for (const deco of component.Decos) {
55 if (!deco.CompletionTime || deco.CompletionTime.getTime() > Date.now()) {
56 deco.CompletionTime = new Date();
57 anyChanges = true;
58 }
59 }
60 if (anyChanges) {
61 msgs.push(
62 `{"from":"000000000000000000000000","to":"dojo","msg":{"dojoMsgType":0,"id":"${component._id.toString()}","op":6,"u":""}}`
63 );
64 }
65 }
66 }
67 return msgs;
68 }
69 }
70 };
Modified src/types/guildTypes.ts +10 -2
@@ -41,8 +41,8 @@ export interface IGuildClient {
41 41 // Fields specific to SNS
42 42 export interface IGuildCheats {
43 43 noDojoRoomBuildStage?: boolean;
44 noDojoDecoBuildStage?: boolean;
45 44 fastDojoRoomDestruction?: boolean;
45 noDojoDecoBuildStage?: boolean;
46 46 noDojoResearchCosts?: boolean;
47 47 noDojoResearchTime?: boolean;
48 48 fastClanAscension?: boolean;
@@ -217,7 +217,15 @@ export interface IDojoComponentSettings {
217 217
218 218 export interface IDojoComponentDatabase extends Omit<
219 219 IDojoComponentClient,
220 "id" | "SortId" | "pi" | "CompletionTime" | "DestructionTime" | "Decos" | "PaintBot"
220 | "id"
221 | "SortId"
222 | "pi"
223 | "CompletionTime"
224 | "TimeRemaining"
225 | "DestructionTime"
226 | "DestructionTimeRemaining"
227 | "Decos"
228 | "PaintBot"
221 229 > {
222 230 _id: Types.ObjectId;
223 231 SortId?: Types.ObjectId;
Modified static/webui/index.html +4 -4
@@ -690,6 +690,10 @@
690 690 <input class="form-check-input" type="checkbox" id="noDojoRoomBuildStage" />
691 691 <label class="form-check-label" for="noDojoRoomBuildStage" data-loc="cheats_noDojoRoomBuildStage"></label>
692 692 </div>
693 <div class="form-check">
694 <input class="form-check-input" type="checkbox" id="fastDojoRoomDestruction" />
695 <label class="form-check-label" for="fastDojoRoomDestruction" data-loc="cheats_fastDojoRoomDestruction"></label>
696 </div>
693 697 <div class="form-check">
694 698 <input class="form-check-input" type="checkbox" id="noDojoDecoBuildStage" />
695 699 <label class="form-check-label" for="noDojoDecoBuildStage" data-loc="cheats_noDojoDecoBuildStage"></label>
@@ -702,10 +706,6 @@
702 706 <input class="form-check-input" type="checkbox" id="noDojoResearchTime" />
703 707 <label class="form-check-label" for="noDojoResearchTime" data-loc="cheats_noDojoResearchTime"></label>
704 708 </div>
705 <div class="form-check">
706 <input class="form-check-input" type="checkbox" id="fastDojoRoomDestruction" />
707 <label class="form-check-label" for="fastDojoRoomDestruction" data-loc="cheats_fastDojoRoomDestruction"></label>
708 </div>
709 709 <div class="form-check">
710 710 <input class="form-check-input" type="checkbox" id="fastClanAscension" />
711 711 <label class="form-check-label" for="fastClanAscension" data-loc="cheats_fastClanAscension"></label>
Modified static/webui/script.js +13 -0
@@ -4094,6 +4094,19 @@ document.querySelectorAll("#guild-cheats input[type=checkbox]").forEach(elm => {
4094 4094 key: elm.id,
4095 4095 value: elm.checked
4096 4096 })
4097 }).done(res => {
4098 if (res == "retroactivable") {
4099 if (window.confirm(loc("cheats_retroactivePrompt"))) {
4100 $.get(
4101 "/custom/retroactivelyApplyGuildCheat?" +
4102 window.authz +
4103 "&guildId=" +
4104 window.guildId +
4105 "&cheat=" +
4106 elm.id
4107 );
4108 }
4109 }
4097 4110 });
4098 4111 });
4099 4112 };