Keep SCUM data parsing plugin-owned
This commit is contained in:
@@ -8,13 +8,13 @@ export type PluginDataActions = {
|
|||||||
transact: (collection: string, mutations: PluginDataMutation[]) => Promise<unknown>;
|
transact: (collection: string, mutations: PluginDataMutation[]) => Promise<unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LogActions = {
|
export type GameClientActions = {
|
||||||
listStreams: () => Promise<unknown>;
|
snapshots: (query?: { profileKey?: string; type?: string; streamKey?: string; observedAfter?: string; limit?: number }) => Promise<unknown>;
|
||||||
query: (request: { logStreamId: string; afterSeq: number; limit: number }) => Promise<unknown>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PluginBridgeExecuteEnvelope = { requestId: string; action: string; payload?: Record<string, string> };
|
export type PluginBridgeExecuteEnvelope = { requestId: string; action: string; payload?: Record<string, string> };
|
||||||
export type PluginBridgeExecutionResult = { status?: string; result?: Record<string, string>; error?: { message?: string } };
|
export type PluginBridgeExecutionResult = { status?: string; result?: Record<string, string>; error?: { message?: string } };
|
||||||
|
export type SCUMQueryTemplateKey = "scum.player.profile" | "scum.squads" | "scum.squad-members" | "scum.vehicles" | "scum.flags" | "scum.positions" | "scum.tasks" | "scum.events" | "scum.native-timed-gifts";
|
||||||
|
|
||||||
export const playerAttributeCatalog = [
|
export const playerAttributeCatalog = [
|
||||||
{ key: "stamina", label: "体力", column: "stamina", sourceKeys: ["attributes.stamina", "stamina", "体力"] },
|
{ key: "stamina", label: "体力", column: "stamina", sourceKeys: ["attributes.stamina", "stamina", "体力"] },
|
||||||
@@ -78,7 +78,7 @@ export async function queuePlayerAttributePatch(actions: SCUMWorkspaceActions, p
|
|||||||
|
|
||||||
export type SCUMWorkspaceActions = {
|
export type SCUMWorkspaceActions = {
|
||||||
pluginData?: PluginDataActions;
|
pluginData?: PluginDataActions;
|
||||||
logs?: LogActions;
|
gameClient?: GameClientActions;
|
||||||
dispatch?: (envelope: PluginBridgeExecuteEnvelope, signal?: AbortSignal) => Promise<PluginBridgeExecutionResult>;
|
dispatch?: (envelope: PluginBridgeExecuteEnvelope, signal?: AbortSignal) => Promise<PluginBridgeExecutionResult>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -134,8 +134,7 @@ export const scumCollections = {
|
|||||||
mapSettings: "scum_map_settings",
|
mapSettings: "scum_map_settings",
|
||||||
vehicles: "scum_vehicles",
|
vehicles: "scum_vehicles",
|
||||||
flags: "scum_flags",
|
flags: "scum_flags",
|
||||||
trajectories: "scum_trajectories",
|
trajectories: "scum_trajectories"
|
||||||
logCursors: "scum_log_cursors"
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type SurfaceKey = keyof SCUMSurfaceData;
|
type SurfaceKey = keyof SCUMSurfaceData;
|
||||||
@@ -149,56 +148,94 @@ const pageCollections: Record<PageKey, SurfaceKey[]> = {
|
|||||||
workflows: ["events", "eventProduces", "eventRuns", "nativeEventRounds", "tasks", "activityEvents", "tradeGoods", "tradeEvents"]
|
workflows: ["events", "eventProduces", "eventRuns", "nativeEventRounds", "tasks", "activityEvents", "tradeGoods", "tradeEvents"]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pageQueryTemplates: Record<PageKey, SCUMQueryTemplateKey[]> = {
|
||||||
|
players: ["scum.player.profile", "scum.positions"],
|
||||||
|
squads: ["scum.squads", "scum.squad-members", "scum.flags"],
|
||||||
|
"live-map": ["scum.player.profile", "scum.vehicles", "scum.flags", "scum.positions"],
|
||||||
|
gifts: ["scum.player.profile", "scum.native-timed-gifts"],
|
||||||
|
workflows: ["scum.player.profile", "scum.squads", "scum.squad-members", "scum.vehicles", "scum.flags", "scum.positions", "scum.tasks", "scum.events", "scum.native-timed-gifts"]
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryTemplatePollSeconds: Record<SCUMQueryTemplateKey, number> = {
|
||||||
|
"scum.player.profile": 5,
|
||||||
|
"scum.positions": 5,
|
||||||
|
"scum.vehicles": 5,
|
||||||
|
"scum.flags": 60,
|
||||||
|
"scum.squads": 1800,
|
||||||
|
"scum.squad-members": 1800,
|
||||||
|
"scum.tasks": 1800,
|
||||||
|
"scum.events": 1800,
|
||||||
|
"scum.native-timed-gifts": 60
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryTemplateMaxRows: Record<SCUMQueryTemplateKey, number> = {
|
||||||
|
"scum.player.profile": 500,
|
||||||
|
"scum.positions": 500,
|
||||||
|
"scum.vehicles": 500,
|
||||||
|
"scum.flags": 500,
|
||||||
|
"scum.squads": 500,
|
||||||
|
"scum.squad-members": 500,
|
||||||
|
"scum.tasks": 500,
|
||||||
|
"scum.events": 500,
|
||||||
|
"scum.native-timed-gifts": 500
|
||||||
|
};
|
||||||
|
|
||||||
|
const queuedQueryBuckets = new Set<string>();
|
||||||
|
|
||||||
export async function loadSCUMSurface(actions: SCUMWorkspaceActions, pageKey: string): Promise<SCUMSurfaceData> {
|
export async function loadSCUMSurface(actions: SCUMWorkspaceActions, pageKey: string): Promise<SCUMSurfaceData> {
|
||||||
if (!actions.pluginData) throw new Error("通用 pluginData 能力不可用。");
|
if (!actions.pluginData) throw new Error("通用 pluginData 能力不可用。");
|
||||||
const canonical = canonicalPageKey(pageKey);
|
const canonical = canonicalPageKey(pageKey);
|
||||||
if (pageCollections[canonical].includes("players")) await projectSCUMLoginLogs(actions).catch(() => undefined);
|
await queueSCUMDatabaseRefresh(actions, canonical).catch(() => undefined);
|
||||||
const data: SCUMSurfaceData = { ...emptySCUMSurfaceData };
|
const data: SCUMSurfaceData = { ...emptySCUMSurfaceData };
|
||||||
const keys = pageCollections[canonical];
|
const keys = pageCollections[canonical];
|
||||||
const records = await Promise.all(keys.map(async (key) => [key, await actions.pluginData!.list(scumCollections[key])] as const));
|
const records = await Promise.all(keys.map(async (key) => [key, await actions.pluginData!.list(scumCollections[key])] as const));
|
||||||
for (const [key, response] of records) data[key] = collectionRecords(response);
|
for (const [key, response] of records) data[key] = collectionRecords(response);
|
||||||
|
await mergeSCUMSnapshots(actions, data, keys).catch(() => undefined);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function projectSCUMLoginLogs(actions: SCUMWorkspaceActions, limitPerStream = 200): Promise<number> {
|
export async function queueSCUMDatabaseRefresh(actions: SCUMWorkspaceActions, pageKey: string, now = Date.now()): Promise<PluginBridgeExecutionResult[]> {
|
||||||
if (!actions.pluginData || !actions.logs) return 0;
|
if (!actions.dispatch) return [];
|
||||||
const streamResponse = await actions.logs.listStreams();
|
const canonical = canonicalPageKey(pageKey);
|
||||||
const streams = logStreams(streamResponse).filter((stream) => textValue(stream.streamKey) === "scum.login" && textValue(stream.id));
|
const templates = [...new Set(pageQueryTemplates[canonical])];
|
||||||
if (!streams.length) return 0;
|
const requests = templates.flatMap((templateKey) => {
|
||||||
const existingPlayers = collectionRecords(await actions.pluginData.list(scumCollections.players));
|
const bucketKey = scumQueryBucketKey(templateKey, now);
|
||||||
const playerByIdentity = playerIndex(existingPlayers);
|
if (queuedQueryBuckets.has(bucketKey)) return [];
|
||||||
const playerMutations = new Map<string, PluginDataMutation>();
|
queuedQueryBuckets.add(bucketKey);
|
||||||
const activityMutations: PluginDataMutation[] = [];
|
return [actions.dispatch!({ requestId: scumQueryRequestId(canonical, templateKey, now), action: "remote.access.request", payload: scumQueryPayload(templateKey, now) })];
|
||||||
let projected = 0;
|
});
|
||||||
for (const stream of streams) {
|
const settled = await Promise.allSettled(requests);
|
||||||
const streamId = textValue(stream.id);
|
return settled.flatMap((item) => item.status === "fulfilled" ? [item.value] : []);
|
||||||
const cursorKey = `scum.login:${streamId}`;
|
}
|
||||||
const cursor = firstCollectionRecord(await actions.pluginData.list(scumCollections.logCursors, cursorKey));
|
|
||||||
const afterSeq = Math.max(0, numberValue(cursor?.nextSeq ?? cursor?.lastSeq));
|
function scumQueryPayload(templateKey: SCUMQueryTemplateKey, now: number): Record<string, string> {
|
||||||
const response = await actions.logs.query({ logStreamId: streamId, afterSeq, limit: limitPerStream });
|
const maxRows = String(queryTemplateMaxRows[templateKey]);
|
||||||
const entries = logEntries(response);
|
const payload: Record<string, string> = {
|
||||||
let lastSeq = afterSeq;
|
capability: "remote.run.db.sqlite.query",
|
||||||
for (const entry of entries) {
|
declarationKey: "scum-database",
|
||||||
const seq = numberValue(entry.seq);
|
targetKey: "scum-database",
|
||||||
if (seq > lastSeq) lastSeq = seq;
|
idempotencyKey: scumQueryIdempotencyKey(templateKey, now),
|
||||||
const event = parseSCUMLoginLogEntry(entry, streamId);
|
timeoutSeconds: "15",
|
||||||
if (!event) continue;
|
maxAttempts: "1",
|
||||||
const match = findPlayer(playerByIdentity, event);
|
"input.templateKey": templateKey,
|
||||||
const playerKeyValue = match ? textValue(existingPlayers[match.index]._recordKey) || scumLogPlayerKey(event) : scumLogPlayerKey(event);
|
"input.limit": maxRows,
|
||||||
const existing = match ? existingPlayers[match.index] : {};
|
"input.maxRows": maxRows
|
||||||
const player = scumLoginPlayerRecord(existing, event);
|
};
|
||||||
playerMutations.set(playerKeyValue, { operation: "put", key: playerKeyValue, value: player });
|
if (templateKey === "scum.player.profile" || templateKey === "scum.positions" || templateKey === "scum.vehicles") payload["input.activeWithinSeconds"] = "600";
|
||||||
if (match) existingPlayers[match.index] = { ...player, _recordKey: playerKeyValue };
|
return payload;
|
||||||
else { existingPlayers.push({ ...player, _recordKey: playerKeyValue }); addPlayerToIndex(playerByIdentity, player, existingPlayers.length - 1); }
|
}
|
||||||
activityMutations.push({ operation: "put", key: `${streamId}:${seq || stableTextHash(textValue(event.rawLine))}`, value: scumLoginActivityRecord(event) });
|
|
||||||
projected++;
|
function scumQueryRequestId(pageKey: PageKey, templateKey: SCUMQueryTemplateKey, now: number): string { return `scum-query:${pageKey}:${templateKey}:${queryBucket(templateKey, now)}`; }
|
||||||
}
|
function scumQueryIdempotencyKey(templateKey: SCUMQueryTemplateKey, now: number): string { return `scum-query:${templateKey}:${queryBucket(templateKey, now)}`; }
|
||||||
const nextSeq = Math.max(lastSeq, numberValue((response as RecordMap).nextSeq), numberValue((response as RecordMap).latestSeq));
|
function scumQueryBucketKey(templateKey: SCUMQueryTemplateKey, now: number): string { return `${templateKey}:${queryBucket(templateKey, now)}`; }
|
||||||
if (nextSeq > afterSeq) await actions.pluginData.put(scumCollections.logCursors, cursorKey, { streamId, streamKey: "scum.login", nextSeq, updatedAt: new Date().toISOString() });
|
function queryBucket(templateKey: SCUMQueryTemplateKey, now: number): number { return Math.floor(now / (queryTemplatePollSeconds[templateKey] * 1000)); }
|
||||||
}
|
|
||||||
if (playerMutations.size) await actions.pluginData.transact(scumCollections.players, [...playerMutations.values()]);
|
async function mergeSCUMSnapshots(actions: SCUMWorkspaceActions, data: SCUMSurfaceData, keys: SurfaceKey[]): Promise<void> {
|
||||||
if (activityMutations.length) await actions.pluginData.transact(scumCollections.activityEvents, activityMutations);
|
if (!actions.gameClient) return;
|
||||||
return projected;
|
const reads: Array<Promise<void>> = [];
|
||||||
|
if (keys.includes("players") && data.players.length === 0) reads.push(actions.gameClient.snapshots({ profileKey: "plugin-owned", type: "players", streamKey: "current", limit: 1 }).then((response) => { data.players = mergePlayerSnapshots(data.players, response); }));
|
||||||
|
if (keys.includes("vehicles") && data.vehicles.length === 0) reads.push(actions.gameClient.snapshots({ profileKey: "plugin-owned", type: "vehicles", streamKey: "current", limit: 1 }).then((response) => { data.vehicles = mergeVehicleSnapshots(data.vehicles, response); }));
|
||||||
|
await Promise.all(reads);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergePlayerSnapshots(players: RecordMap[], playersResponse: unknown): RecordMap[] {
|
export function mergePlayerSnapshots(players: RecordMap[], playersResponse: unknown): RecordMap[] {
|
||||||
@@ -372,69 +409,6 @@ function collectionRecords(response: unknown): RecordMap[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function firstCollectionRecord(response: unknown): RecordMap | undefined { return collectionRecords(response)[0]; }
|
|
||||||
|
|
||||||
function logStreams(response: unknown): RecordMap[] { return isRecord(response) && Array.isArray(response.items) ? response.items.filter(isRecord) : []; }
|
|
||||||
function logEntries(response: unknown): RecordMap[] { return isRecord(response) && Array.isArray(response.entries) ? response.entries.filter(isRecord) : []; }
|
|
||||||
|
|
||||||
function parseSCUMLoginLogEntry(entry: RecordMap, streamId: string): RecordMap | null {
|
|
||||||
const line = textValue(entry.line);
|
|
||||||
if (!line.trim()) return null;
|
|
||||||
const fields = isRecord(entry.fields) ? entry.fields : {};
|
|
||||||
const explicitType = firstText(fields, "eventType", "type", "action").toLowerCase();
|
|
||||||
const lowered = line.toLowerCase();
|
|
||||||
const logout = /\b(logged\s*out|logout|disconnected|left)\b/.test(explicitType) || /(?:\blogged\s*out\b|\blogout\s*:|\bdisconnected\b|\bleft\s+the\s+server\b)/i.test(line);
|
|
||||||
const login = /\b(logged\s*in|login|connected|joined)\b/.test(explicitType) || /(?:\blogged\s*in\b|\blogin\s*:|\bconnected\b|\bjoined\s+the\s+server\b)/i.test(line);
|
|
||||||
if (!login && !logout) return null;
|
|
||||||
const steamId = firstText(fields, "steamId", "steamID", "userId", "userID", "playerId", "gamePlayerId") || firstRegex(line, /\b\d{17}\b/);
|
|
||||||
const displayName = firstText(fields, "displayName", "playerName", "name", "characterName") || extractSCUMPlayerName(line, steamId);
|
|
||||||
const ip = firstText(fields, "lastLoginIp", "loginIp", "ipAddress", "ip") || firstRegex(line, /\b(?:\d{1,3}\.){3}\d{1,3}\b/);
|
|
||||||
if (!steamId && !displayName && !ip) return null;
|
|
||||||
const occurredAt = textValue(entry.timestamp) || new Date().toISOString();
|
|
||||||
return { eventType: logout ? "logout" : "login", steamId, gamePlayerId: steamId, displayName, lastLoginIp: ip, loginIp: ip, occurredAt, observedAt: occurredAt, rawLine: line, streamId, seq: numberValue(entry.seq), source: "plugin.log.scum.login" };
|
|
||||||
}
|
|
||||||
|
|
||||||
function scumLoginPlayerRecord(existing: RecordMap, event: RecordMap): RecordMap {
|
|
||||||
const occurredAt = textValue(event.occurredAt);
|
|
||||||
const login = textValue(event.eventType) === "login";
|
|
||||||
const identity = { steamId: textValue(event.steamId), gamePlayerId: textValue(event.gamePlayerId), displayName: textValue(event.displayName) };
|
|
||||||
const network = textValue(event.lastLoginIp) ? { lastLoginIp: textValue(event.lastLoginIp), loginIp: textValue(event.loginIp) } : {};
|
|
||||||
const status = login
|
|
||||||
? { online: true, status: "online", lastSeenAt: occurredAt, lastLoginAt: occurredAt, lastLoginObservedAt: occurredAt, lastLoginRawLine: textValue(event.rawLine) }
|
|
||||||
: { online: false, status: "offline", lastSeenAt: occurredAt, lastLogoutAt: occurredAt, lastLogoutObservedAt: occurredAt, lastLogoutRawLine: textValue(event.rawLine) };
|
|
||||||
return { ...existing, ...withoutEmpty(identity), ...network, ...status, source: "plugin.log.scum.login", updatedAt: occurredAt };
|
|
||||||
}
|
|
||||||
|
|
||||||
function scumLoginActivityRecord(event: RecordMap): RecordMap {
|
|
||||||
return withoutEmpty({ id: `${textValue(event.streamId)}:${numberValue(event.seq) || stableTextHash(textValue(event.rawLine))}`, eventType: textValue(event.eventType), type: textValue(event.eventType), steamId: textValue(event.steamId), gamePlayerId: textValue(event.gamePlayerId), displayName: textValue(event.displayName), lastLoginIp: textValue(event.lastLoginIp), loginIp: textValue(event.loginIp), occurredAt: textValue(event.occurredAt), observedAt: textValue(event.observedAt), rawLine: textValue(event.rawLine), streamId: textValue(event.streamId), seq: numberValue(event.seq), source: "plugin.log.scum.login" });
|
|
||||||
}
|
|
||||||
|
|
||||||
function scumLogPlayerKey(event: RecordMap): string {
|
|
||||||
const identity = firstText(event, "steamId", "gamePlayerId");
|
|
||||||
if (identity) return identity;
|
|
||||||
const name = textValue(event.displayName).trim().toLowerCase();
|
|
||||||
if (name) return `name:${safeCommandId(name)}`;
|
|
||||||
return `log:${stableTextHash(textValue(event.rawLine))}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractSCUMPlayerName(line: string, steamId: string): string {
|
|
||||||
for (const pattern of [/\b(?:player|user|name|character)\s*[:=]\s*["']?([^"'(),;\[\]]{2,80})/i, /\b(?:player|user|character)\s+["']([^"']{2,80})["']/i, /["']([^"']{2,80})["']\s*(?:\(|\[|SteamID\s*)?\d{17}/i, /\d{17}\s*(?:\)|\])?\s*["']([^"']{2,80})["']/]) {
|
|
||||||
const match = pattern.exec(line);
|
|
||||||
if (match?.[1]) return match[1].trim();
|
|
||||||
}
|
|
||||||
if (steamId) {
|
|
||||||
const index = line.indexOf(steamId);
|
|
||||||
const before = index > 0 ? line.slice(0, index).replace(/.*\b(?:player|user|character)\b\s*/i, "").replace(/[\[(,:;-]+$/g, "").trim() : "";
|
|
||||||
if (before && before.length <= 80) return before;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function firstRegex(value: string, pattern: RegExp): string { return pattern.exec(value)?.[0] ?? ""; }
|
|
||||||
function numberValue(value: unknown): number { const parsed = Number(value); return Number.isFinite(parsed) && parsed > 0 ? parsed : 0; }
|
|
||||||
function withoutEmpty(value: RecordMap): RecordMap { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== "" && item !== undefined && item !== null)); }
|
|
||||||
function stableTextHash(value: string): string { let hash = 2166136261; for (let index = 0; index < value.length; index++) { hash ^= value.charCodeAt(index); hash = Math.imul(hash, 16777619); } return (hash >>> 0).toString(16); }
|
|
||||||
|
|
||||||
function latestSnapshotPayload(response: unknown): RecordMap | undefined {
|
function latestSnapshotPayload(response: unknown): RecordMap | undefined {
|
||||||
if (!isRecord(response) || !Array.isArray(response.items)) return undefined;
|
if (!isRecord(response) || !Array.isArray(response.items)) return undefined;
|
||||||
const snapshots = response.items.filter(isRecord).sort((left, right) => snapshotOrder(right) - snapshotOrder(left));
|
const snapshots = response.items.filter(isRecord).sort((left, right) => snapshotOrder(right) - snapshotOrder(left));
|
||||||
|
|||||||
@@ -141,14 +141,14 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
|
|||||||
if (react.useEffect) react.useEffect(() => {
|
if (react.useEffect) react.useEffect(() => {
|
||||||
if (playerPanel.kind === "closed") refresh();
|
if (playerPanel.kind === "closed") refresh();
|
||||||
if (playerPanel.kind !== "closed") return;
|
if (playerPanel.kind !== "closed") return;
|
||||||
const interval = setInterval(refresh, 3000);
|
const interval = setInterval(refresh, 5000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [input.serverInstanceId, pageKey, input.workspaceActions, playerPanel.kind]);
|
}, [input.serverInstanceId, pageKey, input.workspaceActions, playerPanel.kind]);
|
||||||
|
|
||||||
const data = state.status === "ready" ? state.data : emptySCUMSurfaceData;
|
const data = state.status === "ready" ? state.data : emptySCUMSurfaceData;
|
||||||
return e("section", { className: "console-panel scum-workbench", "aria-label": input.pageTitle ?? surfaceTitle(pageKey) },
|
return e("section", { className: "console-panel scum-workbench", "aria-label": input.pageTitle ?? surfaceTitle(pageKey) },
|
||||||
action.status !== "idle" ? e("p", { className: "page-status", "data-state": action.status }, action.message) : null,
|
action.status !== "idle" ? e("p", { className: "page-status", "data-state": action.status }, action.message) : null,
|
||||||
state.status === "loading" ? e("p", { className: "page-status" }, "正在读取插件自有 SCUM 集合…") : null,
|
state.status === "loading" ? e("p", { className: "page-status" }, "正在派发 SCUM 数据库模板查询并读取插件自有集合…") : null,
|
||||||
state.status === "error" ? e("p", { className: "page-status", "data-state": "error" }, state.reason) : null,
|
state.status === "error" ? e("p", { className: "page-status", "data-state": "error" }, state.reason) : null,
|
||||||
state.status === "ready" ? renderSurfaceBody(e, pageKey, data, input, {
|
state.status === "ready" ? renderSurfaceBody(e, pageKey, data, input, {
|
||||||
playerSearch, setPlayerSearch, playerStatus, setPlayerStatus, playerPanel, setPlayerPanel, attributeDrafts, setAttributeDrafts, squadSearch, setSquadSearch, selectedSquadId, setSelectedSquadId,
|
playerSearch, setPlayerSearch, playerStatus, setPlayerStatus, playerPanel, setPlayerPanel, attributeDrafts, setAttributeDrafts, squadSearch, setSquadSearch, selectedSquadId, setSelectedSquadId,
|
||||||
|
|||||||
@@ -239,21 +239,7 @@
|
|||||||
"sqlRef": "sql/scum-db-v57/users.sql",
|
"sqlRef": "sql/scum-db-v57/users.sql",
|
||||||
"pollIntervalSeconds": 3,
|
"pollIntervalSeconds": 3,
|
||||||
"maxRows": 500,
|
"maxRows": 500,
|
||||||
"timeoutSeconds": 15,
|
"timeoutSeconds": 15
|
||||||
"projections": [
|
|
||||||
{
|
|
||||||
"collection": "scum_users",
|
|
||||||
"rowPath": "rows",
|
|
||||||
"upsertKeys": [
|
|
||||||
"steamId"
|
|
||||||
],
|
|
||||||
"fixedValues": {
|
|
||||||
"source": "run.sqlite.scum.player.profile"
|
|
||||||
},
|
|
||||||
"observedAtField": "profileSampledAt",
|
|
||||||
"mergeExisting": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "scum.squads",
|
"key": "scum.squads",
|
||||||
@@ -295,66 +281,7 @@
|
|||||||
"sqlRef": "sql/scum-db-v57/vehicles.sql",
|
"sqlRef": "sql/scum-db-v57/vehicles.sql",
|
||||||
"pollIntervalSeconds": 3,
|
"pollIntervalSeconds": 3,
|
||||||
"maxRows": 500,
|
"maxRows": 500,
|
||||||
"timeoutSeconds": 15,
|
"timeoutSeconds": 15
|
||||||
"projections": [
|
|
||||||
{
|
|
||||||
"collection": "scum_vehicles",
|
|
||||||
"rowPath": "rows",
|
|
||||||
"upsertKeys": [
|
|
||||||
"vehicleId"
|
|
||||||
],
|
|
||||||
"fixedValues": {
|
|
||||||
"source": "run.sqlite.scum.vehicles"
|
|
||||||
},
|
|
||||||
"observedAtField": "sampledAt"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collection": "scum_trajectories",
|
|
||||||
"rowPath": "rows",
|
|
||||||
"upsertKeys": [
|
|
||||||
"subjectType",
|
|
||||||
"subjectId",
|
|
||||||
"sampledAt"
|
|
||||||
],
|
|
||||||
"fieldMappings": {
|
|
||||||
"subjectId": "vehicleId",
|
|
||||||
"vehicleId": "vehicleId",
|
|
||||||
"entityId": "entityId",
|
|
||||||
"className": "className",
|
|
||||||
"label": "label",
|
|
||||||
"x": "x",
|
|
||||||
"y": "y",
|
|
||||||
"z": "z",
|
|
||||||
"lastAccessTime": "lastAccessTime"
|
|
||||||
},
|
|
||||||
"fixedValues": {
|
|
||||||
"subjectType": "vehicle",
|
|
||||||
"source": "run.sqlite.scum.vehicles"
|
|
||||||
},
|
|
||||||
"observedAtField": "sampledAt"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collection": "scum_trade_goods",
|
|
||||||
"rowPath": "rows",
|
|
||||||
"upsertKeys": [
|
|
||||||
"code"
|
|
||||||
],
|
|
||||||
"fieldMappings": {
|
|
||||||
"className": "className"
|
|
||||||
},
|
|
||||||
"fixedValues": {
|
|
||||||
"code": "#spawnvehicle {{className}}",
|
|
||||||
"spawnCommand": "#spawnvehicle {{className}}",
|
|
||||||
"catalogType": "vehicle",
|
|
||||||
"type": "21",
|
|
||||||
"typeName": "其他载具",
|
|
||||||
"imagePath": "/original/{{className}}.webp",
|
|
||||||
"source": "run.sqlite.scum.vehicles"
|
|
||||||
},
|
|
||||||
"observedAtField": "lastSeenAt",
|
|
||||||
"mergeExisting": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "scum.flags",
|
"key": "scum.flags",
|
||||||
@@ -382,69 +309,7 @@
|
|||||||
"sqlRef": "sql/scum-db-v57/map-points.sql",
|
"sqlRef": "sql/scum-db-v57/map-points.sql",
|
||||||
"pollIntervalSeconds": 3,
|
"pollIntervalSeconds": 3,
|
||||||
"maxRows": 500,
|
"maxRows": 500,
|
||||||
"timeoutSeconds": 15,
|
"timeoutSeconds": 15
|
||||||
"projections": [
|
|
||||||
{
|
|
||||||
"collection": "scum_users",
|
|
||||||
"rowPath": "rows",
|
|
||||||
"matchField": "subjectType",
|
|
||||||
"matchValue": "player",
|
|
||||||
"upsertKeys": [
|
|
||||||
"steamId"
|
|
||||||
],
|
|
||||||
"fieldMappings": {
|
|
||||||
"steamId": "subjectId",
|
|
||||||
"userProfileId": "userProfileId",
|
|
||||||
"gamePlayerId": "gamePlayerId",
|
|
||||||
"x": "x",
|
|
||||||
"y": "y",
|
|
||||||
"z": "z",
|
|
||||||
"lastPositionObservedAt": "observedAt"
|
|
||||||
},
|
|
||||||
"fixedValues": {
|
|
||||||
"source": "run.sqlite.scum.positions"
|
|
||||||
},
|
|
||||||
"observedAtField": "positionSampledAt"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collection": "scum_map_points",
|
|
||||||
"rowPath": "rows",
|
|
||||||
"upsertKeys": [
|
|
||||||
"subjectType",
|
|
||||||
"subjectId"
|
|
||||||
],
|
|
||||||
"fixedValues": {
|
|
||||||
"source": "run.sqlite.scum.positions"
|
|
||||||
},
|
|
||||||
"observedAtField": "sampledAt"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collection": "scum_trajectories",
|
|
||||||
"rowPath": "rows",
|
|
||||||
"matchField": "subjectType",
|
|
||||||
"matchValue": "player",
|
|
||||||
"upsertKeys": [
|
|
||||||
"subjectType",
|
|
||||||
"subjectId",
|
|
||||||
"sampledAt"
|
|
||||||
],
|
|
||||||
"fieldMappings": {
|
|
||||||
"subjectType": "subjectType",
|
|
||||||
"subjectId": "subjectId",
|
|
||||||
"steamId": "subjectId",
|
|
||||||
"userProfileId": "userProfileId",
|
|
||||||
"gamePlayerId": "gamePlayerId",
|
|
||||||
"x": "x",
|
|
||||||
"y": "y",
|
|
||||||
"z": "z",
|
|
||||||
"observedAt": "observedAt"
|
|
||||||
},
|
|
||||||
"fixedValues": {
|
|
||||||
"source": "run.sqlite.scum.positions"
|
|
||||||
},
|
|
||||||
"observedAtField": "sampledAt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "scum.tasks",
|
"key": "scum.tasks",
|
||||||
@@ -489,121 +354,6 @@
|
|||||||
"timeoutSeconds": 15
|
"timeoutSeconds": 15
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"lifecycleProjections": [
|
|
||||||
{
|
|
||||||
"key": "scum.lifecycle.stop-logout",
|
|
||||||
"capabilities": [
|
|
||||||
"process.stop"
|
|
||||||
],
|
|
||||||
"target": {
|
|
||||||
"collection": "scum_users",
|
|
||||||
"matchField": "online",
|
|
||||||
"matchValue": "true",
|
|
||||||
"fixedValues": {
|
|
||||||
"online": "false",
|
|
||||||
"status": "offline",
|
|
||||||
"logoutReason": "server-stop",
|
|
||||||
"source": "run.lifecycle"
|
|
||||||
},
|
|
||||||
"observedAtField": "lastLogoutObservedAt",
|
|
||||||
"activityTarget": {
|
|
||||||
"collection": "scum_activity_events",
|
|
||||||
"upsertKeys": [
|
|
||||||
"steamId",
|
|
||||||
"observedAt",
|
|
||||||
"eventType"
|
|
||||||
],
|
|
||||||
"rowMappings": {
|
|
||||||
"steamId": "steamId",
|
|
||||||
"displayName": "displayName"
|
|
||||||
},
|
|
||||||
"fixedValues": {
|
|
||||||
"eventType": "logout",
|
|
||||||
"reason": "server-stop",
|
|
||||||
"source": "run.lifecycle"
|
|
||||||
},
|
|
||||||
"observedAtField": "observedAt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "scum.lifecycle.restart-logout",
|
|
||||||
"capabilities": [
|
|
||||||
"process.restart"
|
|
||||||
],
|
|
||||||
"target": {
|
|
||||||
"collection": "scum_users",
|
|
||||||
"matchField": "online",
|
|
||||||
"matchValue": "true",
|
|
||||||
"fixedValues": {
|
|
||||||
"online": "false",
|
|
||||||
"status": "offline",
|
|
||||||
"logoutReason": "server-stop",
|
|
||||||
"source": "run.lifecycle"
|
|
||||||
},
|
|
||||||
"observedAtField": "lastLogoutObservedAt",
|
|
||||||
"activityTarget": {
|
|
||||||
"collection": "scum_activity_events",
|
|
||||||
"upsertKeys": [
|
|
||||||
"steamId",
|
|
||||||
"observedAt",
|
|
||||||
"eventType"
|
|
||||||
],
|
|
||||||
"rowMappings": {
|
|
||||||
"steamId": "steamId",
|
|
||||||
"displayName": "displayName"
|
|
||||||
},
|
|
||||||
"fixedValues": {
|
|
||||||
"eventType": "logout",
|
|
||||||
"reason": "server-stop",
|
|
||||||
"source": "run.lifecycle"
|
|
||||||
},
|
|
||||||
"observedAtField": "observedAt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "scum.lifecycle.status-logout",
|
|
||||||
"capabilities": [
|
|
||||||
"process.status"
|
|
||||||
],
|
|
||||||
"processStates": [
|
|
||||||
"stopped",
|
|
||||||
"not-started",
|
|
||||||
"exited"
|
|
||||||
],
|
|
||||||
"target": {
|
|
||||||
"collection": "scum_users",
|
|
||||||
"matchField": "online",
|
|
||||||
"matchValue": "true",
|
|
||||||
"fixedValues": {
|
|
||||||
"online": "false",
|
|
||||||
"status": "offline",
|
|
||||||
"logoutReason": "server-stop",
|
|
||||||
"source": "run.lifecycle"
|
|
||||||
},
|
|
||||||
"observedAtField": "lastLogoutObservedAt",
|
|
||||||
"activityTarget": {
|
|
||||||
"collection": "scum_activity_events",
|
|
||||||
"upsertKeys": [
|
|
||||||
"steamId",
|
|
||||||
"observedAt",
|
|
||||||
"eventType"
|
|
||||||
],
|
|
||||||
"rowMappings": {
|
|
||||||
"steamId": "steamId",
|
|
||||||
"displayName": "displayName"
|
|
||||||
},
|
|
||||||
"fixedValues": {
|
|
||||||
"eventType": "logout",
|
|
||||||
"reason": "server-stop",
|
|
||||||
"source": "run.lifecycle"
|
|
||||||
},
|
|
||||||
"observedAtField": "observedAt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"dataPacks": [
|
"dataPacks": [
|
||||||
{
|
{
|
||||||
"key": "scum-db-v57",
|
"key": "scum-db-v57",
|
||||||
@@ -1068,7 +818,6 @@
|
|||||||
"bundleIntegritySha256": "sha256:3488b316d909e597024f8f31c7bc96ab8019f643d74a528dc442d3df0dc3d54e",
|
"bundleIntegritySha256": "sha256:3488b316d909e597024f8f31c7bc96ab8019f643d74a528dc442d3df0dc3d54e",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"server.read",
|
"server.read",
|
||||||
"server.logs.read",
|
|
||||||
"server.remote.access",
|
"server.remote.access",
|
||||||
"server.game-client.read",
|
"server.game-client.read",
|
||||||
"server.game-client.command",
|
"server.game-client.command",
|
||||||
|
|||||||
+1
@@ -8,6 +8,7 @@
|
|||||||
"userProfileId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
"userProfileId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
||||||
"steamId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
"steamId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
||||||
"search": { "type": "string", "minLength": 1, "maxLength": 80 },
|
"search": { "type": "string", "minLength": 1, "maxLength": 80 },
|
||||||
|
"activeWithinSeconds": { "type": "integer", "minimum": 1, "maximum": 86400 },
|
||||||
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
|
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -11,21 +11,25 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["userProfileId", "steamId", "gamePlayerId", "displayName", "squadId", "squadName", "famePoints", "normalBalance", "goldBalance", "x", "y", "z", "lastLoginTime", "lastSaveTime"],
|
"required": ["userProfileId", "steamId", "gamePlayerId", "displayName", "lastLoginIp", "registeredAt", "squadId", "squadName", "famePoints", "moneyBalance", "normalBalance", "goldBalance", "x", "y", "z", "lastLoginTime", "lastLogoutTime", "lastSaveTime"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"gamePlayerId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
"gamePlayerId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||||
"userProfileId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
"userProfileId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||||
"steamId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
"steamId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||||
"displayName": { "type": "string", "minLength": 1, "maxLength": 80 },
|
"displayName": { "type": "string", "minLength": 1, "maxLength": 80 },
|
||||||
|
"lastLoginIp": { "type": ["string", "null"], "maxLength": 96 },
|
||||||
|
"registeredAt": { "type": ["string", "null"], "maxLength": 120 },
|
||||||
"squadId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
"squadId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||||
"squadName": { "type": ["string", "null"], "minLength": 1, "maxLength": 80 },
|
"squadName": { "type": ["string", "null"], "minLength": 1, "maxLength": 80 },
|
||||||
"famePoints": { "type": ["number", "null"] },
|
"famePoints": { "type": ["number", "null"] },
|
||||||
|
"moneyBalance": { "type": ["number", "null"] },
|
||||||
"normalBalance": { "type": ["number", "null"] },
|
"normalBalance": { "type": ["number", "null"] },
|
||||||
"goldBalance": { "type": ["number", "null"] },
|
"goldBalance": { "type": ["number", "null"] },
|
||||||
"x": { "type": ["number", "null"] },
|
"x": { "type": ["number", "null"] },
|
||||||
"y": { "type": ["number", "null"] },
|
"y": { "type": ["number", "null"] },
|
||||||
"z": { "type": ["number", "null"] },
|
"z": { "type": ["number", "null"] },
|
||||||
"lastLoginTime": { "type": ["string", "null"], "maxLength": 120 },
|
"lastLoginTime": { "type": ["string", "null"], "maxLength": 120 },
|
||||||
|
"lastLogoutTime": { "type": ["string", "null"], "maxLength": 120 },
|
||||||
"lastSaveTime": { "type": ["string", "null"], "format": "date-time" }
|
"lastSaveTime": { "type": ["string", "null"], "format": "date-time" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -6,6 +6,7 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"subjectType": { "enum": ["player", "vehicle", "base", "flag"] },
|
"subjectType": { "enum": ["player", "vehicle", "base", "flag"] },
|
||||||
"subjectId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
"subjectId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||||
|
"activeWithinSeconds": { "type": "integer", "minimum": 1, "maximum": 86400 },
|
||||||
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
|
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -7,6 +7,8 @@
|
|||||||
"vehicleId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
"vehicleId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||||
"ownerProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
"ownerProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||||
"squadId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
"squadId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||||
|
"search": { "type": "string", "minLength": 1, "maxLength": 80 },
|
||||||
|
"activeWithinSeconds": { "type": "integer", "minimum": 1, "maximum": 86400 },
|
||||||
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
|
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -11,7 +11,7 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["vehicleId", "entityId", "className", "label", "x", "y", "z", "lastAccessTime", "isFunctional"],
|
"required": ["vehicleId", "entityId", "className", "label", "x", "y", "z", "lastAccessTime", "isFunctional", "existsInGame", "mountedPrisonerIds", "mountedUserProfileIds", "mountedSteamIds"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"vehicleId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
"vehicleId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||||
"entityId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
"entityId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||||
@@ -19,6 +19,10 @@
|
|||||||
"label": { "type": "string", "maxLength": 120 },
|
"label": { "type": "string", "maxLength": 120 },
|
||||||
"lastAccessTime": { "type": ["string", "null"], "format": "date-time" },
|
"lastAccessTime": { "type": ["string", "null"], "format": "date-time" },
|
||||||
"isFunctional": { "type": "integer", "minimum": 0, "maximum": 1 },
|
"isFunctional": { "type": "integer", "minimum": 0, "maximum": 1 },
|
||||||
|
"existsInGame": { "type": "integer", "minimum": 0, "maximum": 1 },
|
||||||
|
"mountedPrisonerIds": { "type": ["string", "null"], "maxLength": 2048 },
|
||||||
|
"mountedUserProfileIds": { "type": ["string", "null"], "maxLength": 2048 },
|
||||||
|
"mountedSteamIds": { "type": ["string", "null"], "maxLength": 4096 },
|
||||||
"x": { "type": "number" },
|
"x": { "type": "number" },
|
||||||
"y": { "type": "number" },
|
"y": { "type": "number" },
|
||||||
"z": { "type": "number" }
|
"z": { "type": "number" }
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ SELECT
|
|||||||
account.id AS subjectId,
|
account.id AS subjectId,
|
||||||
CAST(profile.id AS TEXT) AS userProfileId,
|
CAST(profile.id AS TEXT) AS userProfileId,
|
||||||
CAST(prisoner.id AS TEXT) AS gamePlayerId,
|
CAST(prisoner.id AS TEXT) AS gamePlayerId,
|
||||||
NULL AS vehicleId,
|
CAST(mount.vehicle_entity_id AS TEXT) AS vehicleId,
|
||||||
CAST(entity.id AS TEXT) AS entityId,
|
CAST(entity.id AS TEXT) AS entityId,
|
||||||
NULL AS baseId,
|
NULL AS baseId,
|
||||||
entity.location_x AS x,
|
entity.location_x AS x,
|
||||||
@@ -15,8 +15,10 @@ JOIN user account ON account.id = profile.user_id
|
|||||||
JOIN prisoner ON prisoner.id = profile.prisoner_id
|
JOIN prisoner ON prisoner.id = profile.prisoner_id
|
||||||
JOIN prisoner_entity ON prisoner_entity.prisoner_id = prisoner.id
|
JOIN prisoner_entity ON prisoner_entity.prisoner_id = prisoner.id
|
||||||
JOIN entity ON entity.id = prisoner_entity.entity_id
|
JOIN entity ON entity.id = prisoner_entity.entity_id
|
||||||
|
LEFT JOIN prisoner_vehicle_mountee_info mount ON mount.prisoner_id = prisoner.id
|
||||||
WHERE (:subjectType IS NULL OR :subjectType = 'player')
|
WHERE (:subjectType IS NULL OR :subjectType = 'player')
|
||||||
AND (:subjectId IS NULL OR account.id = :subjectId)
|
AND (:subjectId IS NULL OR account.id = :subjectId)
|
||||||
|
AND (:activeWithinSeconds IS NULL OR profile.last_logout_time IS NULL OR profile.last_login_time > profile.last_logout_time OR prisoner.last_save_time >= CAST(strftime('%s', 'now') AS INTEGER) - :activeWithinSeconds OR profile.last_login_time >= strftime('%Y-%m-%dT%H:%M:%fZ', 'now', printf('-%d seconds', :activeWithinSeconds)))
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT
|
SELECT
|
||||||
'vehicle', CAST(spawner.vehicle_entity_id AS TEXT), NULL, NULL,
|
'vehicle', CAST(spawner.vehicle_entity_id AS TEXT), NULL, NULL,
|
||||||
@@ -27,6 +29,7 @@ FROM vehicle_spawner spawner
|
|||||||
JOIN entity ON entity.id = spawner.vehicle_entity_id
|
JOIN entity ON entity.id = spawner.vehicle_entity_id
|
||||||
WHERE (:subjectType IS NULL OR :subjectType = 'vehicle')
|
WHERE (:subjectType IS NULL OR :subjectType = 'vehicle')
|
||||||
AND (:subjectId IS NULL OR CAST(spawner.vehicle_entity_id AS TEXT) = :subjectId)
|
AND (:subjectId IS NULL OR CAST(spawner.vehicle_entity_id AS TEXT) = :subjectId)
|
||||||
|
AND (:activeWithinSeconds IS NULL OR spawner.vehicle_last_access_time >= CAST(strftime('%s', 'now') AS INTEGER) - :activeWithinSeconds)
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT
|
SELECT
|
||||||
'base', CAST(base.id AS TEXT), CAST(base.owner_user_profile_id AS TEXT), NULL,
|
'base', CAST(base.id AS TEXT), CAST(base.owner_user_profile_id AS TEXT), NULL,
|
||||||
|
|||||||
@@ -3,15 +3,19 @@ SELECT
|
|||||||
account.id AS steamId,
|
account.id AS steamId,
|
||||||
CAST(prisoner.id AS TEXT) AS gamePlayerId,
|
CAST(prisoner.id AS TEXT) AS gamePlayerId,
|
||||||
COALESCE(NULLIF(profile.name, ''), NULLIF(account.name, ''), account.id) AS displayName,
|
COALESCE(NULLIF(profile.name, ''), NULLIF(account.name, ''), account.id) AS displayName,
|
||||||
|
account.last_direct_connection_address AS lastLoginIp,
|
||||||
|
account.creation_time AS registeredAt,
|
||||||
CAST(member.squad_id AS TEXT) AS squadId,
|
CAST(member.squad_id AS TEXT) AS squadId,
|
||||||
squad.name AS squadName,
|
squad.name AS squadName,
|
||||||
profile.fame_points AS famePoints,
|
profile.fame_points AS famePoints,
|
||||||
|
profile.money_balance AS moneyBalance,
|
||||||
MAX(CASE WHEN currency.currency_type = 1 THEN currency.account_balance END) AS normalBalance,
|
MAX(CASE WHEN currency.currency_type = 1 THEN currency.account_balance END) AS normalBalance,
|
||||||
MAX(CASE WHEN currency.currency_type = 2 THEN currency.account_balance END) AS goldBalance,
|
MAX(CASE WHEN currency.currency_type = 2 THEN currency.account_balance END) AS goldBalance,
|
||||||
entity.location_x AS x,
|
entity.location_x AS x,
|
||||||
entity.location_y AS y,
|
entity.location_y AS y,
|
||||||
entity.location_z AS z,
|
entity.location_z AS z,
|
||||||
profile.last_login_time AS lastLoginTime,
|
profile.last_login_time AS lastLoginTime,
|
||||||
|
profile.last_logout_time AS lastLogoutTime,
|
||||||
strftime('%Y-%m-%dT%H:%M:%SZ', prisoner.last_save_time, 'unixepoch') AS lastSaveTime
|
strftime('%Y-%m-%dT%H:%M:%SZ', prisoner.last_save_time, 'unixepoch') AS lastSaveTime
|
||||||
FROM user account
|
FROM user account
|
||||||
LEFT JOIN user_profile profile ON profile.user_id = account.id
|
LEFT JOIN user_profile profile ON profile.user_id = account.id
|
||||||
@@ -25,6 +29,7 @@ LEFT JOIN bank_account_registry_currencies currency ON currency.bank_account_id
|
|||||||
WHERE (:userProfileId IS NULL OR CAST(profile.id AS TEXT) = :userProfileId)
|
WHERE (:userProfileId IS NULL OR CAST(profile.id AS TEXT) = :userProfileId)
|
||||||
AND (:steamId IS NULL OR account.id = :steamId)
|
AND (:steamId IS NULL OR account.id = :steamId)
|
||||||
AND (:search IS NULL OR COALESCE(NULLIF(profile.name, ''), NULLIF(account.name, ''), account.id) LIKE '%' || :search || '%')
|
AND (:search IS NULL OR COALESCE(NULLIF(profile.name, ''), NULLIF(account.name, ''), account.id) LIKE '%' || :search || '%')
|
||||||
|
AND (:activeWithinSeconds IS NULL OR profile.last_logout_time IS NULL OR profile.last_login_time > profile.last_logout_time OR profile.last_login_time >= strftime('%Y-%m-%dT%H:%M:%fZ', 'now', printf('-%d seconds', :activeWithinSeconds)) OR prisoner.last_save_time >= CAST(strftime('%s', 'now') AS INTEGER) - :activeWithinSeconds)
|
||||||
GROUP BY account.id
|
GROUP BY account.id
|
||||||
ORDER BY profile.last_login_time DESC
|
ORDER BY profile.last_login_time DESC
|
||||||
LIMIT COALESCE(:limit, 500)
|
LIMIT COALESCE(:limit, 500)
|
||||||
|
|||||||
@@ -7,10 +7,19 @@ SELECT
|
|||||||
entity.location_y AS y,
|
entity.location_y AS y,
|
||||||
entity.location_z AS z,
|
entity.location_z AS z,
|
||||||
strftime('%Y-%m-%dT%H:%M:%SZ', spawner.vehicle_last_access_time, 'unixepoch') AS lastAccessTime,
|
strftime('%Y-%m-%dT%H:%M:%SZ', spawner.vehicle_last_access_time, 'unixepoch') AS lastAccessTime,
|
||||||
spawner.is_vehicle_functional AS isFunctional
|
spawner.is_vehicle_functional AS isFunctional,
|
||||||
|
1 AS existsInGame,
|
||||||
|
group_concat(DISTINCT CAST(mount.prisoner_id AS TEXT)) AS mountedPrisonerIds,
|
||||||
|
group_concat(DISTINCT CAST(mounted_profile.id AS TEXT)) AS mountedUserProfileIds,
|
||||||
|
group_concat(DISTINCT mounted_profile.user_id) AS mountedSteamIds
|
||||||
FROM vehicle_spawner spawner
|
FROM vehicle_spawner spawner
|
||||||
JOIN entity ON entity.id = spawner.vehicle_entity_id
|
JOIN entity ON entity.id = spawner.vehicle_entity_id
|
||||||
|
LEFT JOIN prisoner_vehicle_mountee_info mount ON mount.vehicle_entity_id = spawner.vehicle_entity_id
|
||||||
|
LEFT JOIN prisoner mounted_prisoner ON mounted_prisoner.id = mount.prisoner_id
|
||||||
|
LEFT JOIN user_profile mounted_profile ON mounted_profile.prisoner_id = mounted_prisoner.id
|
||||||
WHERE (:vehicleId IS NULL OR CAST(spawner.vehicle_entity_id AS TEXT) = :vehicleId)
|
WHERE (:vehicleId IS NULL OR CAST(spawner.vehicle_entity_id AS TEXT) = :vehicleId)
|
||||||
AND (:search IS NULL OR spawner.vehicle_alias LIKE '%' || :search || '%' OR entity.class LIKE '%' || :search || '%')
|
AND (:search IS NULL OR spawner.vehicle_alias LIKE '%' || :search || '%' OR entity.class LIKE '%' || :search || '%')
|
||||||
|
AND (:activeWithinSeconds IS NULL OR spawner.vehicle_last_access_time >= CAST(strftime('%s', 'now') AS INTEGER) - :activeWithinSeconds)
|
||||||
|
GROUP BY spawner.vehicle_entity_id
|
||||||
ORDER BY spawner.vehicle_last_access_time DESC
|
ORDER BY spawner.vehicle_last_access_time DESC
|
||||||
LIMIT COALESCE(:limit, 500)
|
LIMIT COALESCE(:limit, 500)
|
||||||
|
|||||||
@@ -147,13 +147,14 @@ describe("plugin manifest validation", () => {
|
|||||||
expect(fs.existsSync(path.join(pluginDir, "schemas/bridge/queries/SCUM_DB_CONTRACT.md"))).toBe(true);
|
expect(fs.existsSync(path.join(pluginDir, "schemas/bridge/queries/SCUM_DB_CONTRACT.md"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("declares SCUM user projection through SQLite", () => {
|
it("declares SCUM SQLite templates without platform projections", () => {
|
||||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json"), "utf8")) as {
|
const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json"), "utf8")) as {
|
||||||
gameClientBridge: { queryTemplates: Array<{ key: string; engine: string; transportKey: string; targetKey: string; projections?: Array<{ collection: string; fixedValues?: Record<string, string> }> }> };
|
gameClientBridge: { lifecycleProjections?: unknown[]; queryTemplates: Array<{ key: string; engine: string; transportKey: string; targetKey: string; projections?: unknown[] }> };
|
||||||
};
|
};
|
||||||
const users = manifest.gameClientBridge.queryTemplates.find((template) => template.key === "scum.player.profile");
|
const users = manifest.gameClientBridge.queryTemplates.find((template) => template.key === "scum.player.profile");
|
||||||
expect(users).toMatchObject({ engine: "sqlite", transportKey: "scum-database", targetKey: "scum-database" });
|
expect(users).toMatchObject({ engine: "sqlite", transportKey: "scum-database", targetKey: "scum-database" });
|
||||||
expect(users?.projections).toEqual(expect.arrayContaining([expect.objectContaining({ collection: "scum_users", fixedValues: { source: "run.sqlite.scum.player.profile" } })]));
|
expect(manifest.gameClientBridge.lifecycleProjections).toBeUndefined();
|
||||||
|
for (const template of manifest.gameClientBridge.queryTemplates) expect(template.projections).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("declares SCUM install/update and start lifecycle through plugin assets", () => {
|
it("declares SCUM install/update and start lifecycle through plugin assets", () => {
|
||||||
@@ -246,7 +247,8 @@ describe("plugin manifest validation", () => {
|
|||||||
maxPayloadBytes: number;
|
maxPayloadBytes: number;
|
||||||
}>;
|
}>;
|
||||||
snapshots: Array<{ type: string; schemaVersion: string; schemaRef: string }>;
|
snapshots: Array<{ type: string; schemaVersion: string; schemaRef: string }>;
|
||||||
queryTemplates: Array<{ key: string; projections?: Array<{ collection?: string; fixedValues?: Record<string, string>; mergeExisting?: boolean }> }>;
|
queryTemplates: Array<{ key: string; projections?: unknown[] }>;
|
||||||
|
lifecycleProjections?: unknown[];
|
||||||
pages: Array<{ pageKey: string; commandTypes?: string[]; snapshotTypes?: string[]; queryTemplateKeys?: string[] }>;
|
pages: Array<{ pageKey: string; commandTypes?: string[]; snapshotTypes?: string[]; queryTemplateKeys?: string[] }>;
|
||||||
};
|
};
|
||||||
pages: Array<{ key: string; permissions?: string[] }>;
|
pages: Array<{ key: string; permissions?: string[] }>;
|
||||||
@@ -277,9 +279,8 @@ describe("plugin manifest validation", () => {
|
|||||||
]));
|
]));
|
||||||
expect(manifest.gameClientBridge.snapshots.map((snapshot) => snapshot.type)).toEqual(expect.arrayContaining(["online.sessions", "players", "squads", "vehicles", "flags"]));
|
expect(manifest.gameClientBridge.snapshots.map((snapshot) => snapshot.type)).toEqual(expect.arrayContaining(["online.sessions", "players", "squads", "vehicles", "flags"]));
|
||||||
expect(manifest.runtimeProfiles?.clientManagers).toBeUndefined();
|
expect(manifest.runtimeProfiles?.clientManagers).toBeUndefined();
|
||||||
expect(manifest.gameClientBridge.queryTemplates.find((template) => template.key === "scum.vehicles")?.projections).toEqual(expect.arrayContaining([
|
expect(manifest.gameClientBridge.lifecycleProjections).toBeUndefined();
|
||||||
expect.objectContaining({ collection: "scum_trade_goods", mergeExisting: true, fixedValues: expect.objectContaining({ catalogType: "vehicle", type: "21", typeName: "其他载具" }) })
|
for (const template of manifest.gameClientBridge.queryTemplates) expect(template.projections).toBeUndefined();
|
||||||
]));
|
|
||||||
expect(manifest.gameClientBridge.pages.map((page) => page.pageKey)).toEqual(expect.arrayContaining(["players", "squads", "live-map", "gifts", "workflows"]));
|
expect(manifest.gameClientBridge.pages.map((page) => page.pageKey)).toEqual(expect.arrayContaining(["players", "squads", "live-map", "gifts", "workflows"]));
|
||||||
expect(manifest.gameClientBridge.pages.map((page) => page.pageKey)).not.toContain("files-config");
|
expect(manifest.gameClientBridge.pages.map((page) => page.pageKey)).not.toContain("files-config");
|
||||||
expect(manifest.gameClientBridge.pages.find((page) => page.pageKey === "workflows")?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.player.profile", "scum.squads", "scum.vehicles", "scum.flags", "scum.positions"]));
|
expect(manifest.gameClientBridge.pages.find((page) => page.pageKey === "workflows")?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.player.profile", "scum.squads", "scum.vehicles", "scum.flags", "scum.positions"]));
|
||||||
@@ -423,7 +424,7 @@ describe("plugin manifest validation", () => {
|
|||||||
pollIntervalSeconds: number;
|
pollIntervalSeconds: number;
|
||||||
maxRows: number;
|
maxRows: number;
|
||||||
timeoutSeconds: number;
|
timeoutSeconds: number;
|
||||||
projections?: Array<{ collection: string; rowPath: string; upsertKeys: string[]; observedAtField?: string; mergeExisting?: boolean }>;
|
projections?: unknown[];
|
||||||
}>;
|
}>;
|
||||||
pages: Array<{ pageKey: string; commandTypes?: string[]; queryTemplateKeys?: string[] }>;
|
pages: Array<{ pageKey: string; commandTypes?: string[]; queryTemplateKeys?: string[] }>;
|
||||||
};
|
};
|
||||||
@@ -432,10 +433,10 @@ describe("plugin manifest validation", () => {
|
|||||||
};
|
};
|
||||||
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 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[]> = {
|
const expectedColumnsByKey: Record<string, string[]> = {
|
||||||
"scum.player.profile": ["userProfileId", "steamId", "gamePlayerId", "displayName", "squadId", "squadName", "famePoints", "normalBalance", "goldBalance", "x", "y", "z", "lastLoginTime", "lastSaveTime"],
|
"scum.player.profile": ["userProfileId", "steamId", "gamePlayerId", "displayName", "lastLoginIp", "registeredAt", "squadId", "squadName", "famePoints", "moneyBalance", "normalBalance", "goldBalance", "x", "y", "z", "lastLoginTime", "lastLogoutTime", "lastSaveTime"],
|
||||||
"scum.squads": ["squadId", "name", "leaderProfileId", "leaderPlayerId", "memberCount", "score", "memberLimit", "message", "info", "lastMemberLoginTime"],
|
"scum.squads": ["squadId", "name", "leaderProfileId", "leaderPlayerId", "memberCount", "score", "memberLimit", "message", "info", "lastMemberLoginTime"],
|
||||||
"scum.squad-members": ["squadId", "userProfileId", "gamePlayerId", "steamId", "displayName", "rank", "isLeader"],
|
"scum.squad-members": ["squadId", "userProfileId", "gamePlayerId", "steamId", "displayName", "rank", "isLeader"],
|
||||||
"scum.vehicles": ["vehicleId", "entityId", "className", "label", "x", "y", "z", "lastAccessTime", "isFunctional"],
|
"scum.vehicles": ["vehicleId", "entityId", "className", "label", "x", "y", "z", "lastAccessTime", "isFunctional", "existsInGame", "mountedPrisonerIds", "mountedUserProfileIds", "mountedSteamIds"],
|
||||||
"scum.flags": ["flagId", "entityId", "baseId", "ownerProfileId", "ownerPlayerId", "ownerSquadId", "ownerSquadName", "overtakerProfileId", "overtakeEndTime", "ownershipConfidence", "x", "y", "z"],
|
"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.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.tasks": ["taskRecordId", "taskKind", "userProfileId", "mapId", "trackingDataSetId", "dataAssetPath", "sequenceIndex", "isTracked", "state", "completionDeadline"],
|
||||||
@@ -445,7 +446,7 @@ describe("plugin manifest validation", () => {
|
|||||||
const fastTemplates = new Set(["scum.player.profile", "scum.vehicles", "scum.positions"]);
|
const fastTemplates = new Set(["scum.player.profile", "scum.vehicles", "scum.positions"]);
|
||||||
const templatesByKey = new Map(manifest.gameClientBridge.queryTemplates.map((template) => [template.key, template]));
|
const templatesByKey = new Map(manifest.gameClientBridge.queryTemplates.map((template) => [template.key, template]));
|
||||||
expect([...templatesByKey.keys()]).toEqual(expect.arrayContaining(expectedKeys));
|
expect([...templatesByKey.keys()]).toEqual(expect.arrayContaining(expectedKeys));
|
||||||
expect(templatesByKey.get("scum.player.profile")?.projections).toEqual([expect.objectContaining({ collection: "scum_users", rowPath: "rows", upsertKeys: ["steamId"], observedAtField: "profileSampledAt", mergeExisting: true })]);
|
expect([...templatesByKey.values()].some((template) => Boolean(template.projections))).toBe(false);
|
||||||
expect(manifest.capabilities).toContain("remote.run.db.sqlite.query");
|
expect(manifest.capabilities).toContain("remote.run.db.sqlite.query");
|
||||||
expect(manifest.capabilities).toContain("remote.run.db.sqlite.execute");
|
expect(manifest.capabilities).toContain("remote.run.db.sqlite.execute");
|
||||||
expect(manifest.remoteAccess?.runCapabilities).toContain("remote.run.db.sqlite.query");
|
expect(manifest.remoteAccess?.runCapabilities).toContain("remote.run.db.sqlite.query");
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { dirname, resolve } from "node:path";
|
|||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
import { migrateConfigurationRecord, migrateGiftGrantRecord, migratePlayerProfileRecord, migratePlayerRecord, migrateStatePatchRecord, migrateTrajectoryHistoryRecord, migrateTrajectoryRecord, migrationStatus } from "../examples/scum-server-plugin/features/migration.js";
|
import { migrateConfigurationRecord, migrateGiftGrantRecord, migratePlayerProfileRecord, migratePlayerRecord, migrateStatePatchRecord, migrateTrajectoryHistoryRecord, migrateTrajectoryRecord, migrationStatus } from "../examples/scum-server-plugin/features/migration.js";
|
||||||
import { buildPlayerAttributeMutation, createGiftDelivery, deleteGiftDefinition, loadSCUMSurface, mergePlayerSnapshots, parseGiftCommands, parseGiftItems, playerAttributeDrafts, playerAttributeSqlPreview, projectSCUMLoginLogs, queueGiftDelivery, queuePlayerAttributePatch, resetGiftClaim, resetPendingGift, resolveMapBounds, saveEventProduce, saveGiftDefinition, saveMapSettings, scumCollections, startEvent, type RecordMap, type SCUMSurfaceData, type SCUMWorkspaceActions } from "../examples/scum-server-plugin/features/page-data.js";
|
import { buildPlayerAttributeMutation, createGiftDelivery, deleteGiftDefinition, loadSCUMSurface, mergePlayerSnapshots, parseGiftCommands, parseGiftItems, playerAttributeDrafts, playerAttributeSqlPreview, queueGiftDelivery, queuePlayerAttributePatch, queueSCUMDatabaseRefresh, resetGiftClaim, resetPendingGift, resolveMapBounds, saveEventProduce, saveGiftDefinition, saveMapSettings, scumCollections, startEvent, type RecordMap, type SCUMSurfaceData, type SCUMWorkspaceActions } from "../examples/scum-server-plugin/features/page-data.js";
|
||||||
import { collectMapPoints, mapPointStyle } from "../examples/scum-server-plugin/features/page.js";
|
import { collectMapPoints, mapPointStyle } from "../examples/scum-server-plugin/features/page.js";
|
||||||
import { renderPluginPage } from "../examples/scum-server-plugin/page-bundle/index.js";
|
import { renderPluginPage } from "../examples/scum-server-plugin/page-bundle/index.js";
|
||||||
import { configurationCatalog, validateConfigPatch, validateStatePatch, validateVehicleSpawn, vehicleSpawnCatalog } from "../examples/scum-server-plugin/features/schemas.js";
|
import { configurationCatalog, validateConfigPatch, validateStatePatch, validateVehicleSpawn, vehicleSpawnCatalog } from "../examples/scum-server-plugin/features/schemas.js";
|
||||||
@@ -103,25 +103,17 @@ describe("SCUM plugin feature module", () => {
|
|||||||
expect(dataClientSource).not.toContain("scum-client-manager");
|
expect(dataClientSource).not.toContain("scum-client-manager");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("projects SCUM login log lines inside the plugin-owned page module", async () => {
|
it("queues SCUM database reads through declared template keys instead of log parsing", async () => {
|
||||||
const pluginData = pluginDataActions({ list: async (collection, key) => {
|
const dispatch = vi.fn<NonNullable<SCUMWorkspaceActions["dispatch"]>>(async (envelope) => ({ status: "queued", result: { jobId: envelope.requestId } }));
|
||||||
if (collection === scumCollections.players) return { items: [{ key: "76561198000000001", value: { steamId: "76561198000000001", displayName: "Old", normalBalance: 100 } }] };
|
await expect(queueSCUMDatabaseRefresh({ dispatch }, "players", 5000)).resolves.toHaveLength(2);
|
||||||
if (collection === scumCollections.logCursors && key) return { items: [{ key, value: { nextSeq: 1 } }] };
|
expect(dispatch.mock.calls.map(([envelope]) => envelope.payload?.["input.templateKey"])).toEqual(["scum.player.profile", "scum.positions"]);
|
||||||
return { items: [], count: 0 };
|
expect(dispatch.mock.calls.map(([envelope]) => envelope.payload)).toEqual(expect.arrayContaining([
|
||||||
} });
|
expect.objectContaining({ capability: "remote.run.db.sqlite.query", declarationKey: "scum-database", targetKey: "scum-database", "input.templateKey": "scum.player.profile", "input.activeWithinSeconds": "600" }),
|
||||||
const logs = {
|
expect.objectContaining({ capability: "remote.run.db.sqlite.query", declarationKey: "scum-database", targetKey: "scum-database", "input.templateKey": "scum.positions", "input.activeWithinSeconds": "600" })
|
||||||
listStreams: vi.fn(async () => ({ items: [{ id: "stream-login", streamKey: "scum.login", latestSeq: 3 }], count: 1 })),
|
]));
|
||||||
query: vi.fn(async () => ({ logStreamId: "stream-login", entries: [
|
expect(dispatch.mock.calls[0]?.[0].payload).not.toHaveProperty("input.sqlText");
|
||||||
{ seq: 2, timestamp: "2026-08-10T00:01:00Z", line: "Login: Player 'Mira' SteamID 76561198000000001 IP 203.0.113.7" },
|
expect(dataClientSource).not.toContain("projectSCUMLoginLogs");
|
||||||
{ seq: 3, timestamp: "2026-08-10T00:04:00Z", line: "Logout: Player 'Mira' SteamID 76561198000000001 IP 203.0.113.7" }
|
expect(dataClientSource).not.toContain("logs.query");
|
||||||
], nextSeq: 3, latestSeq: 3 }))
|
|
||||||
};
|
|
||||||
await expect(projectSCUMLoginLogs({ pluginData, logs })).resolves.toBe(2);
|
|
||||||
expect(logs.query).toHaveBeenCalledWith({ logStreamId: "stream-login", afterSeq: 1, limit: 200 });
|
|
||||||
expect(pluginData.transact).toHaveBeenCalledWith(scumCollections.players, [expect.objectContaining({ key: "76561198000000001", value: expect.objectContaining({ displayName: "Mira", online: false, status: "offline", normalBalance: 100, lastLoginIp: "203.0.113.7", lastLogoutObservedAt: "2026-08-10T00:04:00Z", source: "plugin.log.scum.login" }) })]);
|
|
||||||
expect(pluginData.transact).toHaveBeenCalledWith(scumCollections.activityEvents, expect.arrayContaining([expect.objectContaining({ key: "stream-login:2", value: expect.objectContaining({ eventType: "login", rawLine: expect.stringContaining("Login:") }) }), expect.objectContaining({ key: "stream-login:3", value: expect.objectContaining({ eventType: "logout", rawLine: expect.stringContaining("Logout:") }) })]));
|
|
||||||
expect(pluginData.put).toHaveBeenCalledWith(scumCollections.logCursors, "scum.login:stream-login", expect.objectContaining({ nextSeq: 3 }));
|
|
||||||
expect(dataClientSource).toContain("projectSCUMLoginLogs");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses workflows as the manifest activity key and keeps activity as a compatibility alias", async () => {
|
it("uses workflows as the manifest activity key and keeps activity as a compatibility alias", async () => {
|
||||||
@@ -312,9 +304,11 @@ describe("SCUM plugin feature module", () => {
|
|||||||
for (const forbidden of ["listSCUM", "gameGift", "createSCUMOperation", "createSCUMWorkflow", "C:/", "/Users/", "hostPath", "sampleCoordinates", "samplePlayers"]) expect(source).not.toContain(forbidden);
|
for (const forbidden of ["listSCUM", "gameGift", "createSCUMOperation", "createSCUMWorkflow", "C:/", "/Users/", "hostPath", "sampleCoordinates", "samplePlayers"]) expect(source).not.toContain(forbidden);
|
||||||
expect(source).toContain("pluginData");
|
expect(source).toContain("pluginData");
|
||||||
expect(source).toContain("remote.access.request");
|
expect(source).toContain("remote.access.request");
|
||||||
expect(source).not.toContain("input.templateKey");
|
expect(source).toContain("input.templateKey");
|
||||||
|
expect(source).not.toContain("projectSCUMLoginLogs");
|
||||||
|
expect(source).not.toContain("logs.query");
|
||||||
expect(source).not.toContain("requestSCUMPageQueries");
|
expect(source).not.toContain("requestSCUMPageQueries");
|
||||||
expect(pageSource).toContain("setInterval(refresh, 3000)");
|
expect(pageSource).toContain("setInterval(refresh, 5000)");
|
||||||
expect(pageSource).toContain("clearInterval(interval)");
|
expect(pageSource).toContain("clearInterval(interval)");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user