Revert SCUM real data management change

This commit is contained in:
npc0-hue
2026-08-13 15:33:34 +08:00
parent d831e4ade9
commit b07a792784
163 changed files with 5443 additions and 9174 deletions
@@ -1,20 +0,0 @@
{
"key": "scum-login-log-parser-v1",
"parserVersion": "scum-login-log-v1",
"lineEncoding": "utf-16le",
"maxLineBytes": 4096,
"acceptedTemplate": "{timestamp}: '{network} {external_player_id}:{display_name}({profile_local_id})' logged {in|out} at: X={coordinate} Y={coordinate} Z={coordinate}",
"eventTypes": ["scum.login", "scum.logout"],
"transportCursor": ["sourceIdentity", "streamGeneration", "sequence"],
"logicalEventIdentity": {
"algorithm": "sha256",
"fields": ["parserVersion", "serverId", "eventType", "occurredAtSourceText", "externalPlayerId", "displayName", "profileLocalId"],
"excludedFields": ["network", "x", "y", "z", "sourceIdentity", "streamGeneration", "sequence"]
},
"privacy": {
"stripNetworkIdentifiers": true,
"stripCoordinatesFromIdentity": true,
"rawLineStorage": "forbidden"
},
"diagnostics": ["invalid-transport-cursor", "out-of-order-transport", "duplicate-transport-cursor", "partial-line", "oversized-line", "undecodable-line", "failed-login", "malformed-line", "duplicate-logical-event"]
}
@@ -1,46 +0,0 @@
{
"key": "scum-current-service-coordinate-map",
"mapVersion": "scum-current-service-coordinate-map-v1",
"adapterVersion": "scum-live-data-v1",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"sourceEvidence": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
"authorization": {
"redistribution": "first-party-generated-coordinate-metadata",
"baseMapArtwork": "not-packaged",
"renderingAvailability": "unavailable-until-authorized-base-map"
},
"worldBounds": {
"minX": -901009.4375,
"minY": -883992.5625,
"maxX": 612580.0625,
"maxY": 615977.375
},
"image": {
"width": 4096,
"height": 4096
},
"layers": [
{
"key": "players",
"capability": "positions.read",
"subjectType": "player",
"label": "Players",
"sourceQueryKey": "scum-positions-read"
},
{
"key": "vehicles",
"capability": "positions.read",
"subjectType": "vehicle",
"label": "Vehicles",
"sourceQueryKey": "scum-positions-read"
},
{
"key": "flags",
"capability": "positions.read",
"subjectType": "flag",
"label": "Flags",
"sourceQueryKey": "scum-positions-read"
}
],
"transformAssetPath": "assets/scum-live/map/current-service-transform.json"
}
@@ -1,49 +0,0 @@
{
"key": "scum-current-service-coordinate-transform",
"transformVersion": "scum-current-service-transform-v1",
"mapAssetKey": "scum-current-service-coordinate-map",
"adapterVersion": "scum-live-data-v1",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"coordinateSystem": "current-service-observed-entity-envelope",
"sourceEvidence": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
"worldBounds": {
"minX": -901009.4375,
"minY": -883992.5625,
"maxX": 612580.0625,
"maxY": 615977.375
},
"image": {
"width": 4096,
"height": 4096
},
"axisMapping": {
"x": "left-to-right",
"y": "bottom-to-top-inverted-for-image-y"
},
"formula": {
"pixelX": "((x - minX) / (maxX - minX)) * (width - 1)",
"pixelY": "((maxY - y) / (maxY - minY)) * (height - 1)"
},
"validation": {
"rejectNonFinite": true,
"rejectOutOfBounds": true,
"acceptBoundaryPoints": true
},
"fixtures": [
{
"name": "observed-minimum-corner",
"world": { "x": -901009.4375, "y": -883992.5625 },
"pixel": { "x": 0, "y": 4095 }
},
{
"name": "observed-maximum-corner",
"world": { "x": 612580.0625, "y": 615977.375 },
"pixel": { "x": 4095, "y": 0 }
},
{
"name": "observed-center",
"world": { "x": -144214.6875, "y": -134007.59375 },
"pixel": { "x": 2047.5, "y": 2047.5 }
}
]
}
@@ -1,29 +0,0 @@
{
"key": "scum-flags-read-v1",
"adapterVersion": "scum-live-data-v1",
"queryVersion": "scum-sqlite-query-v1",
"capability": "flags.read",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"statementType": "single-select-or-cte",
"statement": "SELECT bef.element_id AS flagElementId, be.element_id AS baseElementId, b.id AS baseId, b.owner_profile_id AS ownerProfileId, up.user_id AS ownerExternalPlayerId, up.name AS ownerDisplayName, CAST(NULL AS INTEGER) AS ownerSquadId, CAST(NULL AS TEXT) AS ownerSquadName, be.location_x AS flagX, be.location_y AS flagY, be.location_z AS flagZ, b.location_x AS territoryX, b.location_y AS territoryY, CAST(NULL AS REAL) AS territoryZ FROM base_element_flag bef INNER JOIN base_element be ON be.element_id = bef.element_id INNER JOIN base b ON b.id = be.element_id LEFT JOIN user_profile up ON up.id = b.owner_profile_id ORDER BY bef.element_id ASC LIMIT :limit OFFSET :offset",
"parameters": [
{ "name": "limit", "binding": ":limit", "type": "integer", "required": true, "minimum": 1, "maximum": 200 },
{ "name": "offset", "binding": ":offset", "type": "integer", "required": true, "minimum": 0, "maximum": 1000000 }
],
"safety": {
"queryOnly": true,
"readOnlyConnection": true,
"forbidMultipleStatements": true,
"forbidAttach": true,
"forbidWritePragmas": true,
"forbidExtensionLoading": true,
"maxRows": 200,
"timeoutMs": 2000,
"maxResultBytes": 65536
},
"evidence": {
"source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
"provenJoins": ["base_element_flag.element_id -> base_element.element_id", "base_element.element_id -> base.id", "base.owner_profile_id -> user_profile.id"],
"ambiguousFieldsKeptNull": ["ownerSquadId", "ownerSquadName", "territoryZ"]
}
}
@@ -1,27 +0,0 @@
{
"key": "scum-player-details-read-v1",
"adapterVersion": "scum-live-data-v1",
"queryVersion": "scum-sqlite-query-v1",
"capability": "player-details.read",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"statementType": "single-select-or-cte",
"statement": "SELECT u.id AS externalPlayerId, u.id_type AS idType, u.provider AS provider, u.creation_time AS userCreationTime, u.last_login_time AS userLastLoginTime, u.is_banned AS isBanned, up.id AS profileId, up.name AS displayName, up.type AS profileType, p.id AS prisonerId, p.user_profile_id AS prisonerUserProfileId, p.last_save_time AS prisonerLastSaveTime, pe.entity_id AS entityId, CASE WHEN up.template_xml IS NULL THEN NULL ELSE length(up.template_xml) END AS profileXmlLength FROM \"user\" u LEFT JOIN user_profile up ON up.user_id = u.id AND up.type = 1 LEFT JOIN prisoner p ON p.id = up.prisoner_id AND p.user_profile_id = up.id LEFT JOIN prisoner_entity pe ON pe.prisoner_id = p.id WHERE u.id = :externalPlayerId LIMIT 1",
"parameters": [
{ "name": "externalPlayerId", "binding": ":externalPlayerId", "type": "string", "required": true, "minLength": 1, "maxLength": 96 }
],
"safety": {
"queryOnly": true,
"readOnlyConnection": true,
"forbidMultipleStatements": true,
"forbidAttach": true,
"forbidWritePragmas": true,
"forbidExtensionLoading": true,
"maxRows": 1,
"timeoutMs": 2000,
"maxResultBytes": 32768
},
"evidence": {
"source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
"provenFields": ["user.id", "user.id_type", "user.provider", "user.creation_time", "user.last_login_time", "user.is_banned", "user_profile.id", "user_profile.name", "user_profile.type", "user_profile.template_xml", "prisoner.id", "prisoner.user_profile_id", "prisoner.last_save_time", "prisoner_entity.entity_id"]
}
}
@@ -1,29 +0,0 @@
{
"key": "scum-player-economy-read-v1",
"adapterVersion": "scum-live-data-v1",
"queryVersion": "scum-sqlite-query-v1",
"capability": "player-details.read",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"statementType": "single-select-or-cte",
"statement": "SELECT u.id AS externalPlayerId, up.id AS profileId, bar.id AS bankAccountId, bac.id AS currencyRowId, bac.currency_type AS currencyType, bac.account_balance AS accountBalance FROM \"user\" u JOIN user_profile up ON up.user_id = u.id AND up.type = 1 JOIN bank_account_registry bar ON bar.account_owner_user_profile_id = up.id JOIN bank_account_registry_currencies bac ON bac.bank_account_id = bar.id WHERE u.id = :externalPlayerId ORDER BY bac.currency_type ASC, bac.id ASC LIMIT :limit",
"parameters": [
{ "name": "externalPlayerId", "binding": ":externalPlayerId", "type": "string", "required": true, "minLength": 1, "maxLength": 96 },
{ "name": "limit", "binding": ":limit", "type": "integer", "required": true, "minimum": 1, "maximum": 16 }
],
"safety": {
"queryOnly": true,
"readOnlyConnection": true,
"forbidMultipleStatements": true,
"forbidAttach": true,
"forbidWritePragmas": true,
"forbidExtensionLoading": true,
"maxRows": 16,
"timeoutMs": 2000,
"maxResultBytes": 32768
},
"evidence": {
"source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
"provenJoins": ["bank_account_registry.account_owner_user_profile_id -> user_profile.id", "bank_account_registry_currencies.bank_account_id -> bank_account_registry.id"],
"ambiguity": "Currency labels, units, command execution, and readback confirmation are unverified; currencyType remains numeric and version-scoped."
}
}
@@ -1,27 +0,0 @@
{
"key": "scum-player-session-enrichment-read-v1",
"adapterVersion": "scum-live-data-v1",
"queryVersion": "scum-sqlite-query-v1",
"capability": "players.read",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"statementType": "single-select-or-cte",
"statement": "SELECT u.id AS externalPlayerId, u.id_type AS idType, u.provider AS provider, u.creation_time AS userCreationTime, u.last_login_time AS userLastLoginTime, u.is_banned AS isBanned, up.id AS profileId, up.name AS displayName, up.type AS profileType, p.id AS prisonerId FROM \"user\" u LEFT JOIN user_profile up ON up.user_id = u.id AND up.type = 1 LEFT JOIN prisoner p ON p.id = up.prisoner_id AND p.user_profile_id = up.id WHERE u.id = :externalPlayerId LIMIT 1",
"parameters": [
{ "name": "externalPlayerId", "binding": ":externalPlayerId", "type": "string", "required": true, "minLength": 1, "maxLength": 96 }
],
"safety": {
"queryOnly": true,
"readOnlyConnection": true,
"forbidMultipleStatements": true,
"forbidAttach": true,
"forbidWritePragmas": true,
"forbidExtensionLoading": true,
"maxRows": 1,
"timeoutMs": 2000,
"maxResultBytes": 32768
},
"evidence": {
"source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
"sessionBoundary": "Online state and local sessions are created from authenticated login/logout events; this query only enriches a login-created identity with proven database facts."
}
}
@@ -1,28 +0,0 @@
{
"key": "scum-players-read-v1",
"adapterVersion": "scum-live-data-v1",
"queryVersion": "scum-sqlite-query-v1",
"capability": "players.read",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"statementType": "single-select-or-cte",
"statement": "WITH player_rows AS (SELECT u.id AS externalPlayerId, u.id_type AS idType, u.provider AS provider, u.creation_time AS userCreationTime, u.last_login_time AS userLastLoginTime, u.is_banned AS isBanned, up.id AS profileId, up.name AS displayName, up.type AS profileType, p.id AS prisonerId, pe.entity_id AS entityId FROM \"user\" u LEFT JOIN user_profile up ON up.user_id = u.id AND up.type = 1 LEFT JOIN prisoner p ON p.id = up.prisoner_id AND p.user_profile_id = up.id LEFT JOIN prisoner_entity pe ON pe.prisoner_id = p.id ORDER BY u.last_login_time DESC, u.id ASC LIMIT :limit OFFSET :offset) SELECT externalPlayerId, idType, provider, userCreationTime, userLastLoginTime, isBanned, profileId, displayName, profileType, prisonerId, entityId FROM player_rows",
"parameters": [
{ "name": "limit", "binding": ":limit", "type": "integer", "required": true, "minimum": 1, "maximum": 100 },
{ "name": "offset", "binding": ":offset", "type": "integer", "required": true, "minimum": 0, "maximum": 1000000 }
],
"safety": {
"queryOnly": true,
"readOnlyConnection": true,
"forbidMultipleStatements": true,
"forbidAttach": true,
"forbidWritePragmas": true,
"forbidExtensionLoading": true,
"maxRows": 100,
"timeoutMs": 2000,
"maxResultBytes": 65536
},
"evidence": {
"source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
"provenJoins": ["user_profile.user_id -> user.id", "user_profile.prisoner_id -> prisoner.id", "prisoner.user_profile_id -> user_profile.id", "prisoner_entity.prisoner_id -> prisoner.id"]
}
}
@@ -1,30 +0,0 @@
{
"key": "scum-positions-read-v1",
"adapterVersion": "scum-live-data-v1",
"queryVersion": "scum-sqlite-query-v1",
"capability": "positions.read",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"statementType": "single-select-or-cte",
"statement": "WITH position_rows AS (SELECT 'player' AS subjectType, u.id AS subjectId, u.id AS externalPlayerId, up.id AS profileId, p.id AS prisonerId, pe.entity_id AS entityId, CAST(NULL AS INTEGER) AS vehicleEntityId, CAST(NULL AS INTEGER) AS vehicleAssetId, CAST(NULL AS INTEGER) AS flagElementId, e.location_x AS x, e.location_y AS y, e.location_z AS z, p.last_save_time AS sourceTimeValue, 'prisoner.last_save_time' AS sourceTimeKind FROM \"user\" u INNER JOIN user_profile up ON up.user_id = u.id AND up.type = 1 INNER JOIN prisoner p ON p.id = up.prisoner_id AND p.user_profile_id = up.id INNER JOIN prisoner_entity pe ON pe.prisoner_id = p.id INNER JOIN entity e ON e.id = pe.entity_id WHERE :subjectType IN ('all', 'player') UNION ALL SELECT 'vehicle' AS subjectType, CAST(vs.vehicle_entity_id AS TEXT) AS subjectId, CAST(NULL AS TEXT) AS externalPlayerId, CAST(NULL AS INTEGER) AS profileId, CAST(NULL AS INTEGER) AS prisonerId, ve.entity_id AS entityId, vs.vehicle_entity_id AS vehicleEntityId, vs.vehicle_asset_id AS vehicleAssetId, CAST(NULL AS INTEGER) AS flagElementId, e.location_x AS x, e.location_y AS y, e.location_z AS z, CAST(NULL AS INTEGER) AS sourceTimeValue, CAST(NULL AS TEXT) AS sourceTimeKind FROM vehicle_spawner vs INNER JOIN vehicle_entity ve ON ve.entity_id = vs.vehicle_entity_id INNER JOIN entity e ON e.id = ve.entity_id WHERE :subjectType IN ('all', 'vehicle') UNION ALL SELECT 'flag' AS subjectType, CAST(bef.element_id AS TEXT) AS subjectId, CAST(NULL AS TEXT) AS externalPlayerId, CAST(NULL AS INTEGER) AS profileId, CAST(NULL AS INTEGER) AS prisonerId, CAST(NULL AS INTEGER) AS entityId, CAST(NULL AS INTEGER) AS vehicleEntityId, CAST(NULL AS INTEGER) AS vehicleAssetId, bef.element_id AS flagElementId, be.location_x AS x, be.location_y AS y, be.location_z AS z, CAST(NULL AS INTEGER) AS sourceTimeValue, CAST(NULL AS TEXT) AS sourceTimeKind FROM base_element_flag bef INNER JOIN base_element be ON be.element_id = bef.element_id INNER JOIN base b ON b.id = be.element_id WHERE :subjectType IN ('all', 'flag')) SELECT subjectType, subjectId, externalPlayerId, profileId, prisonerId, entityId, vehicleEntityId, vehicleAssetId, flagElementId, x, y, z, sourceTimeValue, sourceTimeKind FROM position_rows ORDER BY subjectType ASC, subjectId ASC LIMIT :limit OFFSET :offset",
"parameters": [
{ "name": "subjectType", "binding": ":subjectType", "type": "string", "required": true, "enum": ["all", "player", "vehicle", "flag"] },
{ "name": "limit", "binding": ":limit", "type": "integer", "required": true, "minimum": 1, "maximum": 500 },
{ "name": "offset", "binding": ":offset", "type": "integer", "required": true, "minimum": 0, "maximum": 1000000 }
],
"safety": {
"queryOnly": true,
"readOnlyConnection": true,
"forbidMultipleStatements": true,
"forbidAttach": true,
"forbidWritePragmas": true,
"forbidExtensionLoading": true,
"maxRows": 500,
"timeoutMs": 2000,
"maxResultBytes": 262144
},
"evidence": {
"source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
"provenJoins": ["user_profile.user_id -> user.id", "user_profile.prisoner_id -> prisoner.id", "prisoner_entity.prisoner_id -> prisoner.id", "vehicle_spawner.vehicle_entity_id -> vehicle_entity.entity_id", "vehicle_entity.entity_id -> entity.id", "base_element_flag.element_id -> base_element.element_id", "base_element.element_id -> base.id"],
"cadenceCaveat": "SCUM.db does not prove sub-10s realtime cadence; sourceTimeValue is nullable and only reflects fields proven by the adapter."
}
}
@@ -1,30 +0,0 @@
{
"key": "scum-squad-members-read-v1",
"adapterVersion": "scum-live-data-v1",
"queryVersion": "scum-sqlite-query-v1",
"capability": "squad-members.read",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"statementType": "single-select-or-cte",
"statement": "SELECT sm.squad_id AS squadId, sm.user_profile_id AS userProfileId, sm.rank AS rankCode, CAST(NULL AS TEXT) AS rankMeaning, CAST(NULL AS INTEGER) AS isLeader, up.user_id AS externalPlayerId, u.id_type AS idType, u.provider AS provider, up.name AS displayName, up.type AS profileType FROM squad_member sm INNER JOIN squad s ON s.id = sm.squad_id INNER JOIN user_profile up ON up.id = sm.user_profile_id LEFT JOIN \"user\" u ON u.id = up.user_id WHERE (:squadId IS NULL OR sm.squad_id = :squadId) ORDER BY sm.squad_id ASC, sm.user_profile_id ASC LIMIT :limit OFFSET :offset",
"parameters": [
{ "name": "squadId", "binding": ":squadId", "type": "integer", "required": true, "nullable": true, "minimum": 0, "maximum": 2147483647 },
{ "name": "limit", "binding": ":limit", "type": "integer", "required": true, "minimum": 1, "maximum": 500 },
{ "name": "offset", "binding": ":offset", "type": "integer", "required": true, "minimum": 0, "maximum": 1000000 }
],
"safety": {
"queryOnly": true,
"readOnlyConnection": true,
"forbidMultipleStatements": true,
"forbidAttach": true,
"forbidWritePragmas": true,
"forbidExtensionLoading": true,
"maxRows": 500,
"timeoutMs": 2000,
"maxResultBytes": 131072
},
"evidence": {
"source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
"provenJoins": ["squad_member.squad_id -> squad.id", "squad_member.user_profile_id -> user_profile.id", "user_profile.user_id -> user.id"],
"ambiguousFieldsKeptNull": ["rankMeaning", "isLeader"]
}
}
@@ -1,29 +0,0 @@
{
"key": "scum-squads-read-v1",
"adapterVersion": "scum-live-data-v1",
"queryVersion": "scum-sqlite-query-v1",
"capability": "squads.read",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"statementType": "single-select-or-cte",
"statement": "WITH member_counts AS (SELECT sm.squad_id AS squadId, COUNT(*) AS memberCount FROM squad_member sm GROUP BY sm.squad_id) SELECT s.id AS squadId, s.name AS squadName, COALESCE(mc.memberCount, 0) AS memberCount, CAST(NULL AS INTEGER) AS leaderProfileId, CAST(NULL AS TEXT) AS leaderExternalPlayerId, CAST(NULL AS INTEGER) AS leaderRankCode, CAST(NULL AS TEXT) AS leaderRankMeaning FROM squad s LEFT JOIN member_counts mc ON mc.squadId = s.id ORDER BY s.id ASC LIMIT :limit OFFSET :offset",
"parameters": [
{ "name": "limit", "binding": ":limit", "type": "integer", "required": true, "minimum": 1, "maximum": 100 },
{ "name": "offset", "binding": ":offset", "type": "integer", "required": true, "minimum": 0, "maximum": 1000000 }
],
"safety": {
"queryOnly": true,
"readOnlyConnection": true,
"forbidMultipleStatements": true,
"forbidAttach": true,
"forbidWritePragmas": true,
"forbidExtensionLoading": true,
"maxRows": 100,
"timeoutMs": 2000,
"maxResultBytes": 65536
},
"evidence": {
"source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
"provenFields": ["squad.id", "squad.name", "squad_member.squad_id"],
"ambiguousFieldsKeptNull": ["leaderProfileId", "leaderExternalPlayerId", "leaderRankCode", "leaderRankMeaning"]
}
}
@@ -1,29 +0,0 @@
{
"key": "scum-vehicles-read-v1",
"adapterVersion": "scum-live-data-v1",
"queryVersion": "scum-sqlite-query-v1",
"capability": "vehicles.read",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"statementType": "single-select-or-cte",
"statement": "SELECT vs.vehicle_entity_id AS vehicleEntityId, ve.entity_id AS entityId, vs.vehicle_asset_id AS vehicleAssetId, vs.is_vehicle_functional AS isVehicleFunctional, CAST(NULL AS TEXT) AS vehicleLabel, CAST(NULL AS INTEGER) AS ownerProfileId, CAST(NULL AS TEXT) AS ownerExternalPlayerId, CAST(NULL AS INTEGER) AS ownerSquadId, e.location_x AS x, e.location_y AS y, e.location_z AS z FROM vehicle_spawner vs INNER JOIN vehicle_entity ve ON ve.entity_id = vs.vehicle_entity_id INNER JOIN entity e ON e.id = ve.entity_id ORDER BY vs.vehicle_entity_id ASC LIMIT :limit OFFSET :offset",
"parameters": [
{ "name": "limit", "binding": ":limit", "type": "integer", "required": true, "minimum": 1, "maximum": 500 },
{ "name": "offset", "binding": ":offset", "type": "integer", "required": true, "minimum": 0, "maximum": 1000000 }
],
"safety": {
"queryOnly": true,
"readOnlyConnection": true,
"forbidMultipleStatements": true,
"forbidAttach": true,
"forbidWritePragmas": true,
"forbidExtensionLoading": true,
"maxRows": 500,
"timeoutMs": 2000,
"maxResultBytes": 262144
},
"evidence": {
"source": "openspec/changes/replace-scum-projections-with-real-data-management/evidence/scum-current-service-sqlite-diagnostic-2026-08-12.md",
"provenJoins": ["vehicle_spawner.vehicle_entity_id -> vehicle_entity.entity_id", "vehicle_entity.entity_id -> entity.id"],
"ambiguousFieldsKeptNull": ["vehicleLabel", "ownerProfileId", "ownerExternalPlayerId", "ownerSquadId"]
}
}
@@ -82,7 +82,7 @@ func e2eClaim(id, commandType string, payload map[string]any, stamp time.Time) C
}
func TestSupportedAdaptersDispatchThroughIsolatedTypedPorts(t *testing.T) {
stamp := time.Now().UTC()
stamp := time.Date(2026, time.July, 29, 12, 0, 0, 0, time.UTC)
port := &isolatedAdapterPort{
configFields: map[string]string{"ServerName": "Moonlight", "Password": "never-return", "hostPath": "C:/private/server.ini"},
notifyAccept: true,
@@ -130,7 +130,7 @@ func TestSupportedAdaptersDispatchThroughIsolatedTypedPorts(t *testing.T) {
}
func TestSupportedAdaptersFailClosedForBindingApprovalAndCapability(t *testing.T) {
stamp := time.Now().UTC()
stamp := time.Date(2026, time.July, 29, 12, 0, 0, 0, time.UTC)
for name, testCase := range map[string]struct {
availability HandlerAvailability
adapter RuntimeAdapter
@@ -156,7 +156,7 @@ func TestSupportedAdaptersFailClosedForBindingApprovalAndCapability(t *testing.T
}
func TestSupportedAdapterTransportFailuresCompleteWithoutProtectedOutput(t *testing.T) {
stamp := time.Now().UTC()
stamp := time.Date(2026, time.July, 29, 12, 0, 0, 0, time.UTC)
port := &isolatedAdapterPort{patchErr: errors.New("private port failed"), notifyErr: errors.New("private notification failed")}
adapter := RuntimeAdapter{BoundServerID: "server-1", Config: port, Notification: port}
gateway := &isolatedDispatchGateway{commands: []ClaimedCommand{
@@ -170,11 +170,11 @@ func TestSupportedAdapterTransportFailuresCompleteWithoutProtectedOutput(t *test
for id, results := range gateway.completed {
result := results[0]
if result.Payload["result"] != "failed" || strings.Contains(stringifyPayload(result.Payload), "private") || strings.Contains(result.Summary, "private") {
t.Fatalf("%s did not redact failed adapter output: %+v", id, result)
t.Fatalf("%s did not redact failed typed-port output: %+v", id, result)
}
}
if len(port.notifications) != 1 || port.notifications[0].protectedAuditCommand == "" {
t.Fatalf("notification fixture did not receive one protected request: %+v", port.notifications)
t.Fatalf("notification fixture did not receive one protected typed request: %+v", port.notifications)
}
}
@@ -239,7 +239,7 @@ func safeCommandField(key string, value any) bool {
}
func unsupportedResult(code string) CommandResult {
return CommandResult{Status: "failed", Summary: "operation unavailable", Payload: map[string]any{"result": code}}
return CommandResult{Status: "failed", Summary: "typed operation unavailable", Payload: map[string]any{"result": code}}
}
func redactTypedPayload(payload map[string]any) map[string]any {
result := map[string]any{}
@@ -1,232 +0,0 @@
package companion
import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"regexp"
"strings"
"time"
"unicode/utf16"
)
const (
SCUMLoginLogParserKey = "scum-login-log-parser-v1"
SCUMLoginLogParserVersion = "scum-login-log-v1"
SCUMLoginLogMaxLineBytes = 4096
)
var scumLoginLogPattern = regexp.MustCompile(`^(\d{4}\.\d{2}\.\d{2}-\d{2}\.\d{2}\.\d{2}): '(.+)\s+(\d{17}):(.+)\(([^()]*)\)' logged (in|out) at: X=([+-]?\d+(?:\.\d+)?) Y=([+-]?\d+(?:\.\d+)?) Z=([+-]?\d+(?:\.\d+)?)$`)
// SCUMLoginLogRecord is a single complete or buffered line from a plugin-declared
// UTF-16LE SCUM login log source. It carries transport identity, not host paths.
type SCUMLoginLogRecord struct {
ServerID string
SourceIdentity string
StreamGeneration string
Sequence uint64
RawLine []byte
Partial bool
}
type SCUMLoginLogParseOptions struct {
MaxLineBytes int
AcknowledgedTransportCursors map[SCUMTransportCursor]struct{}
AcknowledgedLogicalEventIdentities map[string]struct{}
}
type SCUMTransportCursor struct {
SourceIdentity string
StreamGeneration string
Sequence uint64
}
type SCUMLoginLogEvent struct {
ServerID string
ParserKey string
ParserVersion string
TransportCursor SCUMTransportCursor
Type string
ExternalPlayerID string
DisplayName string
ProfileLocalID string
OccurredAt time.Time
OccurredAtSourceText string
LogicalEventIdentity string
}
type SCUMLoginLogDiagnostic struct {
ServerID string
TransportCursor SCUMTransportCursor
Code string
}
type SCUMLoginLogBatch struct {
ServerID string
ParserKey string
ParserVersion string
Events []SCUMLoginLogEvent
Diagnostics []SCUMLoginLogDiagnostic
}
type SCUMLoginLogParserAvailability struct {
Available bool
ParserKey string
ParserVersion string
MaxLineBytes int
Reason string
}
func VerifiedSCUMLoginLogParser() SCUMLoginLogParserAvailability {
return SCUMLoginLogParserAvailability{Available: true, ParserKey: SCUMLoginLogParserKey, ParserVersion: SCUMLoginLogParserVersion, MaxLineBytes: SCUMLoginLogMaxLineBytes, Reason: "SCUM UTF-16LE login log parser is available"}
}
func ParseSCUMLoginLogRecords(serverID string, records []SCUMLoginLogRecord, options SCUMLoginLogParseOptions) SCUMLoginLogBatch {
maxLineBytes := options.MaxLineBytes
if maxLineBytes <= 0 || maxLineBytes > SCUMLoginLogMaxLineBytes {
maxLineBytes = SCUMLoginLogMaxLineBytes
}
batch := SCUMLoginLogBatch{ServerID: serverID, ParserKey: SCUMLoginLogParserKey, ParserVersion: SCUMLoginLogParserVersion}
seenTransport := cloneTransportSet(options.AcknowledgedTransportCursors)
seenLogical := cloneStringSet(options.AcknowledgedLogicalEventIdentities)
lastSequenceByGeneration := map[string]uint64{}
if len(records) > 100 {
records = records[:100]
}
for _, record := range records {
cursor := SCUMTransportCursor{SourceIdentity: record.SourceIdentity, StreamGeneration: record.StreamGeneration, Sequence: record.Sequence}
if record.ServerID != serverID || cursor.SourceIdentity == "" || cursor.StreamGeneration == "" || cursor.Sequence == 0 {
batch.Diagnostics = appendSCUMLoginDiagnostic(batch.Diagnostics, serverID, cursor, "invalid-transport-cursor")
continue
}
generationKey := cursor.SourceIdentity + "\x00" + cursor.StreamGeneration
if last := lastSequenceByGeneration[generationKey]; last != 0 && cursor.Sequence < last {
batch.Diagnostics = appendSCUMLoginDiagnostic(batch.Diagnostics, serverID, cursor, "out-of-order-transport")
}
if cursor.Sequence > lastSequenceByGeneration[generationKey] {
lastSequenceByGeneration[generationKey] = cursor.Sequence
}
if _, ok := seenTransport[cursor]; ok {
batch.Diagnostics = appendSCUMLoginDiagnostic(batch.Diagnostics, serverID, cursor, "duplicate-transport-cursor")
continue
}
seenTransport[cursor] = struct{}{}
if record.Partial {
batch.Diagnostics = appendSCUMLoginDiagnostic(batch.Diagnostics, serverID, cursor, "partial-line")
continue
}
if len(record.RawLine) == 0 || len(record.RawLine) > maxLineBytes {
batch.Diagnostics = appendSCUMLoginDiagnostic(batch.Diagnostics, serverID, cursor, "oversized-line")
continue
}
line, ok := decodeUTF16LELine(record.RawLine)
if !ok {
batch.Diagnostics = appendSCUMLoginDiagnostic(batch.Diagnostics, serverID, cursor, "undecodable-line")
continue
}
event, diagnosticCode := parseSCUMLoginLogLine(serverID, cursor, line)
if diagnosticCode != "" {
batch.Diagnostics = appendSCUMLoginDiagnostic(batch.Diagnostics, serverID, cursor, diagnosticCode)
continue
}
if _, ok := seenLogical[event.LogicalEventIdentity]; ok {
batch.Diagnostics = appendSCUMLoginDiagnostic(batch.Diagnostics, serverID, cursor, "duplicate-logical-event")
continue
}
seenLogical[event.LogicalEventIdentity] = struct{}{}
batch.Events = append(batch.Events, event)
}
return batch
}
func parseSCUMLoginLogLine(serverID string, cursor SCUMTransportCursor, rawLine string) (SCUMLoginLogEvent, string) {
line := strings.TrimPrefix(strings.TrimRight(rawLine, "\r\n"), "\ufeff")
lower := strings.ToLower(line)
if strings.Contains(lower, "failed") || strings.Contains(lower, "rejected") {
return SCUMLoginLogEvent{}, "failed-login"
}
match := scumLoginLogPattern.FindStringSubmatch(line)
if match == nil {
return SCUMLoginLogEvent{}, "malformed-line"
}
occurredAt, err := time.ParseInLocation("2006.01.02-15.04.05", match[1], time.UTC)
if err != nil {
return SCUMLoginLogEvent{}, "malformed-line"
}
eventType := "scum.login"
if match[6] == "out" {
eventType = "scum.logout"
}
displayName := strings.TrimSpace(match[4])
profileLocalID := strings.TrimSpace(match[5])
if displayName == "" || profileLocalID == "" {
return SCUMLoginLogEvent{}, "malformed-line"
}
event := SCUMLoginLogEvent{
ServerID: serverID,
ParserKey: SCUMLoginLogParserKey,
ParserVersion: SCUMLoginLogParserVersion,
TransportCursor: cursor,
Type: eventType,
ExternalPlayerID: match[3],
DisplayName: displayName,
ProfileLocalID: profileLocalID,
OccurredAt: occurredAt,
OccurredAtSourceText: match[1],
}
event.LogicalEventIdentity = scumLoginLogicalIdentity(serverID, eventType, match[1], event.ExternalPlayerID, displayName, profileLocalID)
return event, ""
}
func scumLoginLogicalIdentity(serverID, eventType, occurrenceText, externalPlayerID, displayName, profileLocalID string) string {
normalized := strings.Join([]string{SCUMLoginLogParserVersion, serverID, eventType, occurrenceText, externalPlayerID, displayName, profileLocalID}, "\x00")
digest := sha256.Sum256([]byte(normalized))
return "sha256:" + hex.EncodeToString(digest[:])
}
func decodeUTF16LELine(raw []byte) (string, bool) {
if len(raw)%2 != 0 {
return "", false
}
codeUnits := make([]uint16, len(raw)/2)
for index := range codeUnits {
codeUnits[index] = binary.LittleEndian.Uint16(raw[index*2:])
}
for index := 0; index < len(codeUnits); index++ {
unit := codeUnits[index]
if 0xD800 <= unit && unit <= 0xDBFF {
if index+1 >= len(codeUnits) || codeUnits[index+1] < 0xDC00 || codeUnits[index+1] > 0xDFFF {
return "", false
}
index++
continue
}
if 0xDC00 <= unit && unit <= 0xDFFF {
return "", false
}
}
return string(utf16.Decode(codeUnits)), true
}
func appendSCUMLoginDiagnostic(existing []SCUMLoginLogDiagnostic, serverID string, cursor SCUMTransportCursor, code string) []SCUMLoginLogDiagnostic {
if len(existing) >= 64 {
return existing
}
return append(existing, SCUMLoginLogDiagnostic{ServerID: serverID, TransportCursor: cursor, Code: code})
}
func cloneTransportSet(source map[SCUMTransportCursor]struct{}) map[SCUMTransportCursor]struct{} {
target := make(map[SCUMTransportCursor]struct{}, len(source))
for key, value := range source {
target[key] = value
}
return target
}
func cloneStringSet(source map[string]struct{}) map[string]struct{} {
target := make(map[string]struct{}, len(source))
for key, value := range source {
target[key] = value
}
return target
}
@@ -1,149 +0,0 @@
package companion
import (
"encoding/binary"
"encoding/json"
"strings"
"testing"
"unicode/utf16"
)
func TestSCUMLoginLogParserParsesAuthenticUTF16LELoginAndLogout(t *testing.T) {
availability := VerifiedSCUMLoginLogParser()
if !availability.Available || availability.ParserKey != SCUMLoginLogParserKey || availability.ParserVersion != SCUMLoginLogParserVersion {
t.Fatalf("login log parser should be versioned and available: %+v", availability)
}
records := []SCUMLoginLogRecord{
scumLoginFixtureRecord(1, "gen-1", "2026.07.05-05.39.10: '203.0.113.10:7777 76561198000000001:Fixture One(profile-1)' logged in at: X=123.45 Y=-456.78 Z=90"),
scumLoginFixtureRecord(2, "gen-1", "2026.07.05-07.21.15: '203.0.113.10:7777 76561198000000001:Fixture One(profile-1)' logged out at: X=124.45 Y=-455.78 Z=91"),
}
batch := ParseSCUMLoginLogRecords("server-1", records, SCUMLoginLogParseOptions{})
if len(batch.Diagnostics) != 0 {
t.Fatalf("unexpected diagnostics: %+v", batch.Diagnostics)
}
if len(batch.Events) != 2 {
t.Fatalf("expected login and logout events, got %+v", batch.Events)
}
login := batch.Events[0]
if login.Type != "scum.login" || login.ExternalPlayerID != "76561198000000001" || login.DisplayName != "Fixture One" || login.ProfileLocalID != "profile-1" || login.OccurredAtSourceText != "2026.07.05-05.39.10" {
t.Fatalf("unexpected login event: %+v", login)
}
logout := batch.Events[1]
if logout.Type != "scum.logout" || logout.LogicalEventIdentity == login.LogicalEventIdentity || logout.TransportCursor.Sequence != 2 {
t.Fatalf("unexpected logout event: %+v", logout)
}
serialized, err := json.Marshal(batch.Events)
if err != nil {
t.Fatal(err)
}
serializedText := string(serialized)
for _, forbidden := range []string{"203.0.113.10", "7777", "123.45", "-456.78", "124.45", "-455.78"} {
if strings.Contains(serializedText, forbidden) {
t.Fatalf("parser leaked network or coordinate material %q in %s", forbidden, serializedText)
}
}
}
func TestSCUMLoginLogParserRejectsFailedPartialUndecodableOversizedAndMalformedLines(t *testing.T) {
records := []SCUMLoginLogRecord{
scumLoginFixtureRecord(1, "gen-1", "2026.07.05-05.39.10: '203.0.113.20 76561198000000002:Rejected(profile-2)' login failed at: X=1 Y=2 Z=3"),
{ServerID: "server-1", SourceIdentity: "source-1", StreamGeneration: "gen-1", Sequence: 2, RawLine: scumUTF16LE("2026.07.05-05.39.10: '203.0.113.20 76561198000000002:Partial(profile-2)' logged in at: X=1 Y=2 Z=3"), Partial: true},
{ServerID: "server-1", SourceIdentity: "source-1", StreamGeneration: "gen-1", Sequence: 3, RawLine: []byte{0x00, 0xD8}},
{ServerID: "server-1", SourceIdentity: "source-1", StreamGeneration: "gen-1", Sequence: 4, RawLine: make([]byte, SCUMLoginLogMaxLineBytes+2)},
scumLoginFixtureRecord(5, "gen-1", "not a real login line"),
}
batch := ParseSCUMLoginLogRecords("server-1", records, SCUMLoginLogParseOptions{})
if len(batch.Events) != 0 {
t.Fatalf("invalid lines must not create events: %+v", batch.Events)
}
assertDiagnosticCodes(t, batch.Diagnostics, []string{"failed-login", "partial-line", "undecodable-line", "oversized-line", "malformed-line"})
}
func TestSCUMLoginLogParserDeduplicatesRotationOverlapAcrossNewGeneration(t *testing.T) {
line := "2026.06.20-06.36.46: '198.51.100.44 76561198000000003:Overlap(profile-3)' logged in at: X=10 Y=20 Z=30"
first := scumLoginFixtureRecord(5, "gen-old", line)
second := scumLoginFixtureRecord(1, "gen-new", line)
batch := ParseSCUMLoginLogRecords("server-1", []SCUMLoginLogRecord{first, second}, SCUMLoginLogParseOptions{})
if len(batch.Events) != 1 {
t.Fatalf("rotation overlap should emit one logical event, got %+v", batch.Events)
}
assertDiagnosticCodes(t, batch.Diagnostics, []string{"duplicate-logical-event"})
oldGeneration := ParseSCUMLoginLogRecords("server-1", []SCUMLoginLogRecord{first}, SCUMLoginLogParseOptions{})
newGeneration := ParseSCUMLoginLogRecords("server-1", []SCUMLoginLogRecord{second}, SCUMLoginLogParseOptions{})
if oldGeneration.Events[0].LogicalEventIdentity != newGeneration.Events[0].LogicalEventIdentity {
t.Fatalf("logical identity must be generation-independent: %s != %s", oldGeneration.Events[0].LogicalEventIdentity, newGeneration.Events[0].LogicalEventIdentity)
}
}
func TestSCUMLoginLogParserLogicalIdentityExcludesNetworkAndCoordinates(t *testing.T) {
first := scumLoginFixtureRecord(1, "gen-1", "2026.06.20-06.36.46: '198.51.100.44 76561198000000003:Private(profile-3)' logged in at: X=10 Y=20 Z=30")
second := scumLoginFixtureRecord(2, "gen-1", "2026.06.20-06.36.46: '203.0.113.44:7777 76561198000000003:Private(profile-3)' logged in at: X=-999 Y=888 Z=777")
firstBatch := ParseSCUMLoginLogRecords("server-1", []SCUMLoginLogRecord{first}, SCUMLoginLogParseOptions{})
secondBatch := ParseSCUMLoginLogRecords("server-1", []SCUMLoginLogRecord{second}, SCUMLoginLogParseOptions{})
if len(firstBatch.Events) != 1 || len(secondBatch.Events) != 1 {
t.Fatalf("expected both privacy variants to parse: %+v %+v", firstBatch, secondBatch)
}
if firstBatch.Events[0].LogicalEventIdentity != secondBatch.Events[0].LogicalEventIdentity {
t.Fatalf("logical identity must exclude network and coordinates: %s != %s", firstBatch.Events[0].LogicalEventIdentity, secondBatch.Events[0].LogicalEventIdentity)
}
}
func TestSCUMLoginLogParserHonorsRestartResumeAndDuplicateDelivery(t *testing.T) {
record := scumLoginFixtureRecord(9, "gen-1", "2026.06.20-12.30.26: '198.51.100.60 76561198000000004:Resume(profile-4)' logged in at: X=10 Y=20 Z=30")
first := ParseSCUMLoginLogRecords("server-1", []SCUMLoginLogRecord{record}, SCUMLoginLogParseOptions{})
if len(first.Events) != 1 {
t.Fatalf("expected first delivery to emit one event: %+v", first)
}
acknowledgedTransport := map[SCUMTransportCursor]struct{}{first.Events[0].TransportCursor: {}}
acknowledgedLogical := map[string]struct{}{first.Events[0].LogicalEventIdentity: {}}
duplicateCursor := ParseSCUMLoginLogRecords("server-1", []SCUMLoginLogRecord{record}, SCUMLoginLogParseOptions{AcknowledgedTransportCursors: acknowledgedTransport, AcknowledgedLogicalEventIdentities: acknowledgedLogical})
if len(duplicateCursor.Events) != 0 {
t.Fatalf("duplicate acknowledged cursor must not emit events: %+v", duplicateCursor.Events)
}
assertDiagnosticCodes(t, duplicateCursor.Diagnostics, []string{"duplicate-transport-cursor"})
replayedAfterRestart := record
replayedAfterRestart.StreamGeneration = "gen-after-restart"
replayedAfterRestart.Sequence = 1
duplicateLogical := ParseSCUMLoginLogRecords("server-1", []SCUMLoginLogRecord{replayedAfterRestart}, SCUMLoginLogParseOptions{AcknowledgedLogicalEventIdentities: acknowledgedLogical})
if len(duplicateLogical.Events) != 0 {
t.Fatalf("restart replay must be idempotent by logical identity: %+v", duplicateLogical.Events)
}
assertDiagnosticCodes(t, duplicateLogical.Diagnostics, []string{"duplicate-logical-event"})
}
func TestSCUMLoginLogParserAcceptsOutOfOrderNewEventsWithDiagnostic(t *testing.T) {
records := []SCUMLoginLogRecord{
scumLoginFixtureRecord(8, "gen-1", "2026.06.20-12.32.40: '198.51.100.70 76561198000000005:Late(profile-5)' logged out at: X=10 Y=20 Z=30"),
scumLoginFixtureRecord(7, "gen-1", "2026.06.20-12.30.26: '198.51.100.70 76561198000000005:Late(profile-5)' logged in at: X=10 Y=20 Z=30"),
}
batch := ParseSCUMLoginLogRecords("server-1", records, SCUMLoginLogParseOptions{})
if len(batch.Events) != 2 {
t.Fatalf("out-of-order unique events should still be parsed: %+v", batch.Events)
}
assertDiagnosticCodes(t, batch.Diagnostics, []string{"out-of-order-transport"})
}
func scumLoginFixtureRecord(sequence uint64, generation string, line string) SCUMLoginLogRecord {
return SCUMLoginLogRecord{ServerID: "server-1", SourceIdentity: "source-1", StreamGeneration: generation, Sequence: sequence, RawLine: scumUTF16LE(line)}
}
func scumUTF16LE(value string) []byte {
encoded := utf16.Encode([]rune(value))
bytes := make([]byte, len(encoded)*2)
for index, codeUnit := range encoded {
binary.LittleEndian.PutUint16(bytes[index*2:], codeUnit)
}
return bytes
}
func assertDiagnosticCodes(t *testing.T, diagnostics []SCUMLoginLogDiagnostic, expected []string) {
t.Helper()
if len(diagnostics) != len(expected) {
t.Fatalf("expected diagnostics %v, got %+v", expected, diagnostics)
}
for index, code := range expected {
if diagnostics[index].Code != code {
t.Fatalf("expected diagnostic %d to be %q, got %+v", index, code, diagnostics[index])
}
}
}
@@ -0,0 +1,28 @@
import type { SCUMCommandResult, SCUMConfigPatch, SCUMConfigRead, SCUMFeatureAvailability, SCUMFeatureKey, SCUMGiftGrant, SCUMPlayerProfile, SCUMStatePatch, SCUMStateSnapshot, SCUMTrajectoryCollection, SCUMVehicleSpawn } from "./contracts.js";
import { validateConfigPatch, validateStatePatch, validateVehicleSpawn } from "./schemas.js";
export type PluginFeatureBridge = { dispatch(action: "game-client.command" | "game-client.snapshot.read", payload: Record<string, string>): Promise<{ status: string; result?: Record<string, string>; error?: { message: string } }> };
export type SCUMFeatureAPI = {
availability(feature: SCUMFeatureKey): Promise<SCUMFeatureAvailability>; readConfig(): Promise<SCUMConfigRead | null>; patchConfig(patch: SCUMConfigPatch): Promise<SCUMCommandResult>;
playerProfile(playerId: string): Promise<SCUMPlayerProfile | null>; stateSnapshot(playerId: string): Promise<SCUMStateSnapshot | null>; requestStatePatch(patch: SCUMStatePatch): Promise<SCUMCommandResult>;
requestVehicleSpawn(spawn: SCUMVehicleSpawn): Promise<SCUMCommandResult>;
giftGrants(): Promise<SCUMGiftGrant[]>; trajectories(): Promise<SCUMTrajectoryCollection>;
};
export function createSCUMFeatureAPI(bridge: PluginFeatureBridge, availableFeatures: readonly SCUMFeatureAvailability[]): SCUMFeatureAPI {
const availability = async (feature: SCUMFeatureKey) => availableFeatures.find((item) => item.feature === feature) ?? { feature, available: false, reason: "插件未声明此功能。" };
return {
availability,
async readConfig() { const result = await bridge.dispatch("game-client.command", { type: "config.read" }); return result.status === "ok" ? decode<SCUMConfigRead>(result.result) : null; },
async patchConfig(patch) { const error = validateConfigPatch(patch); if (error) return { status: "validation-failed", summary: error }; return commandResult(await bridge.dispatch("game-client.command", { type: "config.patch", patch: JSON.stringify(patch) })); },
async playerProfile(playerId) { const result = await bridge.dispatch("game-client.snapshot.read", { type: "semantic.events", subjectId: playerId }); return result.status === "ok" ? decode<SCUMPlayerProfile>(result.result) : null; },
async stateSnapshot(playerId) { const result = await bridge.dispatch("game-client.command", { type: "player.lookup", playerId }); return result.status === "ok" ? decode<SCUMStateSnapshot>(result.result) : null; },
async requestStatePatch(patch) { const error = validateStatePatch(patch.changes); if (error) return { status: "validation-failed", summary: error }; return commandResult(await bridge.dispatch("game-client.command", { type: "game-state.patch", patch: JSON.stringify(patch) })); },
async requestVehicleSpawn(spawn) { const error = validateVehicleSpawn(spawn); if (error) return { status: "validation-failed", summary: error }; return commandResult(await bridge.dispatch("game-client.command", { type: "vehicle.spawn", vehicleCode: spawn.vehicleCode })); },
async giftGrants() { const result = await bridge.dispatch("game-client.snapshot.read", { type: "semantic.events", projection: "gifts" }); return result.status === "ok" ? decode<SCUMGiftGrant[]>(result.result) ?? [] : []; },
async trajectories() { const result = await bridge.dispatch("game-client.snapshot.read", { type: "semantic.events", projection: "trajectories" }); return result.status === "ok" ? decode<SCUMTrajectoryCollection>(result.result) ?? { available: false, reason: "没有已验证的位置事件源。", trajectories: [] } : { available: false, reason: result.error?.message ?? "没有已验证的位置事件源。", trajectories: [] }; }
};
}
function commandResult(result: { status: string; result?: Record<string, string>; error?: { message: string } }): SCUMCommandResult { if (result.status === "queued") return { status: "queued", summary: result.result?.summary ?? "已进入受控队列。" }; if (result.status === "unsupported") return { status: "unsupported", summary: result.error?.message ?? "当前运行时不支持此操作。" }; return { status: "failed", summary: result.error?.message ?? "受控操作未被接受。" }; }
function decode<T>(result: Record<string, string> | undefined): T | null { const payload = result?.payload; if (!payload) return null; try { return JSON.parse(payload) as T; } catch { return null; } }
@@ -1,8 +1,14 @@
export const scumFeatureKeys = ["configuration", "players", "rewards"] as const;
export const scumFeatureKeys = ["configuration", "players", "rewards", "state-patches", "trajectories"] as const;
export type SCUMFeatureKey = (typeof scumFeatureKeys)[number];
export type SCUMFeatureAvailability = { feature: SCUMFeatureKey; available: boolean; reason?: string };
export type SCUMMigrationProvenance = "plugin" | "transitional-read-only";
export type SCUMMigrationRecord<T = Record<string, unknown>> = { provenance: SCUMMigrationProvenance; readOnly: boolean; payload: T; recordedAt: string; sourceRecordId?: string };
export type SCUMFeatureMigrationAuthority = { serverInstanceId: string; feature: SCUMFeatureKey; authority: "plugin" | "transitional-read-only"; reason?: string };
export type SCUMFeatureMigrationStatus = { authority: "plugin" | "transitional-read-only"; readOnlyHistory: true; pluginWritesEnabled: boolean; reason?: string };
export type SCUMCommandResult = { status: "delivered" | "failed" | "unknown" | "unsupported" | "validation-failed" | "queued"; summary: string; audit?: Record<string, unknown> };
export type SCUMVehicleSpawn = { vehicleCode: string };
export type SCUMVehicleSpawnOption = { code: string; label: string };
export type SCUMLogicalDirectory = { key: string; label: string; scope: "config" | "logs" };
export type SCUMLogicalFile = { key: string; directoryKey: string; label: string; kind: "config" | "log"; streamKey?: string; editable?: boolean };
@@ -15,10 +21,19 @@ export type SCUMConfigPatch = { changes: Array<{ key: string; value: string }>;
export type SCUMPlayer = { id: string; gamePlayerId: string; displayName: string; lastSeenAt?: string; status: "online" | "offline" | "unknown" };
export type SCUMPlayerSession = { id: string; playerId: string; kind: "login" | "logout"; occurredAt: string; networkCorrelation?: string };
export type SCUMPlayerProfile = { player: SCUMPlayer; sessions: SCUMPlayerSession[] };
export type SCUMPlayerRisk = { kind: string; level: "low" | "medium" | "high"; observedAt: string; summary: string };
export type SCUMPlayerProfile = { player: SCUMPlayer; sessions: SCUMPlayerSession[]; risks: SCUMPlayerRisk[] };
export type SCUMGiftItem = { key: string; label: string; quantity: number };
export type SCUMGiftRevision = { id: string; catalogId: string; revision: number; items: SCUMGiftItem[]; publishedAt: string };
export type SCUMGiftGrant = { id: string; revisionId: string; playerId: string; notice: string; status: "pending-approval" | "queued" | "delivered" | "notification_failed" | "failed" | "unknown"; createdAt: string; completedAt?: string };
export type SCUMStateField = { key: string; label: string; value: number; minimum: number; maximum: number; editable: boolean; reason?: string };
export type SCUMStateSnapshot = { playerId: string; stateVersion: string; safetyWindow?: string; fields: SCUMStateField[]; observedAt: string };
export type SCUMStatePatch = { id: string; playerId: string; expectedStateVersion: string; safetyWindow: string; reason: string; changes: Array<{ fieldKey: string; before: number; after: number }>; status: "pending-approval" | "queued" | "succeeded" | "failed" | "unsupported" | "unknown"; createdAt: string };
export type SCUMTrajectoryPoint = { occurredAt: string; subjectId: string; subjectType: "player" | "vehicle"; x: number; y: number; z?: number; source: string };
export type SCUMTrajectory = { subjectId: string; subjectType: "player" | "vehicle"; points: SCUMTrajectoryPoint[]; provenance: SCUMMigrationProvenance };
export type SCUMTrajectoryCollection = { available: boolean; reason?: string; trajectories: SCUMTrajectory[] };
export type SCUMFeatureWorkspace = { defaultDirectoryKey?: string; directories?: SCUMLogicalDirectory[]; files?: SCUMLogicalFile[]; configFields?: SCUMConfigField[]; map?: { mapId: string; mapVersion: string; precision: number; sampleDistance: number; sampleIntervalSeconds: number; retentionSeconds: number } };
@@ -0,0 +1,75 @@
import type { SCUMConfigRead, SCUMFeatureKey, SCUMFeatureMigrationAuthority, SCUMFeatureMigrationStatus, SCUMGiftGrant, SCUMMigrationRecord, SCUMPlayer, SCUMPlayerProfile, SCUMPlayerRisk, SCUMPlayerSession, SCUMStatePatch, SCUMTrajectory, SCUMTrajectoryPoint } from "./contracts.js";
import { configurationCatalog } from "./schemas.js";
export function transitionalReadOnly<T extends Record<string, unknown>>(payload: T, recordedAt: string, sourceRecordId?: string): SCUMMigrationRecord<T> { return { provenance: "transitional-read-only", readOnly: true, payload, recordedAt, sourceRecordId }; }
export function pluginOwned<T extends Record<string, unknown>>(payload: T, recordedAt: string): SCUMMigrationRecord<T> { return { provenance: "plugin", readOnly: false, payload, recordedAt }; }
// The authority flag is exact-server. Missing, duplicate, or
// transitional flags fail closed: history remains readable, but plugin writes
// are not enabled. Execution still additionally requires Companion feature
// availability; this flag never authorizes a command by itself.
export function migrationStatus(flags: readonly SCUMFeatureMigrationAuthority[], serverInstanceId: string, feature: SCUMFeatureKey): SCUMFeatureMigrationStatus {
const matches = flags.filter((flag) => flag.serverInstanceId === serverInstanceId && flag.feature === feature);
if (matches.length !== 1) return { authority: "transitional-read-only", readOnlyHistory: true, pluginWritesEnabled: false, reason: matches.length ? "迁移标记冲突,已保持只读。" : "当前服务器尚未启用插件权威记录。" };
const flag = matches[0];
if (flag.authority !== "plugin") return { authority: "transitional-read-only", readOnlyHistory: true, pluginWritesEnabled: false, reason: flag.reason ?? "过渡记录仅供只读查看。" };
return { authority: "plugin", readOnlyHistory: true, pluginWritesEnabled: true, reason: flag.reason };
}
export function migratePlayerRecord(record: Record<string, unknown>): SCUMMigrationRecord<SCUMPlayer> | null {
const id = text(record.id); const gamePlayerId = text(record.gamePlayerId); const displayName = text(record.displayName); if (!id || !gamePlayerId || !displayName) return null;
const lastSeenAt = timestamp(record.lastSeenAt); const recordedAt = timestamp(record.updatedAt) ?? lastSeenAt ?? new Date(0).toISOString();
return transitionalReadOnly({ id, gamePlayerId, displayName, ...(lastSeenAt ? { lastSeenAt } : {}), status: record.online === true ? "online" : "unknown" }, recordedAt, id);
}
export function migrateConfigurationRecord(record: Record<string, unknown>): SCUMMigrationRecord<SCUMConfigRead> | null {
const fields = allowlistedConfigFields(record.fields); const observedAt = timestamp(record.observedAt) ?? timestamp(record.updatedAt); if (!fields || !observedAt) return null;
return transitionalReadOnly({ fields, observedAt }, observedAt, text(record.id));
}
export function migratePlayerProfileRecord(record: Record<string, unknown>): SCUMMigrationRecord<SCUMPlayerProfile> | null {
const player = migratePlayerRecord(object(record.player) ?? record); if (!player) return null;
const sessions = array(record.sessions).map((item) => migrateSession(item, player.payload.id)).filter((item): item is SCUMPlayerSession => item !== null);
const risks = [...array(record.accessAttempts), ...array(record.securitySignals)].map(migrateRisk).filter((item): item is SCUMPlayerRisk => item !== null);
const recordedAt = timestamp(record.updatedAt) ?? player.recordedAt;
return transitionalReadOnly({ player: player.payload, sessions, risks }, recordedAt, player.sourceRecordId);
}
export function migrateGiftGrantRecord(record: Record<string, unknown>): SCUMMigrationRecord<SCUMGiftGrant> | null {
const id = text(record.id); const revisionId = text(record.revisionId); const playerId = text(record.gamePlayerRecordId) ?? text(record.playerId); const status = giftStatus(record.status); const createdAt = timestamp(record.createdAt); if (!id || !revisionId || !playerId || !status || !createdAt) return null;
const completedAt = timestamp(record.completedAt); const notice = optionalText(record.notice) ?? "";
return transitionalReadOnly({ id, revisionId, playerId, notice, status, createdAt, ...(completedAt ? { completedAt } : {}) }, timestamp(record.updatedAt) ?? createdAt, id);
}
export function migrateStatePatchRecord(record: Record<string, unknown>): SCUMMigrationRecord<SCUMStatePatch> | null {
const id = text(record.id); const playerId = text(record.gamePlayerRecordId) ?? text(record.playerId); const expectedStateVersion = text(record.expectedStateVersion); const safetyWindow = text(record.safetyWindow); const reason = optionalText(record.reason) ?? ""; const status = stateStatus(record.status); const createdAt = timestamp(record.createdAt); const changes = array(record.changes).map(migrateStateChange).filter((item): item is { fieldKey: string; before: number; after: number } => item !== null);
if (!id || !playerId || !expectedStateVersion || !safetyWindow || !status || !createdAt || !changes.length) return null;
return transitionalReadOnly({ id, playerId, expectedStateVersion, safetyWindow, reason, changes, status, createdAt }, timestamp(record.updatedAt) ?? createdAt, id);
}
export function migrateTrajectoryRecord(record: Record<string, unknown>): SCUMTrajectory | null {
const subjectId = text(record.playerRecordId) ?? text(record.gamePlayerRecordId) ?? text(record.vehicleId) ?? text(record.entityId); const subjectType = trajectorySubjectType(record); const points = Array.isArray(record.points) && subjectId && subjectType ? record.points.map((point) => migratePoint(point, subjectId, subjectType)).filter((point): point is SCUMTrajectoryPoint => point !== null) : [];
if (!subjectId || !subjectType || !points.length) return null;
return { subjectId, subjectType, points, provenance: "transitional-read-only" };
}
export function migrateTrajectoryHistoryRecord(record: Record<string, unknown>): SCUMMigrationRecord<SCUMTrajectory> | null {
const trajectory = migrateTrajectoryRecord(record); if (!trajectory) return null;
const recordedAt = timestamp(record.updatedAt) ?? trajectory.points[trajectory.points.length - 1].occurredAt;
return transitionalReadOnly(trajectory, recordedAt, text(record.id));
}
function migratePoint(value: unknown, defaultSubjectId: string, defaultSubjectType: SCUMTrajectoryPoint["subjectType"]): SCUMTrajectoryPoint | null { const record = object(value); if (!record) return null; const x = number(record.mapX) ?? number(record.worldX); const y = number(record.mapY) ?? number(record.worldY); const occurredAt = timestamp(record.recordedAt) ?? timestamp(record.occurredAt); if (x === undefined || y === undefined || !occurredAt) return null; const subjectId = text(record.playerRecordId) ?? text(record.gamePlayerRecordId) ?? text(record.vehicleId) ?? text(record.entityId) ?? defaultSubjectId; const subjectType = trajectorySubjectType(record) ?? defaultSubjectType; return { occurredAt, subjectId, subjectType, x, y, ...(number(record.worldZ) !== undefined ? { z: number(record.worldZ) } : {}), source: "transitional-read-only" }; }
function migrateSession(value: unknown, defaultPlayerId: string): SCUMPlayerSession | null { const record = object(value); const id = record && text(record.id); const playerId = record && (text(record.gamePlayerRecordId) ?? text(record.playerId) ?? defaultPlayerId); const startedAt = record && timestamp(record.startedAt); if (!id || !playerId || !startedAt) return null; const endedAt = timestamp(record.endedAt); return { id, playerId, kind: endedAt ? "logout" : "login", occurredAt: endedAt ?? startedAt }; }
function migrateRisk(value: unknown): SCUMPlayerRisk | null { const record = object(value); const observedAt = record && (timestamp(record.occurredAt) ?? timestamp(record.lastObservedAt)); const kind = record && (text(record.ruleKey) ?? text(record.outcome)); const summary = record && (text(record.summary) ?? text(record.reason)); if (!observedAt || !kind || !summary) return null; return { kind, level: "medium", observedAt, summary }; }
function migrateStateChange(value: unknown): { fieldKey: string; before: number; after: number } | null { const record = object(value); if (!record) return null; const fieldKey = text(record.fieldKey); const before = number(record.before); const after = number(record.after); return fieldKey && before !== undefined && after !== undefined ? { fieldKey, before, after } : null; }
function allowlistedConfigFields(value: unknown): Record<string, string> | null { const fields = object(value); const allowed = new Set(configurationCatalog.map((field) => field.configKey)); if (!fields || !allowed.size) return null; const result: Record<string, string> = {}; for (const [key, field] of Object.entries(fields)) { if (allowed.has(key) && (typeof field === "string" || typeof field === "number" || typeof field === "boolean")) result[key] = String(field); } return Object.keys(result).length ? result : null; }
function giftStatus(value: unknown): SCUMGiftGrant["status"] | null { return value === "pending-approval" || value === "queued" || value === "delivered" || value === "notification_failed" || value === "failed" || value === "unknown" ? value : null; }
function stateStatus(value: unknown): SCUMStatePatch["status"] | null { if (value === "pending-approval" || value === "queued" || value === "unsupported" || value === "unknown" || value === "execution-unknown") return value === "execution-unknown" ? "unknown" : value; if (value === "confirmed") return "succeeded"; return value === "execution-failed" || value === "confirmation-failed" || value === "failed" ? "failed" : null; }
function trajectorySubjectType(record: Record<string, unknown>): SCUMTrajectoryPoint["subjectType"] | null { if (record.kind === "player" || record.kind === "vehicle") return record.kind; return text(record.playerRecordId) || text(record.gamePlayerRecordId) ? "player" : text(record.vehicleId) ? "vehicle" : null; }
function array(value: unknown): unknown[] { return Array.isArray(value) ? value : []; }
function object(value: unknown): Record<string, unknown> | null { return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null; }
function text(value: unknown): string | undefined { return typeof value === "string" && value.trim() ? value : undefined; }
function optionalText(value: unknown): string | undefined { return text(value); }
function number(value: unknown): number | undefined { return typeof value === "number" && Number.isFinite(value) ? value : undefined; }
function timestamp(value: unknown): string | undefined { const candidate = text(value); return candidate && !Number.isNaN(Date.parse(candidate)) ? candidate : undefined; }
@@ -1,17 +1,198 @@
type ReactLike = { createElement: (...args: any[]) => any };
type StateSetter<T> = (next: T | ((previous: T) => T)) => void;
export type SCUMPageContext = { pageKey?: string; pageTitle?: string; serverInstanceId?: string; permissions: string[]; availability: { available: boolean; reason?: string }; featureAvailability?: Array<{ key: string; available: boolean; reason?: string }> };
export type ReactLike = {
createElement: (...args: any[]) => any;
useEffect?: (effect: () => void | (() => void), deps: readonly unknown[]) => void;
useState?: <T>(initialState: T | (() => T)) => [T, StateSetter<T>];
};
export type SCUMPageContext = {
pageKey?: string;
pageTitle?: string;
serverInstanceId?: string;
permissions: string[];
availability: { available: boolean; reason?: string };
featureAvailability?: Array<{ key: string; available: boolean; reason?: string }>;
workspaceActions?: SCUMWorkspaceActions;
};
type SCUMWorkspaceActions = {
listSCUMPlayers?: () => Promise<unknown>;
listSCUMSquads?: () => Promise<unknown>;
listSCUMSquadMembers?: () => Promise<unknown>;
listSCUMVehicles?: () => Promise<unknown>;
listSCUMFlags?: () => Promise<unknown>;
listSCUMPositions?: () => Promise<unknown>;
listSCUMOperations?: () => Promise<unknown>;
createSCUMOperation?: (request: unknown) => Promise<unknown>;
listSCUMWorkflows?: () => Promise<unknown>;
createSCUMWorkflow?: (request: unknown) => Promise<unknown>;
listSCUMWorkflowSteps?: (workflowId?: string) => Promise<unknown>;
};
type RecordMap = Record<string, unknown>;
type DataState = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; data: SCUMSurfaceData };
type ActionState = { status: "idle" | "pending" | "ok" | "error"; message?: string };
type SCUMSurfaceData = { players: RecordMap[]; squads: RecordMap[]; members: RecordMap[]; vehicles: RecordMap[]; flags: RecordMap[]; positions: RecordMap[]; operations: RecordMap[]; workflows: RecordMap[]; steps: RecordMap[] };
const emptyData: SCUMSurfaceData = { players: [], squads: [], members: [], vehicles: [], flags: [], positions: [], operations: [], workflows: [], steps: [] };
export function renderSCUMFeaturePage(react: ReactLike, input: SCUMPageContext) {
const pageKey = normalizePageKey(input.pageKey);
const title = input.pageTitle ?? surfaceTitle(pageKey);
const reason = input.availability.reason ?? "等待当前服务器完成兼容性验证。";
return react.createElement("section", { className: "console-panel", "aria-label": title },
react.createElement("div", { className: "panel-header" }, react.createElement("div", null, react.createElement("h2", null, title), react.createElement("p", { className: "provider-id" }, surfaceSummary(pageKey))), react.createElement("span", { className: "page-status" }, input.availability.available ? "本地数据接口准备中" : reason)),
react.createElement("p", { className: "page-status" }, "当前页面不会读取遗留快照或派发远程查询;兼容的本地数据接口启用后将显示已验证记录。")
const e = react.createElement;
const [state, setState] = usePluginState<DataState>(react, { status: "loading" });
const [action, setAction] = usePluginState<ActionState>(react, { status: "idle" });
const pageKey = input.pageKey ?? "players";
const refresh = () => {
const actions = input.workspaceActions;
if (!input.serverInstanceId || !actions) {
setState({ status: "error", reason: "插件页面没有绑定服务器,无法读取 SCUM 投影。" });
return;
}
setState({ status: "loading" });
void Promise.all([
safeList(actions.listSCUMPlayers), safeList(actions.listSCUMSquads), safeList(actions.listSCUMSquadMembers), safeList(actions.listSCUMVehicles),
safeList(actions.listSCUMFlags), safeList(actions.listSCUMPositions), safeList(actions.listSCUMOperations), safeList(actions.listSCUMWorkflows), safeList(actions.listSCUMWorkflowSteps)
]).then(([players, squads, members, vehicles, flags, positions, operations, workflows, steps]) => setState({ status: "ready", data: { players, squads, members, vehicles, flags, positions, operations, workflows, steps } }))
.catch((error) => setState({ status: "error", reason: error instanceof Error ? error.message : "SCUM 投影读取失败。" }));
};
if (react.useEffect) react.useEffect(() => { refresh(); return undefined; }, [input.serverInstanceId, pageKey, input.workspaceActions]);
const data = state.status === "ready" ? state.data : emptyData;
return e("section", { className: "console-panel", "aria-label": input.pageTitle ?? surfaceTitle(pageKey) },
e("div", { className: "panel-header" },
e("div", null, e("h2", null, input.pageTitle ?? surfaceTitle(pageKey)), e("p", { className: "provider-id" }, surfaceSummary(pageKey))),
e("div", { className: "console-row-actions" },
e("span", { className: "page-status" }, input.availability.available ? "投影/Companion 可用" : input.availability.reason ?? "等待 Run/Companion"),
e("button", { type: "button", className: "icon-command", onClick: refresh }, "刷新投影"),
workflowButton(e, input, setAction, refresh, pageWorkflow(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 === "error" ? e("p", { className: "page-status", "data-state": "error" }, state.reason) : null,
state.status === "ready" ? renderSurfaceBody(e, pageKey, data, input, setAction, refresh) : null
);
}
function normalizePageKey(pageKey?: string): string { return pageKey === "squads" || pageKey === "live-map" || pageKey === "gifts" || pageKey === "players" ? pageKey : "players"; }
function surfaceTitle(pageKey: string): string { return pageKey === "squads" ? "队伍管理" : pageKey === "live-map" ? "实时地图" : pageKey === "gifts" ? "礼包管理" : "用户管理"; }
function surfaceSummary(pageKey: string): string { return pageKey === "live-map" ? "地图仅显示经当前服务器验证的坐标。" : pageKey === "gifts" ? "礼包、资格和发放历史仅来自本地业务记录。" : pageKey === "squads" ? "队伍、成员和旗帜仅显示经当前服务器验证的事实。" : "玩家记录将由已验证登录事件和同步结果建立。"; }
function renderSurfaceBody(e: ReactLike["createElement"], pageKey: string, data: SCUMSurfaceData, input: SCUMPageContext, setAction: StateSetter<ActionState>, refresh: () => void) {
switch (pageKey) {
case "players": return playersSurface(e, data, input, setAction, refresh);
case "squads": return squadsSurface(e, data);
case "live-map": return mapSurface(e, data);
case "gifts": return giftsSurface(e, data, input, setAction, refresh);
case "workflows": return workflowsSurface(e, data);
default: return playersSurface(e, data, input, setAction, refresh);
}
}
function playersSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, input: SCUMPageContext, setAction: StateSetter<ActionState>, refresh: () => void) {
return e("div", { className: "console-record-list" },
statsStrip(e, [["玩家投影", data.players.length], ["在线", data.players.filter((p) => boolField(p, "Online", "online")).length], ["坐标", data.positions.length], ["待审操作", data.operations.filter((op) => field(op, "Status", "status") === "waiting").length]]),
data.players.length ? data.players.slice(0, 80).map((player) => e("article", { key: idOf(player), className: "console-record" },
e("div", { className: "console-record-head" }, e("strong", null, textField(player, "DisplayName", "displayName") || textField(player, "GamePlayerID", "gamePlayerId") || "未知玩家"), e("span", { className: `status-pill ${boolField(player, "Online", "online") ? "status-active" : "status-disabled"}` }, boolField(player, "Online", "online") ? "在线" : "离线/未知")),
e("div", { className: "console-record-meta" }, e("span", null, `Steam ${textField(player, "SteamID", "steamId") || "unknown"}`), e("span", null, `Profile ${textField(player, "UserProfileID", "userProfileId") || "unknown"}`), e("span", null, `队伍 ${textField(player, "SquadName", "squadName") || textField(player, "SquadID", "squadId") || "unknown"}`), e("span", null, freshness(player))),
e("span", { className: "provider-id" }, `Fame ${numField(player, "FamePoints", "famePoints")} · Cash ${numField(player, "NormalBalance", "normalBalance")} · Gold ${numField(player, "GoldBalance", "goldBalance")} · ${coords(field(player, "Position", "position") as RecordMap | undefined)}`),
e("div", { className: "console-row-actions" },
operationButton(e, input, setAction, refresh, player, "player.fame.set", "fame", "Fame +100", 100),
operationButton(e, input, setAction, refresh, player, "player.currency.normal.set", "amount", "现金 +1000", 1000),
operationButton(e, input, setAction, refresh, player, "player.attribute.855.set", "after", "855 审批", Number(numField(player, "855", "855")) || 1, true)
)
)) : e("p", { className: "page-status" }, "暂无玩家投影。先运行 player/world refresh workflow;不会显示假玩家。")
);
}
function squadsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData) {
return e("div", { className: "overview-two-col" },
tablePanel(e, "队伍", data.squads, (squad) => [textField(squad, "Name", "name") || textField(squad, "SquadID", "squadId"), `成员 ${numField(squad, "MemberCount", "memberCount")}`, `队长 ${textField(squad, "LeaderProfileID", "leaderProfileId") || "unknown"}`, freshness(squad)]),
tablePanel(e, "成员 / 旗帜", [...data.members.slice(0, 40), ...data.flags.slice(0, 40)], (item) => [textField(item, "DisplayName", "displayName") || textField(item, "FlagID", "flagId") || "unknown", textField(item, "Rank", "rank") || textField(item, "OwnershipConfidence", "ownershipConfidence") || "unknown", textField(item, "SquadID", "squadId") || textField(item, "OwnerSquadID", "ownerSquadId") || "unknown", freshness(item)])
);
}
function mapSurface(e: ReactLike["createElement"], data: SCUMSurfaceData) {
const overlays = [...data.positions, ...data.vehicles.map((v) => field(v, "Position", "position") as RecordMap).filter(Boolean), ...data.flags.map((f) => field(f, "Position", "position") as RecordMap).filter(Boolean)];
return e("div", { className: "console-record-list" },
statsStrip(e, [["玩家", data.players.length], ["载具", data.vehicles.length], ["旗帜", data.flags.length], ["坐标点", overlays.length]]),
e("div", { className: "map-projection-board" }, overlays.slice(0, 120).map((point, index) => e("span", { key: `${idOf(point)}:${index}`, className: "map-projection-dot", title: `${textField(point, "SubjectType", "subjectType") || "point"} ${coords(point)}`, style: dotStyle(point) }, ""))),
tablePanel(e, "地图覆盖物", overlays, (point) => [textField(point, "SubjectType", "subjectType") || "unknown", textField(point, "SubjectID", "subjectId") || textField(point, "GamePlayerID", "gamePlayerId") || textField(point, "VehicleID", "vehicleId") || "unknown", coords(point), freshness(point)])
);
}
function giftsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData, input: SCUMPageContext, setAction: StateSetter<ActionState>, refresh: () => void) {
return e("div", { className: "console-record-list" },
statsStrip(e, [["可选玩家", data.players.length], ["发放操作", data.operations.filter((op) => textField(op, "TemplateKey", "templateKey") === "reward.deliver").length], ["未知态", data.operations.filter((op) => field(op, "Status", "status") === "unknown").length]]),
e("p", { className: "page-status" }, "礼包只创建 typed delivery workflow;确认结果未知时不会重复发放。"),
data.players.slice(0, 40).map((player) => e("article", { key: idOf(player), className: "console-record" },
e("div", { className: "console-record-head" }, e("strong", null, textField(player, "DisplayName", "displayName") || idOf(player)), e("span", { className: "status-pill status-disabled" }, freshness(player))),
e("div", { className: "console-row-actions" }, operationButton(e, input, setAction, refresh, player, "reward.deliver", "rewardKey", "创建礼包发放", "starter-pack"), operationButton(e, input, setAction, refresh, player, "player.notify", "message", "发送通知", "你的礼包正在审核发放。"))
))
);
}
function workflowsSurface(e: ReactLike["createElement"], data: SCUMSurfaceData) {
return e("div", { className: "console-record-list" },
data.workflows.length ? data.workflows.map((wf) => e("article", { key: idOf(wf), className: "console-record" },
e("div", { className: "console-record-head" }, e("strong", null, textField(wf, "TemplateKey", "templateKey") || idOf(wf)), e("span", { className: "status-pill status-active" }, textField(wf, "Status", "status") || "queued")),
e("div", { className: "console-record-meta" }, e("span", null, `当前步骤 ${textField(wf, "CurrentStepKey", "currentStepKey") || "等待调度"}`), e("span", null, `创建 ${dateField(wf, "CreatedAt", "createdAt")}`)),
e("span", { className: "provider-id" }, summaryText(wf))
)) : e("p", { className: "page-status" }, "暂无 workflow。可以从各页面发起 refresh/audit/correction/gift workflow。"),
tablePanel(e, "步骤", data.steps, (step) => [textField(step, "StepKey", "stepKey"), textField(step, "Status", "status"), textField(step, "Capability", "capability") || textField(step, "QueryTemplateKey", "queryTemplateKey") || textField(step, "OperationKey", "operationKey"), summaryText(step)])
);
}
function workflowButton(e: ReactLike["createElement"], input: SCUMPageContext, setAction: StateSetter<ActionState>, refresh: () => void, templateKey: string) {
if (!templateKey) return null;
return e("button", { type: "button", className: "primary-command", disabled: !input.workspaceActions?.createSCUMWorkflow, onClick: () => createWorkflow(input, setAction, refresh, templateKey) }, workflowLabel(templateKey));
}
function operationButton(e: ReactLike["createElement"], input: SCUMPageContext, setAction: StateSetter<ActionState>, refresh: () => void, player: RecordMap, templateKey: string, valueKey: string, label: string, value: unknown, guarded = false) {
return e("button", { type: "button", className: "icon-command", disabled: !input.workspaceActions?.createSCUMOperation, onClick: () => createOperation(input, setAction, refresh, player, templateKey, valueKey, value, guarded) }, label);
}
function createWorkflow(input: SCUMPageContext, setAction: StateSetter<ActionState>, refresh: () => void, templateKey: string) {
setAction({ status: "pending", message: `正在创建 ${templateKey} workflow…` });
void input.workspaceActions?.createSCUMWorkflow?.({ templateKey, idempotencyKey: `plugin:${templateKey}:${input.serverInstanceId}:${Date.now()}` }).then((result) => {
setAction({ status: "ok", message: `Workflow 已创建:${textField(result as RecordMap, "id") || templateKey}` }); refresh();
}).catch((error) => setAction({ status: "error", message: error instanceof Error ? error.message : "Workflow 创建失败。" }));
}
function createOperation(input: SCUMPageContext, setAction: StateSetter<ActionState>, refresh: () => void, player: RecordMap, templateKey: string, valueKey: string, value: unknown, guarded: boolean) {
const playerId = textField(player, "GamePlayerID", "gamePlayerId") || textField(player, "SteamID", "steamId");
const before = guarded ? field(field(player, "UnknownFields", "unknownFields") as RecordMap | undefined, "855") ?? 0 : undefined;
const payload: RecordMap = guarded ? { fieldKey: "855", before, after: value, safetyWindow: `plugin-maintenance-${Date.now()}`, backupRef: `backup-required:${Date.now()}` } : { [valueKey]: value };
setAction({ status: "pending", message: `正在创建 ${templateKey} typed operation…` });
void input.workspaceActions?.createSCUMOperation?.({ templateKey, playerId, payload, reason: "SCUM plugin projection surface request", idempotencyKey: `plugin:${templateKey}:${playerId}:${Date.now()}` }).then((result) => {
setAction({ status: "ok", message: `操作已进入审批/确认队列:${textField(result as RecordMap, "id") || templateKey}` }); refresh();
}).catch((error) => setAction({ status: "error", message: error instanceof Error ? error.message : "操作创建失败。" }));
}
function pageWorkflow(pageKey: string): string {
switch (pageKey) {
case "players": return "scum.player-refresh";
case "squads": return "scum.territory-audit";
case "live-map": return "scum.world-refresh";
case "gifts": return "scum.gift-delivery";
case "workflows": return "scum.product-cleanup";
default: return "scum.bootstrap-real-data";
}
}
function surfaceTitle(pageKey: string): string { return pageKey === "squads" ? "队伍/旗帜管理" : pageKey === "live-map" ? "实时地图" : pageKey === "gifts" ? "礼包管理" : pageKey === "workflows" ? "Workflow 状态" : "用户管理"; }
function surfaceSummary(pageKey: string): string { return pageKey === "live-map" ? "玩家、载具、旗帜坐标来自平台本地投影;缺失时显示 stale/unknown。" : pageKey === "gifts" ? "礼包发放、通知和确认都通过 typed workflow,不直接改投影。" : pageKey === "squads" ? "队伍、成员、旗帜所有权来自 SCUM.db typed observations。" : "玩家列表由登录日志和 SCUM.db typed observations 创建,不显示样例数据。"; }
function workflowLabel(templateKey: string): string { return templateKey.includes("audit") ? "发起审计" : templateKey.includes("gift") ? "创建发放 workflow" : templateKey.includes("world") ? "刷新世界投影" : templateKey.includes("cleanup") ? "清理旧入口" : "刷新真实数据"; }
function statsStrip(e: ReactLike["createElement"], items: Array<[string, number]>) { return e("div", { className: "console-stat-strip" }, items.map(([label, value]) => e("span", { key: label, className: "server-card-stat" }, e("span", null, label), e("strong", null, String(value))))); }
function tablePanel(e: ReactLike["createElement"], title: string, rows: RecordMap[], render: (row: RecordMap) => unknown[]) { return e("article", { className: "console-module" }, e("div", { className: "panel-header" }, e("h2", null, title), e("span", { className: "page-status" }, `${rows.length}`)), e("div", { className: "console-row-list" }, rows.length ? rows.slice(0, 100).map((row) => e("div", { key: idOf(row), className: "console-row" }, render(row).map((part, i) => i === 0 ? e("span", { key: i }, String(part ?? "unknown")) : e("strong", { key: i }, String(part ?? "unknown"))))) : e("p", { className: "page-status" }, "暂无真实投影数据。"))); }
function dotStyle(point: RecordMap): Record<string, string> { const x = Number(field(point, "X", "x") ?? 0); const y = Number(field(point, "Y", "y") ?? 0); return { left: `${Math.max(2, Math.min(98, 50 + x / 10000))}%`, top: `${Math.max(2, Math.min(98, 50 - y / 10000))}%` }; }
function safeList(fn?: () => Promise<unknown>): Promise<RecordMap[]> { return fn ? fn().then((value) => Array.isArray((value as RecordMap)?.items) ? (value as { items: RecordMap[] }).items : []) : Promise.resolve([]); }
function usePluginState<T>(react: ReactLike, initial: T): [T, StateSetter<T>] { return react.useState ? react.useState<T>(initial) : [initial, () => undefined]; }
function field(row: RecordMap | undefined, ...keys: string[]): unknown { if (!row) return undefined; for (const key of keys) if (row[key] !== undefined) return row[key]; return undefined; }
function textField(row: RecordMap | unknown, ...keys: string[]): string { const value = field(row as RecordMap, ...keys); return value === undefined || value === null ? "" : String(value); }
function boolField(row: RecordMap, ...keys: string[]): boolean { const value = field(row, ...keys); return value === true || value === "true"; }
function numField(row: RecordMap, ...keys: string[]): string { const value = field(row, ...keys); return value === undefined || value === null || value === "" ? "--" : String(value); }
function idOf(row: RecordMap): string { return textField(row, "ID", "id", "GamePlayerID", "gamePlayerId", "SquadID", "squadId", "VehicleID", "vehicleId", "FlagID", "flagId", "StepKey", "stepKey") || Math.random().toString(36).slice(2); }
function freshness(row: RecordMap): string { const fresh = field(row, "Freshness", "freshness") as RecordMap | undefined; return textField(fresh, "Status", "status") || "unknown"; }
function coords(row?: RecordMap): string { if (!row) return "坐标 unknown"; const ok = field(row, "HasCoordinates", "hasCoordinates"); return ok === false ? "坐标 unknown" : `X ${numField(row, "X", "x")} / Y ${numField(row, "Y", "y")} / Z ${numField(row, "Z", "z")}`; }
function summaryText(row: RecordMap): string { const summary = field(row, "SafeSummary", "safeSummary") as RecordMap | undefined; return textField(summary, "Message", "message") || textField(row, "BlockerReason", "blockerReason") || "safe summary pending"; }
function dateField(row: RecordMap, ...keys: string[]): string { const value = textField(row, ...keys); return value ? new Date(value).toLocaleString() : "unknown"; }
@@ -1,4 +1,4 @@
import type { SCUMConfigField, SCUMConfigPatch, SCUMFeatureAvailability } from "./contracts.js";
import type { SCUMConfigField, SCUMConfigPatch, SCUMFeatureAvailability, SCUMStateField, SCUMVehicleSpawn, SCUMVehicleSpawnOption } from "./contracts.js";
// These are safe fallback allowlists. A Companion schema probe may narrow them
// per server, but a game version never enables or disables a feature.
@@ -9,5 +9,10 @@ export const configurationCatalog: readonly SCUMConfigField[] = [
{ key: "max-players", fileKey: "scum-server-settings", configKey: "MaxPlayers", label: "最大玩家数", description: "允许同时进入服务器的玩家上限。", control: "number", minimum: 1, maximum: 128, defaultValue: "128", restartImpact: "restart-required" },
{ key: "welcome-message", fileKey: "scum-server-settings", configKey: "WelcomeMessage", label: "欢迎消息", description: "登录成功后由已声明的服务器扩展显示给玩家。", control: "text", defaultValue: "", restartImpact: "none" }
];
export const vehicleSpawnCatalog: readonly SCUMVehicleSpawnOption[] = [{ code: "BPC_Laika_C", label: "Laika" }, { code: "BPC_WolfsWagen_C", label: "WolfsWagen" }];
export const stateFieldCatalog: readonly Omit<SCUMStateField, "value" | "editable" | "reason">[] = [{ key: "skills.running", label: "跑步技能", minimum: 0, maximum: 1000000 }, { key: "attributes.strength", label: "力量属性", minimum: 1, maximum: 8 }];
export function supportsStateField(field: string): boolean { return stateFieldCatalog.some((candidate) => candidate.key === field); }
export function featureUnavailable(reason: string): SCUMFeatureAvailability { return { feature: "configuration", available: false, reason }; }
export function validateConfigPatch(patch: SCUMConfigPatch): string | null { if (!patch.idempotencyKey.trim() || !patch.reason.trim() || !patch.changes.length) return "配置修改必须包含原因、幂等键和至少一项变更。"; for (const change of patch.changes) { const field = configurationCatalog.find((candidate) => candidate.key === change.key); if (!field) return `字段 ${change.key} 不在受控目录中。`; if (!change.value.trim()) return `字段 ${field.label} 不能为空。`; if ((field.control === "number" || field.control === "port") && (!Number.isInteger(Number(change.value)) || (field.minimum !== undefined && Number(change.value) < field.minimum) || (field.maximum !== undefined && Number(change.value) > field.maximum))) return `字段 ${field.label} 超出允许范围。`; } return null; }
export function validateStatePatch(fields: Array<{ fieldKey: string; before: number; after: number }>): string | null { if (!fields.length) return "状态修改至少需要一个字段。"; for (const field of fields) { const definition = stateFieldCatalog.find((candidate) => candidate.key === field.fieldKey); if (!definition) return `字段 ${field.fieldKey} 不在运行时字段白名单中。`; if (!Number.isFinite(field.before) || !Number.isFinite(field.after) || field.after < definition.minimum || field.after > definition.maximum) return `字段 ${definition.label} 超出允许范围。`; } return null; }
export function validateVehicleSpawn(spawn: SCUMVehicleSpawn): string | null { if (!/^[A-Za-z][A-Za-z0-9_]{2,63}$/.test(spawn.vehicleCode)) return "载具代码格式无效。"; if (!vehicleSpawnCatalog.some((candidate) => candidate.code === spawn.vehicleCode)) return "载具代码未在受控目录中声明。"; return null; }
+353 -308
View File
@@ -70,7 +70,6 @@
"remote.rsync.write",
"remote.run.files.read",
"remote.run.files.write",
"remote.run.db.sqlite.probe",
"remote.run.db.sqlite.query",
"remote.run.process.start",
"remote.run.process.stop",
@@ -96,7 +95,6 @@
"runCapabilities": [
"remote.run.files.read",
"remote.run.files.write",
"remote.run.db.sqlite.probe",
"remote.run.db.sqlite.query",
"remote.run.process.start",
"remote.run.process.stop",
@@ -284,11 +282,219 @@
"maxRecords": 1000
}
],
"queryTemplates": [],
"operationTemplates": [],
"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",
"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",
"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",
"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",
"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",
"maxRows": 500,
"timeoutSeconds": 15
},
{
"key": "scum.positions",
"title": "Read SCUM current player, vehicle, 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",
"maxRows": 500,
"timeoutSeconds": 15
}
],
"operationTemplates": [
{
"key": "player.fame.set",
"title": "Set SCUM player fame through RCON",
"permission": "server.game-client.command",
"approvalLevel": "operator",
"kind": "rcon",
"transportKey": "scum-management",
"targetKey": "scum-management",
"payloadSchemaRef": "schemas/bridge/player-fame-set.payload.schema.json",
"resultSchemaRef": "schemas/bridge/player-rcon-set.result.schema.json",
"confirmationSchemaRef": "schemas/bridge/player-fame-set.confirmation.schema.json",
"timeoutSeconds": 60,
"maxPayloadBytes": 2048,
"safety": {
"requiresApproval": true,
"requiresConfirmation": true
}
},
{
"key": "player.currency.normal.set",
"title": "Set SCUM normal currency through RCON",
"permission": "server.game-client.command",
"approvalLevel": "operator",
"kind": "rcon",
"transportKey": "scum-management",
"targetKey": "scum-management",
"payloadSchemaRef": "schemas/bridge/player-currency-set.payload.schema.json",
"resultSchemaRef": "schemas/bridge/player-rcon-set.result.schema.json",
"confirmationSchemaRef": "schemas/bridge/player-currency-set.confirmation.schema.json",
"timeoutSeconds": 60,
"maxPayloadBytes": 2048,
"safety": {
"requiresApproval": true,
"requiresConfirmation": true
}
},
{
"key": "player.currency.gold.set",
"title": "Set SCUM gold currency through RCON",
"permission": "server.game-client.command",
"approvalLevel": "operator",
"kind": "rcon",
"transportKey": "scum-management",
"targetKey": "scum-management",
"payloadSchemaRef": "schemas/bridge/player-currency-set.payload.schema.json",
"resultSchemaRef": "schemas/bridge/player-rcon-set.result.schema.json",
"confirmationSchemaRef": "schemas/bridge/player-currency-set.confirmation.schema.json",
"timeoutSeconds": 60,
"maxPayloadBytes": 2048,
"safety": {
"requiresApproval": true,
"requiresConfirmation": true
}
},
{
"key": "player.notify",
"title": "Notify SCUM player through RCON chat",
"permission": "server.game-client.command",
"approvalLevel": "operator",
"kind": "rcon",
"transportKey": "scum-management",
"targetKey": "scum-management",
"payloadSchemaRef": "schemas/bridge/player-notify.payload.schema.json",
"resultSchemaRef": "schemas/bridge/player-notify.result.schema.json",
"confirmationSchemaRef": "schemas/bridge/player-notify.confirmation.schema.json",
"timeoutSeconds": 60,
"maxPayloadBytes": 2048,
"safety": {
"requiresApproval": true,
"requiresConfirmation": true
}
},
{
"key": "reward.deliver",
"title": "Deliver approved SCUM reward through typed command workflow",
"permission": "server.game-client.command",
"approvalLevel": "operator",
"kind": "rcon",
"transportKey": "scum-management",
"targetKey": "scum-management",
"payloadSchemaRef": "schemas/bridge/reward-deliver.payload.schema.json",
"resultSchemaRef": "schemas/bridge/reward-deliver.result.schema.json",
"confirmationSchemaRef": "schemas/bridge/reward-deliver.confirmation.schema.json",
"timeoutSeconds": 60,
"maxPayloadBytes": 4096,
"safety": {
"requiresApproval": true,
"requiresConfirmation": true
}
},
{
"key": "player.attribute.855.set",
"title": "Set SCUM DB-only player attribute 855",
"permission": "server.game-client.maintenance",
"approvalLevel": "platform-admin",
"kind": "sqlite-mutation",
"transportKey": "scum-database",
"targetKey": "scum-database",
"payloadSchemaRef": "schemas/bridge/player-attribute-855-set.payload.schema.json",
"resultSchemaRef": "schemas/bridge/player-attribute-855-set.result.schema.json",
"confirmationSchemaRef": "schemas/bridge/player-attribute-855-set.confirmation.schema.json",
"timeoutSeconds": 120,
"maxPayloadBytes": 4096,
"maxRowsAffected": 1,
"mutation": {
"fieldKey": "855",
"tableKey": "prisoner",
"identityKey": "user_profile_id",
"valueKey": "value",
"confirmationQueryKey": "scum.player.profile",
"allowedValueType": "integer",
"minValue": 0,
"maxValue": 100000
},
"safety": {
"requiresApproval": true,
"requiresOfflinePlayer": true,
"requiresMaintenanceWindow": true,
"requiresBeforeValue": true,
"requiresConfirmation": true,
"backupRequired": true
}
}
],
"commandRetentionSeconds": 604800,
"maxCommands": 1000,
"features": [
{
"key": "player.intelligence",
"title": "SCUM player intelligence",
"permission": "server.game-client.read",
"requiredHandlers": [
"player.lookup"
],
"requiredEventProducers": [
"semantic.events"
]
},
{
"key": "reward.delivery",
"title": "SCUM reward delivery",
@@ -313,6 +519,14 @@
"requiredHandlers": [
"vehicle.spawn"
]
},
{
"key": "trajectory.collect",
"title": "SCUM trajectories",
"permission": "server.game-client.read",
"requiredEventProducers": [
"semantic.events"
]
}
],
"pages": [
@@ -322,7 +536,21 @@
"players",
"online.sessions"
],
"featureKeys": []
"queryTemplateKeys": [
"scum.player.profile",
"scum.positions"
],
"operationKeys": [
"player.fame.set",
"player.currency.normal.set",
"player.currency.gold.set",
"player.notify",
"player.attribute.855.set"
],
"featureKeys": [
"player.intelligence",
"state.patch"
]
},
{
"pageKey": "squads",
@@ -330,7 +558,14 @@
"squads",
"flags"
],
"featureKeys": []
"queryTemplateKeys": [
"scum.squads",
"scum.squad-members",
"scum.flags"
],
"featureKeys": [
"player.intelligence"
]
},
{
"pageKey": "live-map",
@@ -339,14 +574,54 @@
"vehicles",
"flags"
],
"featureKeys": []
"queryTemplateKeys": [
"scum.player.profile",
"scum.vehicles",
"scum.flags",
"scum.positions"
],
"featureKeys": [
"trajectory.collect"
]
},
{
"pageKey": "gifts",
"snapshotTypes": [
"players"
],
"featureKeys": []
"operationKeys": [
"reward.deliver",
"player.notify"
],
"featureKeys": [
"reward.delivery"
]
},
{
"pageKey": "workflows",
"queryTemplateKeys": [
"scum.player.profile",
"scum.squads",
"scum.squad-members",
"scum.vehicles",
"scum.flags",
"scum.positions"
],
"operationKeys": [
"player.fame.set",
"player.currency.normal.set",
"player.currency.gold.set",
"player.notify",
"reward.deliver",
"player.attribute.855.set"
],
"featureKeys": [
"player.intelligence",
"reward.delivery",
"state.patch",
"vehicle.spawn",
"trajectory.collect"
]
}
],
"companion": {
@@ -365,223 +640,6 @@
"requestTimeoutSeconds": 15
}
},
"scumLiveData": {
"schemaVersion": "1",
"probe": {
"capability": "remote.run.db.sqlite.probe",
"targetKey": "scum-database",
"bounds": {
"maxObjects": 256,
"maxColumnsPerObject": 128,
"maxIndexesPerObject": 64,
"maxForeignKeys": 64,
"maxCardinalityReads": 64,
"maxSampleRows": 3,
"timeoutMs": 5000,
"maxResultBytes": 524288
}
},
"capabilityGates": [
{ "capability": "schema-probe", "gate": "enabled", "adapterVersion": "scum-live-data-v1", "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", "evidenceStatus": "compatible", "safeReason": "当前绑定 durable schema probe 已返回匹配的 redacted schema fingerprint。" },
{ "capability": "players.read", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", "requiredAssetDigests": ["sha256:b3359f3c5939a410a98ddfccb920791044df76da4f8e5df4e726a9fe58423520", "sha256:d9c262d0b4f1b600b608c8279a6dfb052de1a605fe1286ddc15f74892eace4fc"], "evidenceStatus": "missing", "safeReason": "schema 和 asset 已匹配;等待 SQLite-template terminal envelope 验证后启用。" },
{ "capability": "player-details.read", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", "requiredAssetDigests": ["sha256:c980807b736f8988cd7c37ecb97c72e5e707050450ebc8c539dccec06c60e1f8", "sha256:919c5f68f74fdb0743fa5d9b59aeecd003dcdac5bc51e52d3a486ed1045c9a48"], "evidenceStatus": "missing", "safeReason": "详情和经济只读 asset 已匹配;等待 terminal envelope 验证后启用。" },
{ "capability": "squads.read", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", "requiredAssetDigests": ["sha256:e7746ac4f4ea86038c1a035087154d4dc3a622fdb14b3ad94b45fec819486f50"], "evidenceStatus": "missing", "safeReason": "队伍只读 asset 已匹配;rank 和 leader 语义保持 nullable,等待 envelope 验证。" },
{ "capability": "squad-members.read", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", "requiredAssetDigests": ["sha256:e49b634d8a63b0d1f979706ff4a99050c11b5661d494f72897e89da6554f0654"], "evidenceStatus": "missing", "safeReason": "成员 join asset 已匹配;等待 SQLite-template terminal envelope 验证后启用。" },
{ "capability": "vehicles.read", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", "requiredAssetDigests": ["sha256:e2362451c3a4392adeca38c5e81055674f78e982cee710a33717b3d06bb78dc3"], "evidenceStatus": "missing", "safeReason": "载具 identity 和坐标 asset 已匹配;ownership 保持 nullable,等待 envelope 验证。" },
{ "capability": "flags.read", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", "requiredAssetDigests": ["sha256:42ddc6e8a237b7d576834845d1b1d5b2a2fc087668579eac0e3dc08ce2941000"], "evidenceStatus": "missing", "safeReason": "旗子和 base join asset 已匹配;territory 语义保持 gated,等待 envelope 验证。" },
{ "capability": "positions.read", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0", "requiredAssetDigests": ["sha256:88629de001e6882ce9e72bf5f0b17770d408c665869d04cd768c2e3fdfb9f790", "sha256:74800836553c7e4372a0747c5e9511adcee940b057194488bbf65bf164df372b", "sha256:f1941109167a86818e9e71996821884aeb8bd7e863584454b891e525695ba478"], "evidenceStatus": "missing", "safeReason": "坐标 query 和 map transform 已匹配;等待 envelope 验证且不声明 sub-10s cadence。" },
{ "capability": "profile-xml.write", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "evidenceStatus": "missing", "safeReason": "等待 XML patch safety、backup、offline、readback 和 855 映射验证。" },
{ "capability": "economy-command.write", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "evidenceStatus": "missing", "safeReason": "等待 Fame 和 currency command 的确认读回语义验证。" },
{ "capability": "gift-command.write", "gate": "disabled", "adapterVersion": "scum-live-data-v1", "evidenceStatus": "missing", "safeReason": "等待 gift catalog、transport 和 conclusive receipt 验证。" }
],
"logParsers": [
{
"key": "scum-login-log-login-parser",
"adapterVersion": "scum-live-data-v1",
"assetPath": "assets/scum-live/login-log-parser.json",
"digest": "sha256:264835fb36255071fed46dd50724ec511986db901ac8056b08ddafb10f5f0056",
"parserVersion": "scum-login-log-v1",
"sourceKey": "scum-login-events",
"eventType": "scum.login",
"eventSchemaRef": "schemas/log-events/login.event.schema.json",
"maxLineBytes": 4096,
"cursorPolicy": "source-generation-sequence",
"privacy": { "stripNetworkIdentifiers": true, "logicalEventIdentity": "native-or-sanitized-fields" }
},
{
"key": "scum-login-log-logout-parser",
"adapterVersion": "scum-live-data-v1",
"assetPath": "assets/scum-live/login-log-parser.json",
"digest": "sha256:264835fb36255071fed46dd50724ec511986db901ac8056b08ddafb10f5f0056",
"parserVersion": "scum-login-log-v1",
"sourceKey": "scum-login-events",
"eventType": "scum.logout",
"eventSchemaRef": "schemas/log-events/logout.event.schema.json",
"maxLineBytes": 4096,
"cursorPolicy": "source-generation-sequence",
"privacy": { "stripNetworkIdentifiers": true, "logicalEventIdentity": "native-or-sanitized-fields" }
}
],
"sqliteQueries": [
{
"key": "scum-players-read",
"adapterVersion": "scum-live-data-v1",
"assetPath": "assets/scum-live/queries/players-read.json",
"digest": "sha256:b3359f3c5939a410a98ddfccb920791044df76da4f8e5df4e726a9fe58423520",
"capability": "players.read",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"transportKey": "scum-database",
"targetKey": "scum-database",
"parameterSchemaRef": "schemas/scum-live/players-read.parameters.schema.json",
"resultSchemaRef": "schemas/scum-live/players-read.result.schema.json",
"maxRows": 100,
"timeoutMs": 2000,
"maxResultBytes": 65536
},
{
"key": "scum-player-details-read",
"adapterVersion": "scum-live-data-v1",
"assetPath": "assets/scum-live/queries/player-details-read.json",
"digest": "sha256:c980807b736f8988cd7c37ecb97c72e5e707050450ebc8c539dccec06c60e1f8",
"capability": "player-details.read",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"transportKey": "scum-database",
"targetKey": "scum-database",
"parameterSchemaRef": "schemas/scum-live/player-details-read.parameters.schema.json",
"resultSchemaRef": "schemas/scum-live/player-details-read.result.schema.json",
"maxRows": 1,
"timeoutMs": 2000,
"maxResultBytes": 32768
},
{
"key": "scum-player-economy-read",
"adapterVersion": "scum-live-data-v1",
"assetPath": "assets/scum-live/queries/player-economy-read.json",
"digest": "sha256:919c5f68f74fdb0743fa5d9b59aeecd003dcdac5bc51e52d3a486ed1045c9a48",
"capability": "player-details.read",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"transportKey": "scum-database",
"targetKey": "scum-database",
"parameterSchemaRef": "schemas/scum-live/player-economy-read.parameters.schema.json",
"resultSchemaRef": "schemas/scum-live/player-economy-read.result.schema.json",
"maxRows": 16,
"timeoutMs": 2000,
"maxResultBytes": 32768
},
{
"key": "scum-player-session-enrichment-read",
"adapterVersion": "scum-live-data-v1",
"assetPath": "assets/scum-live/queries/player-session-enrichment-read.json",
"digest": "sha256:d9c262d0b4f1b600b608c8279a6dfb052de1a605fe1286ddc15f74892eace4fc",
"capability": "players.read",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"transportKey": "scum-database",
"targetKey": "scum-database",
"parameterSchemaRef": "schemas/scum-live/player-session-enrichment-read.parameters.schema.json",
"resultSchemaRef": "schemas/scum-live/player-session-enrichment-read.result.schema.json",
"maxRows": 1,
"timeoutMs": 2000,
"maxResultBytes": 32768
},
{
"key": "scum-squads-read",
"adapterVersion": "scum-live-data-v1",
"assetPath": "assets/scum-live/queries/squads-read.json",
"digest": "sha256:e7746ac4f4ea86038c1a035087154d4dc3a622fdb14b3ad94b45fec819486f50",
"capability": "squads.read",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"transportKey": "scum-database",
"targetKey": "scum-database",
"parameterSchemaRef": "schemas/scum-live/squads-read.parameters.schema.json",
"resultSchemaRef": "schemas/scum-live/squads-read.result.schema.json",
"maxRows": 100,
"timeoutMs": 2000,
"maxResultBytes": 65536
},
{
"key": "scum-squad-members-read",
"adapterVersion": "scum-live-data-v1",
"assetPath": "assets/scum-live/queries/squad-members-read.json",
"digest": "sha256:e49b634d8a63b0d1f979706ff4a99050c11b5661d494f72897e89da6554f0654",
"capability": "squad-members.read",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"transportKey": "scum-database",
"targetKey": "scum-database",
"parameterSchemaRef": "schemas/scum-live/squad-members-read.parameters.schema.json",
"resultSchemaRef": "schemas/scum-live/squad-members-read.result.schema.json",
"maxRows": 500,
"timeoutMs": 2000,
"maxResultBytes": 131072
},
{
"key": "scum-vehicles-read",
"adapterVersion": "scum-live-data-v1",
"assetPath": "assets/scum-live/queries/vehicles-read.json",
"digest": "sha256:e2362451c3a4392adeca38c5e81055674f78e982cee710a33717b3d06bb78dc3",
"capability": "vehicles.read",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"transportKey": "scum-database",
"targetKey": "scum-database",
"parameterSchemaRef": "schemas/scum-live/vehicles-read.parameters.schema.json",
"resultSchemaRef": "schemas/scum-live/vehicles-read.result.schema.json",
"maxRows": 500,
"timeoutMs": 2000,
"maxResultBytes": 262144
},
{
"key": "scum-flags-read",
"adapterVersion": "scum-live-data-v1",
"assetPath": "assets/scum-live/queries/flags-read.json",
"digest": "sha256:42ddc6e8a237b7d576834845d1b1d5b2a2fc087668579eac0e3dc08ce2941000",
"capability": "flags.read",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"transportKey": "scum-database",
"targetKey": "scum-database",
"parameterSchemaRef": "schemas/scum-live/flags-read.parameters.schema.json",
"resultSchemaRef": "schemas/scum-live/flags-read.result.schema.json",
"maxRows": 200,
"timeoutMs": 2000,
"maxResultBytes": 65536
},
{
"key": "scum-positions-read",
"adapterVersion": "scum-live-data-v1",
"assetPath": "assets/scum-live/queries/positions-read.json",
"digest": "sha256:88629de001e6882ce9e72bf5f0b17770d408c665869d04cd768c2e3fdfb9f790",
"capability": "positions.read",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"transportKey": "scum-database",
"targetKey": "scum-database",
"parameterSchemaRef": "schemas/scum-live/positions-read.parameters.schema.json",
"resultSchemaRef": "schemas/scum-live/positions-read.result.schema.json",
"maxRows": 500,
"timeoutMs": 2000,
"maxResultBytes": 262144
}
],
"mapAssets": [
{
"key": "scum-current-service-coordinate-map",
"adapterVersion": "scum-live-data-v1",
"assetPath": "assets/scum-live/map/current-service-map.json",
"digest": "sha256:74800836553c7e4372a0747c5e9511adcee940b057194488bbf65bf164df372b",
"requiredSchemaFingerprint": "sha256:ebd477d6c6ead9c34c41169af489236d762a76186d45dedd753d50f1b81e26f0",
"metadataSchemaRef": "schemas/scum-live/map-metadata.schema.json",
"transformAssetPath": "assets/scum-live/map/current-service-transform.json",
"transformDigest": "sha256:f1941109167a86818e9e71996821884aeb8bd7e863584454b891e525695ba478",
"worldBounds": {
"minX": -901009.4375,
"minY": -883992.5625,
"maxX": 612580.0625,
"maxY": 615977.375
},
"image": {
"width": 4096,
"height": 4096
}
}
]
},
"permissions": [
"server.create",
"server.read",
@@ -635,54 +693,6 @@
{
"path": "bin/scum-start.cmd",
"mode": 448
},
{
"path": "assets/scum-live/login-log-parser.json",
"mode": 384
},
{
"path": "assets/scum-live/queries/players-read.json",
"mode": 384
},
{
"path": "assets/scum-live/queries/player-details-read.json",
"mode": 384
},
{
"path": "assets/scum-live/queries/player-economy-read.json",
"mode": 384
},
{
"path": "assets/scum-live/queries/player-session-enrichment-read.json",
"mode": 384
},
{
"path": "assets/scum-live/queries/squads-read.json",
"mode": 384
},
{
"path": "assets/scum-live/queries/squad-members-read.json",
"mode": 384
},
{
"path": "assets/scum-live/queries/vehicles-read.json",
"mode": 384
},
{
"path": "assets/scum-live/queries/flags-read.json",
"mode": 384
},
{
"path": "assets/scum-live/queries/positions-read.json",
"mode": 384
},
{
"path": "assets/scum-live/map/current-service-map.json",
"mode": 384
},
{
"path": "assets/scum-live/map/current-service-transform.json",
"mode": 384
}
],
"productionLifecycle": {
@@ -708,8 +718,8 @@
"title": "用户管理",
"path": "/players",
"bundleKey": "scum-server-plugin",
"bundleVersion": "1.0.4",
"bundleIntegritySha256": "sha256:8b57e66b529dd35cd277ee2e5b839d8844c557b690f1d57db3ec026c26b713dc",
"bundleVersion": "1.0.3",
"bundleIntegritySha256": "sha256:3488b316d909e597024f8f31c7bc96ab8019f643d74a528dc442d3df0dc3d54e",
"permissions": [
"server.read",
"server.game-client.read",
@@ -717,58 +727,95 @@
"server.game-client.maintenance"
],
"bridgeActions": [
"server.instances.read"
"server.instances.read",
"remote.access.request"
],
"featureKeys": []
"featureKeys": [
"player.intelligence",
"state.patch"
]
},
{
"key": "squads",
"title": "队伍管理",
"path": "/squads",
"bundleKey": "scum-server-plugin",
"bundleVersion": "1.0.4",
"bundleIntegritySha256": "sha256:8b57e66b529dd35cd277ee2e5b839d8844c557b690f1d57db3ec026c26b713dc",
"bundleVersion": "1.0.3",
"bundleIntegritySha256": "sha256:3488b316d909e597024f8f31c7bc96ab8019f643d74a528dc442d3df0dc3d54e",
"permissions": [
"server.read",
"server.game-client.read"
],
"bridgeActions": [
"server.instances.read"
"server.instances.read",
"remote.access.request"
],
"featureKeys": []
"featureKeys": [
"player.intelligence"
]
},
{
"key": "live-map",
"title": "实时地图",
"path": "/live-map",
"bundleKey": "scum-server-plugin",
"bundleVersion": "1.0.4",
"bundleIntegritySha256": "sha256:8b57e66b529dd35cd277ee2e5b839d8844c557b690f1d57db3ec026c26b713dc",
"bundleVersion": "1.0.3",
"bundleIntegritySha256": "sha256:3488b316d909e597024f8f31c7bc96ab8019f643d74a528dc442d3df0dc3d54e",
"permissions": [
"server.read",
"server.game-client.read"
],
"bridgeActions": [
"server.instances.read"
"server.instances.read",
"remote.access.request"
],
"featureKeys": []
"featureKeys": [
"trajectory.collect"
]
},
{
"key": "gifts",
{
"key": "gifts",
"title": "礼包管理",
"path": "/gifts",
"bundleKey": "scum-server-plugin",
"bundleVersion": "1.0.4",
"bundleIntegritySha256": "sha256:8b57e66b529dd35cd277ee2e5b839d8844c557b690f1d57db3ec026c26b713dc",
"bundleVersion": "1.0.3",
"bundleIntegritySha256": "sha256:3488b316d909e597024f8f31c7bc96ab8019f643d74a528dc442d3df0dc3d54e",
"permissions": [
"server.read",
"server.game-client.read",
"server.game-client.command"
],
"bridgeActions": [
"server.instances.read"
"bridgeActions": [
"server.instances.read"
],
"featureKeys": [
"reward.delivery"
]
},
{
"key": "workflows",
"title": "Workflow 状态",
"path": "/workflows",
"bundleKey": "scum-server-plugin",
"bundleVersion": "1.0.3",
"bundleIntegritySha256": "sha256:3488b316d909e597024f8f31c7bc96ab8019f643d74a528dc442d3df0dc3d54e",
"permissions": [
"server.read",
"server.game-client.read",
"server.game-client.command",
"server.game-client.maintenance"
],
"featureKeys": []
"bridgeActions": [
"server.instances.read",
"remote.access.request"
],
"featureKeys": [
"player.intelligence",
"reward.delivery",
"state.patch",
"vehicle.spawn",
"trajectory.collect"
]
}
],
"ai": {
@@ -778,6 +825,20 @@
"mediation": "platform",
"configWritePolicy": "review-required"
},
"mapTrajectories": {
"mapId": "scum-island",
"mapVersion": "0.9",
"worldMinX": -500000,
"worldMinY": -500000,
"worldMaxX": 500000,
"worldMaxY": 500000,
"imageWidth": 2048,
"imageHeight": 2048,
"precision": 1,
"sampleDistance": 4,
"sampleIntervalSeconds": 20,
"retentionSeconds": 604800
},
"runtimeProfiles": {
"discovery": [
{
@@ -1138,7 +1199,6 @@
"kind": "sqlite",
"targetKey": "scum-database",
"capabilities": [
"remote.run.db.sqlite.probe",
"remote.run.db.sqlite.query",
"remote.run.protected.sql"
]
@@ -1160,21 +1220,6 @@
]
}
],
"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"
]
}
],
"clientManagers": [
{
"key": "scum-client-manager",
@@ -1,5 +1,5 @@
import { renderSCUMFeaturePage } from "../features/page.js";
export const pluginPageBundle = { key: "scum-server-plugin", version: "1.0.4", integritySha256: "sha256:8b57e66b529dd35cd277ee2e5b839d8844c557b690f1d57db3ec026c26b713dc" };
export const pluginPageBundle = { key: "scum-server-plugin", version: "1.0.3", integritySha256: "sha256:3488b316d909e597024f8f31c7bc96ab8019f643d74a528dc442d3df0dc3d54e" };
export function renderPluginPage(react: any, input: any) { return renderSCUMFeaturePage(react, { pageKey: input.page?.key ?? "players", pageTitle: input.page?.title ?? "用户管理", serverInstanceId: input.context.serverInstanceId, permissions: input.context.permissions, availability: input.availability, featureAvailability: input.availability.features }); }
export function renderPluginPage(react: any, input: any) { return renderSCUMFeaturePage(react, { pageKey: input.page?.key ?? "players", pageTitle: input.page?.title ?? "用户管理", serverInstanceId: input.context.serverInstanceId, permissions: input.context.permissions, availability: input.availability, featureAvailability: input.availability.features, workspaceActions: input.workspaceActions }); }
@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["playerId", "fieldKey", "value", "observedAt", "checksum"],
"properties": {
"playerId": { "type": "string", "minLength": 1, "maxLength": 96 },
"fieldKey": { "const": "855" },
"value": { "type": "integer", "minimum": 0, "maximum": 100000 },
"observedAt": { "type": "string", "format": "date-time" },
"checksum": { "type": "string", "minLength": 1, "maxLength": 160 }
}
}
@@ -0,0 +1,15 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["playerId", "fieldKey", "before", "after", "safetyWindow", "backupRef"],
"properties": {
"playerId": { "type": "string", "minLength": 1, "maxLength": 96 },
"fieldKey": { "const": "855" },
"before": { "type": "integer", "minimum": 0, "maximum": 100000 },
"after": { "type": "integer", "minimum": 0, "maximum": 100000 },
"safetyWindow": { "type": "string", "minLength": 1, "maxLength": 120 },
"backupRef": { "type": "string", "minLength": 1, "maxLength": 180 },
"reason": { "type": "string", "maxLength": 240 }
}
}
@@ -0,0 +1,12 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["outcome", "affectedRows", "mutationChecksum"],
"properties": {
"outcome": { "enum": ["succeeded", "failed", "unknown", "stale-before"] },
"affectedRows": { "type": "integer", "minimum": 0, "maximum": 1 },
"mutationChecksum": { "type": "string", "minLength": 1, "maxLength": 160 },
"safeMessage": { "type": "string", "maxLength": 240 }
}
}
@@ -0,0 +1,14 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMPlayerCurrencySetConfirmation",
"type": "object",
"additionalProperties": false,
"required": ["playerId", "amount", "observedAt"],
"properties": {
"playerId": { "type": "string", "minLength": 17, "maxLength": 17, "pattern": "^[0-9]{17}$" },
"amount": { "type": "integer", "minimum": 0, "maximum": 2147483647 },
"currency": { "enum": ["normal", "gold"] },
"observationId": { "type": "string", "minLength": 1, "maxLength": 160 },
"observedAt": { "type": "string", "format": "date-time" }
}
}
@@ -0,0 +1,12 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMPlayerCurrencySetPayload",
"type": "object",
"additionalProperties": false,
"required": ["playerId", "amount"],
"properties": {
"playerId": { "type": "string", "minLength": 17, "maxLength": 17, "pattern": "^[0-9]{17}$" },
"amount": { "type": "integer", "minimum": 0, "maximum": 2147483647 },
"reason": { "type": "string", "minLength": 1, "maxLength": 240 }
}
}
@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMPlayerFameSetConfirmation",
"type": "object",
"additionalProperties": false,
"required": ["playerId", "fame", "observedAt"],
"properties": {
"playerId": { "type": "string", "minLength": 17, "maxLength": 17, "pattern": "^[0-9]{17}$" },
"fame": { "type": "integer", "minimum": -2147483648, "maximum": 2147483647 },
"observationId": { "type": "string", "minLength": 1, "maxLength": 160 },
"observedAt": { "type": "string", "format": "date-time" }
}
}
@@ -0,0 +1,12 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMPlayerFameSetPayload",
"type": "object",
"additionalProperties": false,
"required": ["playerId", "fame"],
"properties": {
"playerId": { "type": "string", "minLength": 17, "maxLength": 17, "pattern": "^[0-9]{17}$" },
"fame": { "type": "integer", "minimum": -2147483648, "maximum": 2147483647 },
"reason": { "type": "string", "minLength": 1, "maxLength": 240 }
}
}
@@ -0,0 +1,12 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMPlayerNotifyConfirmation",
"type": "object",
"additionalProperties": false,
"required": ["playerId", "outcome", "observedAt"],
"properties": {
"playerId": { "type": "string", "minLength": 17, "maxLength": 17, "pattern": "^[0-9]{17}$" },
"outcome": { "enum": ["sent", "failed", "unknown"] },
"observedAt": { "type": "string", "format": "date-time" }
}
}
@@ -0,0 +1,12 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMPlayerRCONSetResult",
"type": "object",
"additionalProperties": false,
"required": ["outcome"],
"properties": {
"outcome": { "enum": ["queued", "succeeded", "failed", "unknown"] },
"runJobId": { "type": "string", "minLength": 1, "maxLength": 160 },
"summary": { "type": "string", "maxLength": 240 }
}
}
@@ -0,0 +1,14 @@
# 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.
@@ -0,0 +1,12 @@
{
"$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 }
}
}
@@ -0,0 +1,31 @@
{
"$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"],
"properties": {
"flagId": { "type": "string", "minLength": 1, "maxLength": 96 },
"entityId": { "type": "string", "minLength": 1, "maxLength": 96 },
"ownerProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
"ownerPlayerId": { "type": "string", "minLength": 1, "maxLength": 96 },
"ownerSquadId": { "type": "string", "minLength": 1, "maxLength": 96 },
"ownerSquadName": { "type": "string", "minLength": 1, "maxLength": 80 },
"ownershipConfidence": { "enum": ["direct", "member", "squad", "unknown"] },
"x": { "type": "number" },
"y": { "type": "number" },
"z": { "type": "number" }
}
}
},
"truncated": { "type": "boolean" }
}
}
@@ -0,0 +1,13 @@
{
"$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 },
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
}
}
@@ -0,0 +1,34 @@
{
"$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"],
"properties": {
"gamePlayerId": { "type": "string", "minLength": 1, "maxLength": 96 },
"userProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
"steamId": { "type": "string", "minLength": 1, "maxLength": 96 },
"displayName": { "type": "string", "minLength": 1, "maxLength": 80 },
"squadId": { "type": "string", "minLength": 1, "maxLength": 96 },
"squadName": { "type": "string", "minLength": 1, "maxLength": 80 },
"famePoints": { "type": "number" },
"normalBalance": { "type": "number" },
"goldBalance": { "type": "number" },
"x": { "type": "number" },
"y": { "type": "number" },
"z": { "type": "number" },
"lastSaveTime": { "type": "string", "format": "date-time" }
}
}
},
"truncated": { "type": "boolean" }
}
}
@@ -1,11 +1,11 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMVehiclesReadParameters",
"title": "SCUMPositionsParameters",
"type": "object",
"additionalProperties": false,
"required": ["limit", "offset"],
"properties": {
"limit": { "type": "integer", "minimum": 1, "maximum": 500 },
"offset": { "type": "integer", "minimum": 0, "maximum": 1000000 }
"subjectType": { "enum": ["player", "vehicle", "flag"] },
"subjectId": { "type": "string", "minLength": 1, "maxLength": 96 },
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
}
}
@@ -0,0 +1,30 @@
{
"$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", "x", "y"],
"properties": {
"subjectType": { "enum": ["player", "vehicle", "flag"] },
"subjectId": { "type": "string", "minLength": 1, "maxLength": 96 },
"gamePlayerId": { "type": "string", "minLength": 1, "maxLength": 96 },
"vehicleId": { "type": "string", "minLength": 1, "maxLength": 96 },
"entityId": { "type": "string", "minLength": 1, "maxLength": 96 },
"x": { "type": "number" },
"y": { "type": "number" },
"z": { "type": "number" },
"lastSaveTime": { "type": "string", "format": "date-time" }
}
}
},
"truncated": { "type": "boolean" }
}
}
@@ -0,0 +1,11 @@
{
"$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 }
}
}
@@ -0,0 +1,29 @@
{
"$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"],
"properties": {
"squadId": { "type": "string", "minLength": 1, "maxLength": 96 },
"userProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
"gamePlayerId": { "type": "string", "minLength": 1, "maxLength": 96 },
"steamId": { "type": "string", "minLength": 1, "maxLength": 96 },
"displayName": { "type": "string", "minLength": 1, "maxLength": 80 },
"rank": { "type": "string", "minLength": 1, "maxLength": 32 },
"isLeader": { "type": "boolean" },
"joinedAt": { "type": "string", "format": "date-time" }
}
}
},
"truncated": { "type": "boolean" }
}
}
@@ -0,0 +1,11 @@
{
"$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 }
}
}
@@ -0,0 +1,27 @@
{
"$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"],
"properties": {
"squadId": { "type": "string", "minLength": 1, "maxLength": 96 },
"name": { "type": "string", "minLength": 1, "maxLength": 80 },
"leaderProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
"leaderPlayerId": { "type": "string", "minLength": 1, "maxLength": 96 },
"memberCount": { "type": "integer", "minimum": 0, "maximum": 1000 },
"score": { "type": "number" }
}
}
},
"truncated": { "type": "boolean" }
}
}
@@ -0,0 +1,12 @@
{
"$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 },
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
}
}
@@ -0,0 +1,31 @@
{
"$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"],
"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", "minLength": 1, "maxLength": 120 },
"ownerProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
"ownerPlayerId": { "type": "string", "minLength": 1, "maxLength": 96 },
"squadId": { "type": "string", "minLength": 1, "maxLength": 96 },
"x": { "type": "number" },
"y": { "type": "number" },
"z": { "type": "number" }
}
}
},
"truncated": { "type": "boolean" }
}
}
@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMRewardDeliverConfirmation",
"type": "object",
"additionalProperties": false,
"required": ["grantId", "outcome", "observedAt"],
"properties": {
"grantId": { "type": "string", "minLength": 1, "maxLength": 160 },
"outcome": { "enum": ["delivered", "failed", "unknown"] },
"observationId": { "type": "string", "minLength": 1, "maxLength": 160 },
"observedAt": { "type": "string", "format": "date-time" }
}
}
@@ -1 +1 @@
{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["occurredAt","collectedAt","source","mapId","mapVersion","playerId","worldX","worldY"],"properties":{"occurredAt":{"type":"string","format":"date-time","maxLength":40},"collectedAt":{"type":"string","format":"date-time","maxLength":40},"source":{"const":"companion"},"mapId":{"const":"scum-island"},"mapVersion":{"type":"string","maxLength":80},"playerId":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,96}$","maxLength":96},"worldX":{"type":"number","minimum":-500000,"maximum":500000},"worldY":{"type":"number","minimum":-500000,"maximum":500000}}}
{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["occurredAt","collectedAt","source","mapId","mapVersion","playerId","worldX","worldY"],"properties":{"occurredAt":{"type":"string","format":"date-time","maxLength":40},"collectedAt":{"type":"string","format":"date-time","maxLength":40},"source":{"enum":["companion","log-projection"]},"mapId":{"const":"scum-island"},"mapVersion":{"type":"string","maxLength":80},"playerId":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,96}$","maxLength":96},"worldX":{"type":"number","minimum":-500000,"maximum":500000},"worldY":{"type":"number","minimum":-500000,"maximum":500000}}}
@@ -1 +1 @@
{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["occurredAt","source","mapId","mapVersion","playerId","vehicleId"],"properties":{"occurredAt":{"type":"string","format":"date-time","maxLength":40},"source":{"const":"companion"},"mapId":{"const":"scum-island"},"mapVersion":{"type":"string","maxLength":80},"playerId":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,96}$","maxLength":96},"vehicleId":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,96}$","maxLength":96}}}
{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["occurredAt","source","mapId","mapVersion","playerId","vehicleId"],"properties":{"occurredAt":{"type":"string","format":"date-time","maxLength":40},"source":{"enum":["companion","log-projection"]},"mapId":{"const":"scum-island"},"mapVersion":{"type":"string","maxLength":80},"playerId":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,96}$","maxLength":96},"vehicleId":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,96}$","maxLength":96}}}
@@ -1 +1 @@
{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["occurredAt","source","mapId","mapVersion","playerId","vehicleId"],"properties":{"occurredAt":{"type":"string","format":"date-time","maxLength":40},"source":{"const":"companion"},"mapId":{"const":"scum-island"},"mapVersion":{"type":"string","maxLength":80},"playerId":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,96}$","maxLength":96},"vehicleId":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,96}$","maxLength":96}}}
{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["occurredAt","source","mapId","mapVersion","playerId","vehicleId"],"properties":{"occurredAt":{"type":"string","format":"date-time","maxLength":40},"source":{"enum":["companion","log-projection"]},"mapId":{"const":"scum-island"},"mapVersion":{"type":"string","maxLength":80},"playerId":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,96}$","maxLength":96},"vehicleId":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,96}$","maxLength":96}}}
@@ -1 +1 @@
{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["occurredAt","collectedAt","source","mapId","mapVersion","vehicleId","worldX","worldY"],"properties":{"occurredAt":{"type":"string","format":"date-time","maxLength":40},"collectedAt":{"type":"string","format":"date-time","maxLength":40},"source":{"const":"companion"},"mapId":{"const":"scum-island"},"mapVersion":{"type":"string","maxLength":80},"vehicleId":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,96}$","maxLength":96},"worldX":{"type":"number","minimum":-500000,"maximum":500000},"worldY":{"type":"number","minimum":-500000,"maximum":500000}}}
{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","additionalProperties":false,"required":["occurredAt","collectedAt","source","mapId","mapVersion","vehicleId","worldX","worldY"],"properties":{"occurredAt":{"type":"string","format":"date-time","maxLength":40},"collectedAt":{"type":"string","format":"date-time","maxLength":40},"source":{"enum":["companion","log-projection"]},"mapId":{"const":"scum-island"},"mapVersion":{"type":"string","maxLength":80},"vehicleId":{"type":"string","pattern":"^[A-Za-z0-9_.:-]{1,96}$","maxLength":96},"worldX":{"type":"number","minimum":-500000,"maximum":500000},"worldY":{"type":"number","minimum":-500000,"maximum":500000}}}
@@ -1,11 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMFlagsReadParameters",
"type": "object",
"additionalProperties": false,
"required": ["limit", "offset"],
"properties": {
"limit": { "type": "integer", "minimum": 1, "maximum": 200 },
"offset": { "type": "integer", "minimum": 0, "maximum": 1000000 }
}
}
@@ -1,34 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMFlagsReadResult",
"type": "object",
"additionalProperties": false,
"required": ["rows"],
"properties": {
"rows": {
"type": "array",
"maxItems": 200,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["flagElementId", "baseElementId", "baseId", "ownerProfileId", "ownerExternalPlayerId", "ownerDisplayName", "ownerSquadId", "ownerSquadName", "flagX", "flagY", "flagZ", "territoryX", "territoryY", "territoryZ"],
"properties": {
"flagElementId": { "type": "integer", "minimum": 0, "maximum": 2147483647 },
"baseElementId": { "type": "integer", "minimum": 0, "maximum": 2147483647 },
"baseId": { "type": "integer", "minimum": 0, "maximum": 2147483647 },
"ownerProfileId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"ownerExternalPlayerId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
"ownerDisplayName": { "type": ["string", "null"], "minLength": 1, "maxLength": 80 },
"ownerSquadId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"ownerSquadName": { "type": ["string", "null"], "minLength": 1, "maxLength": 80 },
"flagX": { "type": ["number", "null"] },
"flagY": { "type": ["number", "null"] },
"flagZ": { "type": ["number", "null"] },
"territoryX": { "type": ["number", "null"] },
"territoryY": { "type": ["number", "null"] },
"territoryZ": { "type": ["number", "null"] }
}
}
}
}
}
@@ -1,54 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMCurrentServiceMapMetadata",
"type": "object",
"additionalProperties": false,
"required": ["key", "mapVersion", "adapterVersion", "requiredSchemaFingerprint", "sourceEvidence", "authorization", "worldBounds", "image", "layers", "transformAssetPath"],
"properties": {
"key": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._/-]{0,119}$" },
"mapVersion": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,79}$" },
"adapterVersion": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,79}$" },
"requiredSchemaFingerprint": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$" },
"sourceEvidence": { "type": "string", "minLength": 1, "maxLength": 240 },
"authorization": {
"type": "object",
"additionalProperties": false,
"required": ["redistribution", "baseMapArtwork", "renderingAvailability"],
"properties": {
"redistribution": { "const": "first-party-generated-coordinate-metadata" },
"baseMapArtwork": { "const": "not-packaged" },
"renderingAvailability": { "const": "unavailable-until-authorized-base-map" }
}
},
"worldBounds": {
"type": "object",
"additionalProperties": false,
"required": ["minX", "minY", "maxX", "maxY"],
"properties": { "minX": { "type": "number" }, "minY": { "type": "number" }, "maxX": { "type": "number" }, "maxY": { "type": "number" } }
},
"image": {
"type": "object",
"additionalProperties": false,
"required": ["width", "height"],
"properties": { "width": { "type": "integer", "minimum": 1 }, "height": { "type": "integer", "minimum": 1 } }
},
"layers": {
"type": "array",
"minItems": 1,
"maxItems": 8,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["key", "capability", "subjectType", "label", "sourceQueryKey"],
"properties": {
"key": { "enum": ["players", "vehicles", "flags"] },
"capability": { "const": "positions.read" },
"subjectType": { "enum": ["player", "vehicle", "flag"] },
"label": { "type": "string", "minLength": 1, "maxLength": 40 },
"sourceQueryKey": { "const": "scum-positions-read" }
}
}
},
"transformAssetPath": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._/-]{0,240}$" }
}
}
@@ -1,10 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMPlayerDetailsReadParameters",
"type": "object",
"additionalProperties": false,
"required": ["externalPlayerId"],
"properties": {
"externalPlayerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" }
}
}
@@ -1,34 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMPlayerDetailsReadResult",
"type": "object",
"additionalProperties": false,
"required": ["rows"],
"properties": {
"rows": {
"type": "array",
"maxItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["externalPlayerId", "idType", "provider", "userCreationTime", "userLastLoginTime", "isBanned", "profileId", "displayName", "profileType", "prisonerId", "prisonerUserProfileId", "prisonerLastSaveTime", "entityId", "profileXmlLength"],
"properties": {
"externalPlayerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
"idType": { "type": "string", "minLength": 1, "maxLength": 32 },
"provider": { "type": "string", "minLength": 1, "maxLength": 32 },
"userCreationTime": { "type": "string", "minLength": 1, "maxLength": 64 },
"userLastLoginTime": { "type": "string", "minLength": 1, "maxLength": 64 },
"isBanned": { "type": "integer", "minimum": 0, "maximum": 1 },
"profileId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"displayName": { "type": ["string", "null"], "minLength": 1, "maxLength": 80 },
"profileType": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"prisonerId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"prisonerUserProfileId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"prisonerLastSaveTime": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"entityId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"profileXmlLength": { "type": ["integer", "null"], "minimum": 0, "maximum": 1048576 }
}
}
}
}
}
@@ -1,11 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMPlayerEconomyReadParameters",
"type": "object",
"additionalProperties": false,
"required": ["externalPlayerId", "limit"],
"properties": {
"externalPlayerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
"limit": { "type": "integer", "minimum": 1, "maximum": 16 }
}
}
@@ -1,26 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMPlayerEconomyReadResult",
"type": "object",
"additionalProperties": false,
"required": ["rows"],
"properties": {
"rows": {
"type": "array",
"maxItems": 16,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["externalPlayerId", "profileId", "bankAccountId", "currencyRowId", "currencyType", "accountBalance"],
"properties": {
"externalPlayerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
"profileId": { "type": "integer", "minimum": 0, "maximum": 2147483647 },
"bankAccountId": { "type": "integer", "minimum": 0, "maximum": 2147483647 },
"currencyRowId": { "type": "integer", "minimum": 0, "maximum": 2147483647 },
"currencyType": { "type": "integer", "minimum": 0, "maximum": 2147483647 },
"accountBalance": { "type": ["integer", "null"], "minimum": -2147483648, "maximum": 2147483647 }
}
}
}
}
}
@@ -1,10 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMPlayerSessionEnrichmentReadParameters",
"type": "object",
"additionalProperties": false,
"required": ["externalPlayerId"],
"properties": {
"externalPlayerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" }
}
}
@@ -1,30 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMPlayerSessionEnrichmentReadResult",
"type": "object",
"additionalProperties": false,
"required": ["rows"],
"properties": {
"rows": {
"type": "array",
"maxItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["externalPlayerId", "idType", "provider", "userCreationTime", "userLastLoginTime", "isBanned", "profileId", "displayName", "profileType", "prisonerId"],
"properties": {
"externalPlayerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
"idType": { "type": "string", "minLength": 1, "maxLength": 32 },
"provider": { "type": "string", "minLength": 1, "maxLength": 32 },
"userCreationTime": { "type": "string", "minLength": 1, "maxLength": 64 },
"userLastLoginTime": { "type": "string", "minLength": 1, "maxLength": 64 },
"isBanned": { "type": "integer", "minimum": 0, "maximum": 1 },
"profileId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"displayName": { "type": ["string", "null"], "minLength": 1, "maxLength": 80 },
"profileType": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"prisonerId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 }
}
}
}
}
}
@@ -1,11 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMPlayersReadParameters",
"type": "object",
"additionalProperties": false,
"required": ["limit", "offset"],
"properties": {
"limit": { "type": "integer", "minimum": 1, "maximum": 100 },
"offset": { "type": "integer", "minimum": 0, "maximum": 1000000 }
}
}
@@ -1,31 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMPlayersReadResult",
"type": "object",
"additionalProperties": false,
"required": ["rows"],
"properties": {
"rows": {
"type": "array",
"maxItems": 100,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["externalPlayerId", "idType", "provider", "userCreationTime", "userLastLoginTime", "isBanned", "profileId", "displayName", "profileType", "prisonerId", "entityId"],
"properties": {
"externalPlayerId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
"idType": { "type": "string", "minLength": 1, "maxLength": 32 },
"provider": { "type": "string", "minLength": 1, "maxLength": 32 },
"userCreationTime": { "type": "string", "minLength": 1, "maxLength": 64 },
"userLastLoginTime": { "type": "string", "minLength": 1, "maxLength": 64 },
"isBanned": { "type": "integer", "minimum": 0, "maximum": 1 },
"profileId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"displayName": { "type": ["string", "null"], "minLength": 1, "maxLength": 80 },
"profileType": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"prisonerId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"entityId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 }
}
}
}
}
}
@@ -1,12 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMPositionsReadParameters",
"type": "object",
"additionalProperties": false,
"required": ["subjectType", "limit", "offset"],
"properties": {
"subjectType": { "enum": ["all", "player", "vehicle", "flag"] },
"limit": { "type": "integer", "minimum": 1, "maximum": 500 },
"offset": { "type": "integer", "minimum": 0, "maximum": 1000000 }
}
}
@@ -1,34 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMPositionsReadResult",
"type": "object",
"additionalProperties": false,
"required": ["rows"],
"properties": {
"rows": {
"type": "array",
"maxItems": 500,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["subjectType", "subjectId", "externalPlayerId", "profileId", "prisonerId", "entityId", "vehicleEntityId", "vehicleAssetId", "flagElementId", "x", "y", "z", "sourceTimeValue", "sourceTimeKind"],
"properties": {
"subjectType": { "enum": ["player", "vehicle", "flag"] },
"subjectId": { "type": "string", "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
"externalPlayerId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
"profileId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"prisonerId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"entityId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"vehicleEntityId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"vehicleAssetId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"flagElementId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"x": { "type": ["number", "null"] },
"y": { "type": ["number", "null"] },
"z": { "type": ["number", "null"] },
"sourceTimeValue": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"sourceTimeKind": { "type": ["string", "null"], "enum": ["prisoner.last_save_time", null] }
}
}
}
}
}
@@ -1,12 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMSquadMembersReadParameters",
"type": "object",
"additionalProperties": false,
"required": ["squadId", "limit", "offset"],
"properties": {
"squadId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"limit": { "type": "integer", "minimum": 1, "maximum": 500 },
"offset": { "type": "integer", "minimum": 0, "maximum": 1000000 }
}
}
@@ -1,30 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMSquadMembersReadResult",
"type": "object",
"additionalProperties": false,
"required": ["rows"],
"properties": {
"rows": {
"type": "array",
"maxItems": 500,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["squadId", "userProfileId", "rankCode", "rankMeaning", "isLeader", "externalPlayerId", "idType", "provider", "displayName", "profileType"],
"properties": {
"squadId": { "type": "integer", "minimum": 0, "maximum": 2147483647 },
"userProfileId": { "type": "integer", "minimum": 0, "maximum": 2147483647 },
"rankCode": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"rankMeaning": { "type": ["string", "null"], "minLength": 1, "maxLength": 32 },
"isLeader": { "type": ["boolean", "null"] },
"externalPlayerId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
"idType": { "type": ["string", "null"], "minLength": 1, "maxLength": 32 },
"provider": { "type": ["string", "null"], "minLength": 1, "maxLength": 32 },
"displayName": { "type": ["string", "null"], "minLength": 1, "maxLength": 80 },
"profileType": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 }
}
}
}
}
}
@@ -1,11 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMSquadsReadParameters",
"type": "object",
"additionalProperties": false,
"required": ["limit", "offset"],
"properties": {
"limit": { "type": "integer", "minimum": 1, "maximum": 100 },
"offset": { "type": "integer", "minimum": 0, "maximum": 1000000 }
}
}
@@ -1,27 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMSquadsReadResult",
"type": "object",
"additionalProperties": false,
"required": ["rows"],
"properties": {
"rows": {
"type": "array",
"maxItems": 100,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["squadId", "squadName", "memberCount", "leaderProfileId", "leaderExternalPlayerId", "leaderRankCode", "leaderRankMeaning"],
"properties": {
"squadId": { "type": "integer", "minimum": 0, "maximum": 2147483647 },
"squadName": { "type": ["string", "null"], "minLength": 1, "maxLength": 80 },
"memberCount": { "type": "integer", "minimum": 0, "maximum": 1000 },
"leaderProfileId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"leaderExternalPlayerId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
"leaderRankCode": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"leaderRankMeaning": { "type": ["string", "null"], "minLength": 1, "maxLength": 32 }
}
}
}
}
}
@@ -1,31 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SCUMVehiclesReadResult",
"type": "object",
"additionalProperties": false,
"required": ["rows"],
"properties": {
"rows": {
"type": "array",
"maxItems": 500,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["vehicleEntityId", "entityId", "vehicleAssetId", "isVehicleFunctional", "vehicleLabel", "ownerProfileId", "ownerExternalPlayerId", "ownerSquadId", "x", "y", "z"],
"properties": {
"vehicleEntityId": { "type": "integer", "minimum": 0, "maximum": 2147483647 },
"entityId": { "type": "integer", "minimum": 0, "maximum": 2147483647 },
"vehicleAssetId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"isVehicleFunctional": { "type": ["integer", "null"], "minimum": 0, "maximum": 1 },
"vehicleLabel": { "type": ["string", "null"], "minLength": 1, "maxLength": 120 },
"ownerProfileId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"ownerExternalPlayerId": { "type": ["string", "null"], "minLength": 1, "maxLength": 96, "pattern": "^[A-Za-z0-9_.:-]{1,96}$" },
"ownerSquadId": { "type": ["integer", "null"], "minimum": 0, "maximum": 2147483647 },
"x": { "type": ["number", "null"] },
"y": { "type": ["number", "null"] },
"z": { "type": ["number", "null"] }
}
}
}
}
}