Implement SCUM direct data plane
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"maps": [
|
||||
{ "key": "server-settings", "format": "ini", "encoding": "utf-8", "fileName": "ServerSettings.ini", "section": "General", "fields": { "scum.ServerName": "serverName", "scum.ServerDescription": "serverDescription", "scum.WelcomeMessage": "welcomeMessage", "scum.MessageOfTheDay": "motd", "scum.MaxPlayers": "maxPlayers", "scum.ServerPlaystyle": "playstyle" } },
|
||||
{ "key": "admin-users", "format": "ini-list", "encoding": "utf-8", "fileName": "AdminUsers.ini", "fields": { "steamId": "steamId", "permissions": "permissions" } },
|
||||
{ "key": "banned-users", "format": "ini-list", "encoding": "utf-8", "fileName": "BannedUsers.ini", "fields": { "steamId": "steamId" } },
|
||||
{ "key": "whitelisted-users", "format": "ini-list", "encoding": "utf-8", "fileName": "WhitelistedUsers.ini", "fields": { "steamId": "steamId" } },
|
||||
{ "key": "economy-override", "format": "json", "encoding": "utf-8", "fileName": "EconomyOverride.json", "rootPath": "economy-override", "fields": { "traders": "traders", "tradeable-code-prices": "tradeable-code-prices" } },
|
||||
{ "key": "raid-times", "format": "json", "encoding": "utf-8", "fileName": "RaidTimes.json", "rootPath": "raiding-times", "fields": { "Weekdays": "weekdays", "Weekend": "weekend" } },
|
||||
{ "key": "notifications", "format": "json", "encoding": "utf-8", "fileName": "Notifications.json", "rootPath": "Notifications", "fields": { "Notifications": "notifications" } }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"encoding": "utf-16le",
|
||||
"lineEnding": "crlf",
|
||||
"timestampFormat": "yyyy.MM.dd-HH.mm.ss",
|
||||
"parsers": [
|
||||
{ "key": "login", "eventType": "scum.login", "pattern": "^([^:]+): '([^ ]+) ([^:]+):(.+)\\([0-9]+\\)' logged in at: X=([-0-9.]+) Y=([-0-9.]+) Z=([-0-9.]+)$", "fields": ["occurredAt", "ip", "steamId", "displayName", "session", "x", "y", "z"] },
|
||||
{ "key": "logout", "eventType": "scum.logout", "pattern": "^([^:]+): '([^ ]+) ([^:]+):(.+)\\([0-9]+\\)' logged out at: X=([-0-9.]+) Y=([-0-9.]+) Z=([-0-9.]+)$", "fields": ["occurredAt", "ip", "steamId", "displayName", "session", "x", "y", "z"] },
|
||||
{ "key": "chat", "eventType": "scum.chat", "pattern": "^([^:]+): '([^:]+):(.+)\\([0-9]+\\)' '([^:]+): (.*)'$", "fields": ["occurredAt", "steamId", "displayName", "channel", "message"] },
|
||||
{ "key": "admin", "eventType": "scum.admin", "pattern": "^([^:]+): '([^:]+):(.+)\\([0-9]+\\)' Command: '(.*)'$", "fields": ["occurredAt", "steamId", "displayName", "command"] },
|
||||
{ "key": "vehicle-destruction", "eventType": "scum.vehicle.destruction", "pattern": "^([^:]+): \\[VehicleInactiveTimerReached\\] ([^.]+)\\. VehicleId: ([0-9]+)\\. Owner: (.+)\\. Location: X=([-0-9.]+) Y=([-0-9.]+) Z=([-0-9.]+)$", "fields": ["occurredAt", "vehicleClass", "vehicleId", "owner", "x", "y", "z"] }
|
||||
]
|
||||
}
|
||||
@@ -292,6 +292,10 @@
|
||||
"targetKey": "scum-database",
|
||||
"parameterSchemaRef": "schemas/bridge/queries/scum-player-profile.parameters.schema.json",
|
||||
"resultSchemaRef": "schemas/bridge/queries/scum-player-profile.result.schema.json",
|
||||
"sqlRef": "sql/scum-db-v57/users.sql",
|
||||
"targetTable": "scum_users",
|
||||
"upsertKeys": ["userProfileId"],
|
||||
"columnMappings": { "userProfileId": "userProfileId", "steamId": "steamId", "gamePlayerId": "gamePlayerId", "displayName": "displayName", "famePoints": "famePoints", "normalBalance": "normalBalance", "goldBalance": "goldBalance", "x": "x", "y": "y", "z": "z", "lastSaveTime": "lastSaveTime", "lastLoginTime": "lastLoginTime", "lastLogoutTime": "lastLogoutTime", "isAlive": "isAlive" },
|
||||
"maxRows": 500,
|
||||
"timeoutSeconds": 15
|
||||
},
|
||||
@@ -304,6 +308,10 @@
|
||||
"targetKey": "scum-database",
|
||||
"parameterSchemaRef": "schemas/bridge/queries/scum-squads.parameters.schema.json",
|
||||
"resultSchemaRef": "schemas/bridge/queries/scum-squads.result.schema.json",
|
||||
"sqlRef": "sql/scum-db-v57/squads.sql",
|
||||
"targetTable": "scum_squads",
|
||||
"upsertKeys": ["squadId"],
|
||||
"columnMappings": { "squadId": "squadId", "name": "name", "leaderProfileId": "leaderProfileId", "leaderPlayerId": "leaderPlayerId", "memberCount": "memberCount", "score": "score", "memberLimit": "memberLimit", "lastMemberLoginTime": "lastMemberLoginTime", "lastMemberLogoutTime": "lastMemberLogoutTime" },
|
||||
"maxRows": 500,
|
||||
"timeoutSeconds": 15
|
||||
},
|
||||
@@ -316,6 +324,10 @@
|
||||
"targetKey": "scum-database",
|
||||
"parameterSchemaRef": "schemas/bridge/queries/scum-squad-members.parameters.schema.json",
|
||||
"resultSchemaRef": "schemas/bridge/queries/scum-squad-members.result.schema.json",
|
||||
"sqlRef": "sql/scum-db-v57/squad-members.sql",
|
||||
"targetTable": "scum_squad_members",
|
||||
"upsertKeys": ["squadId", "userProfileId"],
|
||||
"columnMappings": { "squadId": "squadId", "userProfileId": "userProfileId", "gamePlayerId": "gamePlayerId", "steamId": "steamId", "displayName": "displayName", "rank": "rank", "isLeader": "isLeader" },
|
||||
"maxRows": 500,
|
||||
"timeoutSeconds": 15
|
||||
},
|
||||
@@ -328,6 +340,10 @@
|
||||
"targetKey": "scum-database",
|
||||
"parameterSchemaRef": "schemas/bridge/queries/scum-vehicles.parameters.schema.json",
|
||||
"resultSchemaRef": "schemas/bridge/queries/scum-vehicles.result.schema.json",
|
||||
"sqlRef": "sql/scum-db-v57/vehicles.sql",
|
||||
"targetTable": "scum_vehicles",
|
||||
"upsertKeys": ["vehicleId"],
|
||||
"columnMappings": { "vehicleId": "vehicleId", "entityId": "entityId", "className": "className", "label": "label", "x": "x", "y": "y", "z": "z", "lastAccessTime": "lastAccessTime", "isFunctional": "isFunctional" },
|
||||
"maxRows": 500,
|
||||
"timeoutSeconds": 15
|
||||
},
|
||||
@@ -340,20 +356,68 @@
|
||||
"targetKey": "scum-database",
|
||||
"parameterSchemaRef": "schemas/bridge/queries/scum-flags.parameters.schema.json",
|
||||
"resultSchemaRef": "schemas/bridge/queries/scum-flags.result.schema.json",
|
||||
"sqlRef": "sql/scum-db-v57/flags.sql",
|
||||
"targetTable": "scum_flags",
|
||||
"upsertKeys": ["flagId"],
|
||||
"columnMappings": { "flagId": "flagId", "entityId": "entityId", "baseId": "baseId", "ownerProfileId": "ownerProfileId", "ownerPlayerId": "ownerPlayerId", "overtakerProfileId": "overtakerProfileId", "overtakeEndTime": "overtakeEndTime", "x": "x", "y": "y", "z": "z" },
|
||||
"maxRows": 500,
|
||||
"timeoutSeconds": 15
|
||||
},
|
||||
{
|
||||
"key": "scum.positions",
|
||||
"title": "Read SCUM current player, vehicle, and flag coordinates",
|
||||
"title": "Read SCUM v57 player, vehicle, and base map points",
|
||||
"permission": "server.game-client.read",
|
||||
"engine": "sqlite",
|
||||
"transportKey": "scum-database",
|
||||
"targetKey": "scum-database",
|
||||
"parameterSchemaRef": "schemas/bridge/queries/scum-positions.parameters.schema.json",
|
||||
"resultSchemaRef": "schemas/bridge/queries/scum-positions.result.schema.json",
|
||||
"sqlRef": "sql/scum-db-v57/map-points.sql",
|
||||
"targetTable": "scum_map_points",
|
||||
"upsertKeys": ["subjectType", "subjectId"],
|
||||
"columnMappings": { "subjectType": "subjectType", "subjectId": "subjectId", "userProfileId": "userProfileId", "gamePlayerId": "gamePlayerId", "vehicleId": "vehicleId", "baseId": "baseId", "x": "x", "y": "y", "z": "z", "observedAt": "observedAt" },
|
||||
"maxRows": 500,
|
||||
"timeoutSeconds": 15
|
||||
},
|
||||
{
|
||||
"key": "scum.activity",
|
||||
"title": "Read SCUM v57 active tasks",
|
||||
"permission": "server.game-client.read",
|
||||
"engine": "sqlite",
|
||||
"transportKey": "scum-database",
|
||||
"targetKey": "scum-database",
|
||||
"parameterSchemaRef": "schemas/bridge/queries/scum-activity.parameters.schema.json",
|
||||
"resultSchemaRef": "schemas/bridge/queries/scum-activity.result.schema.json",
|
||||
"sqlRef": "sql/scum-db-v57/activity.sql",
|
||||
"targetTable": "scum_activity_events",
|
||||
"upsertKeys": ["activityId"],
|
||||
"columnMappings": { "activityId": "activityId", "activityType": "activityType", "userProfileId": "userProfileId", "mapId": "mapId", "subject": "subject", "sequenceIndex": "sequenceIndex", "occurredAt": "occurredAt", "state": "state" },
|
||||
"maxRows": 500,
|
||||
"timeoutSeconds": 15
|
||||
},
|
||||
{
|
||||
"key": "scum.gifts",
|
||||
"title": "Read SCUM v57 finished_timed_gift_spawner records",
|
||||
"permission": "server.game-client.read",
|
||||
"engine": "sqlite",
|
||||
"transportKey": "scum-database",
|
||||
"targetKey": "scum-database",
|
||||
"parameterSchemaRef": "schemas/bridge/queries/scum-gifts.parameters.schema.json",
|
||||
"resultSchemaRef": "schemas/bridge/queries/scum-gifts.result.schema.json",
|
||||
"sqlRef": "sql/scum-db-v57/gifts.sql",
|
||||
"targetTable": "scum_gift_catalogs",
|
||||
"upsertKeys": ["giftId"],
|
||||
"columnMappings": { "giftId": "giftId", "giftType": "giftType", "userProfileId": "userProfileId", "mapId": "mapId", "spawnTime": "spawnTime", "spawnAt": "spawnAt", "displayName": "displayName" },
|
||||
"maxRows": 500,
|
||||
"timeoutSeconds": 15
|
||||
}
|
||||
],
|
||||
"dataPacks": [
|
||||
{
|
||||
"key": "scum-db-v57",
|
||||
"databaseUserVersion": 57,
|
||||
"logParserRefs": ["data-packs/scum-db-v57/log-parsers.json"],
|
||||
"configMapRefs": ["data-packs/scum-db-v57/config-maps.json"]
|
||||
}
|
||||
],
|
||||
"operationTemplates": [
|
||||
@@ -538,7 +602,8 @@
|
||||
],
|
||||
"queryTemplateKeys": [
|
||||
"scum.player.profile",
|
||||
"scum.positions"
|
||||
"scum.positions",
|
||||
"scum.activity"
|
||||
],
|
||||
"operationKeys": [
|
||||
"player.fame.set",
|
||||
@@ -575,10 +640,9 @@
|
||||
"flags"
|
||||
],
|
||||
"queryTemplateKeys": [
|
||||
"scum.player.profile",
|
||||
"scum.positions",
|
||||
"scum.vehicles",
|
||||
"scum.flags",
|
||||
"scum.positions"
|
||||
"scum.flags"
|
||||
],
|
||||
"featureKeys": [
|
||||
"trajectory.collect"
|
||||
@@ -589,6 +653,9 @@
|
||||
"snapshotTypes": [
|
||||
"players"
|
||||
],
|
||||
"queryTemplateKeys": [
|
||||
"scum.gifts"
|
||||
],
|
||||
"operationKeys": [
|
||||
"reward.deliver",
|
||||
"player.notify"
|
||||
@@ -605,7 +672,9 @@
|
||||
"scum.squad-members",
|
||||
"scum.vehicles",
|
||||
"scum.flags",
|
||||
"scum.positions"
|
||||
"scum.positions",
|
||||
"scum.activity",
|
||||
"scum.gifts"
|
||||
],
|
||||
"operationKeys": [
|
||||
"player.fame.set",
|
||||
@@ -693,6 +762,46 @@
|
||||
{
|
||||
"path": "bin/scum-start.cmd",
|
||||
"mode": 448
|
||||
},
|
||||
{
|
||||
"path": "sql/scum-db-v57/users.sql",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "sql/scum-db-v57/squads.sql",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "sql/scum-db-v57/squad-members.sql",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "sql/scum-db-v57/vehicles.sql",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "sql/scum-db-v57/flags.sql",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "sql/scum-db-v57/activity.sql",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "sql/scum-db-v57/gifts.sql",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "sql/scum-db-v57/map-points.sql",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "data-packs/scum-db-v57/log-parsers.json",
|
||||
"mode": 384
|
||||
},
|
||||
{
|
||||
"path": "data-packs/scum-db-v57/config-maps.json",
|
||||
"mode": 384
|
||||
}
|
||||
],
|
||||
"productionLifecycle": {
|
||||
@@ -773,7 +882,7 @@
|
||||
"trajectory.collect"
|
||||
]
|
||||
},
|
||||
{
|
||||
{
|
||||
"key": "gifts",
|
||||
"title": "礼包管理",
|
||||
"path": "/gifts",
|
||||
@@ -785,9 +894,10 @@
|
||||
"server.game-client.read",
|
||||
"server.game-client.command"
|
||||
],
|
||||
"bridgeActions": [
|
||||
"server.instances.read"
|
||||
],
|
||||
"bridgeActions": [
|
||||
"server.instances.read",
|
||||
"remote.access.request"
|
||||
],
|
||||
"featureKeys": [
|
||||
"reward.delivery"
|
||||
]
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMActivityParameters",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"userProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMActivityResult",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["rows"],
|
||||
"properties": {
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"maxItems": 500,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["activityId", "activityType", "state"],
|
||||
"properties": {
|
||||
"activityId": { "type": "string", "minLength": 1, "maxLength": 160 },
|
||||
"activityType": { "type": "string", "minLength": 1, "maxLength": 64 },
|
||||
"userProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"mapId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"subject": { "type": "string", "minLength": 1, "maxLength": 240 },
|
||||
"sequenceIndex": { "type": "integer" },
|
||||
"occurredAt": { "type": "string", "format": "date-time" },
|
||||
"state": { "type": "string", "minLength": 1, "maxLength": 64 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncated": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -17,9 +17,9 @@
|
||||
"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"] },
|
||||
"baseId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"overtakerProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"overtakeEndTime": { "type": "string", "format": "date-time" },
|
||||
"x": { "type": "number" },
|
||||
"y": { "type": "number" },
|
||||
"z": { "type": "number" }
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMGiftsParameters",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"userProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "SCUMGiftsResult",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["rows"],
|
||||
"properties": {
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"maxItems": 500,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["giftId", "giftType", "spawnTime"],
|
||||
"properties": {
|
||||
"giftId": { "type": "string", "minLength": 1, "maxLength": 160 },
|
||||
"giftType": { "type": "string", "minLength": 1, "maxLength": 64 },
|
||||
"userProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"mapId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"spawnTime": { "type": "integer" },
|
||||
"spawnAt": { "type": "string", "format": "date-time" },
|
||||
"displayName": { "type": "string", "minLength": 1, "maxLength": 80 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncated": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -25,7 +25,10 @@
|
||||
"x": { "type": "number" },
|
||||
"y": { "type": "number" },
|
||||
"z": { "type": "number" },
|
||||
"lastSaveTime": { "type": "string", "format": "date-time" }
|
||||
"lastSaveTime": { "type": "string", "format": "date-time" },
|
||||
"lastLoginTime": { "type": "string", "format": "date-time" },
|
||||
"lastLogoutTime": { "type": "string", "format": "date-time" },
|
||||
"isAlive": { "type": "integer", "minimum": 0, "maximum": 1 }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"subjectType": { "enum": ["player", "vehicle", "flag"] },
|
||||
"subjectType": { "enum": ["player", "vehicle", "base", "flag"] },
|
||||
"subjectId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"limit": { "type": "integer", "minimum": 1, "maximum": 500 }
|
||||
}
|
||||
|
||||
+3
-2
@@ -13,15 +13,16 @@
|
||||
"additionalProperties": false,
|
||||
"required": ["subjectType", "subjectId", "x", "y"],
|
||||
"properties": {
|
||||
"subjectType": { "enum": ["player", "vehicle", "flag"] },
|
||||
"subjectType": { "enum": ["player", "vehicle", "base", "flag"] },
|
||||
"subjectId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"gamePlayerId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"vehicleId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"baseId": { "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" }
|
||||
"observedAt": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+4
-1
@@ -18,7 +18,10 @@
|
||||
"leaderProfileId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"leaderPlayerId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"memberCount": { "type": "integer", "minimum": 0, "maximum": 1000 },
|
||||
"score": { "type": "number" }
|
||||
"score": { "type": "number" },
|
||||
"memberLimit": { "type": "integer", "minimum": 0, "maximum": 1000 },
|
||||
"lastMemberLoginTime": { "type": "string", "format": "date-time" },
|
||||
"lastMemberLogoutTime": { "type": "string", "format": "date-time" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+3
-1
@@ -22,7 +22,9 @@
|
||||
"squadId": { "type": "string", "minLength": 1, "maxLength": 96 },
|
||||
"x": { "type": "number" },
|
||||
"y": { "type": "number" },
|
||||
"z": { "type": "number" }
|
||||
"z": { "type": "number" },
|
||||
"lastAccessTime": { "type": "string", "format": "date-time" },
|
||||
"isFunctional": { "type": "integer", "minimum": 0, "maximum": 1 }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
SELECT
|
||||
'active-task' AS activityType,
|
||||
CAST(task.id AS TEXT) AS activityId,
|
||||
CAST(task.user_profile_id AS TEXT) AS userProfileId,
|
||||
CAST(task.map_id AS TEXT) AS mapId,
|
||||
available.task_data_asset_path AS subject,
|
||||
task.sequence_index AS sequenceIndex,
|
||||
NULL AS occurredAt,
|
||||
CASE WHEN available.was_ever_completed = 1 THEN 'completed-before' ELSE 'active' END AS state
|
||||
FROM active_task active
|
||||
JOIN tracking_data_set task ON task.id = active.id
|
||||
JOIN available_task available ON available.id = active.available_task_id
|
||||
WHERE (:userProfileId IS NULL OR CAST(task.user_profile_id AS TEXT) = :userProfileId)
|
||||
ORDER BY task.id DESC
|
||||
LIMIT COALESCE(:limit, 500)
|
||||
@@ -0,0 +1,18 @@
|
||||
SELECT
|
||||
CAST(flag.element_id AS TEXT) AS flagId,
|
||||
CAST(flag.element_id AS TEXT) AS entityId,
|
||||
CAST(element.base_id AS TEXT) AS baseId,
|
||||
CAST(element.owner_profile_id AS TEXT) AS ownerProfileId,
|
||||
CAST(owner.prisoner_id AS TEXT) AS ownerPlayerId,
|
||||
CAST(flag.overtaker_user_profile_id AS TEXT) AS overtakerProfileId,
|
||||
datetime(flag.overtake_end_time, 'unixepoch') AS overtakeEndTime,
|
||||
element.location_x AS x,
|
||||
element.location_y AS y,
|
||||
element.location_z AS z
|
||||
FROM base_element_flag flag
|
||||
JOIN base_element element ON element.element_id = flag.element_id
|
||||
LEFT JOIN user_profile owner ON owner.id = element.owner_profile_id
|
||||
WHERE (:flagId IS NULL OR CAST(flag.element_id AS TEXT) = :flagId)
|
||||
AND (:ownerProfileId IS NULL OR CAST(element.owner_profile_id AS TEXT) = :ownerProfileId)
|
||||
ORDER BY flag.element_id
|
||||
LIMIT COALESCE(:limit, 500)
|
||||
@@ -0,0 +1,14 @@
|
||||
SELECT
|
||||
'finished-timed-gift' AS giftType,
|
||||
CAST(gift.user_profile_id AS TEXT) || ':' || CAST(gift.map_id AS TEXT) || ':' || CAST(gift.spawn_time AS TEXT) AS giftId,
|
||||
CAST(gift.user_profile_id AS TEXT) AS userProfileId,
|
||||
CAST(gift.map_id AS TEXT) AS mapId,
|
||||
gift.spawn_time AS spawnTime,
|
||||
datetime(gift.spawn_time, 'unixepoch') AS spawnAt,
|
||||
COALESCE(profile.name, user.name, '') AS displayName
|
||||
FROM finished_timed_gift_spawner gift
|
||||
LEFT JOIN user_profile profile ON profile.id = gift.user_profile_id
|
||||
LEFT JOIN user ON user.id = profile.user_id
|
||||
WHERE (:userProfileId IS NULL OR CAST(gift.user_profile_id AS TEXT) = :userProfileId)
|
||||
ORDER BY gift.spawn_time DESC
|
||||
LIMIT COALESCE(:limit, 500)
|
||||
@@ -0,0 +1,62 @@
|
||||
SELECT
|
||||
'player' AS subjectType,
|
||||
CAST(profile.id AS TEXT) AS subjectId,
|
||||
CAST(profile.id AS TEXT) AS userProfileId,
|
||||
CAST(prisoner.id AS TEXT) AS gamePlayerId,
|
||||
NULL AS vehicleId,
|
||||
NULL AS baseId,
|
||||
entity.location_x AS x,
|
||||
entity.location_y AS y,
|
||||
entity.location_z AS z,
|
||||
datetime(prisoner.last_save_time, 'unixepoch') AS observedAt
|
||||
FROM user_profile profile
|
||||
JOIN prisoner ON prisoner.id = profile.prisoner_id
|
||||
JOIN prisoner_entity ON prisoner_entity.prisoner_id = prisoner.id
|
||||
JOIN entity ON entity.id = prisoner_entity.entity_id
|
||||
WHERE (:subjectType IS NULL OR :subjectType = 'player')
|
||||
UNION ALL
|
||||
SELECT
|
||||
'vehicle' AS subjectType,
|
||||
CAST(spawner.vehicle_entity_id AS TEXT) AS subjectId,
|
||||
NULL AS userProfileId,
|
||||
NULL AS gamePlayerId,
|
||||
CAST(spawner.vehicle_entity_id AS TEXT) AS vehicleId,
|
||||
NULL AS baseId,
|
||||
entity.location_x AS x,
|
||||
entity.location_y AS y,
|
||||
entity.location_z AS z,
|
||||
datetime(spawner.vehicle_last_access_time, 'unixepoch') AS observedAt
|
||||
FROM vehicle_spawner spawner
|
||||
JOIN entity ON entity.id = spawner.vehicle_entity_id
|
||||
WHERE (:subjectType IS NULL OR :subjectType = 'vehicle')
|
||||
UNION ALL
|
||||
SELECT
|
||||
'base' AS subjectType,
|
||||
CAST(base.id AS TEXT) AS subjectId,
|
||||
CAST(base.owner_user_profile_id AS TEXT) AS userProfileId,
|
||||
NULL AS gamePlayerId,
|
||||
NULL AS vehicleId,
|
||||
CAST(base.id AS TEXT) AS baseId,
|
||||
base.location_x AS x,
|
||||
base.location_y AS y,
|
||||
0 AS z,
|
||||
NULL AS observedAt
|
||||
FROM base
|
||||
WHERE (:subjectType IS NULL OR :subjectType = 'base')
|
||||
UNION ALL
|
||||
SELECT
|
||||
'flag' AS subjectType,
|
||||
CAST(flag.element_id AS TEXT) AS subjectId,
|
||||
CAST(element.owner_profile_id AS TEXT) AS userProfileId,
|
||||
CAST(owner.prisoner_id AS TEXT) AS gamePlayerId,
|
||||
NULL AS vehicleId,
|
||||
CAST(element.base_id AS TEXT) AS baseId,
|
||||
element.location_x AS x,
|
||||
element.location_y AS y,
|
||||
element.location_z AS z,
|
||||
datetime(flag.overtake_end_time, 'unixepoch') AS observedAt
|
||||
FROM base_element_flag flag
|
||||
JOIN base_element element ON element.element_id = flag.element_id
|
||||
LEFT JOIN user_profile owner ON owner.id = element.owner_profile_id
|
||||
WHERE (:subjectType IS NULL OR :subjectType = 'flag')
|
||||
LIMIT COALESCE(:limit, 500)
|
||||
@@ -0,0 +1,15 @@
|
||||
SELECT
|
||||
CAST(member.squad_id AS TEXT) AS squadId,
|
||||
CAST(member.user_profile_id AS TEXT) AS userProfileId,
|
||||
CAST(profile.prisoner_id AS TEXT) AS gamePlayerId,
|
||||
user.id AS steamId,
|
||||
COALESCE(profile.name, user.name, '') AS displayName,
|
||||
CAST(member.rank AS TEXT) AS rank,
|
||||
CASE WHEN member.rank = 4 THEN 1 ELSE 0 END AS isLeader
|
||||
FROM squad_member member
|
||||
JOIN user_profile profile ON profile.id = member.user_profile_id
|
||||
LEFT JOIN user ON user.id = profile.user_id
|
||||
WHERE (:squadId IS NULL OR CAST(member.squad_id AS TEXT) = :squadId)
|
||||
AND (:userProfileId IS NULL OR CAST(member.user_profile_id AS TEXT) = :userProfileId)
|
||||
ORDER BY member.squad_id, member.rank DESC, profile.name
|
||||
LIMIT COALESCE(:limit, 500)
|
||||
@@ -0,0 +1,19 @@
|
||||
SELECT
|
||||
CAST(s.id AS TEXT) AS squadId,
|
||||
COALESCE(s.name, '') AS name,
|
||||
CAST(leader.user_profile_id AS TEXT) AS leaderProfileId,
|
||||
CAST(leader_profile.prisoner_id AS TEXT) AS leaderPlayerId,
|
||||
COUNT(member.id) AS memberCount,
|
||||
s.score AS score,
|
||||
s.member_limit AS memberLimit,
|
||||
s.last_member_login_time AS lastMemberLoginTime,
|
||||
s.last_member_logout_time AS lastMemberLogoutTime
|
||||
FROM squad s
|
||||
LEFT JOIN squad_member member ON member.squad_id = s.id
|
||||
LEFT JOIN squad_member leader ON leader.squad_id = s.id AND leader.rank = 4
|
||||
LEFT JOIN user_profile leader_profile ON leader_profile.id = leader.user_profile_id
|
||||
WHERE (:squadId IS NULL OR CAST(s.id AS TEXT) = :squadId)
|
||||
AND (:search IS NULL OR COALESCE(s.name, '') LIKE '%' || :search || '%')
|
||||
GROUP BY s.id
|
||||
ORDER BY s.score DESC, s.id
|
||||
LIMIT COALESCE(:limit, 500)
|
||||
@@ -0,0 +1,28 @@
|
||||
SELECT
|
||||
CAST(up.id AS TEXT) AS userProfileId,
|
||||
u.id AS steamId,
|
||||
CAST(p.id AS TEXT) AS gamePlayerId,
|
||||
COALESCE(up.name, u.name, '') AS displayName,
|
||||
up.fame_points AS famePoints,
|
||||
MAX(CASE WHEN currency.currency_type = 1 THEN currency.account_balance END) AS normalBalance,
|
||||
MAX(CASE WHEN currency.currency_type = 2 THEN currency.account_balance END) AS goldBalance,
|
||||
e.location_x AS x,
|
||||
e.location_y AS y,
|
||||
e.location_z AS z,
|
||||
datetime(p.last_save_time, 'unixepoch') AS lastSaveTime,
|
||||
up.last_login_time AS lastLoginTime,
|
||||
up.last_logout_time AS lastLogoutTime,
|
||||
p.is_alive AS isAlive
|
||||
FROM user_profile up
|
||||
JOIN user u ON u.id = up.user_id
|
||||
LEFT JOIN prisoner p ON p.id = up.prisoner_id
|
||||
LEFT JOIN prisoner_entity pe ON pe.prisoner_id = p.id
|
||||
LEFT JOIN entity e ON e.id = pe.entity_id
|
||||
LEFT JOIN bank_account_registry account ON account.account_owner_user_profile_id = up.id
|
||||
LEFT JOIN bank_account_registry_currencies currency ON currency.bank_account_id = account.id
|
||||
WHERE (:userProfileId IS NULL OR CAST(up.id AS TEXT) = :userProfileId)
|
||||
AND (:steamId IS NULL OR u.id = :steamId)
|
||||
AND (:search IS NULL OR COALESCE(up.name, u.name, '') LIKE '%' || :search || '%')
|
||||
GROUP BY up.id
|
||||
ORDER BY up.last_login_time DESC
|
||||
LIMIT COALESCE(:limit, 500)
|
||||
@@ -0,0 +1,16 @@
|
||||
SELECT
|
||||
CAST(spawner.vehicle_entity_id AS TEXT) AS vehicleId,
|
||||
CAST(spawner.vehicle_entity_id AS TEXT) AS entityId,
|
||||
entity.class AS className,
|
||||
spawner.vehicle_alias AS label,
|
||||
entity.location_x AS x,
|
||||
entity.location_y AS y,
|
||||
entity.location_z AS z,
|
||||
datetime(spawner.vehicle_last_access_time, 'unixepoch') AS lastAccessTime,
|
||||
spawner.is_vehicle_functional AS isFunctional
|
||||
FROM vehicle_spawner spawner
|
||||
JOIN entity ON entity.id = spawner.vehicle_entity_id
|
||||
WHERE (:vehicleId IS NULL OR CAST(spawner.vehicle_entity_id AS TEXT) = :vehicleId)
|
||||
AND (:search IS NULL OR spawner.vehicle_alias LIKE '%' || :search || '%' OR entity.class LIKE '%' || :search || '%')
|
||||
ORDER BY spawner.vehicle_last_access_time DESC
|
||||
LIMIT COALESCE(:limit, 500)
|
||||
@@ -271,6 +271,11 @@
|
||||
"items": { "$ref": "#/$defs/gameClientBridgeQueryTemplate" },
|
||||
"maxItems": 128
|
||||
},
|
||||
"dataPacks": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/gameClientBridgeDataPack" },
|
||||
"maxItems": 32
|
||||
},
|
||||
"operationTemplates": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/gameClientBridgeOperationTemplate" },
|
||||
@@ -360,10 +365,25 @@
|
||||
"targetKey": { "$ref": "#/$defs/logicalKey" },
|
||||
"parameterSchemaRef": { "$ref": "#/$defs/relativeJsonRef" },
|
||||
"resultSchemaRef": { "$ref": "#/$defs/relativeJsonRef" },
|
||||
"sqlRef": { "$ref": "#/$defs/relativeSqlRef" },
|
||||
"targetTable": { "type": "string", "pattern": "^scum_[a-z][a-z0-9_]{0,62}$" },
|
||||
"upsertKeys": { "type": "array", "items": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9._-]{0,79}$" }, "uniqueItems": true, "minItems": 1, "maxItems": 8 },
|
||||
"columnMappings": { "type": "object", "minProperties": 1, "maxProperties": 64, "propertyNames": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9._-]{0,79}$" }, "additionalProperties": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9._-]{0,79}$" } },
|
||||
"maxRows": { "type": "integer", "minimum": 1, "maximum": 500 },
|
||||
"timeoutSeconds": { "type": "integer", "minimum": 1, "maximum": 60 }
|
||||
}
|
||||
},
|
||||
"gameClientBridgeDataPack": {
|
||||
"type": "object",
|
||||
"required": ["key", "databaseUserVersion", "logParserRefs", "configMapRefs"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": { "$ref": "#/$defs/logicalKey" },
|
||||
"databaseUserVersion": { "type": "integer", "minimum": 1, "maximum": 1000000 },
|
||||
"logParserRefs": { "type": "array", "items": { "$ref": "#/$defs/relativeJsonRef" }, "uniqueItems": true, "minItems": 1, "maxItems": 64 },
|
||||
"configMapRefs": { "type": "array", "items": { "$ref": "#/$defs/relativeJsonRef" }, "uniqueItems": true, "minItems": 1, "maxItems": 64 }
|
||||
}
|
||||
},
|
||||
"gameClientBridgeOperationSafety": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@@ -440,6 +460,10 @@
|
||||
"type": "string",
|
||||
"pattern": "^(?!/)(?![A-Za-z]:)(?!.*://)(?!.*\\.\\.)[a-zA-Z0-9_./-]+\\.json$"
|
||||
},
|
||||
"relativeSqlRef": {
|
||||
"type": "string",
|
||||
"pattern": "^(?!/)(?![A-Za-z]:)(?!.*://)(?!.*\\.\\.)sql/[a-zA-Z0-9_./-]+\\.sql$"
|
||||
},
|
||||
"runCapability": {
|
||||
"enum": [
|
||||
"process.install",
|
||||
|
||||
@@ -134,6 +134,10 @@ function isSafeRelativeJsonRef(value: string): boolean {
|
||||
return /^(?!\/)(?![A-Za-z]:)(?!.*:\/\/)(?!.*\.\.)[a-zA-Z0-9_./-]+\.json$/.test(value);
|
||||
}
|
||||
|
||||
function isSafeRelativeSQLRef(value: string): boolean {
|
||||
return /^(?!\/)(?![A-Za-z]:)(?!.*:\/\/)(?!.*\.\.)sql\/[a-zA-Z0-9_./-]+\.sql$/.test(value);
|
||||
}
|
||||
|
||||
function isSafeRelativePathRef(value: string): boolean {
|
||||
return /^(?!\/)(?![A-Za-z]:)(?!.*:\/\/)(?!.*\.\.)[a-zA-Z0-9_./-]+$/.test(value);
|
||||
}
|
||||
@@ -672,6 +676,10 @@ export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
|
||||
targetKey?: string;
|
||||
parameterSchemaRef?: string;
|
||||
resultSchemaRef?: string;
|
||||
sqlRef?: string;
|
||||
targetTable?: string;
|
||||
upsertKeys?: string[];
|
||||
columnMappings?: Record<string, string>;
|
||||
maxRows?: number;
|
||||
timeoutSeconds?: number;
|
||||
};
|
||||
@@ -840,6 +848,24 @@ export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
|
||||
errors.push(`${location}.${field}: raw host paths and unsafe schema references are not allowed`);
|
||||
}
|
||||
}
|
||||
const persistsSCUMRows = queryTemplate.targetTable !== undefined || queryTemplate.sqlRef !== undefined || queryTemplate.upsertKeys !== undefined || queryTemplate.columnMappings !== undefined;
|
||||
if (persistsSCUMRows) {
|
||||
if (!queryTemplate.sqlRef || !isSafeRelativeSQLRef(queryTemplate.sqlRef)) {
|
||||
errors.push(`${location}.sqlRef: persisted SCUM queries must reference a package-relative .sql asset`);
|
||||
}
|
||||
if (!/^scum_[a-z][a-z0-9_]{0,62}$/.test(queryTemplate.targetTable ?? "")) {
|
||||
errors.push(`${location}.targetTable: persisted SCUM queries must target a scum_* table`);
|
||||
}
|
||||
if (!Array.isArray(queryTemplate.upsertKeys) || queryTemplate.upsertKeys.length === 0 || !queryTemplate.upsertKeys.every((key) => /^[A-Za-z][A-Za-z0-9._-]{0,79}$/.test(key))) {
|
||||
errors.push(`${location}.upsertKeys: persisted SCUM queries require non-empty safe keys`);
|
||||
}
|
||||
const mappings = queryTemplate.columnMappings;
|
||||
if (!mappings || typeof mappings !== "object" || Array.isArray(mappings) || Object.keys(mappings).length === 0 || !Object.entries(mappings).every(([target, source]) => /^[A-Za-z][A-Za-z0-9._-]{0,79}$/.test(target) && typeof source === "string" && /^[A-Za-z][A-Za-z0-9._-]{0,79}$/.test(source))) {
|
||||
errors.push(`${location}.columnMappings: persisted SCUM queries require safe target-to-source mappings`);
|
||||
} else if (Array.isArray(queryTemplate.upsertKeys) && !queryTemplate.upsertKeys.every((key) => key in mappings)) {
|
||||
errors.push(`${location}.upsertKeys: every upsert key must be declared in columnMappings`);
|
||||
}
|
||||
}
|
||||
if (!Number.isInteger(queryTemplate.maxRows) || (queryTemplate.maxRows ?? 0) < 1 || (queryTemplate.maxRows ?? 0) > 500) {
|
||||
errors.push(`${location}.maxRows: must be an integer between 1 and 500`);
|
||||
}
|
||||
@@ -990,6 +1016,59 @@ export function validateGameClientBridgeCatalog(manifest: unknown): string[] {
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateGameClientBridgeDataPacks(manifest: unknown, manifestDir: string, declaredAssets: Set<string>): string[] {
|
||||
if (typeof manifest !== "object" || manifest === null) return [];
|
||||
const dataPacks = (manifest as { gameClientBridge?: { dataPacks?: Array<{ key?: string; databaseUserVersion?: number; logParserRefs?: string[]; configMapRefs?: string[] }> } }).gameClientBridge?.dataPacks ?? [];
|
||||
const errors: string[] = [];
|
||||
const keys = new Set<string>();
|
||||
for (const [index, dataPack] of dataPacks.entries()) {
|
||||
const location = `manifest.gameClientBridge.dataPacks[${index}]`;
|
||||
if (!/^[A-Za-z][A-Za-z0-9._-]{0,79}$/.test(dataPack.key ?? "") || keys.has(dataPack.key ?? "")) errors.push(`${location}.key: must be a unique safe data-pack key`);
|
||||
keys.add(dataPack.key ?? "");
|
||||
if (!Number.isInteger(dataPack.databaseUserVersion) || (dataPack.databaseUserVersion ?? 0) < 1) errors.push(`${location}.databaseUserVersion: must be a positive SQLite user_version`);
|
||||
for (const field of ["logParserRefs", "configMapRefs"] as const) {
|
||||
const refs = dataPack[field];
|
||||
if (!Array.isArray(refs) || refs.length === 0) {
|
||||
errors.push(`${location}.${field}: must declare at least one package mapping asset`);
|
||||
continue;
|
||||
}
|
||||
for (const ref of refs) {
|
||||
if (!isSafeRelativeJsonRef(ref)) {
|
||||
errors.push(`${location}.${field}: must use package-relative JSON assets`);
|
||||
continue;
|
||||
}
|
||||
if (!declaredAssets.has(ref)) errors.push(`${location}.${field}: ${ref} must be declared in manifest.assetFiles`);
|
||||
const target = path.resolve(manifestDir, ref);
|
||||
if (!fs.existsSync(target) || !fs.statSync(target).isFile()) errors.push(`${location}.${field}: missing package mapping asset ${ref}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateGameClientBridgeSQLAssets(manifest: unknown, manifestDir: string, declaredAssets: Set<string>): string[] {
|
||||
if (typeof manifest !== "object" || manifest === null) return [];
|
||||
const templates = (manifest as { gameClientBridge?: { queryTemplates?: Array<{ sqlRef?: string }> } }).gameClientBridge?.queryTemplates ?? [];
|
||||
const errors: string[] = [];
|
||||
for (const [index, template] of templates.entries()) {
|
||||
if (!template.sqlRef) continue;
|
||||
const location = `manifest.gameClientBridge.queryTemplates[${index}].sqlRef`;
|
||||
if (!isSafeRelativeSQLRef(template.sqlRef)) {
|
||||
errors.push(`${location}: must be a package-relative .sql asset`);
|
||||
continue;
|
||||
}
|
||||
if (!declaredAssets.has(template.sqlRef)) errors.push(`${location}: ${template.sqlRef} must be declared in manifest.assetFiles`);
|
||||
const assetPath = path.resolve(manifestDir, template.sqlRef);
|
||||
if (!fs.existsSync(assetPath) || !fs.statSync(assetPath).isFile()) {
|
||||
errors.push(`${location}: missing SQL asset ${template.sqlRef}`);
|
||||
continue;
|
||||
}
|
||||
const body = fs.readFileSync(assetPath, "utf8").trim();
|
||||
if (!/^select\b/i.test(body) || /;\s*\S/.test(body) || /\b(?:insert|update|delete|drop|alter|create|attach|pragma)\b/i.test(body)) errors.push(`${location}: SQL assets must contain one read-only SELECT statement`);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function validateRuntimeLogEventCatalog(manifest: unknown): string[] {
|
||||
if (typeof manifest !== "object" || manifest === null) {
|
||||
return [];
|
||||
@@ -1378,6 +1457,8 @@ export function validateManifestFile(manifestPath: string): string[] {
|
||||
errors.push(...validateRuntimeLogEventSchemaFiles(manifest, manifestDir));
|
||||
const assetValidation = validateManifestAssetFiles(manifest, manifestDir);
|
||||
errors.push(...assetValidation.errors);
|
||||
errors.push(...validateGameClientBridgeSQLAssets(manifest, manifestDir, assetValidation.declared));
|
||||
errors.push(...validateGameClientBridgeDataPacks(manifest, manifestDir, assetValidation.declared));
|
||||
|
||||
for (const declaration of referencedLifecycleActions(manifest)) {
|
||||
if (!isSafeRelativeJsonRef(declaration.ref)) {
|
||||
|
||||
@@ -56,3 +56,10 @@ The bridge must not expose:
|
||||
# Client Manager lifecycle bridge
|
||||
|
||||
The bridge may request typed `deploy`, `start`, `stop`, `restart`, `status`, `update`, `rollback`, `revoke`, `retry`, or `uninstall` intents when Platform action gating says they are available. Results are safe logical projections with real job phase/progress and redacted recovery guidance. The bridge is not a transport for Run sessions, component keys, artifact bytes, machine paths, process IDs, sockets, or credentials; component registration and heartbeat remain component-to-Platform contracts outside the plugin page.
|
||||
## Plugin-packaged database data packs
|
||||
|
||||
Database-backed game data stays with the game plugin. A browser request names a declared query template and supplies only values accepted by its parameter schema; it never supplies a SQL statement or a database path.
|
||||
|
||||
For a persisted game dataset, a query template declares package-relative `sqlRef`, `targetTable`, `upsertKeys`, and `columnMappings`. `sqlRef` must name a declared `sql/.../*.sql` asset containing one read-only `SELECT` statement. `targetTable` is a `scum_*` relation and every upsert key must have a mapping. The Platform resolves and distributes the plugin asset, while Run executes the declared statement through the template's SQLite transport.
|
||||
|
||||
Version-specific log parsers and configuration maps are declared in `gameClientBridge.dataPacks`. Each pack names the SQLite `databaseUserVersion` plus JSON parser/map assets. These assets describe the game format; they do not contain host paths, credentials, or browser-provided SQL.
|
||||
|
||||
@@ -262,10 +262,21 @@ export interface GameClientBridgeQueryTemplateDeclaration {
|
||||
targetKey: string;
|
||||
parameterSchemaRef: string;
|
||||
resultSchemaRef: string;
|
||||
sqlRef?: string;
|
||||
targetTable?: string;
|
||||
upsertKeys?: string[];
|
||||
columnMappings?: Record<string, string>;
|
||||
maxRows: number;
|
||||
timeoutSeconds: number;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeDataPackDeclaration {
|
||||
key: string;
|
||||
databaseUserVersion: number;
|
||||
logParserRefs: string[];
|
||||
configMapRefs: string[];
|
||||
}
|
||||
|
||||
export type GameClientBridgeOperationKind = "rcon" | "sqlite-mutation";
|
||||
|
||||
export interface GameClientBridgeOperationSafety {
|
||||
@@ -337,6 +348,7 @@ export interface GameClientBridgeManifest {
|
||||
commands: GameClientBridgeCommandDeclaration[];
|
||||
snapshots: GameClientBridgeSnapshotDeclaration[];
|
||||
queryTemplates?: GameClientBridgeQueryTemplateDeclaration[];
|
||||
dataPacks?: GameClientBridgeDataPackDeclaration[];
|
||||
operationTemplates?: GameClientBridgeOperationTemplateDeclaration[];
|
||||
commandRetentionSeconds: number;
|
||||
maxCommands: number;
|
||||
|
||||
@@ -617,7 +617,7 @@ describe("plugin manifest validation", () => {
|
||||
expect(manifest.gameClientBridge.pages.find((page) => page.pageKey === "live-map")?.snapshotTypes).toEqual(expect.arrayContaining(["players", "vehicles", "flags"]));
|
||||
});
|
||||
|
||||
it("declares typed SCUM.db query templates without browser-visible SQL", () => {
|
||||
it("declares packaged SCUM v57 query templates without browser-provided SQL", () => {
|
||||
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
|
||||
permissions: string[];
|
||||
@@ -633,6 +633,10 @@ describe("plugin manifest validation", () => {
|
||||
targetKey: string;
|
||||
parameterSchemaRef: string;
|
||||
resultSchemaRef: string;
|
||||
sqlRef: string;
|
||||
targetTable: string;
|
||||
upsertKeys: string[];
|
||||
columnMappings: Record<string, string>;
|
||||
maxRows: number;
|
||||
timeoutSeconds: number;
|
||||
}>;
|
||||
@@ -641,7 +645,7 @@ describe("plugin manifest validation", () => {
|
||||
pages: Array<{ key: string; permissions?: string[]; bridgeActions?: string[] }>;
|
||||
runtimeProfiles?: { transportProfiles?: Array<{ key: string; kind: string; targetKey?: string; capabilities: string[] }> };
|
||||
};
|
||||
const expectedKeys = ["scum.player.profile", "scum.squads", "scum.squad-members", "scum.vehicles", "scum.flags", "scum.positions"];
|
||||
const expectedKeys = ["scum.player.profile", "scum.squads", "scum.squad-members", "scum.vehicles", "scum.flags", "scum.positions", "scum.activity", "scum.gifts"];
|
||||
const templatesByKey = new Map(manifest.gameClientBridge.queryTemplates.map((template) => [template.key, template]));
|
||||
expect([...templatesByKey.keys()]).toEqual(expect.arrayContaining(expectedKeys));
|
||||
expect(manifest.capabilities).toContain("remote.run.db.sqlite.query");
|
||||
@@ -655,7 +659,14 @@ describe("plugin manifest validation", () => {
|
||||
expect(template.engine).toBe("sqlite");
|
||||
expect(template.transportKey).toBe("scum-database");
|
||||
expect(template.targetKey).toBe("scum-database");
|
||||
expect(JSON.stringify(template).toLowerCase()).not.toMatch(/select\s|from\s|sqlite:|scum\.db|databasepath|hostpath|dsn/);
|
||||
expect(template.sqlRef).toMatch(/^sql\/scum-db-v57\/.+\.sql$/);
|
||||
expect(template.targetTable).toMatch(/^scum_/);
|
||||
expect(template.upsertKeys.length).toBeGreaterThan(0);
|
||||
expect(Object.keys(template.columnMappings).length).toBeGreaterThan(0);
|
||||
expect(template.upsertKeys.every((key) => key in template.columnMappings)).toBe(true);
|
||||
const sql = fs.readFileSync(path.join(pluginDir, template.sqlRef), "utf8");
|
||||
expect(sql).toMatch(/^SELECT\b/i);
|
||||
expect(sql).not.toMatch(/\b(?:INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|ATTACH|PRAGMA)\b/i);
|
||||
const parameters = JSON.parse(fs.readFileSync(path.join(pluginDir, template.parameterSchemaRef), "utf8"));
|
||||
const result = JSON.parse(fs.readFileSync(path.join(pluginDir, template.resultSchemaRef), "utf8"));
|
||||
expect(parameters).toMatchObject({ type: "object", additionalProperties: false });
|
||||
@@ -665,7 +676,7 @@ describe("plugin manifest validation", () => {
|
||||
const playersPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "players");
|
||||
const squadsPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "squads");
|
||||
const mapPage = manifest.gameClientBridge.pages.find((page) => page.pageKey === "live-map");
|
||||
expect(playersPage?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.player.profile", "scum.positions"]));
|
||||
expect(playersPage?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.player.profile", "scum.positions", "scum.activity"]));
|
||||
expect(squadsPage?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.squads", "scum.squad-members", "scum.flags"]));
|
||||
expect(mapPage?.queryTemplateKeys).toEqual(expect.arrayContaining(["scum.vehicles", "scum.flags", "scum.positions"]));
|
||||
for (const pageKey of ["players", "squads", "live-map"]) {
|
||||
@@ -675,6 +686,19 @@ describe("plugin manifest validation", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("packages SCUM v57 UTF-16LE log and config maps", () => {
|
||||
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as { gameClientBridge: { dataPacks: Array<{ key: string; databaseUserVersion: number; logParserRefs: string[]; configMapRefs: string[] }> } };
|
||||
const pack = manifest.gameClientBridge.dataPacks.find((candidate) => candidate.key === "scum-db-v57");
|
||||
expect(pack).toMatchObject({ databaseUserVersion: 57 });
|
||||
const logParsers = JSON.parse(fs.readFileSync(path.join(pluginDir, pack!.logParserRefs[0]), "utf8"));
|
||||
const configMaps = JSON.parse(fs.readFileSync(path.join(pluginDir, pack!.configMapRefs[0]), "utf8"));
|
||||
expect(logParsers.encoding).toBe("utf-16le");
|
||||
expect(logParsers.parsers.map((parser: { key: string }) => parser.key)).toEqual(expect.arrayContaining(["login", "logout", "chat", "admin", "vehicle-destruction"]));
|
||||
const serverSettings = configMaps.maps.find((map: { key: string }) => map.key === "server-settings");
|
||||
expect(serverSettings.fields).toMatchObject({ "scum.WelcomeMessage": "welcomeMessage", "scum.MessageOfTheDay": "motd" });
|
||||
});
|
||||
|
||||
it("declares typed SCUM RCON operations without arbitrary command inputs", () => {
|
||||
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
|
||||
@@ -905,6 +929,20 @@ describe("plugin manifest validation", () => {
|
||||
expect(errors.some((error) => error.includes("timeoutSeconds"))).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects inline SQL and incomplete SCUM row declarations", () => {
|
||||
const inlineErrors = validateTemporaryBridgeManifest((manifest) => {
|
||||
Object.assign(manifest.gameClientBridge.queryTemplates![0], { sqlRef: "SELECT * FROM user_profile", targetTable: "scum_users", upsertKeys: ["userProfileId"], columnMappings: { userProfileId: "userProfileId" } });
|
||||
});
|
||||
expect(inlineErrors.some((error) => error.includes("sqlRef"))).toBe(true);
|
||||
|
||||
const incompleteErrors = validateTemporaryBridgeManifest((manifest) => {
|
||||
Object.assign(manifest.gameClientBridge.queryTemplates![0], { sqlRef: "sql/scum-db-v57/users.sql", targetTable: "users", upsertKeys: [], columnMappings: {} });
|
||||
});
|
||||
expect(incompleteErrors.some((error) => error.includes("targetTable"))).toBe(true);
|
||||
expect(incompleteErrors.some((error) => error.includes("upsertKeys"))).toBe(true);
|
||||
expect(incompleteErrors.some((error) => error.includes("columnMappings"))).toBe(true);
|
||||
});
|
||||
|
||||
it("requires query templates to match a declared sqlite transport target and capability", () => {
|
||||
const targetErrors = validateTemporaryBridgeManifest((manifest) => {
|
||||
manifest.gameClientBridge.queryTemplates![0].targetKey = "db/other";
|
||||
|
||||
Reference in New Issue
Block a user