From e31308120480f56dfaecc4124f9601839db4fd5e Mon Sep 17 00:00:00 2001 From: npc0-hue Date: Thu, 13 Aug 2026 09:20:42 +0800 Subject: [PATCH] Add SCUM world query assets --- .../tasks.md | 2 +- .../assets/scum-live/queries/flags-read.json | 29 ++++++ .../scum-live/queries/positions-read.json | 30 ++++++ .../scum-live/queries/squad-members-read.json | 30 ++++++ .../assets/scum-live/queries/squads-read.json | 29 ++++++ .../scum-live/queries/vehicles-read.json | 29 ++++++ .../examples/scum-server-plugin/manifest.json | 95 +++++++++++++++++++ .../flags-read.parameters.schema.json | 11 +++ .../scum-live/flags-read.result.schema.json | 34 +++++++ .../positions-read.parameters.schema.json | 12 +++ .../positions-read.result.schema.json | 34 +++++++ .../squad-members-read.parameters.schema.json | 12 +++ .../squad-members-read.result.schema.json | 30 ++++++ .../squads-read.parameters.schema.json | 11 +++ .../scum-live/squads-read.result.schema.json | 27 ++++++ .../vehicles-read.parameters.schema.json | 11 +++ .../vehicles-read.result.schema.json | 31 ++++++ plugins/tests/manifest-validation.test.ts | 17 +++- 18 files changed, 471 insertions(+), 3 deletions(-) create mode 100644 plugins/examples/scum-server-plugin/assets/scum-live/queries/flags-read.json create mode 100644 plugins/examples/scum-server-plugin/assets/scum-live/queries/positions-read.json create mode 100644 plugins/examples/scum-server-plugin/assets/scum-live/queries/squad-members-read.json create mode 100644 plugins/examples/scum-server-plugin/assets/scum-live/queries/squads-read.json create mode 100644 plugins/examples/scum-server-plugin/assets/scum-live/queries/vehicles-read.json create mode 100644 plugins/examples/scum-server-plugin/schemas/scum-live/flags-read.parameters.schema.json create mode 100644 plugins/examples/scum-server-plugin/schemas/scum-live/flags-read.result.schema.json create mode 100644 plugins/examples/scum-server-plugin/schemas/scum-live/positions-read.parameters.schema.json create mode 100644 plugins/examples/scum-server-plugin/schemas/scum-live/positions-read.result.schema.json create mode 100644 plugins/examples/scum-server-plugin/schemas/scum-live/squad-members-read.parameters.schema.json create mode 100644 plugins/examples/scum-server-plugin/schemas/scum-live/squad-members-read.result.schema.json create mode 100644 plugins/examples/scum-server-plugin/schemas/scum-live/squads-read.parameters.schema.json create mode 100644 plugins/examples/scum-server-plugin/schemas/scum-live/squads-read.result.schema.json create mode 100644 plugins/examples/scum-server-plugin/schemas/scum-live/vehicles-read.parameters.schema.json create mode 100644 plugins/examples/scum-server-plugin/schemas/scum-live/vehicles-read.result.schema.json diff --git a/openspec/changes/replace-scum-projections-with-real-data-management/tasks.md b/openspec/changes/replace-scum-projections-with-real-data-management/tasks.md index 0763eaa..abc04cc 100644 --- a/openspec/changes/replace-scum-projections-with-real-data-management/tasks.md +++ b/openspec/changes/replace-scum-projections-with-real-data-management/tasks.md @@ -60,7 +60,7 @@ - [x] 3.3 Capture sanitized authentic login-log fixtures from the active service and bind their expected events to server, Run binding, plugin version, parser version/digest, a transport cursor `(source identity, stream generation, sequence)`, and a separate privacy-safe logical event identity stable across rotation overlap. - [x] 3.4 Implement the versioned SCUM login/logout parser and tests for successful login/logout, failed login, partial/undecodable/oversized/malformed lines, copy-truncate/rotation overlap under a new generation, Run restart/resume, duplicate delivery, and out-of-order delivery while discarding IP/network material before storage or logical fingerprinting. - [x] 3.5 Add parameterized, read-only player identity/detail/economy/session-enrichment query assets and exact result schemas only for joins and fields proven by the probe. -- [ ] 3.6 Add parameterized squad/member, vehicle, flag/territory, and position query assets and exact result schemas, keeping ambiguous ranks, ownership, coordinates, and missing numeric values null. +- [x] 3.6 Add parameterized squad/member, vehicle, flag/territory, and position query assets and exact result schemas, keeping ambiguous ranks, ownership, coordinates, and missing numeric values null. - [ ] 3.7 Add query-asset tests for single SELECT/CTE or approved introspection boundaries, parameter binding, pagination/cursors, timeout/row/byte limits, schema-version matching, and rejection of DDL, mutation, `ATTACH`, extension loading, write PRAGMAs, and multi-statement input. - [ ] 3.8 Package the authorized SCUM map asset, identity/version, verified world bounds, layer metadata, and coordinate transform, with fixture tests for known points, out-of-bounds/non-finite coordinates, and adapter incompatibility. - [ ] 3.9 Declare only verified typed RCON templates for supported Fame/currency/notification/gift operations and a version-scoped gift item catalog; omit any command whose execution and confirmation semantics remain unknown. diff --git a/plugins/examples/scum-server-plugin/assets/scum-live/queries/flags-read.json b/plugins/examples/scum-server-plugin/assets/scum-live/queries/flags-read.json new file mode 100644 index 0000000..9ae0187 --- /dev/null +++ b/plugins/examples/scum-server-plugin/assets/scum-live/queries/flags-read.json @@ -0,0 +1,29 @@ +{ + "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"] + } +} diff --git a/plugins/examples/scum-server-plugin/assets/scum-live/queries/positions-read.json b/plugins/examples/scum-server-plugin/assets/scum-live/queries/positions-read.json new file mode 100644 index 0000000..ecc0d2c --- /dev/null +++ b/plugins/examples/scum-server-plugin/assets/scum-live/queries/positions-read.json @@ -0,0 +1,30 @@ +{ + "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." + } +} diff --git a/plugins/examples/scum-server-plugin/assets/scum-live/queries/squad-members-read.json b/plugins/examples/scum-server-plugin/assets/scum-live/queries/squad-members-read.json new file mode 100644 index 0000000..385d061 --- /dev/null +++ b/plugins/examples/scum-server-plugin/assets/scum-live/queries/squad-members-read.json @@ -0,0 +1,30 @@ +{ + "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"] + } +} diff --git a/plugins/examples/scum-server-plugin/assets/scum-live/queries/squads-read.json b/plugins/examples/scum-server-plugin/assets/scum-live/queries/squads-read.json new file mode 100644 index 0000000..8104c14 --- /dev/null +++ b/plugins/examples/scum-server-plugin/assets/scum-live/queries/squads-read.json @@ -0,0 +1,29 @@ +{ + "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"] + } +} diff --git a/plugins/examples/scum-server-plugin/assets/scum-live/queries/vehicles-read.json b/plugins/examples/scum-server-plugin/assets/scum-live/queries/vehicles-read.json new file mode 100644 index 0000000..6cd8c27 --- /dev/null +++ b/plugins/examples/scum-server-plugin/assets/scum-live/queries/vehicles-read.json @@ -0,0 +1,29 @@ +{ + "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"] + } +} diff --git a/plugins/examples/scum-server-plugin/manifest.json b/plugins/examples/scum-server-plugin/manifest.json index 6276048..7dc1c9c 100644 --- a/plugins/examples/scum-server-plugin/manifest.json +++ b/plugins/examples/scum-server-plugin/manifest.json @@ -482,6 +482,81 @@ "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 } ] }, @@ -558,6 +633,26 @@ { "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 } ], "productionLifecycle": { diff --git a/plugins/examples/scum-server-plugin/schemas/scum-live/flags-read.parameters.schema.json b/plugins/examples/scum-server-plugin/schemas/scum-live/flags-read.parameters.schema.json new file mode 100644 index 0000000..6266940 --- /dev/null +++ b/plugins/examples/scum-server-plugin/schemas/scum-live/flags-read.parameters.schema.json @@ -0,0 +1,11 @@ +{ + "$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 } + } +} diff --git a/plugins/examples/scum-server-plugin/schemas/scum-live/flags-read.result.schema.json b/plugins/examples/scum-server-plugin/schemas/scum-live/flags-read.result.schema.json new file mode 100644 index 0000000..da65a7d --- /dev/null +++ b/plugins/examples/scum-server-plugin/schemas/scum-live/flags-read.result.schema.json @@ -0,0 +1,34 @@ +{ + "$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"] } + } + } + } + } +} diff --git a/plugins/examples/scum-server-plugin/schemas/scum-live/positions-read.parameters.schema.json b/plugins/examples/scum-server-plugin/schemas/scum-live/positions-read.parameters.schema.json new file mode 100644 index 0000000..aabc299 --- /dev/null +++ b/plugins/examples/scum-server-plugin/schemas/scum-live/positions-read.parameters.schema.json @@ -0,0 +1,12 @@ +{ + "$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 } + } +} diff --git a/plugins/examples/scum-server-plugin/schemas/scum-live/positions-read.result.schema.json b/plugins/examples/scum-server-plugin/schemas/scum-live/positions-read.result.schema.json new file mode 100644 index 0000000..c12370b --- /dev/null +++ b/plugins/examples/scum-server-plugin/schemas/scum-live/positions-read.result.schema.json @@ -0,0 +1,34 @@ +{ + "$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] } + } + } + } + } +} diff --git a/plugins/examples/scum-server-plugin/schemas/scum-live/squad-members-read.parameters.schema.json b/plugins/examples/scum-server-plugin/schemas/scum-live/squad-members-read.parameters.schema.json new file mode 100644 index 0000000..4b1d7ce --- /dev/null +++ b/plugins/examples/scum-server-plugin/schemas/scum-live/squad-members-read.parameters.schema.json @@ -0,0 +1,12 @@ +{ + "$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 } + } +} diff --git a/plugins/examples/scum-server-plugin/schemas/scum-live/squad-members-read.result.schema.json b/plugins/examples/scum-server-plugin/schemas/scum-live/squad-members-read.result.schema.json new file mode 100644 index 0000000..3fbdec1 --- /dev/null +++ b/plugins/examples/scum-server-plugin/schemas/scum-live/squad-members-read.result.schema.json @@ -0,0 +1,30 @@ +{ + "$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 } + } + } + } + } +} diff --git a/plugins/examples/scum-server-plugin/schemas/scum-live/squads-read.parameters.schema.json b/plugins/examples/scum-server-plugin/schemas/scum-live/squads-read.parameters.schema.json new file mode 100644 index 0000000..f9d1174 --- /dev/null +++ b/plugins/examples/scum-server-plugin/schemas/scum-live/squads-read.parameters.schema.json @@ -0,0 +1,11 @@ +{ + "$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 } + } +} diff --git a/plugins/examples/scum-server-plugin/schemas/scum-live/squads-read.result.schema.json b/plugins/examples/scum-server-plugin/schemas/scum-live/squads-read.result.schema.json new file mode 100644 index 0000000..3ed1e39 --- /dev/null +++ b/plugins/examples/scum-server-plugin/schemas/scum-live/squads-read.result.schema.json @@ -0,0 +1,27 @@ +{ + "$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 } + } + } + } + } +} diff --git a/plugins/examples/scum-server-plugin/schemas/scum-live/vehicles-read.parameters.schema.json b/plugins/examples/scum-server-plugin/schemas/scum-live/vehicles-read.parameters.schema.json new file mode 100644 index 0000000..748c86b --- /dev/null +++ b/plugins/examples/scum-server-plugin/schemas/scum-live/vehicles-read.parameters.schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "SCUMVehiclesReadParameters", + "type": "object", + "additionalProperties": false, + "required": ["limit", "offset"], + "properties": { + "limit": { "type": "integer", "minimum": 1, "maximum": 500 }, + "offset": { "type": "integer", "minimum": 0, "maximum": 1000000 } + } +} diff --git a/plugins/examples/scum-server-plugin/schemas/scum-live/vehicles-read.result.schema.json b/plugins/examples/scum-server-plugin/schemas/scum-live/vehicles-read.result.schema.json new file mode 100644 index 0000000..666f146 --- /dev/null +++ b/plugins/examples/scum-server-plugin/schemas/scum-live/vehicles-read.result.schema.json @@ -0,0 +1,31 @@ +{ + "$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"] } + } + } + } + } +} diff --git a/plugins/tests/manifest-validation.test.ts b/plugins/tests/manifest-validation.test.ts index a17df9f..847cdb8 100644 --- a/plugins/tests/manifest-validation.test.ts +++ b/plugins/tests/manifest-validation.test.ts @@ -218,12 +218,22 @@ describe("plugin manifest validation", () => { expect(JSON.stringify(manifest.scumLiveData).toLowerCase()).not.toMatch(/select\s+.+from|sqlite:\/\/|mysql:\/\/|password|credential|socket|hostpath/); }); - it("packages SCUM player read query assets with exact bounded result schemas", () => { + it("packages SCUM live read query assets with exact bounded result schemas", () => { const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin"); const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as GamePluginManifest & { scumLiveData: SCUMLiveDataManifestDeclaration }; const assetPaths = new Set(manifest.assetFiles?.map((file) => file.path) ?? []); const queries = manifest.scumLiveData.sqliteQueries ?? []; - expect(queries.map((query) => query.key)).toEqual(["scum-players-read", "scum-player-details-read", "scum-player-economy-read", "scum-player-session-enrichment-read"]); + expect(queries.map((query) => query.key)).toEqual([ + "scum-players-read", + "scum-player-details-read", + "scum-player-economy-read", + "scum-player-session-enrichment-read", + "scum-squads-read", + "scum-squad-members-read", + "scum-vehicles-read", + "scum-flags-read", + "scum-positions-read" + ]); for (const query of queries) { expect(assetPaths.has(query.assetPath)).toBe(true); @@ -235,6 +245,9 @@ describe("plugin manifest validation", () => { expect(asset.safety).toMatchObject({ queryOnly: true, readOnlyConnection: true }); expect(asset.statement).toMatch(/^(WITH|SELECT)\b/i); expect(asset.statement).not.toMatch(/;|\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|ATTACH)\b/i); + if (["scum-squads-read", "scum-squad-members-read", "scum-vehicles-read", "scum-flags-read"].includes(query.key)) { + expect(asset.statement).toContain("CAST(NULL"); + } for (const schemaRef of [query.parameterSchemaRef, query.resultSchemaRef]) { expect(fs.existsSync(path.join(pluginDir, schemaRef))).toBe(true);