feat(scum): rebuild plugin-owned management data

This commit is contained in:
npc0-hue
2026-08-15 12:43:09 +08:00
parent 92b1159cc8
commit 65353cf269
113 changed files with 3116 additions and 8058 deletions
+62 -10
View File
@@ -211,7 +211,7 @@ describe("plugin manifest validation", () => {
const installScript = fs.readFileSync(path.join(pluginDir, installAction.executableKey), "utf8");
const startScript = fs.readFileSync(path.join(pluginDir, startAction.executableKey), "utf8");
expect(manifest.runtimeProfiles.serverDeployments).toBeUndefined();
expect(assetPaths).toEqual(expect.arrayContaining(["actions/install.json", "actions/start.json", "bin/scum-install-update.cmd", "bin/scum-start.cmd"]));
expect(assetPaths).toEqual(expect.arrayContaining(["actions/install.json", "actions/start.json", "bin/scum-install-update.cmd", "bin/scum-start.cmd", "assets/map/scum-map-overview.jpg"]));
expect(installAction).toMatchObject({ executableKey: "bin/scum-install-update.cmd", environment: { SERVER_STEAM_APP_ID: "3792580", SERVER_STEAMCMD_UPDATE_ARGS: "+login anonymous +app_update 3792580 +quit" } });
expect(installAction.timeoutMs).toBe(7200000);
expect(startAction).toMatchObject({ executableKey: "bin/scum-start.cmd", environment: { SERVER_LOG_FLAG: "-log" } });
@@ -478,7 +478,7 @@ describe("plugin manifest validation", () => {
const serialized = JSON.stringify(manifest).toLowerCase();
expect(serialized).not.toContain("local-proof");
expect(manifest.version).toBe("0.1.6");
expect(manifest.version).toBe("0.1.7");
expect(installAction.environment?.SERVER_TEMPLATE).toBe("scum-server");
expect(manifest.permissions).toEqual(expect.arrayContaining(["server.game-client.read", "server.game-client.command", "server.game-client.maintenance"]));
expect(manifest.gameClientBridge.commands.map((command) => command.type)).toEqual(expect.arrayContaining([
@@ -525,13 +525,13 @@ describe("plugin manifest validation", () => {
};
};
const expected = {
"announcement.send": { permission: "server.game-client.command", approvalLevel: "operator" },
"announcement.send": { permission: "server.game-client.command", approvalLevel: "none" },
"companion.diagnostics": { permission: "server.game-client.read", approvalLevel: "none" },
"player.lookup": { permission: "server.game-client.read", approvalLevel: "none" },
"reward.deliver": { permission: "server.game-client.command", approvalLevel: "operator" },
"event.start": { permission: "server.game-client.command", approvalLevel: "operator" },
"restart.prepare": { permission: "server.game-client.maintenance", approvalLevel: "operator" },
"maintenance.prepare": { permission: "server.game-client.maintenance", approvalLevel: "platform-admin" }
"reward.deliver": { permission: "server.game-client.command", approvalLevel: "none" },
"event.start": { permission: "server.game-client.command", approvalLevel: "none" },
"restart.prepare": { permission: "server.game-client.maintenance", approvalLevel: "none" },
"maintenance.prepare": { permission: "server.game-client.maintenance", approvalLevel: "none" }
} as const;
expect(manifest.gameClientBridge.commands.map((command) => command.type)).toEqual(expect.arrayContaining(Object.keys(expected)));
@@ -633,15 +633,28 @@ describe("plugin manifest validation", () => {
targetKey: string;
parameterSchemaRef: string;
resultSchemaRef: string;
sqlRef: string;
rowTarget: { collection: string; upsertKeys: string[]; columnMappings: Record<string, string> };
maxRows: number;
timeoutSeconds: number;
}>;
pages: Array<{ pageKey: string; queryTemplateKeys?: string[] }>;
pages: Array<{ pageKey: string; commandTypes?: string[]; queryTemplateKeys?: string[] }>;
};
pages: Array<{ key: string; permissions?: string[]; bridgeActions?: string[] }>;
runtimeProfiles?: { transportProfiles?: Array<{ key: string; kind: string; targetKey?: string; capabilities: string[] }> };
};
const expectedKeys = ["scum.player.profile", "scum.squads", "scum.squad-members", "scum.vehicles", "scum.flags", "scum.positions"];
const expectedKeys = ["scum.player.profile", "scum.squads", "scum.squad-members", "scum.vehicles", "scum.flags", "scum.positions", "scum.tasks", "scum.events", "scum.native-timed-gifts"];
const expectedColumnsByKey: Record<string, string[]> = {
"scum.player.profile": ["userProfileId", "steamId", "gamePlayerId", "displayName", "squadId", "squadName", "famePoints", "normalBalance", "goldBalance", "x", "y", "z", "lastLoginTime", "lastSaveTime"],
"scum.squads": ["squadId", "name", "leaderProfileId", "leaderPlayerId", "memberCount", "score", "memberLimit", "message", "info", "lastMemberLoginTime"],
"scum.squad-members": ["squadId", "userProfileId", "gamePlayerId", "steamId", "displayName", "rank", "isLeader"],
"scum.vehicles": ["vehicleId", "entityId", "className", "label", "x", "y", "z", "lastAccessTime", "isFunctional"],
"scum.flags": ["flagId", "entityId", "baseId", "ownerProfileId", "ownerPlayerId", "ownerSquadId", "ownerSquadName", "overtakerProfileId", "overtakeEndTime", "ownershipConfidence", "x", "y", "z"],
"scum.positions": ["subjectType", "subjectId", "userProfileId", "gamePlayerId", "vehicleId", "entityId", "baseId", "x", "y", "z", "observedAt"],
"scum.tasks": ["taskRecordId", "taskKind", "userProfileId", "mapId", "trackingDataSetId", "dataAssetPath", "sequenceIndex", "isTracked", "state", "completionDeadline"],
"scum.events": ["eventRecordId", "eventId", "roundId", "userProfileId", "startTime", "endTime", "state", "score", "enemyKills", "teamKills", "deaths", "assists", "headshots"],
"scum.native-timed-gifts": ["timedGiftId", "userProfileId", "mapId", "spawnTime", "spawnAt"]
};
const templatesByKey = new Map(manifest.gameClientBridge.queryTemplates.map((template) => [template.key, template]));
expect([...templatesByKey.keys()]).toEqual(expect.arrayContaining(expectedKeys));
expect(manifest.capabilities).toContain("remote.run.db.sqlite.query");
@@ -655,24 +668,45 @@ describe("plugin manifest validation", () => {
expect(template.engine).toBe("sqlite");
expect(template.transportKey).toBe("scum-database");
expect(template.targetKey).toBe("scum-database");
expect(template.sqlRef).toMatch(/^sql\/scum-db-v57\/.+\.sql$/);
expect(template.rowTarget.collection).toMatch(/^scum_/);
expect(template.rowTarget.upsertKeys.length).toBeGreaterThan(0);
expect(template.rowTarget.upsertKeys.every((upsertKey) => upsertKey in template.rowTarget.columnMappings)).toBe(true);
expect(fs.existsSync(path.join(pluginDir, template.sqlRef))).toBe(true);
expect(JSON.stringify(template).toLowerCase()).not.toMatch(/select\s|from\s|sqlite:|scum\.db|databasepath|hostpath|dsn/);
const parameters = JSON.parse(fs.readFileSync(path.join(pluginDir, template.parameterSchemaRef), "utf8"));
const result = JSON.parse(fs.readFileSync(path.join(pluginDir, template.resultSchemaRef), "utf8"));
const sql = fs.readFileSync(path.join(pluginDir, template.sqlRef), "utf8");
const expectedColumns = expectedColumnsByKey[key];
expect(parameters).toMatchObject({ type: "object", additionalProperties: false });
expect(result).toMatchObject({ type: "object", additionalProperties: false, required: ["rows"] });
expect(result.properties.rows.maxItems).toBeLessThanOrEqual(template.maxRows);
expect(template.rowTarget.columnMappings).toEqual(Object.fromEntries(expectedColumns.map((column) => [column, column])));
expect(Object.keys(result.properties.rows.items.properties).sort()).toEqual([...expectedColumns].sort());
expect([...result.properties.rows.items.required].sort()).toEqual([...expectedColumns].sort());
for (const column of expectedColumns) {
expect(sql).toMatch(new RegExp(`\\bAS\\s+${column}\\b`, "i"));
}
}
const playersPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "players");
const squadsPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "squads");
const mapPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "live-map");
const giftsPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "gifts");
const workflowsPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "workflows");
expect(playersPage?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.player.profile", "scum.positions"]));
expect(squadsPage?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.squads", "scum.squad-members", "scum.flags"]));
expect(mapPage?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.vehicles", "scum.flags", "scum.positions"]));
for (const pageKey of ["players", "squads", "live-map"]) {
expect(giftsPage?.commandTypes).toEqual(["reward.deliver"]);
expect(workflowsPage?.commandTypes).toEqual(["event.start"]);
for (const pageKey of ["players", "squads", "live-map", "gifts", "workflows"]) {
const pluginPage = manifest.pages.find((page) => page.key === pageKey);
expect(pluginPage?.permissions).toContain("server.game-client.read");
expect(pluginPage?.permissions).toContain("server.remote.access");
expect(pluginPage?.bridgeActions).toContain("remote.access.request");
}
for (const pageKey of ["gifts", "workflows"]) {
expect(manifest.pages.find((page) => page.key === pageKey)?.permissions).toContain("server.game-client.command");
}
});
it("declares typed SCUM RCON operations without arbitrary command inputs", () => {
@@ -709,6 +743,24 @@ describe("plugin manifest validation", () => {
expect(manifest.pages.find((page) => page.key === "gifts")?.permissions).toContain("server.game-client.command");
});
it("packages SCUM v57 config, UTF-16LE logs, and gift metadata inside the plugin", () => {
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
gameClientBridge: { dataPacks: Array<{ key: string; databaseUserVersion: number; logParserRefs: string[]; configMapRefs: string[]; dataRefs?: string[] }> };
};
const pack = manifest.gameClientBridge.dataPacks.find((candidate) => candidate.key === "scum-db-v57");
expect(pack).toMatchObject({ databaseUserVersion: 57 });
const logParsers = JSON.parse(fs.readFileSync(path.join(pluginDir, pack!.logParserRefs[0]), "utf8"));
const configMaps = JSON.parse(fs.readFileSync(path.join(pluginDir, pack!.configMapRefs[0]), "utf8"));
const giftMetadata = JSON.parse(fs.readFileSync(path.join(pluginDir, pack!.dataRefs![0]), "utf8"));
const mapGeometry = JSON.parse(fs.readFileSync(path.join(pluginDir, pack!.dataRefs![1]), "utf8"));
expect(logParsers).toMatchObject({ encoding: "utf-16le", lineEnding: "lf", continuationPolicy: "append-to-previous-timestamped-record", timestampFormat: "yyyy.MM.dd-HH.mm.ss" });
expect(logParsers.parsers.map((parser: { key: string }) => parser.key)).toEqual(expect.arrayContaining(["login", "chat", "admin", "kill", "event-kill", "quests", "vehicle-destruction"]));
expect(configMaps.maps.map((map: { key: string }) => map.key)).toEqual(expect.arrayContaining(["server-settings", "economy-override", "raid-times", "notifications", "admin-users", "banned-users"]));
expect(giftMetadata).toMatchObject({ databaseUserVersion: 57, catalogSource: { configMapKey: "economy-override" } });
expect(mapGeometry).toMatchObject({ databaseUserVersion: 57, image: { path: "assets/map/scum-map-overview.jpg", width: 256, height: 256 }, runtimeOverride: { kilometersToWorldUnits: 100000 } });
});
it("declares typed SCUM semantic log events with bounded schemas", () => {
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {