Remove SCUM sqlite user projections
This commit is contained in:
@@ -14,71 +14,19 @@ export type GameClientActions = {
|
||||
|
||||
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 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 = [
|
||||
{ key: "stamina", label: "体力", column: "stamina", sourceKeys: ["attributes.stamina", "stamina", "体力"] },
|
||||
{ key: "dexterity", label: "敏捷", column: "dexterity", sourceKeys: ["attributes.dexterity", "dexterity", "敏捷"] },
|
||||
{ key: "intelligence", label: "智力", column: "intelligence", sourceKeys: ["attributes.intelligence", "intelligence", "智力"] }
|
||||
] as const;
|
||||
|
||||
export type PlayerAttributeDraft = { fieldKey: string; label: string; before: string; after: string };
|
||||
|
||||
export function playerAttributeDrafts(player: RecordMap): PlayerAttributeDraft[] {
|
||||
return playerAttributeCatalog.map((field) => ({ fieldKey: field.key, label: field.label, before: firstText(player, ...field.sourceKeys), after: firstText(player, ...field.sourceKeys) }));
|
||||
}
|
||||
|
||||
export function playerAttributeSqlPreview(drafts: PlayerAttributeDraft[]): string {
|
||||
const changes = drafts.filter((draft) => draft.after.trim() && draft.after.trim() !== draft.before.trim());
|
||||
if (!changes.length) return "等待输入要提交的属性变更。";
|
||||
return buildPlayerAttributeSqlText({ gamePlayerId: ":playerId" }, changes.flatMap((draft) => {
|
||||
const definition = playerAttributeCatalog.find((candidate) => candidate.key === draft.fieldKey);
|
||||
const after = Number(draft.after);
|
||||
return definition && Number.isFinite(after) ? [{ column: definition.column, after }] : [];
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildPlayerAttributeMutation(player: RecordMap, drafts: PlayerAttributeDraft[]): RecordMap {
|
||||
const playerId = firstText(player, "steamId", "gamePlayerId", "playerId", "userProfileId", "id");
|
||||
if (!playerId) throw new Error("用户没有可用的 Steam ID 或游戏用户编号。");
|
||||
const changes = drafts.filter((draft) => draft.after.trim() && draft.after.trim() !== draft.before.trim()).map((draft) => {
|
||||
const definition = playerAttributeCatalog.find((candidate) => candidate.key === draft.fieldKey);
|
||||
const before = draft.before.trim() ? Number(draft.before) : Number.NaN;
|
||||
const after = Number(draft.after);
|
||||
if (!definition || !Number.isFinite(after)) throw new Error(`${draft.label}目标值必须是数字。`);
|
||||
return { fieldKey: draft.fieldKey, label: draft.label, column: definition.column, before: Number.isFinite(before) ? before : null, after };
|
||||
});
|
||||
if (!changes.length) throw new Error("至少填写一项与当前值不同的属性。");
|
||||
const idempotencyKey = safeCommandId(`player-attributes:${playerId}:${changes.map((change) => `${change.fieldKey}:${change.after}`).join(",")}:${Date.now()}`);
|
||||
return { playerId, reason: "管理员在 SCUM 用户管理中编辑属性", sqlText: buildPlayerAttributeSqlText(player, changes), changes, idempotencyKey };
|
||||
}
|
||||
|
||||
export async function queuePlayerAttributePatch(actions: SCUMWorkspaceActions, player: RecordMap, drafts: PlayerAttributeDraft[]): Promise<unknown> {
|
||||
if (!actions.dispatch) throw new Error("通用 remote.access.request 能力不可用,无法提交 SQL 执行任务。");
|
||||
const mutation = buildPlayerAttributeMutation(player, drafts);
|
||||
const idempotencyKey = textValue(mutation.idempotencyKey);
|
||||
const result = await actions.dispatch({
|
||||
requestId: idempotencyKey,
|
||||
action: "remote.access.request",
|
||||
payload: {
|
||||
capability: "remote.run.db.sqlite.execute",
|
||||
declarationKey: "scum-database",
|
||||
targetKey: "scum-database",
|
||||
idempotencyKey,
|
||||
timeoutSeconds: "60",
|
||||
maxAttempts: "1",
|
||||
"input.mode": "execute",
|
||||
"input.sqlText": textValue(mutation.sqlText),
|
||||
"input.reason": textValue(mutation.reason)
|
||||
}
|
||||
});
|
||||
if (result?.status && !["queued", "ok"].includes(result.status)) throw new Error(result.error?.message || "SQL 执行任务未进入 Run 队列。");
|
||||
return result;
|
||||
}
|
||||
export type SCUMPlatformActions = {
|
||||
users: (query?: { limit?: number; changedAfter?: string; online?: boolean; steamId?: string }) => Promise<unknown>;
|
||||
vehicles: (query?: { limit?: number; changedAfter?: string; exists?: boolean; gameVehicleId?: string }) => Promise<unknown>;
|
||||
userTrajectories: (query?: { limit?: number; after?: string; steamId?: string; scumUserId?: string }) => Promise<unknown>;
|
||||
vehicleTrajectories: (query?: { limit?: number; after?: string; gameVehicleId?: string; scumVehicleId?: string }) => Promise<unknown>;
|
||||
vehicleLocks: (query?: { limit?: number; after?: string; gameVehicleId?: string; scumVehicleId?: string; scumUserId?: string; steamId?: string }) => Promise<unknown>;
|
||||
};
|
||||
|
||||
export type SCUMWorkspaceActions = {
|
||||
pluginData?: PluginDataActions;
|
||||
gameClient?: GameClientActions;
|
||||
scum?: SCUMPlatformActions;
|
||||
dispatch?: (envelope: PluginBridgeExecuteEnvelope, signal?: AbortSignal) => Promise<PluginBridgeExecutionResult>;
|
||||
};
|
||||
|
||||
@@ -103,17 +51,17 @@ export type SCUMSurfaceData = {
|
||||
mapRegions: RecordMap[];
|
||||
mapSettings: RecordMap[];
|
||||
vehicles: RecordMap[];
|
||||
vehicleLocks: RecordMap[];
|
||||
flags: RecordMap[];
|
||||
trajectories: RecordMap[];
|
||||
};
|
||||
|
||||
export const emptySCUMSurfaceData: SCUMSurfaceData = {
|
||||
players: [], squads: [], members: [], events: [], eventProduces: [], eventRuns: [], nativeEventRounds: [], tasks: [], activityEvents: [],
|
||||
tradeGoods: [], tradeEvents: [], gifts: [], giftClaims: [], pendingGifts: [], giftDeliveries: [], timedGiftEvents: [], mapPoints: [], mapRegions: [], mapSettings: [], vehicles: [], flags: [], trajectories: []
|
||||
tradeGoods: [], tradeEvents: [], gifts: [], giftClaims: [], pendingGifts: [], giftDeliveries: [], timedGiftEvents: [], mapPoints: [], mapRegions: [], mapSettings: [], vehicles: [], vehicleLocks: [], flags: [], trajectories: []
|
||||
};
|
||||
|
||||
export const scumCollections = {
|
||||
players: "scum_users",
|
||||
squads: "scum_squads",
|
||||
members: "scum_squad_members",
|
||||
events: "scum_activity_definitions",
|
||||
@@ -132,151 +80,53 @@ export const scumCollections = {
|
||||
mapPoints: "scum_map_points",
|
||||
mapRegions: "scum_map_regions",
|
||||
mapSettings: "scum_map_settings",
|
||||
vehicles: "scum_vehicles",
|
||||
flags: "scum_flags",
|
||||
trajectories: "scum_trajectories"
|
||||
flags: "scum_flags"
|
||||
} as const;
|
||||
|
||||
type SurfaceKey = keyof SCUMSurfaceData;
|
||||
type PageKey = "players" | "squads" | "live-map" | "gifts" | "workflows";
|
||||
type PluginCollectionSurfaceKey = keyof typeof scumCollections;
|
||||
type SCUMPlatformSurfaceKey = "players" | "vehicles" | "trajectories" | "vehicleLocks";
|
||||
|
||||
const pageCollections: Record<PageKey, SurfaceKey[]> = {
|
||||
players: ["players", "members", "activityEvents", "giftClaims", "pendingGifts", "giftDeliveries", "trajectories", "vehicles"],
|
||||
const pageCollections: Record<PageKey, PluginCollectionSurfaceKey[]> = {
|
||||
players: ["members", "activityEvents", "giftClaims", "pendingGifts", "giftDeliveries"],
|
||||
squads: ["squads", "members", "flags"],
|
||||
"live-map": ["mapPoints", "mapRegions", "mapSettings", "players", "vehicles", "flags", "trajectories", "tradeGoods"],
|
||||
gifts: ["gifts", "giftClaims", "pendingGifts", "giftDeliveries", "timedGiftEvents", "players", "tradeGoods"],
|
||||
"live-map": ["mapPoints", "mapRegions", "mapSettings", "flags", "tradeGoods"],
|
||||
gifts: ["gifts", "giftClaims", "pendingGifts", "giftDeliveries", "timedGiftEvents", "tradeGoods"],
|
||||
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 pageSCUMTables: Record<PageKey, SCUMPlatformSurfaceKey[]> = {
|
||||
players: ["players", "vehicles", "trajectories", "vehicleLocks"],
|
||||
squads: ["players"],
|
||||
"live-map": ["players", "vehicles", "trajectories", "vehicleLocks"],
|
||||
gifts: ["players"],
|
||||
workflows: ["players", "vehicles", "trajectories", "vehicleLocks"]
|
||||
};
|
||||
|
||||
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> {
|
||||
if (!actions.pluginData) throw new Error("通用 pluginData 能力不可用。");
|
||||
const canonical = canonicalPageKey(pageKey);
|
||||
void queueSCUMDatabaseRefresh(actions, canonical).catch(() => undefined);
|
||||
const data: SCUMSurfaceData = { ...emptySCUMSurfaceData };
|
||||
const keys = pageCollections[canonical];
|
||||
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);
|
||||
await mergeSCUMSnapshots(actions, data, keys).catch(() => undefined);
|
||||
if (keys.length) {
|
||||
const pluginData = requirePluginData(actions);
|
||||
const records = await Promise.all(keys.map(async (key) => [key, await pluginData.list(scumCollections[key])] as const));
|
||||
for (const [key, response] of records) data[key] = collectionRecords(response);
|
||||
}
|
||||
await loadPlatformSCUMTables(actions, data, pageSCUMTables[canonical]);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function queueSCUMDatabaseRefresh(actions: SCUMWorkspaceActions, pageKey: string, now = Date.now()): Promise<PluginBridgeExecutionResult[]> {
|
||||
if (!actions.dispatch) return [];
|
||||
const canonical = canonicalPageKey(pageKey);
|
||||
const templates = [...new Set(pageQueryTemplates[canonical])];
|
||||
const requests = templates.flatMap((templateKey) => {
|
||||
const bucketKey = scumQueryBucketKey(templateKey, now);
|
||||
if (queuedQueryBuckets.has(bucketKey)) return [];
|
||||
queuedQueryBuckets.add(bucketKey);
|
||||
return [actions.dispatch!({ requestId: scumQueryRequestId(canonical, templateKey, now), action: "remote.access.request", payload: scumQueryPayload(templateKey, now) })];
|
||||
});
|
||||
const settled = await Promise.allSettled(requests);
|
||||
return settled.flatMap((item) => item.status === "fulfilled" ? [item.value] : []);
|
||||
}
|
||||
|
||||
function scumQueryPayload(templateKey: SCUMQueryTemplateKey, now: number): Record<string, string> {
|
||||
const maxRows = String(queryTemplateMaxRows[templateKey]);
|
||||
const payload: Record<string, string> = {
|
||||
capability: "remote.run.db.sqlite.query",
|
||||
declarationKey: "scum-database",
|
||||
targetKey: "scum-database",
|
||||
idempotencyKey: scumQueryIdempotencyKey(templateKey, now),
|
||||
timeoutSeconds: "15",
|
||||
maxAttempts: "1",
|
||||
"input.templateKey": templateKey,
|
||||
"input.limit": maxRows,
|
||||
"input.maxRows": maxRows
|
||||
};
|
||||
if (templateKey === "scum.player.profile" || templateKey === "scum.positions" || templateKey === "scum.vehicles") payload["input.activeWithinSeconds"] = "600";
|
||||
return payload;
|
||||
}
|
||||
|
||||
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)}`; }
|
||||
function scumQueryBucketKey(templateKey: SCUMQueryTemplateKey, now: number): string { return `${templateKey}:${queryBucket(templateKey, now)}`; }
|
||||
function queryBucket(templateKey: SCUMQueryTemplateKey, now: number): number { return Math.floor(now / (queryTemplatePollSeconds[templateKey] * 1000)); }
|
||||
|
||||
async function mergeSCUMSnapshots(actions: SCUMWorkspaceActions, data: SCUMSurfaceData, keys: SurfaceKey[]): Promise<void> {
|
||||
if (!actions.gameClient) return;
|
||||
async function loadPlatformSCUMTables(actions: SCUMWorkspaceActions, data: SCUMSurfaceData, keys: SCUMPlatformSurfaceKey[]): Promise<void> {
|
||||
if (!keys.length) return;
|
||||
if (!actions.scum) throw new Error("平台 SCUM 数据能力不可用。");
|
||||
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); }));
|
||||
if (keys.includes("players")) reads.push(actions.scum.users({ limit: 500 }).then((response) => { data.players = collectionRecords(response); }));
|
||||
if (keys.includes("vehicles")) reads.push(actions.scum.vehicles({ limit: 500 }).then((response) => { data.vehicles = collectionRecords(response); }));
|
||||
if (keys.includes("vehicleLocks")) reads.push(actions.scum.vehicleLocks({ limit: 500 }).then((response) => { data.vehicleLocks = collectionRecords(response); }));
|
||||
if (keys.includes("trajectories")) reads.push(Promise.all([actions.scum.userTrajectories({ limit: 500 }), actions.scum.vehicleTrajectories({ limit: 500 })]).then(([players, vehicles]) => { data.trajectories = [...collectionRecords(players), ...collectionRecords(vehicles)]; }));
|
||||
await Promise.all(reads);
|
||||
}
|
||||
|
||||
export function mergePlayerSnapshots(players: RecordMap[], playersResponse: unknown): RecordMap[] {
|
||||
const playerSnapshot = latestSnapshotPayload(playersResponse);
|
||||
const merged = players.map((player) => ({ ...player }));
|
||||
const snapshotPlayers = Array.isArray(playerSnapshot?.players) ? playerSnapshot.players.filter(isRecord) : [];
|
||||
if (snapshotPlayers.length) {
|
||||
const byIdentity = playerIndex(merged);
|
||||
for (const snapshotPlayer of snapshotPlayers) {
|
||||
const match = findPlayer(byIdentity, snapshotPlayer);
|
||||
const value = { ...(match ? merged[match.index] : {}), ...snapshotPlayer, online: onlineValue(snapshotPlayer), onlineObservedAt: textValue(playerSnapshot?.observedAt) };
|
||||
if (match) merged[match.index] = value;
|
||||
else {
|
||||
const created = { ...value, gamePlayerId: firstText(snapshotPlayer, "gamePlayerId", "playerId", "steamId", "id") };
|
||||
merged.push(created);
|
||||
addPlayerToIndex(byIdentity, created, merged.length - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function mergeVehicleSnapshots(vehicles: RecordMap[], vehiclesResponse: unknown): RecordMap[] {
|
||||
const vehicleSnapshot = latestSnapshotPayload(vehiclesResponse);
|
||||
const merged = vehicles.map((vehicle) => ({ ...vehicle }));
|
||||
const snapshotVehicles = Array.isArray(vehicleSnapshot?.vehicles) ? vehicleSnapshot.vehicles.filter(isRecord) : [];
|
||||
if (!snapshotVehicles.length) return merged;
|
||||
const byIdentity = vehicleIndex(merged);
|
||||
for (const snapshotVehicle of snapshotVehicles) {
|
||||
const match = findVehicle(byIdentity, snapshotVehicle);
|
||||
const value = { ...(match ? merged[match.index] : {}), ...snapshotVehicle, vehicleObservedAt: textValue(vehicleSnapshot?.observedAt) };
|
||||
if (match) merged[match.index] = value;
|
||||
else {
|
||||
const created = { ...value, vehicleId: firstText(snapshotVehicle, "vehicleId", "entityId", "id") };
|
||||
merged.push(created);
|
||||
addVehicleToIndex(byIdentity, created, merged.length - 1);
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
export async function saveGiftDefinition(actions: SCUMWorkspaceActions, gift: RecordMap): Promise<unknown> {
|
||||
const key = requiredKey(gift, "code", "礼包编号");
|
||||
return requirePluginData(actions).transact(scumCollections.gifts, [{ operation: "put", key, value: gift }]);
|
||||
@@ -409,29 +259,6 @@ function collectionRecords(response: unknown): RecordMap[] {
|
||||
});
|
||||
}
|
||||
|
||||
function latestSnapshotPayload(response: unknown): RecordMap | undefined {
|
||||
if (!isRecord(response) || !Array.isArray(response.items)) return undefined;
|
||||
const snapshots = response.items.filter(isRecord).sort((left, right) => snapshotOrder(right) - snapshotOrder(left));
|
||||
const latest = snapshots[0];
|
||||
if (!latest) return undefined;
|
||||
return isRecord(latest.payload) ? { ...latest.payload, observedAt: textValue(latest.observedAt) || textValue(latest.payload.observedAt) } : undefined;
|
||||
}
|
||||
|
||||
function snapshotOrder(snapshot: RecordMap): number { const observed = Date.parse(textValue(snapshot.observedAt)); return Number.isNaN(observed) ? Number(snapshot.sequence) || 0 : observed; }
|
||||
function playerIndex(players: RecordMap[]): Map<string, number> { const result = new Map<string, number>(); players.forEach((player, index) => addPlayerToIndex(result, player, index)); return result; }
|
||||
function addPlayerToIndex(index: Map<string, number>, player: RecordMap, playerIndex: number): void { playerIdentities(player).forEach((identity) => index.set(identity, playerIndex)); }
|
||||
function findPlayer(index: Map<string, number>, player: RecordMap): { index: number } | undefined { for (const identity of playerIdentities(player)) { const found = index.get(identity); if (found !== undefined) return { index: found }; } return undefined; }
|
||||
function vehicleIndex(vehicles: RecordMap[]): Map<string, number> { const result = new Map<string, number>(); vehicles.forEach((vehicle, index) => addVehicleToIndex(result, vehicle, index)); return result; }
|
||||
function addVehicleToIndex(index: Map<string, number>, vehicle: RecordMap, vehicleIndex: number): void { vehicleIdentities(vehicle).forEach((identity) => index.set(identity, vehicleIndex)); }
|
||||
function findVehicle(index: Map<string, number>, vehicle: RecordMap): { index: number } | undefined { for (const identity of vehicleIdentities(vehicle)) { const found = index.get(identity); if (found !== undefined) return { index: found }; } return undefined; }
|
||||
function vehicleIdentities(vehicle: RecordMap): string[] { return ["vehicleId", "entityId", "id"].map((key) => textValue(vehicle[key])).filter(Boolean).map((value) => `vehicle:${value}`); }
|
||||
function playerIdentities(player: RecordMap): string[] {
|
||||
const identities = new Set<string>();
|
||||
for (const key of ["gamePlayerId", "playerId", "steamId", "id"]) { const value = textValue(player[key]); if (value) identities.add(`player:${value}`); }
|
||||
for (const key of ["userProfileId", "profileId"]) { const value = textValue(player[key]); if (value) identities.add(`profile:${value}`); }
|
||||
return [...identities];
|
||||
}
|
||||
function onlineValue(player: RecordMap): boolean { const status = firstText(player, "status", "state").toLowerCase(); return booleanValue(player.online) || ["online", "active", "connected"].includes(status); }
|
||||
function booleanValue(value: unknown): boolean { return value === true || value === 1 || value === "1" || String(value).toLowerCase() === "true"; }
|
||||
|
||||
function requirePluginData(actions: SCUMWorkspaceActions): PluginDataActions {
|
||||
@@ -439,23 +266,6 @@ function requirePluginData(actions: SCUMWorkspaceActions): PluginDataActions {
|
||||
return actions.pluginData;
|
||||
}
|
||||
|
||||
function buildPlayerAttributeSqlText(player: RecordMap, changes: Array<{ column: string; after: number }>): string {
|
||||
const playerId = firstText(player, "gamePlayerId", "playerId", "id");
|
||||
const profileId = firstText(player, "userProfileId", "profileId");
|
||||
const steamId = firstText(player, "steamId", "providerId");
|
||||
const where = playerId && playerId !== ":playerId"
|
||||
? `id = ${sqlLiteral(playerId)}`
|
||||
: profileId
|
||||
? `id = (SELECT prisoner_id FROM user_profile WHERE CAST(id AS TEXT) = ${sqlLiteral(profileId)} LIMIT 1)`
|
||||
: steamId
|
||||
? `id = (SELECT profile.prisoner_id FROM user_profile profile WHERE profile.user_id = ${sqlLiteral(steamId)} LIMIT 1)`
|
||||
: "id = :playerId";
|
||||
return changes.map((change) => `UPDATE prisoner SET ${change.column} = ${sqlNumber(change.after)} WHERE ${where};`).join("\n");
|
||||
}
|
||||
|
||||
function sqlLiteral(value: string): string { return value === ":playerId" ? value : `'${value.replace(/'/g, "''")}'`; }
|
||||
function sqlNumber(value: number): string { return Number.isInteger(value) ? String(value) : String(value); }
|
||||
|
||||
function requiredKey(value: RecordMap, key: string, label: string): string {
|
||||
const result = textValue(value[key]);
|
||||
if (!result) throw new Error(`${label}不能为空。`);
|
||||
|
||||
@@ -6,9 +6,6 @@ import {
|
||||
loadSCUMSurface,
|
||||
parseGiftItems,
|
||||
parseGiftCommands,
|
||||
playerAttributeDrafts,
|
||||
playerAttributeSqlPreview,
|
||||
queuePlayerAttributePatch,
|
||||
queueGiftDelivery,
|
||||
resetGiftClaim,
|
||||
resetPendingGift,
|
||||
@@ -27,13 +24,11 @@ type StateSetter<T> = (next: T | ((previous: T) => T)) => void;
|
||||
type InputEvent = { target?: { value?: string; checked?: boolean; open?: boolean }; stopPropagation?: () => void };
|
||||
type GiftTab = "definitions" | "claims" | "deliveries" | "timed";
|
||||
type MapLayer = "players" | "vehicles" | "flags" | "regions" | "other";
|
||||
type PlayerPanelKind = "closed" | "attributes" | "gifts" | "items" | "history" | "trajectory";
|
||||
type PlayerPanelKind = "closed" | "gifts" | "items" | "history" | "trajectory";
|
||||
type PlayerPanelState = { kind: PlayerPanelKind; playerId: string };
|
||||
type AttributeDraft = { fieldKey: string; label: string; before: string; after: string };
|
||||
const scumMapBackground = new URL("../assets/map/scum-map-overview.jpg", import.meta.url).href;
|
||||
const scumMapSize = 256;
|
||||
const scumSurfaceRefreshMs = 15000;
|
||||
const rideDistanceThreshold = 50000;
|
||||
const vehicleIconByClass: Record<string, string> = {
|
||||
BPC_Barba: new URL("../assets/vehicles/vehicle-BPC_Barba.webp", import.meta.url).href,
|
||||
BPC_CityBike: new URL("../assets/vehicles/vehicle-BPC_CityBike.webp", import.meta.url).href,
|
||||
@@ -77,7 +72,6 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
|
||||
const [playerSearch, setPlayerSearch] = usePluginState(react, "");
|
||||
const [playerStatus, setPlayerStatus] = usePluginState(react, "all");
|
||||
const [playerPanel, setPlayerPanel] = usePluginState<PlayerPanelState>(react, { kind: "closed", playerId: "" });
|
||||
const [attributeDrafts, setAttributeDrafts] = usePluginState<AttributeDraft[]>(react, []);
|
||||
const [squadSearch, setSquadSearch] = usePluginState(react, "");
|
||||
const [selectedSquadId, setSelectedSquadId] = usePluginState(react, "");
|
||||
const [activityStatus, setActivityStatus] = usePluginState(react, "all");
|
||||
@@ -130,8 +124,8 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
|
||||
const pageKey = input.pageKey ?? "players";
|
||||
|
||||
const refresh = () => {
|
||||
if (!input.serverInstanceId || !input.workspaceActions?.pluginData) {
|
||||
setState({ status: "error", reason: "插件页面没有绑定服务器或通用 pluginData 能力。" });
|
||||
if (!input.serverInstanceId || !input.workspaceActions?.scum || !input.workspaceActions?.pluginData) {
|
||||
setState({ status: "error", reason: "插件页面没有绑定服务器、平台 SCUM 数据能力或通用 pluginData 能力。" });
|
||||
return;
|
||||
}
|
||||
void loadSCUMSurface(input.workspaceActions, pageKey)
|
||||
@@ -149,10 +143,10 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
|
||||
const data = state.status === "ready" ? state.data : emptySCUMSurfaceData;
|
||||
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,
|
||||
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 === "ready" ? renderSurfaceBody(e, pageKey, data, input, {
|
||||
playerSearch, setPlayerSearch, playerStatus, setPlayerStatus, playerPanel, setPlayerPanel, attributeDrafts, setAttributeDrafts, squadSearch, setSquadSearch, selectedSquadId, setSelectedSquadId,
|
||||
playerSearch, setPlayerSearch, playerStatus, setPlayerStatus, playerPanel, setPlayerPanel, squadSearch, setSquadSearch, selectedSquadId, setSelectedSquadId,
|
||||
activityStatus, setActivityStatus, eventId, setEventId, eventName, setEventName, eventType, setEventType, eventSchedule, setEventSchedule,
|
||||
eventCommand, setEventCommand, eventClass, setEventClass, eventPlacard, setEventPlacard, eventPercent, setEventPercent, eventDuration, setEventDuration, eventNpc, setEventNpc, eventItem, setEventItem, eventZombie, setEventZombie, eventAnimal, setEventAnimal,
|
||||
produceEventId, setProduceEventId, produceId, setProduceId, produceTradeGoodsId, setProduceTradeGoodsId, producePercent, setProducePercent, produceValue, setProduceValue, produceRadius, setProduceRadius, produceX, setProduceX, produceY, setProduceY, produceZ, setProduceZ,
|
||||
@@ -167,7 +161,7 @@ export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext)
|
||||
|
||||
type ViewState = {
|
||||
playerSearch: string; setPlayerSearch: StateSetter<string>; playerStatus: string; setPlayerStatus: StateSetter<string>;
|
||||
playerPanel: PlayerPanelState; setPlayerPanel: StateSetter<PlayerPanelState>; attributeDrafts: AttributeDraft[]; setAttributeDrafts: StateSetter<AttributeDraft[]>;
|
||||
playerPanel: PlayerPanelState; setPlayerPanel: StateSetter<PlayerPanelState>;
|
||||
squadSearch: string; setSquadSearch: StateSetter<string>; selectedSquadId: string; setSelectedSquadId: StateSetter<string>;
|
||||
activityStatus: string; setActivityStatus: StateSetter<string>; giftTab: GiftTab; setGiftTab: StateSetter<GiftTab>;
|
||||
eventId: string; setEventId: StateSetter<string>; eventName: string; setEventName: StateSetter<string>;
|
||||
@@ -225,7 +219,7 @@ function playerTableRow(e: ReactLike["createElement"], player: RecordMap, index:
|
||||
const name = textField(player, "displayName", "playerName", "name") || textField(player, "gamePlayerId", "playerId", "steamId") || "未知用户";
|
||||
const steamId = textField(player, "steamId", "providerId");
|
||||
const uuid = playerUuid(player);
|
||||
const open = (kind: PlayerPanelKind) => { view.setPlayerPanel({ kind, playerId: playerKey(player) }); if (kind === "attributes") view.setAttributeDrafts(playerAttributeDrafts(player)); };
|
||||
const open = (kind: PlayerPanelKind) => view.setPlayerPanel({ kind, playerId: playerKey(player) });
|
||||
return e("tr", { key: idOf(player, `player-${index}`) },
|
||||
e("td", null, e("strong", null, name)),
|
||||
e("td", null, e("span", { className: "provider-id scum-mono-cell", title: uuid ? `UUID ${uuid}` : undefined }, steamId || "未同步")),
|
||||
@@ -242,7 +236,6 @@ function playerActionMenu(e: ReactLike["createElement"], player: RecordMap, view
|
||||
return e("details", { className: "inline-action-control" },
|
||||
e("summary", { className: "icon-command", "aria-label": `打开${textField(player, "displayName", "playerName") || "用户"}操作菜单` }, "操作"),
|
||||
e("div", { className: "inline-action-menu", role: "menu", "aria-label": "用户操作" },
|
||||
e("button", { type: "button", className: "runtime-action-item", role: "menuitem", onClick: () => open("attributes") }, "编辑属性"),
|
||||
e("button", { type: "button", className: "runtime-action-item", role: "menuitem", onClick: () => open("gifts") }, "礼包状态"),
|
||||
e("button", { type: "button", className: "runtime-action-item", role: "menuitem", onClick: () => open("items") }, "他的物品"),
|
||||
e("button", { type: "button", className: "runtime-action-item", role: "menuitem", onClick: () => open("history") }, "登录历史"),
|
||||
@@ -255,8 +248,8 @@ function playerDrawer(e: ReactLike["createElement"], player: RecordMap, data: SC
|
||||
const kind = view.playerPanel.kind;
|
||||
const name = textField(player, "displayName", "playerName", "name") || playerKey(player);
|
||||
const close = () => view.setPlayerPanel({ kind: "closed", playerId: "" });
|
||||
const content = kind === "attributes" ? playerAttributesPanel(e, player, input, view) : kind === "gifts" ? playerGiftPanel(e, player, data, input, view) : kind === "items" ? playerItemsPanel(e, player) : kind === "history" ? playerHistoryPanel(e, player, data) : playerTrajectoryPanel(e, player, data);
|
||||
const title = kind === "attributes" ? "编辑属性" : kind === "gifts" ? "礼包状态" : kind === "items" ? "他的物品" : kind === "history" ? "登录历史" : "用户轨迹";
|
||||
const content = kind === "gifts" ? playerGiftPanel(e, player, data, input, view) : kind === "items" ? playerItemsPanel(e, player) : kind === "history" ? playerHistoryPanel(e, player) : playerTrajectoryPanel(e, player, data);
|
||||
const title = kind === "gifts" ? "礼包状态" : kind === "items" ? "他的物品" : kind === "history" ? "登录历史" : "用户轨迹";
|
||||
return e("div", { className: "confirm-backdrop", role: "presentation", onClick: close },
|
||||
e("aside", { className: "drawer-panel", role: "dialog", "aria-modal": "true", "aria-label": `${name} / ${title}`, onClick: (event: InputEvent) => event.stopPropagation?.() },
|
||||
e("div", { className: "panel-header" }, e("div", null, e("h2", null, title), e("span", { className: "provider-id" }, `${name} · Steam ${textField(player, "steamId", "providerId") || "未同步"}`)), e("button", { type: "button", className: "drawer-close", onClick: close }, "关闭")),
|
||||
@@ -266,29 +259,13 @@ function playerDrawer(e: ReactLike["createElement"], player: RecordMap, data: SC
|
||||
e("span", null, `登录 IP ${textField(player, "lastLoginIp", "loginIp", "ipAddress", "ip") || "未同步"}`),
|
||||
e("span", null, `网络相关 ${shortHash(textField(player, "networkCorrelation")) || "未同步"}`),
|
||||
e("span", null, `Fame ${numField(player, "famePoints")}`),
|
||||
e("span", null, `渣币 ${playerCurrency(player)} · Gold ${playerGold(player)}`),
|
||||
e("span", null, `渣币 ${playerCurrency(player)} · 金条 ${playerGold(player)}`),
|
||||
e("span", null, `上次登录 ${userDateField(player, "lastLoginTime", "lastLoginAt", "lastLoginObservedAt")}`)),
|
||||
content
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function playerAttributesPanel(e: ReactLike["createElement"], player: RecordMap, input: SCUMPageContext, view: ViewState) {
|
||||
const preview = playerAttributeSqlPreview(view.attributeDrafts);
|
||||
const canSubmit = view.attributeDrafts.some((draft) => draft.after.trim() && draft.after.trim() !== draft.before.trim()) && view.attributeDrafts.every((draft) => !draft.after.trim() || Number.isFinite(Number(draft.after)));
|
||||
const save = () => runAction(view.setAction, "正在生成 SQL 并提交到 Run…", async () => { await queuePlayerAttributePatch(input.workspaceActions ?? {}, player, view.attributeDrafts); view.refresh(); return "SQL 已进入平台到 Run 的执行队列。"; });
|
||||
return e("div", { className: "console-record-list" },
|
||||
e("p", { className: "dialog-description" }, "快捷项会生成 SCUM.db SQL,并通过平台 remote.access.request 提交给 Run 执行;当前值未同步也可以直接提交。"),
|
||||
e("div", { className: "provider-form" },
|
||||
e("div", { className: "form-grid" }, view.attributeDrafts.map((draft) => e("label", { key: draft.fieldKey }, `${draft.label}(当前 ${draft.before || "未同步"})`, e("input", { type: "number", value: draft.after, "aria-label": `${draft.label}目标值`, onChange: (event: InputEvent) => view.setAttributeDrafts((previous) => previous.map((candidate) => candidate.fieldKey === draft.fieldKey ? { ...candidate, after: inputValue(event) } : candidate)) })))
|
||||
)
|
||||
),
|
||||
e("div", { className: "diff-view", "aria-label": "SQL 预览" }, e("strong", null, "SQL 预览"), e("code", null, preview)),
|
||||
e("p", { className: "field-help" }, "执行任务使用 remote.run.db.sqlite.execute;SQL 文本会随任务进入 Run 队列。"),
|
||||
e("div", { className: "confirm-actions" }, e("button", { type: "button", className: "drawer-close", onClick: () => view.setPlayerPanel({ kind: "closed", playerId: "" }) }, "取消"), e("button", { type: "button", className: "primary-command", disabled: !canSubmit || !input.workspaceActions?.dispatch, onClick: save }, "生成并执行"))
|
||||
);
|
||||
}
|
||||
|
||||
function playerGiftPanel(e: ReactLike["createElement"], player: RecordMap, data: SCUMSurfaceData, input: SCUMPageContext, view: ViewState) {
|
||||
const claims = playerRecords(data.giftClaims, player);
|
||||
const pending = playerRecords(data.pendingGifts, player);
|
||||
@@ -309,17 +286,20 @@ function giftStatusRecord(e: ReactLike["createElement"], title: string, rows: Re
|
||||
function playerItemsPanel(e: ReactLike["createElement"], player: RecordMap) {
|
||||
const items = field(player, "items", "inventory", "inventoryItems");
|
||||
const rows = Array.isArray(items) ? items : [];
|
||||
return e("div", { className: "console-record-list" }, e("p", { className: "dialog-description" }, "物品清单只展示插件已同步到用户记录的内容。"), e("div", { className: "console-row-list" }, rows.length ? rows.map((item, index) => e("div", { key: `item-${index}`, className: "console-row" }, e("span", null, isRecord(item) ? textField(item, "name", "label", "itemId", "className") || "未命名物品" : String(item)), e("strong", null, isRecord(item) ? `× ${numField(item, "quantity", "count")}` : ""))) : e("p", { className: "page-status" }, "没有该用户的真实物品记录,等待插件同步。")));
|
||||
return e("div", { className: "console-record-list" }, e("p", { className: "dialog-description" }, "物品清单只展示平台用户事实中已经同步的内容。"), e("div", { className: "console-row-list" }, rows.length ? rows.map((item, index) => e("div", { key: `item-${index}`, className: "console-row" }, e("span", null, isRecord(item) ? textField(item, "name", "label", "itemId", "className") || "未命名物品" : String(item)), e("strong", null, isRecord(item) ? `× ${numField(item, "quantity", "count")}` : ""))) : e("p", { className: "page-status" }, "没有该用户的真实物品记录,等待 Run 推送用户事实。")));
|
||||
}
|
||||
|
||||
function playerHistoryPanel(e: ReactLike["createElement"], player: RecordMap, data: SCUMSurfaceData) {
|
||||
const rows = playerRecords(data.activityEvents, player).filter((row) => ["login", "logout", "scum.login", "scum.logout"].includes(textField(row, "eventType", "type").toLowerCase()));
|
||||
return e("div", { className: "console-record-list" }, e("p", { className: "dialog-description" }, "登录历史来自插件声明的 SCUM 登录日志投影。"), e("div", { className: "console-row-list" }, rows.length ? rows.map((row, index) => e("div", { key: idOf(row, `history-${index}`), className: "console-row" }, e("span", null, textField(row, "eventType", "type") || "登录事件"), e("strong", null, dateField(row, "occurredAt", "observedAt", "createdAt")), e("strong", null, textField(row, "lastLoginIp", "loginIp", "ipAddress", "ip") || shortHash(textField(row, "networkCorrelation")) || textField(row, "reason") || "无网络字段"))) : e("p", { className: "page-status" }, "没有该用户的真实登录历史。")));
|
||||
function playerHistoryPanel(e: ReactLike["createElement"], player: RecordMap) {
|
||||
const rows = [
|
||||
{ label: "最近登录", at: textField(player, "lastLoginAt", "lastLoginTime", "lastLoginObservedAt"), meta: textField(player, "lastLoginIp", "loginIp", "ipAddress", "ip") || "无登录 IP" },
|
||||
{ label: "最后活动", at: textField(player, "lastSeenAt", "lastActivityAt", "updatedAt"), meta: playerOnline(player) ? "在线" : "离线/未知" }
|
||||
].filter((row) => row.at);
|
||||
return e("div", { className: "console-record-list" }, e("p", { className: "dialog-description" }, "登录状态来自平台 scum_user 表,由 Run 推送的登录事实维护。"), e("div", { className: "console-row-list" }, rows.length ? rows.map((row, index) => e("div", { key: `history-${index}`, className: "console-row" }, e("span", null, row.label), e("strong", null, dateField(row as RecordMap, "at")), e("strong", null, row.meta))) : e("p", { className: "page-status" }, "没有该用户的真实登录历史。")));
|
||||
}
|
||||
|
||||
function playerTrajectoryPanel(e: ReactLike["createElement"], player: RecordMap, data: SCUMSurfaceData) {
|
||||
const rows = playerRecords(data.trajectories, player).filter((row) => layerOf(row) === "players" && hasCoordinates(positionOf(row))).sort((left, right) => trajectoryOrder(right) - trajectoryOrder(left)).slice(0, 120);
|
||||
return e("div", { className: "console-record-list" }, e("p", { className: "dialog-description" }, "用户轨迹来自 Run 每 3 秒查询 SCUM.db 的采样投影;乘车状态按同一时刻附近载具保守标识。"), e("div", { className: "console-row-list" }, rows.length ? rows.map((row, index) => { const ride = nearbyVehicle(row, data.vehicles); return e("div", { key: idOf(row, `trajectory-${index}`), className: "console-row" }, e("span", null, dateField(row, "sampledAt", "observedAt", "createdAt")), e("strong", null, coords(positionOf(row))), e("strong", null, ride ? `疑似乘坐 ${pointTitle(ride)}` : textField(row, "source") || "run.sqlite")); }) : e("p", { className: "page-status" }, "没有该用户的真实轨迹记录。")));
|
||||
return e("div", { className: "console-record-list" }, e("p", { className: "dialog-description" }, "用户轨迹来自平台 scum_user_trajectory 表;乘车状态使用轨迹记录声明的载具。"), e("div", { className: "console-row-list" }, rows.length ? rows.map((row, index) => { const ride = textField(row, "riddenVehicleId", "gameVehicleId"); return e("div", { key: idOf(row, `trajectory-${index}`), className: "console-row" }, e("span", null, dateField(row, "sampledAt", "observedAt", "createdAt")), e("strong", null, coords(positionOf(row))), e("strong", null, ride ? `乘坐 ${ride}` : textField(row, "source") || "平台轨迹表")); }) : e("p", { className: "page-status" }, "没有该用户的真实轨迹记录。")));
|
||||
}
|
||||
|
||||
function playerRecords(rows: RecordMap[], player: RecordMap): RecordMap[] { const identities = playerIdentities(player); return rows.filter((row) => identities.includes(textField(row, "steamId", "playerId", "gamePlayerId", "userProfileId", "profileId"))); }
|
||||
@@ -552,6 +532,7 @@ function mapSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, input:
|
||||
const selected = visible.find((point, index) => idOf(point, `point-${index}`) === view.selectedMapPoint) ?? visible[0];
|
||||
const trails = visibleTrajectoryPoints(data.trajectories, view.mapLayers, search).sort((left, right) => trajectoryOrder(right) - trajectoryOrder(left)).slice(0, 180).reverse();
|
||||
const selectedTrails = selected ? trajectoryRecordsForPoint(data.trajectories, selected).sort((left, right) => trajectoryOrder(right) - trajectoryOrder(left)).slice(0, 8) : [];
|
||||
const selectedLocks = selected && layerOf(selected) === "vehicles" ? vehicleLockRecordsForPoint(data.vehicleLocks, selected).sort((left, right) => rowTime(right, ["lockedAt", "createdAt"]) - rowTime(left, ["lockedAt", "createdAt"])).slice(0, 5) : [];
|
||||
return e("div", { className: "console-record-list" },
|
||||
statsStrip(e, [["地图点", points.length], ["用户", data.players.length], ["载具", data.vehicles.length], ["载具目录", vehicleCatalog.length]]),
|
||||
e("div", { className: "resource-filter-bar scum-filter-bar" },
|
||||
@@ -568,8 +549,8 @@ function mapSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, input:
|
||||
e("button", { type: "button", className: "primary-command", disabled: !actions?.pluginData, onClick: () => runAction(view.setAction, "正在保存地图范围…", async () => { await saveMapSettings(actions ?? {}, { customMapEnabled: customEnabled, centerX: numberInput(centerX, 0), centerY: numberInput(centerY, 0), widthKm: numberInput(widthKm, 15.24), heightKm: numberInput(heightKm, 15.24) }); view.refresh(); return "地图范围已保存。"; }) }, "保存地图范围"))
|
||||
),
|
||||
e("div", { className: "overview-two-col" },
|
||||
e("div", { className: "map-projection-board", "aria-label": "SCUM 地图图层", style: { backgroundImage: `url(${scumMapBackground})` } }, mapGridOverlay(e), trails.map((point, index) => e("span", { key: `trail-${index}-${idOf(point, "sample")}`, className: `map-trajectory-dot map-layer-${layerOf(point)}`, title: `${pointTitle(point)} ${dateField(point, "sampledAt", "observedAt")}`, style: mapPointStyle(point, bounds) })), visible.map((point, index) => { const ride = layerOf(point) === "players" ? nearbyVehicle(point, data.vehicles) : undefined; return e("button", { key: idOf(point, `point-${index}`), type: "button", className: `map-projection-dot map-layer-${layerOf(point)}${ride ? " map-projection-dot-riding" : ""}`, title: `${pointTitle(point)} ${coords(point)}${ride ? ` · 疑似乘坐 ${pointTitle(ride)}` : ""}`, "aria-label": pointTitle(point), style: mapPointStyle(point, bounds), onClick: () => view.setSelectedMapPoint(idOf(point, `point-${index}`)) }, vehicleIconFor(point) ? e("img", { src: vehicleIconFor(point), alt: "" }) : ""); })),
|
||||
e("article", { className: "console-module" }, e("div", { className: "panel-header" }, e("h2", null, "地图点详情"), e("span", { className: "page-status" }, `${visible.length} 个可见点`)), selected ? e("div", { className: "console-record" }, e("strong", null, pointTitle(selected)), e("span", { className: "status-pill status-active" }, layerLabel(layerOf(selected))), e("span", { className: "provider-id" }, coords(selected)), e("div", { className: "console-record-meta" }, e("span", null, `ID ${textField(selected, "subjectId", "id", "_recordKey") || "unknown"}`), e("span", null, `来源 ${textField(selected, "source") || "plugin collection"}`), e("span", null, freshness(selected))), layerOf(selected) === "vehicles" ? e("div", { className: "console-record-meta" }, e("span", null, `类型 ${textField(selected, "className", "vehicleClass", "vehicleType") || "unknown"}`), e("span", null, `状态 ${textField(selected, "status", "state", "isFunctional") || "unknown"}`), e("span", null, `访问 ${dateField(selected, "lastAccessTime", "vehicleObservedAt", "sampledAt")}`)) : null, selectedTrails.length ? e("div", { className: "console-row-list" }, selectedTrails.map((row, index) => e("div", { key: `selected-trail-${index}`, className: "console-row" }, e("span", null, dateField(row, "sampledAt", "observedAt")), e("strong", null, coords(row)), e("strong", null, textField(row, "source") || "run.sqlite")))) : null) : e("p", { className: "page-status" }, "当前图层和筛选条件下没有真实地图点。"))
|
||||
e("div", { className: "map-surface-board", "aria-label": "SCUM 地图图层", style: { backgroundImage: `url(${scumMapBackground})` } }, mapGridOverlay(e), trails.map((point, index) => e("span", { key: `trail-${index}-${idOf(point, "sample")}`, className: `map-trajectory-dot map-layer-${layerOf(point)}`, title: `${pointTitle(point)} ${dateField(point, "sampledAt", "observedAt")}`, style: mapPointStyle(point, bounds) })), visible.map((point, index) => { const ride = layerOf(point) === "players" ? textField(point, "riddenVehicleId", "gameVehicleId") : ""; return e("button", { key: idOf(point, `point-${index}`), type: "button", className: `map-surface-dot map-layer-${layerOf(point)}${ride ? " map-surface-dot-riding" : ""}`, title: `${pointTitle(point)} ${coords(point)}${ride ? ` · 乘坐 ${ride}` : ""}`, "aria-label": pointTitle(point), style: mapPointStyle(point, bounds), onClick: () => view.setSelectedMapPoint(idOf(point, `point-${index}`)) }, vehicleIconFor(point) ? e("img", { src: vehicleIconFor(point), alt: "" }) : ""); })),
|
||||
e("article", { className: "console-module" }, e("div", { className: "panel-header" }, e("h2", null, "地图点详情"), e("span", { className: "page-status" }, `${visible.length} 个可见点`)), selected ? e("div", { className: "console-record" }, e("strong", null, pointTitle(selected)), e("span", { className: "status-pill status-active" }, layerLabel(layerOf(selected))), e("span", { className: "provider-id" }, coords(selected)), e("div", { className: "console-record-meta" }, e("span", null, `ID ${textField(selected, "subjectId", "id", "_recordKey") || "unknown"}`), e("span", null, `来源 ${textField(selected, "source") || "平台表"}`), e("span", null, freshness(selected))), layerOf(selected) === "vehicles" ? e("div", { className: "console-record-meta" }, e("span", null, `类型 ${textField(selected, "className", "vehicleClass", "vehicleType") || "unknown"}`), e("span", null, `状态 ${textField(selected, "status", "state", "isFunctional") || "unknown"}`), e("span", null, `锁 ${boolField(selected, "locked") ? "已上锁" : "未上锁/未知"}`), e("span", null, `访问 ${dateField(selected, "lastAccessTime", "vehicleObservedAt", "sampledAt")}`)) : null, selectedLocks.length ? e("div", { className: "console-row-list" }, selectedLocks.map((row, index) => e("div", { key: `selected-lock-${index}`, className: "console-row" }, e("span", null, dateField(row, "lockedAt", "createdAt")), e("strong", null, textField(row, "scumUserId") || "unknown"), e("strong", null, textField(row, "steamId") || "unknown")))) : null, selectedTrails.length ? e("div", { className: "console-row-list" }, selectedTrails.map((row, index) => e("div", { key: `selected-trail-${index}`, className: "console-row" }, e("span", null, dateField(row, "sampledAt", "observedAt")), e("strong", null, coords(row)), e("strong", null, textField(row, "source") || "平台轨迹表")))) : null) : e("p", { className: "page-status" }, "当前图层和筛选条件下没有真实地图点。"))
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -633,10 +614,9 @@ function mapGridOverlay(e: ReactLike["createElement"]) {
|
||||
|
||||
function visibleTrajectoryPoints(rows: RecordMap[], layers: Record<MapLayer, boolean>, search: string): RecordMap[] { return rows.filter((row) => (layerOf(row) === "players" || layerOf(row) === "vehicles") && layers[layerOf(row)] && hasCoordinates(positionOf(row)) && matchesText(row, search, "displayName", "label", "subjectId", "steamId", "vehicleId", "subjectType")); }
|
||||
function trajectoryRecordsForPoint(rows: RecordMap[], point: RecordMap): RecordMap[] { const ids = new Set([textField(point, "subjectId"), textField(point, "steamId"), textField(point, "gamePlayerId"), textField(point, "vehicleId"), textField(point, "id")].filter(Boolean)); const layer = layerOf(point); return rows.filter((row) => layerOf(row) === layer && trajectoryIdentity(row).some((identity) => ids.has(identity))); }
|
||||
function vehicleLockRecordsForPoint(rows: RecordMap[], point: RecordMap): RecordMap[] { const ids = new Set([textField(point, "id"), textField(point, "scumVehicleId"), textField(point, "vehicleId"), textField(point, "gameVehicleId"), textField(point, "entityId"), textField(point, "subjectId")].filter(Boolean)); return rows.filter((row) => [textField(row, "scumVehicleId"), textField(row, "vehicleId"), textField(row, "gameVehicleId")].some((identity) => ids.has(identity))); }
|
||||
function trajectoryIdentity(row: RecordMap): string[] { return [textField(row, "subjectId"), textField(row, "steamId"), textField(row, "gamePlayerId"), textField(row, "vehicleId"), textField(row, "id")].filter(Boolean); }
|
||||
function trajectoryOrder(row: RecordMap): number { const stamp = Date.parse(textField(row, "sampledAt", "observedAt", "createdAt")); return Number.isNaN(stamp) ? 0 : stamp; }
|
||||
function nearbyVehicle(point: RecordMap, vehicles: RecordMap[]): RecordMap | undefined { if (!hasCoordinates(positionOf(point))) return undefined; let best: { vehicle: RecordMap; distance: number } | undefined; for (const vehicle of vehicles) { if (!hasCoordinates(positionOf(vehicle))) continue; const distance = distance2D(positionOf(point)!, positionOf(vehicle)!); if (distance <= rideDistanceThreshold && (!best || distance < best.distance)) best = { vehicle, distance }; } return best?.vehicle; }
|
||||
function distance2D(left: RecordMap, right: RecordMap): number { const dx = Number(field(left, "x", "locationX")) - Number(field(right, "x", "locationX")); const dy = Number(field(left, "y", "locationY")) - Number(field(right, "y", "locationY")); return Math.sqrt(dx * dx + dy * dy); }
|
||||
function vehicleIconFor(point: RecordMap): string { const explicit = textField(point, "imagePath", "image_path"); if (explicit) return explicit.startsWith("/") ? explicit : `/${explicit}`; return vehicleIconByClass[normalizedVehicleClass(textField(point, "className", "vehicleClass", "entityClass", "vehicleType"))] ?? ""; }
|
||||
function vehicleClassKey(value: string): string { return normalizedVehicleClass(value.replace(/^#spawnvehicle\s+/i, "")); }
|
||||
function normalizedVehicleClass(value: string): string { return value.replace(/_C$/i, "").split(".").pop()?.trim() ?? value.trim(); }
|
||||
|
||||
@@ -70,8 +70,6 @@
|
||||
"remote.rsync.write",
|
||||
"remote.run.files.read",
|
||||
"remote.run.files.write",
|
||||
"remote.run.db.sqlite.query",
|
||||
"remote.run.db.sqlite.execute",
|
||||
"remote.run.process.start",
|
||||
"remote.run.process.stop",
|
||||
"remote.run.logs.transfer",
|
||||
@@ -90,8 +88,6 @@
|
||||
"runCapabilities": [
|
||||
"remote.run.files.read",
|
||||
"remote.run.files.write",
|
||||
"remote.run.db.sqlite.query",
|
||||
"remote.run.db.sqlite.execute",
|
||||
"remote.run.process.start",
|
||||
"remote.run.process.stop",
|
||||
"remote.run.logs.transfer",
|
||||
@@ -99,9 +95,7 @@
|
||||
"remote.run.program.command"
|
||||
],
|
||||
"rcon": true,
|
||||
"databaseEngines": [
|
||||
"sqlite"
|
||||
],
|
||||
"databaseEngines": [],
|
||||
"logTransfer": true
|
||||
},
|
||||
"bridge": {
|
||||
@@ -226,144 +220,16 @@
|
||||
"maxRecords": 1000
|
||||
}
|
||||
],
|
||||
"queryTemplates": [
|
||||
{
|
||||
"key": "scum.player.profile",
|
||||
"title": "Read SCUM player profile, economy, squad, and position facts",
|
||||
"permission": "server.game-client.read",
|
||||
"engine": "sqlite",
|
||||
"transportKey": "scum-database",
|
||||
"targetKey": "scum-database",
|
||||
"parameterSchemaRef": "schemas/bridge/queries/scum-player-profile.parameters.schema.json",
|
||||
"resultSchemaRef": "schemas/bridge/queries/scum-player-profile.result.schema.json",
|
||||
"sqlRef": "sql/scum-db-v57/users.sql",
|
||||
"pollIntervalSeconds": 3,
|
||||
"maxRows": 500,
|
||||
"timeoutSeconds": 15
|
||||
},
|
||||
{
|
||||
"key": "scum.squads",
|
||||
"title": "Read SCUM squad records",
|
||||
"permission": "server.game-client.read",
|
||||
"engine": "sqlite",
|
||||
"transportKey": "scum-database",
|
||||
"targetKey": "scum-database",
|
||||
"parameterSchemaRef": "schemas/bridge/queries/scum-squads.parameters.schema.json",
|
||||
"resultSchemaRef": "schemas/bridge/queries/scum-squads.result.schema.json",
|
||||
"sqlRef": "sql/scum-db-v57/squads.sql",
|
||||
"pollIntervalSeconds": 1800,
|
||||
"maxRows": 500,
|
||||
"timeoutSeconds": 15
|
||||
},
|
||||
{
|
||||
"key": "scum.squad-members",
|
||||
"title": "Read SCUM squad membership records",
|
||||
"permission": "server.game-client.read",
|
||||
"engine": "sqlite",
|
||||
"transportKey": "scum-database",
|
||||
"targetKey": "scum-database",
|
||||
"parameterSchemaRef": "schemas/bridge/queries/scum-squad-members.parameters.schema.json",
|
||||
"resultSchemaRef": "schemas/bridge/queries/scum-squad-members.result.schema.json",
|
||||
"sqlRef": "sql/scum-db-v57/squad-members.sql",
|
||||
"pollIntervalSeconds": 1800,
|
||||
"maxRows": 500,
|
||||
"timeoutSeconds": 15
|
||||
},
|
||||
{
|
||||
"key": "scum.vehicles",
|
||||
"title": "Read SCUM vehicle records and coordinates",
|
||||
"permission": "server.game-client.read",
|
||||
"engine": "sqlite",
|
||||
"transportKey": "scum-database",
|
||||
"targetKey": "scum-database",
|
||||
"parameterSchemaRef": "schemas/bridge/queries/scum-vehicles.parameters.schema.json",
|
||||
"resultSchemaRef": "schemas/bridge/queries/scum-vehicles.result.schema.json",
|
||||
"sqlRef": "sql/scum-db-v57/vehicles.sql",
|
||||
"pollIntervalSeconds": 3,
|
||||
"maxRows": 500,
|
||||
"timeoutSeconds": 15
|
||||
},
|
||||
{
|
||||
"key": "scum.flags",
|
||||
"title": "Read SCUM flag and ownership records",
|
||||
"permission": "server.game-client.read",
|
||||
"engine": "sqlite",
|
||||
"transportKey": "scum-database",
|
||||
"targetKey": "scum-database",
|
||||
"parameterSchemaRef": "schemas/bridge/queries/scum-flags.parameters.schema.json",
|
||||
"resultSchemaRef": "schemas/bridge/queries/scum-flags.result.schema.json",
|
||||
"sqlRef": "sql/scum-db-v57/flags.sql",
|
||||
"pollIntervalSeconds": 1800,
|
||||
"maxRows": 500,
|
||||
"timeoutSeconds": 15
|
||||
},
|
||||
{
|
||||
"key": "scum.positions",
|
||||
"title": "Read SCUM player, vehicle, base, and flag coordinates",
|
||||
"permission": "server.game-client.read",
|
||||
"engine": "sqlite",
|
||||
"transportKey": "scum-database",
|
||||
"targetKey": "scum-database",
|
||||
"parameterSchemaRef": "schemas/bridge/queries/scum-positions.parameters.schema.json",
|
||||
"resultSchemaRef": "schemas/bridge/queries/scum-positions.result.schema.json",
|
||||
"sqlRef": "sql/scum-db-v57/map-points.sql",
|
||||
"pollIntervalSeconds": 3,
|
||||
"maxRows": 500,
|
||||
"timeoutSeconds": 15
|
||||
},
|
||||
{
|
||||
"key": "scum.tasks",
|
||||
"title": "Read SCUM v57 quest and task records",
|
||||
"permission": "server.game-client.read",
|
||||
"engine": "sqlite",
|
||||
"transportKey": "scum-database",
|
||||
"targetKey": "scum-database",
|
||||
"parameterSchemaRef": "schemas/bridge/queries/scum-tasks.parameters.schema.json",
|
||||
"resultSchemaRef": "schemas/bridge/queries/scum-tasks.result.schema.json",
|
||||
"sqlRef": "sql/scum-db-v57/tasks.sql",
|
||||
"pollIntervalSeconds": 1800,
|
||||
"maxRows": 500,
|
||||
"timeoutSeconds": 15
|
||||
},
|
||||
{
|
||||
"key": "scum.events",
|
||||
"title": "Read SCUM v57 native event rounds and statistics",
|
||||
"permission": "server.game-client.read",
|
||||
"engine": "sqlite",
|
||||
"transportKey": "scum-database",
|
||||
"targetKey": "scum-database",
|
||||
"parameterSchemaRef": "schemas/bridge/queries/scum-events.parameters.schema.json",
|
||||
"resultSchemaRef": "schemas/bridge/queries/scum-events.result.schema.json",
|
||||
"sqlRef": "sql/scum-db-v57/events.sql",
|
||||
"pollIntervalSeconds": 1800,
|
||||
"maxRows": 500,
|
||||
"timeoutSeconds": 15
|
||||
},
|
||||
{
|
||||
"key": "scum.native-timed-gifts",
|
||||
"title": "Read SCUM v57 native timed gift completion records",
|
||||
"permission": "server.game-client.read",
|
||||
"engine": "sqlite",
|
||||
"transportKey": "scum-database",
|
||||
"targetKey": "scum-database",
|
||||
"parameterSchemaRef": "schemas/bridge/queries/scum-native-timed-gifts.parameters.schema.json",
|
||||
"resultSchemaRef": "schemas/bridge/queries/scum-native-timed-gifts.result.schema.json",
|
||||
"sqlRef": "sql/scum-db-v57/native-timed-gifts.sql",
|
||||
"pollIntervalSeconds": 1800,
|
||||
"maxRows": 500,
|
||||
"timeoutSeconds": 15
|
||||
}
|
||||
],
|
||||
"dataPacks": [
|
||||
{
|
||||
"key": "scum-db-v57",
|
||||
"key": "scum-config-v57",
|
||||
"databaseUserVersion": 57,
|
||||
"configMapRefs": [
|
||||
"data-packs/scum-db-v57/config-maps.json"
|
||||
"data-packs/scum-config-v57/config-maps.json"
|
||||
],
|
||||
"dataRefs": [
|
||||
"data-packs/scum-db-v57/gift-items.json",
|
||||
"data-packs/scum-db-v57/map-geometry.json"
|
||||
"data-packs/scum-config-v57/gift-items.json",
|
||||
"data-packs/scum-config-v57/map-geometry.json"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -412,10 +278,6 @@
|
||||
"players",
|
||||
"online.sessions"
|
||||
],
|
||||
"queryTemplateKeys": [
|
||||
"scum.player.profile",
|
||||
"scum.positions"
|
||||
],
|
||||
"featureKeys": [
|
||||
"player.intelligence"
|
||||
]
|
||||
@@ -426,11 +288,6 @@
|
||||
"squads",
|
||||
"flags"
|
||||
],
|
||||
"queryTemplateKeys": [
|
||||
"scum.squads",
|
||||
"scum.squad-members",
|
||||
"scum.flags"
|
||||
],
|
||||
"featureKeys": [
|
||||
"player.intelligence"
|
||||
]
|
||||
@@ -442,12 +299,6 @@
|
||||
"vehicles",
|
||||
"flags"
|
||||
],
|
||||
"queryTemplateKeys": [
|
||||
"scum.player.profile",
|
||||
"scum.vehicles",
|
||||
"scum.flags",
|
||||
"scum.positions"
|
||||
],
|
||||
"featureKeys": [
|
||||
"trajectory.collect"
|
||||
]
|
||||
@@ -457,9 +308,6 @@
|
||||
"snapshotTypes": [
|
||||
"players"
|
||||
],
|
||||
"queryTemplateKeys": [
|
||||
"scum.native-timed-gifts"
|
||||
],
|
||||
"commandTypes": [
|
||||
"reward.deliver"
|
||||
],
|
||||
@@ -469,17 +317,6 @@
|
||||
},
|
||||
{
|
||||
"pageKey": "workflows",
|
||||
"queryTemplateKeys": [
|
||||
"scum.player.profile",
|
||||
"scum.squads",
|
||||
"scum.squad-members",
|
||||
"scum.vehicles",
|
||||
"scum.flags",
|
||||
"scum.positions",
|
||||
"scum.tasks",
|
||||
"scum.events",
|
||||
"scum.native-timed-gifts"
|
||||
],
|
||||
"commandTypes": [
|
||||
"event.start"
|
||||
],
|
||||
@@ -706,58 +543,202 @@
|
||||
"restartImpact": "restart-required"
|
||||
},
|
||||
{
|
||||
"key": "allow-map-screen", "fileKey": "scum-server-settings", "configKey": "scum.AllowMapScreen", "label": "允许地图", "description": "是否允许玩家打开地图。", "control": "boolean", "defaultValue": "True", "restartImpact": "none"
|
||||
"key": "allow-map-screen",
|
||||
"fileKey": "scum-server-settings",
|
||||
"configKey": "scum.AllowMapScreen",
|
||||
"label": "允许地图",
|
||||
"description": "是否允许玩家打开地图。",
|
||||
"control": "boolean",
|
||||
"defaultValue": "True",
|
||||
"restartImpact": "none"
|
||||
},
|
||||
{
|
||||
"key": "allow-kill-claiming", "fileKey": "scum-server-settings", "configKey": "scum.AllowKillClaiming", "label": "允许领取击杀", "description": "是否允许玩家领取击杀奖励。", "control": "boolean", "defaultValue": "True", "restartImpact": "none"
|
||||
"key": "allow-kill-claiming",
|
||||
"fileKey": "scum-server-settings",
|
||||
"configKey": "scum.AllowKillClaiming",
|
||||
"label": "允许领取击杀",
|
||||
"description": "是否允许玩家领取击杀奖励。",
|
||||
"control": "boolean",
|
||||
"defaultValue": "True",
|
||||
"restartImpact": "none"
|
||||
},
|
||||
{
|
||||
"key": "allow-coma", "fileKey": "scum-server-settings", "configKey": "scum.AllowComa", "label": "允许昏迷", "description": "是否启用昏迷机制。", "control": "boolean", "defaultValue": "True", "restartImpact": "restart-required"
|
||||
"key": "allow-coma",
|
||||
"fileKey": "scum-server-settings",
|
||||
"configKey": "scum.AllowComa",
|
||||
"label": "允许昏迷",
|
||||
"description": "是否启用昏迷机制。",
|
||||
"control": "boolean",
|
||||
"defaultValue": "True",
|
||||
"restartImpact": "restart-required"
|
||||
},
|
||||
{
|
||||
"key": "allow-mines-and-traps", "fileKey": "scum-server-settings", "configKey": "scum.AllowMinesAndTraps", "label": "允许地雷和陷阱", "description": "是否允许使用地雷和陷阱。", "control": "boolean", "defaultValue": "False", "restartImpact": "restart-required"
|
||||
"key": "allow-mines-and-traps",
|
||||
"fileKey": "scum-server-settings",
|
||||
"configKey": "scum.AllowMinesAndTraps",
|
||||
"label": "允许地雷和陷阱",
|
||||
"description": "是否允许使用地雷和陷阱。",
|
||||
"control": "boolean",
|
||||
"defaultValue": "False",
|
||||
"restartImpact": "restart-required"
|
||||
},
|
||||
{
|
||||
"key": "allow-events", "fileKey": "scum-server-settings", "configKey": "scum.AllowEvents", "label": "允许服务器事件", "description": "是否启用服务器事件。", "control": "boolean", "defaultValue": "True", "restartImpact": "restart-required"
|
||||
"key": "allow-events",
|
||||
"fileKey": "scum-server-settings",
|
||||
"configKey": "scum.AllowEvents",
|
||||
"label": "允许服务器事件",
|
||||
"description": "是否启用服务器事件。",
|
||||
"control": "boolean",
|
||||
"defaultValue": "True",
|
||||
"restartImpact": "restart-required"
|
||||
},
|
||||
{
|
||||
"key": "logout-timer", "fileKey": "scum-server-settings", "configKey": "scum.LogoutTimer", "label": "下线倒计时", "description": "玩家下线前的倒计时秒数。", "control": "number", "minimum": 0, "maximum": 3600, "defaultValue": "60", "restartImpact": "restart-required"
|
||||
"key": "logout-timer",
|
||||
"fileKey": "scum-server-settings",
|
||||
"configKey": "scum.LogoutTimer",
|
||||
"label": "下线倒计时",
|
||||
"description": "玩家下线前的倒计时秒数。",
|
||||
"control": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 3600,
|
||||
"defaultValue": "60",
|
||||
"restartImpact": "restart-required"
|
||||
},
|
||||
{
|
||||
"key": "fame-gain-multiplier", "fileKey": "scum-server-settings", "configKey": "scum.FameGainMultiplier", "label": "声望获取倍率", "description": "玩家获得声望的倍率。", "control": "number", "minimum": 0, "maximum": 100, "defaultValue": "1", "restartImpact": "none"
|
||||
"key": "fame-gain-multiplier",
|
||||
"fileKey": "scum-server-settings",
|
||||
"configKey": "scum.FameGainMultiplier",
|
||||
"label": "声望获取倍率",
|
||||
"description": "玩家获得声望的倍率。",
|
||||
"control": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"defaultValue": "1",
|
||||
"restartImpact": "none"
|
||||
},
|
||||
{
|
||||
"key": "max-allowed-characters", "fileKey": "scum-server-settings", "configKey": "scum.MaxAllowedCharacters", "label": "世界角色上限", "description": "世界中同时存在的角色数量上限。", "control": "number", "minimum": 0, "maximum": 10000, "defaultValue": "500", "restartImpact": "restart-required"
|
||||
"key": "max-allowed-characters",
|
||||
"fileKey": "scum-server-settings",
|
||||
"configKey": "scum.MaxAllowedCharacters",
|
||||
"label": "世界角色上限",
|
||||
"description": "世界中同时存在的角色数量上限。",
|
||||
"control": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 10000,
|
||||
"defaultValue": "500",
|
||||
"restartImpact": "restart-required"
|
||||
},
|
||||
{
|
||||
"key": "max-allowed-zombies", "fileKey": "scum-server-settings", "configKey": "scum.MaxAllowedZombies", "label": "僵尸上限", "description": "世界中同时存在的僵尸数量上限。", "control": "number", "minimum": 0, "maximum": 10000, "defaultValue": "300", "restartImpact": "restart-required"
|
||||
"key": "max-allowed-zombies",
|
||||
"fileKey": "scum-server-settings",
|
||||
"configKey": "scum.MaxAllowedZombies",
|
||||
"label": "僵尸上限",
|
||||
"description": "世界中同时存在的僵尸数量上限。",
|
||||
"control": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 10000,
|
||||
"defaultValue": "300",
|
||||
"restartImpact": "restart-required"
|
||||
},
|
||||
{
|
||||
"key": "max-allowed-animals", "fileKey": "scum-server-settings", "configKey": "scum.MaxAllowedAnimals", "label": "动物上限", "description": "世界中同时存在的动物数量上限。", "control": "number", "minimum": 0, "maximum": 10000, "defaultValue": "-1", "restartImpact": "restart-required"
|
||||
"key": "max-allowed-animals",
|
||||
"fileKey": "scum-server-settings",
|
||||
"configKey": "scum.MaxAllowedAnimals",
|
||||
"label": "动物上限",
|
||||
"description": "世界中同时存在的动物数量上限。",
|
||||
"control": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 10000,
|
||||
"defaultValue": "-1",
|
||||
"restartImpact": "restart-required"
|
||||
},
|
||||
{
|
||||
"key": "exterior-zombie-amount", "fileKey": "scum-server-settings", "configKey": "scum.ExteriorZombieAmountModifier", "label": "室外僵尸倍率", "description": "室外僵尸数量倍率。", "control": "number", "minimum": 0, "maximum": 100, "defaultValue": "3.9", "restartImpact": "restart-required"
|
||||
"key": "exterior-zombie-amount",
|
||||
"fileKey": "scum-server-settings",
|
||||
"configKey": "scum.ExteriorZombieAmountModifier",
|
||||
"label": "室外僵尸倍率",
|
||||
"description": "室外僵尸数量倍率。",
|
||||
"control": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"defaultValue": "3.9",
|
||||
"restartImpact": "restart-required"
|
||||
},
|
||||
{
|
||||
"key": "interior-zombie-amount", "fileKey": "scum-server-settings", "configKey": "scum.InteriorZombieAmountModifier", "label": "室内僵尸倍率", "description": "室内僵尸数量倍率。", "control": "number", "minimum": 0, "maximum": 100, "defaultValue": "2.8", "restartImpact": "restart-required"
|
||||
"key": "interior-zombie-amount",
|
||||
"fileKey": "scum-server-settings",
|
||||
"configKey": "scum.InteriorZombieAmountModifier",
|
||||
"label": "室内僵尸倍率",
|
||||
"description": "室内僵尸数量倍率。",
|
||||
"control": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"defaultValue": "2.8",
|
||||
"restartImpact": "restart-required"
|
||||
},
|
||||
{
|
||||
"key": "fuel-drain-multiplier", "fileKey": "scum-server-settings", "configKey": "scum.FuelDrainFromEngineMultiplier", "label": "车辆耗油倍率", "description": "车辆发动机燃油消耗倍率。", "control": "number", "minimum": 0, "maximum": 100, "defaultValue": "1", "restartImpact": "none"
|
||||
"key": "fuel-drain-multiplier",
|
||||
"fileKey": "scum-server-settings",
|
||||
"configKey": "scum.FuelDrainFromEngineMultiplier",
|
||||
"label": "车辆耗油倍率",
|
||||
"description": "车辆发动机燃油消耗倍率。",
|
||||
"control": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"defaultValue": "1",
|
||||
"restartImpact": "none"
|
||||
},
|
||||
{
|
||||
"key": "vehicle-inactivity-time", "fileKey": "scum-server-settings", "configKey": "scum.MaximumTimeOfVehicleInactivity", "label": "车辆不活跃时间", "description": "车辆不活跃后被处理的时间。", "control": "text", "defaultValue": "168:10:00", "restartImpact": "restart-required"
|
||||
"key": "vehicle-inactivity-time",
|
||||
"fileKey": "scum-server-settings",
|
||||
"configKey": "scum.MaximumTimeOfVehicleInactivity",
|
||||
"label": "车辆不活跃时间",
|
||||
"description": "车辆不活跃后被处理的时间。",
|
||||
"control": "text",
|
||||
"defaultValue": "168:10:00",
|
||||
"restartImpact": "restart-required"
|
||||
},
|
||||
{
|
||||
"key": "log-vehicle-destroyed", "fileKey": "scum-server-settings", "configKey": "scum.LogVehicleDestroyed", "label": "记录车辆摧毁", "description": "是否记录车辆被摧毁事件。", "control": "boolean", "defaultValue": "True", "restartImpact": "none"
|
||||
"key": "log-vehicle-destroyed",
|
||||
"fileKey": "scum-server-settings",
|
||||
"configKey": "scum.LogVehicleDestroyed",
|
||||
"label": "记录车辆摧毁",
|
||||
"description": "是否记录车辆被摧毁事件。",
|
||||
"control": "boolean",
|
||||
"defaultValue": "True",
|
||||
"restartImpact": "none"
|
||||
},
|
||||
{
|
||||
"key": "raid-protection-type", "fileKey": "scum-server-settings", "configKey": "scum.RaidProtectionType", "label": "抄家保护类型", "description": "服务器抄家保护规则类型。", "control": "number", "minimum": 0, "maximum": 10, "defaultValue": "3", "restartImpact": "restart-required"
|
||||
"key": "raid-protection-type",
|
||||
"fileKey": "scum-server-settings",
|
||||
"configKey": "scum.RaidProtectionType",
|
||||
"label": "抄家保护类型",
|
||||
"description": "服务器抄家保护规则类型。",
|
||||
"control": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 10,
|
||||
"defaultValue": "3",
|
||||
"restartImpact": "restart-required"
|
||||
},
|
||||
{
|
||||
"key": "quests-enabled", "fileKey": "scum-server-settings", "configKey": "scum.QuestsEnabled", "label": "启用任务", "description": "是否启用商人任务系统。", "control": "boolean", "defaultValue": "True", "restartImpact": "none"
|
||||
"key": "quests-enabled",
|
||||
"fileKey": "scum-server-settings",
|
||||
"configKey": "scum.QuestsEnabled",
|
||||
"label": "启用任务",
|
||||
"description": "是否启用商人任务系统。",
|
||||
"control": "boolean",
|
||||
"defaultValue": "True",
|
||||
"restartImpact": "none"
|
||||
},
|
||||
{
|
||||
"key": "new-player-protection", "fileKey": "scum-server-settings", "configKey": "scum.EnableNewPlayerProtection", "label": "新人保护", "description": "是否启用新人保护。", "control": "boolean", "defaultValue": "True", "restartImpact": "restart-required"
|
||||
"key": "new-player-protection",
|
||||
"fileKey": "scum-server-settings",
|
||||
"configKey": "scum.EnableNewPlayerProtection",
|
||||
"label": "新人保护",
|
||||
"description": "是否启用新人保护。",
|
||||
"control": "boolean",
|
||||
"defaultValue": "True",
|
||||
"restartImpact": "restart-required"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -858,51 +839,15 @@
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "sql/scum-db-v57/users.sql",
|
||||
"path": "data-packs/scum-config-v57/config-maps.json",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "sql/scum-db-v57/squads.sql",
|
||||
"path": "data-packs/scum-config-v57/gift-items.json",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "sql/scum-db-v57/squad-members.sql",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "sql/scum-db-v57/vehicles.sql",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "sql/scum-db-v57/flags.sql",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "sql/scum-db-v57/map-points.sql",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "sql/scum-db-v57/tasks.sql",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "sql/scum-db-v57/events.sql",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "sql/scum-db-v57/native-timed-gifts.sql",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "data-packs/scum-db-v57/config-maps.json",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "data-packs/scum-db-v57/gift-items.json",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "data-packs/scum-db-v57/map-geometry.json",
|
||||
"path": "data-packs/scum-config-v57/map-geometry.json",
|
||||
"mode": 384
|
||||
}
|
||||
],
|
||||
@@ -1072,7 +1017,6 @@
|
||||
},
|
||||
"transportKeys": [
|
||||
"server-files",
|
||||
"scum-database",
|
||||
"scum-management"
|
||||
],
|
||||
"dllExtensionRefs": [
|
||||
@@ -1093,16 +1037,6 @@
|
||||
"windows",
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "sqlite",
|
||||
"kind": "package.installed",
|
||||
"targetKey": "sqlite-driver",
|
||||
"required": false,
|
||||
"platforms": [
|
||||
"windows",
|
||||
"linux"
|
||||
]
|
||||
}
|
||||
],
|
||||
"installPlans": [
|
||||
@@ -1196,21 +1130,7 @@
|
||||
"retentionDays": 30
|
||||
}
|
||||
],
|
||||
"dataTargets": [
|
||||
{
|
||||
"key": "scum-database",
|
||||
"kind": "sqlite.snapshot",
|
||||
"transportKey": "scum-database",
|
||||
"sourceRootKey": "server-root",
|
||||
"sourcePath": "SCUM/Saved/SaveFiles/SCUM.db",
|
||||
"workspaceKey": "databases/scum-database",
|
||||
"refreshPolicy": "on-demand-snapshot",
|
||||
"maxBytes": 1073741824,
|
||||
"platforms": [
|
||||
"windows"
|
||||
]
|
||||
}
|
||||
],
|
||||
"dataTargets": [],
|
||||
"transportProfiles": [
|
||||
{
|
||||
"key": "server-files",
|
||||
@@ -1239,15 +1159,6 @@
|
||||
"remote.rsync.write"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "scum-database",
|
||||
"kind": "sqlite",
|
||||
"targetKey": "scum-database",
|
||||
"capabilities": [
|
||||
"remote.run.db.sqlite.query",
|
||||
"remote.run.db.sqlite.execute"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "scum-management",
|
||||
"kind": "rcon",
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# SCUM.db Query Contract
|
||||
|
||||
These query template keys are browser-safe declarations. They intentionally do not carry SQL text, host paths, DSNs, sockets, or credentials. The bound run/agent beside the current SCUM service owns the actual SQLite read implementation and must return rows matching the referenced result schemas.
|
||||
|
||||
| Template key | SCUM.db source tables | Projection target |
|
||||
| --- | --- | --- |
|
||||
| `scum.player.profile` | `user_profile`, `prisoner`, `prisoner_entity`, `entity`, `bank_account_registry`, `bank_account_registry_currencies`, optional `squad_member` / `squad` joins | Player identity, economy, squad summary, and current position |
|
||||
| `scum.squads` | `squad`, optional `squad_member`, optional `user_profile` leader joins | Squad records and leader/member counts |
|
||||
| `scum.squad-members` | `squad_member`, `user_profile`, optional `squad` joins | Squad roster and member identity mapping |
|
||||
| `scum.vehicles` | `vehicle_spawner`, `entity`, optional owner/squad joins when present | Vehicle inventory and coordinates; unknown class labels remain unknown |
|
||||
| `scum.flags` | `base_element`, `entity`, `user_profile`, `squad_member`, `squad` where available | Flag ownership, ownership confidence, and coordinates |
|
||||
| `scum.positions` | `prisoner_entity`, `vehicle_spawner`, `base_element`, `entity` | Current player, vehicle, and flag coordinates |
|
||||
|
||||
`last_save_time` is freshness evidence only. It must not be treated as proof that a player is online; online state comes from login/logoff evidence or an explicit typed online field.
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMFlagOwnershipParameters",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["flagId"],
|
||||
"properties": {
|
||||
"flagId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" }
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMFlagOwnershipResult",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["ownership"],
|
||||
"properties": {
|
||||
"ownership": {
|
||||
"type": "array",
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["flagId", "status"],
|
||||
"properties": {
|
||||
"flagId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
||||
"ownerPlayerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
||||
"ownerPlayerName": { "type": "string", "minLength": 1, "maxLength": 80 },
|
||||
"squadId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
||||
"squadName": { "type": "string", "minLength": 1, "maxLength": 80 },
|
||||
"status": { "type": "string", "minLength": 1, "maxLength": 16, "enum": ["active", "inactive", "contested", "unknown"] },
|
||||
"lastUpdatedAt": { "type": "string", "minLength": 1, "maxLength": 64, "format": "date-time" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMPlayerByIdParameters",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["playerId"],
|
||||
"properties": {
|
||||
"playerId": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 96,
|
||||
"pattern": "^[A-Za-z0-9_.:-]{1,96}$"
|
||||
}
|
||||
}
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMPlayerByIdResult",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["players"],
|
||||
"properties": {
|
||||
"players": {
|
||||
"type": "array",
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["playerId", "playerName"],
|
||||
"properties": {
|
||||
"playerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
||||
"playerName": { "type": "string", "minLength": 1, "maxLength": 80 },
|
||||
"platformUserId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
||||
"squadId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
||||
"famePoints": { "type": "integer", "minimum": 0, "maximum": 2147483647 },
|
||||
"lastSeenAt": { "type": "string", "minLength": 1, "maxLength": 64, "format": "date-time" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMPlayerSearchParameters",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["nameContains", "limit"],
|
||||
"properties": {
|
||||
"nameContains": { "type": "string", "minLength": 1, "maxLength": 80 },
|
||||
"limit": { "type": "integer", "minimum": 1, "maximum": 50 }
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMPlayerSearchResult",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["players"],
|
||||
"properties": {
|
||||
"players": {
|
||||
"type": "array",
|
||||
"maxItems": 50,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["playerId", "playerName", "online"],
|
||||
"properties": {
|
||||
"playerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
||||
"playerName": { "type": "string", "minLength": 1, "maxLength": 80 },
|
||||
"platformUserId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
||||
"squadId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
||||
"online": { "type": "boolean" },
|
||||
"lastSeenAt": { "type": "string", "minLength": 1, "maxLength": 64, "format": "date-time" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncated": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMEventsParameters",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"eventId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"userProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
|
||||
}
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMEventsResult",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["rows"],
|
||||
"properties": {
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"maxItems": 500,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["eventRecordId", "eventId", "roundId", "userProfileId", "startTime", "endTime", "state", "score", "enemyKills", "teamKills", "deaths", "assists", "headshots"],
|
||||
"properties": {
|
||||
"eventRecordId": { "type": "string", "minLength": 1, "maxLength": 192 },
|
||||
"eventId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"roundId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"userProfileId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"startTime": { "type": ["string", "null"], "maxLength": 120 },
|
||||
"endTime": { "type": ["string", "null"], "maxLength": 120 },
|
||||
"state": { "enum": ["active", "finished"] },
|
||||
"score": { "type": ["number", "null"] },
|
||||
"enemyKills": { "type": ["integer", "null"] },
|
||||
"teamKills": { "type": ["integer", "null"] },
|
||||
"deaths": { "type": ["integer", "null"] },
|
||||
"assists": { "type": ["integer", "null"] },
|
||||
"headshots": { "type": ["integer", "null"] }
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncated": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMFlagsParameters",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"flagId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"ownerProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"squadId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
|
||||
}
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMFlagsResult",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["rows"],
|
||||
"properties": {
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"maxItems": 500,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["flagId", "entityId", "baseId", "ownerProfileId", "ownerPlayerId", "ownerSquadId", "ownerSquadName", "overtakerProfileId", "overtakeEndTime", "ownershipConfidence", "x", "y", "z"],
|
||||
"properties": {
|
||||
"flagId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"entityId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"baseId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"ownerProfileId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"ownerPlayerId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"ownerSquadId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"ownerSquadName": { "type": ["string", "null"], "minLength": 1, "maxLength": 80 },
|
||||
"overtakerProfileId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"overtakeEndTime": { "type": ["string", "null"], "format": "date-time" },
|
||||
"ownershipConfidence": { "enum": ["direct", "member", "squad", "unknown"] },
|
||||
"x": { "type": ["number", "null"] },
|
||||
"y": { "type": ["number", "null"] },
|
||||
"z": { "type": ["number", "null"] }
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncated": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMNativeTimedGiftsParameters",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"userProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
|
||||
}
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMNativeTimedGiftsResult",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["rows"],
|
||||
"properties": {
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"maxItems": 500,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["timedGiftId", "userProfileId", "mapId", "spawnTime", "spawnAt"],
|
||||
"properties": {
|
||||
"timedGiftId": { "type": "string", "minLength": 1, "maxLength": 192 },
|
||||
"userProfileId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"mapId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"spawnTime": { "type": ["integer", "null"] },
|
||||
"spawnAt": { "type": ["string", "null"], "format": "date-time" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncated": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMPlayerProfileParameters",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"gamePlayerId": { "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}$" },
|
||||
"search": { "type": "string", "minLength": 1, "maxLength": 80 },
|
||||
"activeWithinSeconds": { "type": "integer", "minimum": 1, "maximum": 86400 },
|
||||
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
|
||||
}
|
||||
}
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMPlayerProfileResult",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["rows"],
|
||||
"properties": {
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"maxItems": 500,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["userProfileId", "steamId", "gamePlayerId", "displayName", "lastLoginIp", "registeredAt", "squadId", "squadName", "famePoints", "moneyBalance", "normalBalance", "goldBalance", "x", "y", "z", "lastLoginTime", "lastLogoutTime", "lastSaveTime"],
|
||||
"properties": {
|
||||
"gamePlayerId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"userProfileId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"steamId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"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 },
|
||||
"squadName": { "type": ["string", "null"], "minLength": 1, "maxLength": 80 },
|
||||
"famePoints": { "type": ["number", "null"] },
|
||||
"moneyBalance": { "type": ["number", "null"] },
|
||||
"normalBalance": { "type": ["number", "null"] },
|
||||
"goldBalance": { "type": ["number", "null"] },
|
||||
"x": { "type": ["number", "null"] },
|
||||
"y": { "type": ["number", "null"] },
|
||||
"z": { "type": ["number", "null"] },
|
||||
"lastLoginTime": { "type": ["string", "null"], "maxLength": 120 },
|
||||
"lastLogoutTime": { "type": ["string", "null"], "maxLength": 120 },
|
||||
"lastSaveTime": { "type": ["string", "null"], "format": "date-time" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncated": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMPositionsParameters",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"subjectType": { "enum": ["player", "vehicle", "base", "flag"] },
|
||||
"subjectId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"activeWithinSeconds": { "type": "integer", "minimum": 1, "maximum": 86400 },
|
||||
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMPositionsResult",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["rows"],
|
||||
"properties": {
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"maxItems": 500,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["subjectType", "subjectId", "userProfileId", "gamePlayerId", "vehicleId", "entityId", "baseId", "x", "y", "z", "observedAt"],
|
||||
"properties": {
|
||||
"subjectType": { "enum": ["player", "vehicle", "base", "flag"] },
|
||||
"subjectId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"userProfileId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"gamePlayerId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"vehicleId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"entityId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"baseId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"x": { "type": ["number", "null"] },
|
||||
"y": { "type": ["number", "null"] },
|
||||
"z": { "type": ["number", "null"] },
|
||||
"observedAt": { "type": ["string", "null"], "format": "date-time" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncated": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMSquadMembersParameters",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"squadId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"userProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
|
||||
}
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMSquadMembersResult",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["rows"],
|
||||
"properties": {
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"maxItems": 500,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["squadId", "userProfileId", "gamePlayerId", "steamId", "displayName", "rank", "isLeader"],
|
||||
"properties": {
|
||||
"squadId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"userProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"gamePlayerId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"steamId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"displayName": { "type": "string", "minLength": 1, "maxLength": 80 },
|
||||
"rank": { "type": ["string", "null"], "minLength": 1, "maxLength": 32 },
|
||||
"isLeader": { "type": "integer", "minimum": 0, "maximum": 1 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncated": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMSquadsParameters",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"squadId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"search": { "type": "string", "minLength": 1, "maxLength": 80 },
|
||||
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
|
||||
}
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMSquadsResult",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["rows"],
|
||||
"properties": {
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"maxItems": 500,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["squadId", "name", "leaderProfileId", "leaderPlayerId", "memberCount", "score", "memberLimit", "message", "info", "lastMemberLoginTime"],
|
||||
"properties": {
|
||||
"squadId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"name": { "type": "string", "minLength": 1, "maxLength": 80 },
|
||||
"leaderProfileId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"leaderPlayerId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"memberCount": { "type": "integer", "minimum": 0, "maximum": 1000 },
|
||||
"score": { "type": ["number", "null"] },
|
||||
"memberLimit": { "type": ["integer", "null"], "minimum": 0 },
|
||||
"message": { "type": ["string", "null"], "maxLength": 4096 },
|
||||
"info": { "type": ["string", "null"], "maxLength": 4096 },
|
||||
"lastMemberLoginTime": { "type": ["string", "null"], "maxLength": 120 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncated": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMTasksParameters",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"userProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
|
||||
}
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMTasksResult",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["rows"],
|
||||
"properties": {
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"maxItems": 500,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["taskRecordId", "taskKind", "userProfileId", "mapId", "trackingDataSetId", "dataAssetPath", "sequenceIndex", "isTracked", "state", "completionDeadline"],
|
||||
"properties": {
|
||||
"taskRecordId": { "type": "string", "minLength": 1, "maxLength": 160 },
|
||||
"taskKind": { "enum": ["active-quest", "active-task", "available-task"] },
|
||||
"userProfileId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"mapId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"trackingDataSetId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96 },
|
||||
"dataAssetPath": { "type": "string", "minLength": 1, "maxLength": 512 },
|
||||
"sequenceIndex": { "type": ["integer", "null"] },
|
||||
"isTracked": { "type": "integer", "minimum": 0, "maximum": 1 },
|
||||
"state": { "enum": ["active", "available", "completed-before"] },
|
||||
"completionDeadline": { "type": ["number", "null"] }
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncated": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMVehiclesParameters",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"vehicleId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"ownerProfileId": { "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 }
|
||||
}
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMVehiclesResult",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["rows"],
|
||||
"properties": {
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"maxItems": 500,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["vehicleId", "entityId", "className", "label", "x", "y", "z", "lastAccessTime", "isFunctional", "existsInGame", "mountedPrisonerIds", "mountedUserProfileIds", "mountedSteamIds"],
|
||||
"properties": {
|
||||
"vehicleId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"entityId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"className": { "type": "string", "minLength": 1, "maxLength": 120 },
|
||||
"label": { "type": "string", "maxLength": 120 },
|
||||
"lastAccessTime": { "type": ["string", "null"], "format": "date-time" },
|
||||
"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" },
|
||||
"y": { "type": "number" },
|
||||
"z": { "type": "number" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncated": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMSquadMembersParameters",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["squadId", "limit"],
|
||||
"properties": {
|
||||
"squadId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
||||
"limit": { "type": "integer", "minimum": 1, "maximum": 64 }
|
||||
}
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMSquadMembersResult",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["members"],
|
||||
"properties": {
|
||||
"members": {
|
||||
"type": "array",
|
||||
"maxItems": 64,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["squadId", "playerId", "playerName", "role"],
|
||||
"properties": {
|
||||
"squadId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
||||
"playerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
||||
"playerName": { "type": "string", "minLength": 1, "maxLength": 80 },
|
||||
"role": { "type": "string", "minLength": 1, "maxLength": 16, "enum": ["leader", "member", "unknown"] },
|
||||
"joinedAt": { "type": "string", "minLength": 1, "maxLength": 64, "format": "date-time" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncated": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMVehicleOwnerParameters",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["vehicleId"],
|
||||
"properties": {
|
||||
"vehicleId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" }
|
||||
}
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMVehicleOwnerResult",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["ownership"],
|
||||
"properties": {
|
||||
"ownership": {
|
||||
"type": "array",
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["vehicleId", "vehicleType", "status"],
|
||||
"properties": {
|
||||
"vehicleId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
||||
"vehicleType": { "type": "string", "minLength": 1, "maxLength": 80 },
|
||||
"ownerPlayerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
||||
"ownerPlayerName": { "type": "string", "minLength": 1, "maxLength": 80 },
|
||||
"squadId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
|
||||
"status": { "type": "string", "minLength": 1, "maxLength": 16, "enum": ["owned", "unowned", "unknown"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
SELECT
|
||||
CAST(round.event_id AS TEXT) || ':' || CAST(round.id AS TEXT) || ':' || COALESCE(CAST(stats.user_profile_id AS TEXT), 'summary') AS eventRecordId,
|
||||
CAST(round.event_id AS TEXT) AS eventId,
|
||||
CAST(round.id AS TEXT) AS roundId,
|
||||
CAST(stats.user_profile_id AS TEXT) AS userProfileId,
|
||||
round.start_time AS startTime,
|
||||
round.end_time AS endTime,
|
||||
CASE WHEN round.end_time IS NULL OR round.end_time = '' THEN 'active' ELSE 'finished' END AS state,
|
||||
stats.score AS score,
|
||||
stats.enemy_kills AS enemyKills,
|
||||
stats.team_kills AS teamKills,
|
||||
stats.deaths AS deaths,
|
||||
stats.assists AS assists,
|
||||
stats.headshots AS headshots
|
||||
FROM event_round round
|
||||
LEFT JOIN event_round_stats stats ON stats.round_id = round.id
|
||||
WHERE (:eventId IS NULL OR CAST(round.event_id AS TEXT) = :eventId)
|
||||
AND (:userProfileId IS NULL OR CAST(stats.user_profile_id AS TEXT) = :userProfileId)
|
||||
ORDER BY round.id DESC, stats.score DESC
|
||||
LIMIT COALESCE(:limit, 500)
|
||||
@@ -1,23 +0,0 @@
|
||||
SELECT
|
||||
CAST(flag.element_id AS TEXT) AS flagId,
|
||||
CAST(flag.element_id AS TEXT) AS entityId,
|
||||
CAST(element.base_id AS TEXT) AS baseId,
|
||||
CAST(element.owner_profile_id AS TEXT) AS ownerProfileId,
|
||||
CAST(owner.prisoner_id AS TEXT) AS ownerPlayerId,
|
||||
CAST(owner_member.squad_id AS TEXT) AS ownerSquadId,
|
||||
owner_squad.name AS ownerSquadName,
|
||||
CAST(flag.overtaker_user_profile_id AS TEXT) AS overtakerProfileId,
|
||||
strftime('%Y-%m-%dT%H:%M:%SZ', flag.overtake_end_time, 'unixepoch') AS overtakeEndTime,
|
||||
CASE WHEN element.owner_profile_id IS NULL THEN 'unknown' ELSE 'direct' END AS ownershipConfidence,
|
||||
element.location_x AS x,
|
||||
element.location_y AS y,
|
||||
element.location_z AS z
|
||||
FROM base_element_flag flag
|
||||
JOIN base_element element ON element.element_id = flag.element_id
|
||||
LEFT JOIN user_profile owner ON owner.id = element.owner_profile_id
|
||||
LEFT JOIN squad_member owner_member ON owner_member.user_profile_id = element.owner_profile_id
|
||||
LEFT JOIN squad owner_squad ON owner_squad.id = owner_member.squad_id
|
||||
WHERE (:flagId IS NULL OR CAST(flag.element_id AS TEXT) = :flagId)
|
||||
AND (:ownerProfileId IS NULL OR CAST(element.owner_profile_id AS TEXT) = :ownerProfileId)
|
||||
ORDER BY flag.element_id
|
||||
LIMIT COALESCE(:limit, 500)
|
||||
@@ -1,51 +0,0 @@
|
||||
SELECT
|
||||
'player' AS subjectType,
|
||||
account.id AS subjectId,
|
||||
CAST(profile.id AS TEXT) AS userProfileId,
|
||||
CAST(prisoner.id AS TEXT) AS gamePlayerId,
|
||||
CAST(mount.vehicle_entity_id AS TEXT) AS vehicleId,
|
||||
CAST(entity.id AS TEXT) AS entityId,
|
||||
NULL AS baseId,
|
||||
entity.location_x AS x,
|
||||
entity.location_y AS y,
|
||||
entity.location_z AS z,
|
||||
strftime('%Y-%m-%dT%H:%M:%SZ', prisoner.last_save_time, 'unixepoch') AS observedAt
|
||||
FROM user_profile profile
|
||||
JOIN user account ON account.id = profile.user_id
|
||||
JOIN prisoner ON prisoner.id = profile.prisoner_id
|
||||
JOIN prisoner_entity ON prisoner_entity.prisoner_id = prisoner.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')
|
||||
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
|
||||
SELECT
|
||||
'vehicle', CAST(spawner.vehicle_entity_id AS TEXT), NULL, NULL,
|
||||
CAST(spawner.vehicle_entity_id AS TEXT), CAST(entity.id AS TEXT), NULL,
|
||||
entity.location_x, entity.location_y, entity.location_z,
|
||||
strftime('%Y-%m-%dT%H:%M:%SZ', spawner.vehicle_last_access_time, 'unixepoch')
|
||||
FROM vehicle_spawner spawner
|
||||
JOIN entity ON entity.id = spawner.vehicle_entity_id
|
||||
WHERE (:subjectType IS NULL OR :subjectType = 'vehicle')
|
||||
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
|
||||
SELECT
|
||||
'base', CAST(base.id AS TEXT), CAST(base.owner_user_profile_id AS TEXT), NULL,
|
||||
NULL, NULL, CAST(base.id AS TEXT), base.location_x, base.location_y, 0, NULL
|
||||
FROM base
|
||||
WHERE (:subjectType IS NULL OR :subjectType = 'base')
|
||||
AND (:subjectId IS NULL OR CAST(base.id AS TEXT) = :subjectId)
|
||||
UNION ALL
|
||||
SELECT
|
||||
'flag', CAST(flag.element_id AS TEXT), CAST(element.owner_profile_id AS TEXT), CAST(owner.prisoner_id AS TEXT),
|
||||
NULL, CAST(flag.element_id AS TEXT), CAST(element.base_id AS TEXT),
|
||||
element.location_x, element.location_y, element.location_z,
|
||||
strftime('%Y-%m-%dT%H:%M:%SZ', flag.overtake_end_time, 'unixepoch')
|
||||
FROM base_element_flag flag
|
||||
JOIN base_element element ON element.element_id = flag.element_id
|
||||
LEFT JOIN user_profile owner ON owner.id = element.owner_profile_id
|
||||
WHERE (:subjectType IS NULL OR :subjectType = 'flag')
|
||||
AND (:subjectId IS NULL OR CAST(flag.element_id AS TEXT) = :subjectId)
|
||||
LIMIT COALESCE(:limit, 500)
|
||||
@@ -1,10 +0,0 @@
|
||||
SELECT
|
||||
'native:' || CAST(gift.rowid AS TEXT) || ':' || COALESCE(CAST(gift.user_profile_id AS TEXT), 'unknown') || ':' || COALESCE(CAST(gift.map_id AS TEXT), 'unknown') || ':' || COALESCE(CAST(gift.spawn_time AS TEXT), 'unknown') AS timedGiftId,
|
||||
CAST(gift.user_profile_id AS TEXT) AS userProfileId,
|
||||
CAST(gift.map_id AS TEXT) AS mapId,
|
||||
gift.spawn_time AS spawnTime,
|
||||
strftime('%Y-%m-%dT%H:%M:%SZ', gift.spawn_time, 'unixepoch') AS spawnAt
|
||||
FROM finished_timed_gift_spawner gift
|
||||
WHERE (:userProfileId IS NULL OR CAST(gift.user_profile_id AS TEXT) = :userProfileId)
|
||||
ORDER BY gift.spawn_time DESC
|
||||
LIMIT COALESCE(:limit, 500)
|
||||
@@ -1,15 +0,0 @@
|
||||
SELECT
|
||||
CAST(member.squad_id AS TEXT) AS squadId,
|
||||
CAST(member.user_profile_id AS TEXT) AS userProfileId,
|
||||
CAST(profile.prisoner_id AS TEXT) AS gamePlayerId,
|
||||
account.id AS steamId,
|
||||
COALESCE(profile.name, account.name, '') AS displayName,
|
||||
CAST(member.rank AS TEXT) AS rank,
|
||||
CASE WHEN member.rank = 4 THEN 1 ELSE 0 END AS isLeader
|
||||
FROM squad_member member
|
||||
JOIN user_profile profile ON profile.id = member.user_profile_id
|
||||
JOIN user account ON account.id = profile.user_id
|
||||
WHERE (:squadId IS NULL OR CAST(member.squad_id AS TEXT) = :squadId)
|
||||
AND (:userProfileId IS NULL OR CAST(member.user_profile_id AS TEXT) = :userProfileId)
|
||||
ORDER BY member.squad_id, member.rank DESC, profile.name
|
||||
LIMIT COALESCE(:limit, 500)
|
||||
@@ -1,19 +0,0 @@
|
||||
SELECT
|
||||
CAST(squad.id AS TEXT) AS squadId,
|
||||
COALESCE(squad.name, '') AS name,
|
||||
MAX(CASE WHEN member.rank = 4 THEN CAST(member.user_profile_id AS TEXT) END) AS leaderProfileId,
|
||||
MAX(CASE WHEN member.rank = 4 THEN CAST(member_profile.prisoner_id AS TEXT) END) AS leaderPlayerId,
|
||||
COUNT(member.id) AS memberCount,
|
||||
squad.score AS score,
|
||||
squad.member_limit AS memberLimit,
|
||||
squad.message AS message,
|
||||
squad.information AS info,
|
||||
squad.last_member_login_time AS lastMemberLoginTime
|
||||
FROM squad
|
||||
LEFT JOIN squad_member member ON member.squad_id = squad.id
|
||||
LEFT JOIN user_profile member_profile ON member_profile.id = member.user_profile_id
|
||||
WHERE (:squadId IS NULL OR CAST(squad.id AS TEXT) = :squadId)
|
||||
AND (:search IS NULL OR COALESCE(squad.name, '') LIKE '%' || :search || '%')
|
||||
GROUP BY squad.id
|
||||
ORDER BY squad.score DESC, squad.id
|
||||
LIMIT COALESCE(:limit, 500)
|
||||
@@ -1,32 +0,0 @@
|
||||
SELECT
|
||||
'active-quest:' || CAST(quest.id AS TEXT) AS taskRecordId,
|
||||
'active-quest' AS taskKind,
|
||||
CAST(quest.user_profile_id AS TEXT) AS userProfileId,
|
||||
CAST(quest.map_id AS TEXT) AS mapId,
|
||||
CAST(quest.id AS TEXT) AS trackingDataSetId,
|
||||
quest.quest_data_asset_path AS dataAssetPath,
|
||||
tracking.sequence_index AS sequenceIndex,
|
||||
CASE WHEN EXISTS (SELECT 1 FROM tracked_quest tracked WHERE tracked.quest_id = quest.id) THEN 1 ELSE 0 END AS isTracked,
|
||||
'active' AS state,
|
||||
quest.completion_deadline AS completionDeadline
|
||||
FROM active_quest quest
|
||||
JOIN tracking_data_set tracking ON tracking.id = quest.id
|
||||
WHERE (:userProfileId IS NULL OR CAST(quest.user_profile_id AS TEXT) = :userProfileId)
|
||||
UNION ALL
|
||||
SELECT
|
||||
'active-task:' || CAST(task.id AS TEXT), 'active-task', CAST(task.user_profile_id AS TEXT), CAST(task.map_id AS TEXT),
|
||||
CAST(task.id AS TEXT), available.task_data_asset_path, tracking.sequence_index,
|
||||
CASE WHEN EXISTS (SELECT 1 FROM tracked_quest tracked WHERE tracked.quest_id = task.id) THEN 1 ELSE 0 END,
|
||||
'active', NULL
|
||||
FROM active_task task
|
||||
JOIN tracking_data_set tracking ON tracking.id = task.id
|
||||
JOIN available_task available ON available.id = task.available_task_id
|
||||
WHERE (:userProfileId IS NULL OR CAST(task.user_profile_id AS TEXT) = :userProfileId)
|
||||
UNION ALL
|
||||
SELECT
|
||||
'available-task:' || CAST(available.id AS TEXT), 'available-task', CAST(available.user_profile_id AS TEXT), CAST(available.map_id AS TEXT),
|
||||
NULL, available.task_data_asset_path, NULL, 0,
|
||||
CASE WHEN available.was_ever_completed = 1 THEN 'completed-before' ELSE 'available' END, NULL
|
||||
FROM available_task available
|
||||
WHERE (:userProfileId IS NULL OR CAST(available.user_profile_id AS TEXT) = :userProfileId)
|
||||
LIMIT COALESCE(:limit, 500)
|
||||
@@ -1,35 +0,0 @@
|
||||
SELECT
|
||||
CAST(profile.id AS TEXT) AS userProfileId,
|
||||
account.id AS steamId,
|
||||
CAST(prisoner.id AS TEXT) AS gamePlayerId,
|
||||
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,
|
||||
squad.name AS squadName,
|
||||
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 = 2 THEN currency.account_balance END) AS goldBalance,
|
||||
entity.location_x AS x,
|
||||
entity.location_y AS y,
|
||||
entity.location_z AS z,
|
||||
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
|
||||
FROM user account
|
||||
LEFT JOIN user_profile profile ON profile.user_id = account.id
|
||||
LEFT JOIN prisoner ON prisoner.id = profile.prisoner_id
|
||||
LEFT JOIN prisoner_entity ON prisoner_entity.prisoner_id = prisoner.id
|
||||
LEFT JOIN entity ON entity.id = prisoner_entity.entity_id
|
||||
LEFT JOIN squad_member member ON member.user_profile_id = profile.id
|
||||
LEFT JOIN squad ON squad.id = member.squad_id
|
||||
LEFT JOIN bank_account_registry bank ON bank.account_owner_user_profile_id = profile.id
|
||||
LEFT JOIN bank_account_registry_currencies currency ON currency.bank_account_id = bank.id
|
||||
WHERE (:userProfileId IS NULL OR CAST(profile.id AS TEXT) = :userProfileId)
|
||||
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 (: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
|
||||
ORDER BY profile.last_login_time DESC
|
||||
LIMIT COALESCE(:limit, 500)
|
||||
@@ -1,25 +0,0 @@
|
||||
SELECT
|
||||
CAST(spawner.vehicle_entity_id AS TEXT) AS vehicleId,
|
||||
CAST(spawner.vehicle_entity_id AS TEXT) AS entityId,
|
||||
entity.class AS className,
|
||||
spawner.vehicle_alias AS label,
|
||||
entity.location_x AS x,
|
||||
entity.location_y AS y,
|
||||
entity.location_z AS z,
|
||||
strftime('%Y-%m-%dT%H:%M:%SZ', spawner.vehicle_last_access_time, 'unixepoch') AS lastAccessTime,
|
||||
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
|
||||
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)
|
||||
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
|
||||
LIMIT COALESCE(:limit, 500)
|
||||
Reference in New Issue
Block a user