Remove SCUM server-management client path

This commit is contained in:
npc0-hue
2026-09-02 17:56:49 +08:00
parent 6e614d3fa3
commit a82f1ff01a
52 changed files with 245 additions and 5131 deletions
+24 -45
View File
@@ -18,7 +18,7 @@ const surfaceData: SCUMSurfaceData = {
players: [{ gamePlayerId: "steam-1", steamId: "76561198000000001", userProfileId: "profile-1", displayName: "Mira", squadName: "Wolves", squadId: "squad-1", online: true, famePoints: 42, normalBalance: 1000, goldBalance: 3, lastLoginIp: "203.0.113.7", position: { x: 10, y: 20, z: 3 }, freshness: { status: "fresh" } }],
squads: [{ squadId: "squad-1", name: "Wolves", memberCount: 1, memberLimit: 12, leaderProfileId: "profile-1", score: 88, message: "Hold the north", freshness: { status: "fresh" } }],
members: [{ gamePlayerId: "steam-1", steamId: "76561198000000001", displayName: "Mira", squadId: "squad-1", rank: "Leader", score: 42, lastLoginAt: "2026-08-10T00:00:00Z", freshness: { status: "fresh" } }],
events: [{ id: "event-1", name: "Friday Range", eventType: "range", class: 1, corn: "0 20 * * 5", placard: "Event starting", percent: 75, npc: 1, item: 3, zombie: 12, animal: 2, status: "enabled" }],
events: [{ id: "event-1", name: "Friday Range", eventType: "range", class: 1, corn: "0 20 * * 5", rconCommand: "#start_event event-1", placard: "Event starting", percent: 75, npc: 1, item: 3, zombie: 12, animal: 2, status: "enabled" }],
eventProduces: [{ _recordKey: "event-1:produce-1", id: "produce-1", eventId: "event-1", tradeGoodsId: "goods-1", percent: 80, value: 2, r: 100, x: 10, y: 20, z: 3 }],
eventRuns: [{ id: "run-1", eventId: "event-1", status: "running", startedAt: "2026-08-10T00:00:00Z", summary: "Round 1" }],
nativeEventRounds: [{ eventRecordId: "native-1", eventId: "native-event", state: "active", startTime: "2026-08-10T00:00:00Z", enemyKills: 2 }],
@@ -93,17 +93,14 @@ describe("SCUM plugin feature module", () => {
expect(data.gifts[0]).toMatchObject({ collection: scumCollections.gifts, _recordKey: `${scumCollections.gifts}-1` });
});
it("merges player snapshots only by stable identifiers and ignores name-only online sessions", async () => {
it("reads player records only from plugin-owned collections", async () => {
const pluginData = pluginDataActions({ list: async (collection) => collection === scumCollections.players ? { items: [{ key: "steam-1", value: { gamePlayerId: "steam-1", displayName: "Mira", online: false } }] } : { items: [] } });
const gameClient = gameClientActions();
gameClient.snapshots.mockResolvedValue({ items: [{ sequence: 2, observedAt: "2026-08-10T00:00:00Z", payload: { players: [{ playerId: "steam-1", playerName: "Mira", status: "online", pingMs: 32 }] } }] });
const data = await loadSCUMSurface({ pluginData, gameClient }, "players");
expect(gameClient.snapshots.mock.calls.map(([query]) => query?.type)).toEqual(["players", "vehicles"]);
expect(data.players[0]).toMatchObject({ gamePlayerId: "steam-1", status: "online", online: true, pingMs: 32, onlineObservedAt: "2026-08-10T00:00:00Z" });
const data = await loadSCUMSurface({ pluginData }, "players");
expect(data.players[0]).toMatchObject({ gamePlayerId: "steam-1", displayName: "Mira", online: false });
const sameName = mergePlayerSnapshots([{ steamId: "steam-2", displayName: "Noah", online: false }], { items: [{ observedAt: "2026-08-10T00:02:00Z", payload: { players: [{ playerId: "steam-3", playerName: "Noah", status: "online" }] } }] });
expect(sameName).toHaveLength(2);
expect(sameName.find((player) => player.steamId === "steam-2")).toMatchObject({ online: false });
expect(dataClientSource).not.toContain('type: "online.sessions"');
expect(dataClientSource).not.toContain("scum-client-manager");
});
it("uses workflows as the manifest activity key and keeps activity as a compatibility alias", async () => {
@@ -131,14 +128,13 @@ describe("SCUM plugin feature module", () => {
it("persists event produces, event runs, and gift resets in plugin-owned collections", async () => {
const pluginData = pluginDataActions();
const gameClient = gameClientActions();
const actions: SCUMWorkspaceActions = { pluginData, gameClient };
const dispatch = vi.fn<NonNullable<SCUMWorkspaceActions["dispatch"]>>(async (envelope) => ({ status: "queued", result: { jobId: envelope.requestId } }));
const actions: SCUMWorkspaceActions = { pluginData, dispatch };
await saveEventProduce(actions, { id: "produce-1", eventId: "event-1", tradeGoodsId: "goods-1", percent: 80, value: 2, r: 100, x: 10, y: 20, z: 3 });
expect(pluginData.put).toHaveBeenCalledWith(scumCollections.eventProduces, "event-1:produce-1", expect.objectContaining({ eventId: "event-1", tradeGoodsId: "goods-1" }));
await startEvent(actions, surfaceData.events[0], surfaceData.eventProduces);
expect(gameClient.queue).toHaveBeenLastCalledWith(expect.objectContaining({ commandType: "event.start", payload: expect.objectContaining({
eventType: "range", class: 1, placard: "Event starting", percent: 75, npc: 1, item: 3, zombie: 12, animal: 2,
produces: [{ tradeGoodsId: "goods-1", percent: 80, value: 2, r: 100, x: 10, y: 20, z: 3 }]
await startEvent(actions, { ...surfaceData.events[0], rconCommand: "#start_event event-1" }, surfaceData.eventProduces);
expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({ action: "remote.access.request", payload: expect.objectContaining({
capability: "remote.run.rcon.command", "input.command": "#start_event event-1"
}) }));
expect(pluginData.put).toHaveBeenCalledWith(scumCollections.eventRuns, expect.any(String), expect.objectContaining({ eventId: "event-1", status: "queued", produces: surfaceData.eventProduces }));
await resetGiftClaim(actions, { _recordKey: "claim-1" });
@@ -147,38 +143,31 @@ describe("SCUM plugin feature module", () => {
expect(pluginData.put).toHaveBeenCalledWith(scumCollections.pendingGifts, "pending-1", expect.objectContaining({ status: "pending", receivedAt: null }));
});
it("queues gift and event commands through the host-compatible generic gameClient bridge", async () => {
it("queues gift and event commands through platform-mediated SCUM RCON", async () => {
const pluginData = pluginDataActions();
const gameClient = gameClientActions();
const actions: SCUMWorkspaceActions = { pluginData, gameClient };
const dispatch = vi.fn<NonNullable<SCUMWorkspaceActions["dispatch"]>>(async (envelope) => ({ status: "queued", result: { jobId: envelope.requestId } }));
const actions: SCUMWorkspaceActions = { pluginData, dispatch };
await queueGiftDelivery(actions, { ...surfaceData.gifts[0], operations: ["#announce Starter pack", "#SetFamePoints 250"] }, surfaceData.players[0]);
expect(gameClient.queue).toHaveBeenCalledWith(expect.objectContaining({ profileKey: "scum-client-manager", commandType: "reward.deliver", payload: expect.objectContaining({ playerId: "steam-1", items: [{ catalogCode: "BP_Cash_01", quantity: 2 }], operations: ["#announce Starter pack", "#SetFamePoints 250"] }) }));
expect(dispatch.mock.calls.map(([request]) => request.payload?.["input.command"])).toEqual(["#SpawnItem BP_Cash_01 2", "#announce Starter pack", "#SetFamePoints 250"]);
expect(pluginData.put).toHaveBeenCalledWith(scumCollections.giftDeliveries, expect.any(String), expect.objectContaining({ giftCode: "starter-pack", playerId: "steam-1", status: "queued" }));
await startEvent(actions, surfaceData.events[0], surfaceData.eventProduces);
expect(gameClient.queue).toHaveBeenLastCalledWith(expect.objectContaining({ profileKey: "scum-client-manager", commandType: "event.start", payload: expect.objectContaining({ eventId: "event-1", eventType: "range", class: 1, placard: "Event starting", percent: 75, produces: [{ tradeGoodsId: "goods-1", percent: 80, value: 2, r: 100, x: 10, y: 20, z: 3 }] }) }));
expect(Object.keys(gameClient).sort()).toEqual(["get", "list", "queue", "snapshots"]);
await startEvent(actions, { ...surfaceData.events[0], rconCommand: "#start_event event-1" }, surfaceData.eventProduces);
expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({ payload: expect.objectContaining({ "input.command": "#start_event event-1" }) }));
});
it("defaults activity class to range and strips collection metadata from queued produces", async () => {
const pluginData = pluginDataActions();
const gameClient = gameClientActions();
await startEvent({ pluginData, gameClient }, { id: "event-default", name: "Default Event" }, [{
const dispatch = vi.fn<NonNullable<SCUMWorkspaceActions["dispatch"]>>(async (envelope) => ({ status: "queued", result: { jobId: envelope.requestId } }));
await startEvent({ pluginData, dispatch }, { id: "event-default", name: "Default Event", rconCommand: "#start_event event-default" }, [{
_recordKey: "event-default:produce-1", id: "produce-1", eventId: "event-default", updatedAt: "2026-08-10T00:00:00Z",
tradeGoodsId: "cargo-drop", percent: 80, value: 2, r: 500, x: 1000, y: 2000, z: 300
}]);
expect(gameClient.queue).toHaveBeenCalledWith(expect.objectContaining({ commandType: "event.start", payload: expect.objectContaining({
eventType: "range", class: 1, npc: 0, item: 0, zombie: 0, animal: 0,
produces: [{ tradeGoodsId: "cargo-drop", percent: 80, value: 2, r: 500, x: 1000, y: 2000, z: 300 }]
}) }));
expect(dispatch).toHaveBeenCalledWith(expect.objectContaining({ payload: expect.objectContaining({ "input.command": "#start_event event-default" }) }));
});
it("keeps positive event duration and counts above the removed arbitrary limits", async () => {
const gameClient = gameClientActions();
await startEvent({ pluginData: pluginDataActions(), gameClient }, { id: "event-large", name: "Large Event", durationSeconds: 86401, npc: 10001, item: 10002, zombie: 10003, animal: 10004 }, [{ tradeGoodsId: "cargo-drop", percent: 80, value: 10001, r: 2000001, x: 3000000, y: -3000000, z: 0 }]);
expect(gameClient.queue).toHaveBeenCalledWith(expect.objectContaining({ payload: expect.objectContaining({
durationSeconds: 86401, npc: 10001, item: 10002, zombie: 10003, animal: 10004,
produces: [{ tradeGoodsId: "cargo-drop", percent: 80, value: 10001, r: 2000001, x: 3000000, y: -3000000, z: 0 }]
}) }));
const dispatch = vi.fn<NonNullable<SCUMWorkspaceActions["dispatch"]>>(async (envelope) => ({ status: "queued", result: { jobId: envelope.requestId } }));
await startEvent({ pluginData: pluginDataActions(), dispatch }, { id: "event-large", name: "Large Event", rconCommand: "#start_event event-large", durationSeconds: 86401, npc: 10001, item: 10002, zombie: 10003, animal: 10004 }, [{ tradeGoodsId: "cargo-drop", percent: 80, value: 10001, r: 2000001, x: 3000000, y: -3000000, z: 0 }]);
expect(dispatch).toHaveBeenCalledWith(expect.objectContaining({ payload: expect.objectContaining({ "input.command": "#start_event event-large" }) }));
});
it("renders searchable user management from real collection values", () => {
@@ -241,7 +230,7 @@ describe("SCUM plugin feature module", () => {
it("renders activity definitions, status filters, runs, and records", () => {
const view = renderAndCollect({ pageKey: "workflows", pageTitle: "活动管理" });
expect(view.inputs.map((input) => input.label)).toContain("活动状态");
expect(view.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["生成类型", "活动公告", "活动概率", "活动持续秒数", "生成物品编号", "生成半径", "生成 X", "生成 Y", "生成 Z"]));
expect(view.inputs.map((input) => input.label)).toEqual(expect.arrayContaining(["生成类型", "RCON 命令", "活动公告", "活动概率", "活动持续秒数", "生成物品编号", "生成半径", "生成 X", "生成 Y", "生成 Z"]));
expect(view.texts).toContain("Friday Range");
expect(view.texts).toContain("running");
expect(view.texts).toContain("最近活动记录");
@@ -318,16 +307,6 @@ function pluginDataActions(overrides: Partial<{ list: (collection: string, key?:
};
}
function gameClientActions() {
const queue = vi.fn<NonNullable<SCUMWorkspaceActions["gameClient"]>["queue"]>(async () => ({ id: "command-1", state: "pending" }));
const get = vi.fn<NonNullable<SCUMWorkspaceActions["gameClient"]>["get"]>(async () => ({ id: "command-1", state: "pending" }));
const list = vi.fn<NonNullable<SCUMWorkspaceActions["gameClient"]>["list"]>(async () => ({ items: [], count: 0 }));
const snapshots = vi.fn<NonNullable<SCUMWorkspaceActions["gameClient"]>["snapshots"]>(async () => ({ items: [], count: 0 }));
return {
queue, get, list, snapshots
};
}
function renderAndCollect(options: { data?: SCUMSurfaceData; permissions?: string[]; pageKey?: string; pageTitle?: string; giftTab?: "definitions" | "claims" | "deliveries" | "timed"; playerSearch?: string } = {}) {
const nodes: string[] = [];
const texts: string[] = [];
@@ -355,7 +334,7 @@ function renderAndCollect(options: { data?: SCUMSurfaceData; permissions?: strin
return [value, () => undefined];
}
};
const actions: SCUMWorkspaceActions = { pluginData: pluginDataActions(), gameClient: gameClientActions(), dispatch: async () => ({ status: "queued", result: { jobId: "job-1" } }) };
const actions: SCUMWorkspaceActions = { pluginData: pluginDataActions(), dispatch: async () => ({ status: "queued", result: { jobId: "job-1" } }) };
renderPluginPage(react, {
page: { key: options.pageKey ?? "players", title: options.pageTitle ?? "用户管理" },
context: { serverInstanceId: "server-1", permissions: options.permissions ?? ["server.read", "server.remote.access"] },