From 6ba3036de621654bc239d9609ee20c061dfd3b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BD=C3=A1=C4=8Dek?= Date: Sun, 1 Nov 2026 19:29:54 +0100 Subject: [PATCH] Add telemetry dashboard and automatic server names; record Workshop 0.0.137 --- CHANGELOG.md | 7 +- .../Scripts/Game/ERR/00_Core/ERR_Telemetry.c | 2 +- .../ERR/00_Core/ERR_TelemetryServerName.c | 24 + .../launch-source-sha256.json | 721 ++++++++++++++++++ .../native-checks.log | 15 + .../native-probe.c | 29 + .../runner.json | 34 + .../telemetry-server-name-2026-11-01/setup.py | 16 + .../verification.json | 12 + docs/telemetry.md | 34 + services/telemetry-worker/README.md | 72 +- .../migrations/0003_dashboard_receipts.sql | 3 + .../migrations/0004_reported_server_names.sql | 24 + services/telemetry-worker/package.json | 2 +- .../telemetry-worker/src/dashboard-api.js | 44 ++ .../telemetry-worker/src/dashboard-client.js | 209 +++++ services/telemetry-worker/src/dashboard.js | 36 + services/telemetry-worker/src/index.js | 27 +- .../test/dashboard-client.test.js | 73 ++ services/telemetry-worker/test/worker.test.js | 98 ++- services/telemetry-worker/wrangler.jsonc | 3 + tests/test_telemetry_server_name.py | 39 + tools/check.py | 1 + 23 files changed, 1509 insertions(+), 16 deletions(-) create mode 100644 addons/core/Scripts/Game/ERR/00_Core/ERR_TelemetryServerName.c create mode 100644 docs/evidence/telemetry-server-name-2026-11-01/launch-source-sha256.json create mode 100644 docs/evidence/telemetry-server-name-2026-11-01/native-checks.log create mode 100644 docs/evidence/telemetry-server-name-2026-11-01/native-probe.c create mode 100644 docs/evidence/telemetry-server-name-2026-11-01/runner.json create mode 100644 docs/evidence/telemetry-server-name-2026-11-01/setup.py create mode 100644 docs/evidence/telemetry-server-name-2026-11-01/verification.json create mode 100644 services/telemetry-worker/migrations/0003_dashboard_receipts.sql create mode 100644 services/telemetry-worker/migrations/0004_reported_server_names.sql create mode 100644 services/telemetry-worker/src/dashboard-api.js create mode 100644 services/telemetry-worker/src/dashboard-client.js create mode 100644 services/telemetry-worker/src/dashboard.js create mode 100644 services/telemetry-worker/test/dashboard-client.test.js create mode 100644 tests/test_telemetry_server_name.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ae0d5d..50baa69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog -Latest published Workshop version: **0.0.136** +Latest published Workshop version: **0.0.137** + +## 2026-11-01 — Statistics dashboard + +- Added a private statistics webpage with server win/loss records, last received reports, mission details, player totals and event history, with manual refresh. +- Mission telemetry automatically reports server names and keeps dashboard labels updated when servers are renamed. ## 2026-11-01 — Checkpoints and reconnects diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_Telemetry.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_Telemetry.c index 72f8566..66ab4d7 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_Telemetry.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_Telemetry.c @@ -309,7 +309,7 @@ class ERR_Telemetry json.WriteValue("schemaVersion", 1); string eventId = UUID.GenV4(); json.WriteValue("eventId", eventId); json.WriteValue("missionId", m_MissionId); - json.WriteValue("serverId", m_State.serverId); json.WriteValue("serverName", m_Config.ServerName); + json.WriteValue("serverId", m_State.serverId); json.WriteValue("serverName", ERR_TelemetryServerName.Resolve(m_Config.ServerName)); json.WriteValue("type", type); json.WriteValue("occurredAt", System.GetUnixTime()); json.WriteValue("partial", m_Partial); int elapsedSeconds = Math.Max(0, (System.GetTickCount() - m_Started) / 1000); diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_TelemetryServerName.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_TelemetryServerName.c new file mode 100644 index 0000000..a5991cc --- /dev/null +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_TelemetryServerName.c @@ -0,0 +1,24 @@ +// Resolve on the reporting server for every new event: lobby metadata can be +// unavailable during initial startup. Never infer identity from the scenario. +class ERR_TelemetryServerName +{ + static string Resolve(string configured) + { + string name = configured; + name.TrimInPlace(); + if (name.IsEmpty()) + { + ServerConfig config = ServerLobbyApi.GetServerConfig(); + if (config) name = config.GetName(); + name.TrimInPlace(); + } + if (name.IsEmpty()) + { + ServerInfo info = GetGame().GetServerInfo(); + if (info) name = info.GetName(); + name.TrimInPlace(); + } + if (name.Length() > 120) name = name.Substring(0, 120); + return name; + } +} diff --git a/docs/evidence/telemetry-server-name-2026-11-01/launch-source-sha256.json b/docs/evidence/telemetry-server-name-2026-11-01/launch-source-sha256.json new file mode 100644 index 0000000..4dd122b --- /dev/null +++ b/docs/evidence/telemetry-server-name-2026-11-01/launch-source-sha256.json @@ -0,0 +1,721 @@ +{ + "addons/core/AI/BehaviorTrees/ERR/MortarRapidFire.bt": "16b263bcb2fb3ae361367a600753a829425620d2e6b350a891c51fba47437ab6", + "addons/core/AI/BehaviorTrees/ERR/MortarRapidFire.bt.meta": "f2a44ece28891240e6bfa1d01af29428adcadbe82c80e287eb33cd5e58f1f90a", + "addons/core/Assets/Items/ERR_Atlas/AtlasCover.emat": "0a536cd31d1917965ab36d33e273a881be384eb8b863eb5da03d1666e141d45a", + "addons/core/Assets/Items/ERR_Atlas/AtlasCover.emat.meta": "9f4d59cd6d103547e3e494baf7cc8fce70f31f87c66fe6d26b70011bbd27e38b", + "addons/core/Assets/Items/ERR_Atlas/AtlasPaper.emat": "88a2c1061c01eb96ce8a0f753bd3e89e1d7af01471ccc4abb4774a7aa9a532fb", + "addons/core/Assets/Items/ERR_Atlas/AtlasPaper.emat.meta": "e0093b7df155499444f93a1148a07f66cc755489192db17c81f4035d17378efb", + "addons/core/Assets/Items/ERR_Atlas/AtlasPrint.emat": "9877c5b175c5f30dd80bea36c711b21ab38219aa7075adf9ae79932e1d4c76bc", + "addons/core/Assets/Items/ERR_Atlas/AtlasPrint.emat.meta": "33a9eaa49871813f2e8537f071ea0f5a4422eb08d536355a47314b6b6d845b19", + "addons/core/Assets/Items/ERR_Atlas/AtlasTurn.agf": "48dc42771b7e44994839dfabe9b007c23e37bd9be900196c757f42351e5c6b04", + "addons/core/Assets/Items/ERR_Atlas/AtlasTurn.agf.meta": "c081aec31c927c0c374dcc0f8eb3eaec498f71cb0e2ac746879b926f8dc7d00e", + "addons/core/Assets/Items/ERR_Atlas/AtlasTurn.agr": "8ec57b278c9d63324d00f7af62abf75230c5ef631d1cc070661585ee235c8a1f", + "addons/core/Assets/Items/ERR_Atlas/AtlasTurn.agr.meta": "b045e26f86116703d1b3599fdf747b413cc0064d8101eff659ded0355eff31fe", + "addons/core/Assets/Items/ERR_Atlas/AtlasWire.emat": "372247aac7b84ddf6c66e276d03321d22496e65726f55e490ee7287a90151318", + "addons/core/Assets/Items/ERR_Atlas/AtlasWire.emat.meta": "ea10c7bd630b31febf511fb28a42cc73597fcc1e3010597dc074c651813ede94", + "addons/core/Assets/Items/ERR_Atlas/ERR_Atlas.fbx": "03a654900103878223ab5c4a42e71d17022ca4885ced71b99c67d8c91652b5fc", + "addons/core/Assets/Items/ERR_Atlas/ERR_Atlas.txo": "9838bcb2e4ff1cd649586535ca7d5ad2497e6118cdfb76b8aadabded9cafb938", + "addons/core/Assets/Items/ERR_Atlas/ERR_Atlas.xob": "a4602d65e84cec1b23b6b09b5bc5847cbd52efe82925986c6eaf1c43bec1ac78", + "addons/core/Assets/Items/ERR_Atlas/ERR_Atlas.xob.meta": "04f4039be7950ca393ea74469898dabcfec4dc8b8f9e08068898c00333445878", + "addons/core/Assets/Items/ERR_Atlas/ERR_AtlasLeaf.fbx": "4ca13bbeafaa0dc0bf007ae9024b8d6c2a5279afc62e0ebc227e2bec2ad67a30", + "addons/core/Assets/Items/ERR_Atlas/ERR_AtlasLeaf.txo": "8ed37705192d3d9b18f26b5deb51af6f2a6d694e7e2dcdbd04b317497610b50d", + "addons/core/Assets/Items/ERR_Atlas/ERR_AtlasLeaf.xob": "fe6ea401412fdbb3bb46367cf7dc9215f7d01c6373c8ac954cbf23f2a6927bf7", + "addons/core/Assets/Items/ERR_Atlas/ERR_AtlasLeaf.xob.meta": "492a44fc323d3df469753d4bbbc35ceb3d362e734f48083ba633b3de5dd42d8b", + "addons/core/Configs/ERR/AdminMenus.conf": "5a4b405060135efd0f7faf913c7f061f6250a1e6e9d27f2bb394281d784b20e6", + "addons/core/Configs/ERR/AdminMenus.conf.meta": "5faf5ddc0a51c1d0f82d60af00d91edc22fe9c39aac50d8d04b772a2c9271be1", + "addons/core/Configs/ERR/Content/Registry.conf": "546a5a407c9e6c25c9157ae4392898fa3f40c04c7956649e62201f9ac0ba187c", + "addons/core/Configs/ERR/Content/Registry.conf.meta": "046aa988bf8e972afcf7a60b6f2312f605f016f98625dbbd9cd75ebf9ea88800", + "addons/core/Configs/ERR/EveronSites.conf": "781a9a3f4d2d4ea5b913fbfd15c2f1a991efdee7e033b7ba4241ad632609f52f", + "addons/core/Configs/ERR/EveronSites.conf.meta": "d5fe68025ef4e7c1e16082cf5873b7c7172eacbfaef4ab29b7e0e3c98f8dca56", + "addons/core/Configs/ERR/Factions/MissionVehicles.conf": "5f74f4a7089c7e7b54ad62463159ce8f0bab25a6b85268dc3872ea6001e048fb", + "addons/core/Configs/ERR/Factions/MissionVehicles.conf.meta": "00c368b6271d3440cc8bf627b03a384d247635723923fc5ad2104b8b4007b035", + "addons/core/Configs/ERR/Factions/US.conf": "4c3da063abb6e3ee39a6e4e0c5a9ffd5eae9d209d58470ea18b45dba5528db1b", + "addons/core/Configs/ERR/Factions/US.conf.meta": "a05469aef028e9e17eba545e81e2af2887b59f0fef17dac51a23c7b8155e381b", + "addons/core/Configs/ERR/Factions/USSR.conf": "407ea27f80a64c5184c089d43cdd0bdabfc5974c84f1d2a245081fdf2afa3f30", + "addons/core/Configs/ERR/Factions/USSR.conf.meta": "057397d97096346791b51b7b13fbe18af218bcf78e9984d9c01b49a2403de611", + "addons/core/Configs/ERR/GameOverScreens.conf": "1bb83c26a57d64595e91c3ba7926987e22b33dce2c6c876016e3ec2d04f9eb2a", + "addons/core/Configs/ERR/GameOverScreens.conf.meta": "eb654b745c0b97b41c991694569ef4877a3c2315e0826f9556337cd68690843e", + "addons/core/Configs/ERR/ObserverInput.conf": "1efd2e1823f80e6b8a3e4a697cdeb72dd87888ec5a2d16ae289937575b880072", + "addons/core/Configs/ERR/ObserverInput.conf.meta": "9714dce6c38ee81bcc513c8ebef51d7af63fd15bcdda2e40946937c625dc3ba4", + "addons/core/Configs/ERR/Telemetry.conf": "12d895ba4fcd159e23c2f3e94dc59e6f1e54702470cf0b1dad7d04f15572cf7a", + "addons/core/Configs/ERR/Telemetry.conf.meta": "afd52b410ce1a1026f4b0900b10aae2ee50c30764a212a59668233f4f8b8c5a1", + "addons/core/Configs/ERR/Tests/LegacySites.conf": "3864eb638d0ed4dd313013d2d787f975ae6ce4bbe238569919f76b73d5f7ca42", + "addons/core/Configs/ERR/Tests/LegacySites.conf.meta": "097ac7daef4fc18344699a8471fdd77f9d158f8c70bab394068d7a08e4777ac0", + "addons/core/Configs/ERR/VanillaFactions.conf": "db890b61b48ee15484041fd2245a862d9fa9e7ba21b1229c281ff37c448a35b1", + "addons/core/Configs/ERR/VanillaFactions.conf.meta": "ac77725c0c21974991ef47a466b88122e939de8b46fd23e68889e7674cbb2bfd", + "addons/core/Materials/ERRM_MapRenderTarget.emat": "4ab251037d40a850e3767c0de85136842bcd4fd130eda47bf8e19352b0d030e3", + "addons/core/Materials/ERRM_MapRenderTarget.emat.meta": "5ee1a705f8011a44020ca2691f5ccc17d44bf596fef9a36e98d1f267eb58fab0", + "addons/core/Missions/ERR_Everon.conf": "c6cce48484301a76b81f3a0cd76a4012f5808b901754d4114034e92beeefcff3", + "addons/core/Missions/ERR_Everon.conf.meta": "0a23c98bf83873536c65fa17d38dea33a1d7b9e4ab210f90633ff688abcc551f", + "addons/core/Prefabs/Characters/Core/Character_Base.et": "74a6f03a1e76173a0cfa20b23c711d02e70bac810d6743e16c60476bfe5be457", + "addons/core/Prefabs/Characters/Core/Character_Base.et.meta": "c6b142d1985719236268cdf12ac522fee4b1206871a6f22b568d9198d1fcc077", + "addons/core/Prefabs/ERR/AssaultParachute.et": "b556997989605e2a05982418b7e9f26d12d427eb3ea6e3bc50e85c67ea7aa4b8", + "addons/core/Prefabs/ERR/AssaultParachute.et.meta": "e9dea2d533ad1ce6ce689e0e2815c97a6382e1b90c35ce1efa88a885527e8e18", + "addons/core/Prefabs/ERR/CombatEpinephrine.et": "aa57d78bae861dc4a0fc509f7916190d2f2f0b36a1c3867f9bf9a071717cd7f5", + "addons/core/Prefabs/ERR/CombatEpinephrine.et.meta": "0e19ebd1dd4d81f739125a4dcbff898424ffe39572c2668bd56455d939b3f604", + "addons/core/Prefabs/ERR/ConvoyCrew.et": "29a525a29605386dfcf292dc2316e9fc7650ce39db9fbaa4ce4372ad0de66db4", + "addons/core/Prefabs/ERR/ConvoyCrew.et.meta": "0a909a15b7dcca34202ded136b3bdbbe8898153c9589e1cce1388433d362da02", + "addons/core/Prefabs/ERR/CrashSite.et": "8d06afe6e2db8986f06a7c5d6649e52df009f9eead7345bf2b477f50a0725b3c", + "addons/core/Prefabs/ERR/CrashSite.et.meta": "0f54fe80ed96394374c1922e1d0b05f7e0200ce5b979ff58325b033f51641f69", + "addons/core/Prefabs/ERR/CrashWreck.et": "f5de4785c0e3cdf02f1d5372e9b01171bd5a37acb9c36aa2a94bae3f429b2272", + "addons/core/Prefabs/ERR/CrashWreck.et.meta": "bde3299bdbbddb91d2ef471b61035b6cced29899cff4cd90c080fe584ce63f26", + "addons/core/Prefabs/ERR/Epinephrine.et": "e09c70b3d47a4dc5b92fd332c8222b99554b28a8a0c93cb5caac18eec4501571", + "addons/core/Prefabs/ERR/Epinephrine.et.meta": "f1861f0b0ccace1b7b3d9c29768e37a83bf0c6fae5a411b1bd61d6b2eec7ed89", + "addons/core/Prefabs/ERR/Extraction.et": "4d24c7811415c06386205e5bf5c132a23fcf21e96c75c7657ccd5f3ac914ac50", + "addons/core/Prefabs/ERR/Extraction.et.meta": "c2a838633001e2e3a202d39cb33aa744ebcd996017ad032359069641b5d13853", + "addons/core/Prefabs/ERR/FactionManager.et": "d34a8bbc5dce259c4a467c7bfa2ba746683bbeb9448de8c226856e65319f4911", + "addons/core/Prefabs/ERR/FactionManager.et.meta": "9bf53d5e4bcbe60dd1a4ece1409fc23c349055c860708620f5f6e9716290a97d", + "addons/core/Prefabs/ERR/FieldCover.et": "e403d5cb3a5966f0cea81f15e0e825d482ba1a7a27b12498319ba45a414963b0", + "addons/core/Prefabs/ERR/FieldCover.et.meta": "83461ffa76244e1d69f20b053a9741ccdf7d127009182bd540a7e5ba6150c3d9", + "addons/core/Prefabs/ERR/GameMode.et": "a2b7e632714e646a7010592cc7ac75f0afff7da6c995bf91aaf65d9473a6af3f", + "addons/core/Prefabs/ERR/GameMode.et.meta": "106769cd34525c9d440057072836cb7401a6752148c40edd7fe61c03bd58328c", + "addons/core/Prefabs/ERR/Items/EmergencyRepairKit.et": "0157bc235eeb78a0e1373b561fd22a1dcb225c833ff7c2d4abae688bca0ec92a", + "addons/core/Prefabs/ERR/Items/EmergencyRepairKit.et.meta": "da6f065cb2414b5a4cbe9ef0f0b1295b2466f26ad406c0bddb83845d4598efb3", + "addons/core/Prefabs/ERR/MortarCrew.et": "21086e886fe3199624208c9a866f883ec109f7d08746c2b56b497e59857fb656", + "addons/core/Prefabs/ERR/MortarCrew.et.meta": "fdfd4fcd21a67c3d698fa4aadc4e5c76e665cebf4be86013bc389e493fab63cc", + "addons/core/Prefabs/ERR/ObjectiveDocument.et": "5118ecaf9d2038eea44a4b785f05d2e78781dc1387249aa58a6782c7e6752e14", + "addons/core/Prefabs/ERR/ObjectiveDocument.et.meta": "eaba9903be147b98a1106e2273e2270773df73b9b22f2cd4e20136a8e281382e", + "addons/core/Prefabs/ERR/Patrol.et": "d3eca8fe9197c57a27a285cdb3baf168b037b198d92b39df28a338ae2cfd0c74", + "addons/core/Prefabs/ERR/Patrol.et.meta": "7c5da568fa2fcf0709ee2959253cb068d7c7ed7c2467af3b247a7e70efd96b69", + "addons/core/Prefabs/ERR/Prison.et": "c0ca5219aa2d6322ba7d094b92000640b259112ff6611a745c0713c8875d09d4", + "addons/core/Prefabs/ERR/Prison.et.meta": "2e426ef3de68f4107907854b51e818bc11d84649a01c538e3f18dd9ebec766bb", + "addons/core/Prefabs/ERR/PrisonBackpack.et": "ae750f08fc9af645561f59d7e5809bd22f4cec26ca8fa6409d09730765958c91", + "addons/core/Prefabs/ERR/PrisonBackpack.et.meta": "7da6aa45467250edc910ad7230dc244d1b041e8a6f55881eb3747ce51f9212ba", + "addons/core/Prefabs/ERR/PrisonDoor.et": "b0ff94443fe334032113a663917de23f920a8b73ffadc9c8e9f11b545022b3d2", + "addons/core/Prefabs/ERR/PrisonDoor.et.meta": "b8f3db52bf186426026668fbb9d633c3cbd4d438ab301fba880398f6a0e781c2", + "addons/core/Prefabs/ERR/PrisonPatrol.et": "dc88c31cf5c415573a874f74e13572f0add9e6122c8fcad507fe82860d789c47", + "addons/core/Prefabs/ERR/PrisonPatrol.et.meta": "5528e0d778dce5ed39c3e504c6427625527127b23269d20fbf12ca2bffe52ffe", + "addons/core/Prefabs/ERR/PrisonWall.et": "9b410b7c85bb001be4601ad93f750e17b8318b850306d84f29c4dc1b02723d7c", + "addons/core/Prefabs/ERR/PrisonWall.et.meta": "0294bc5dbb56f90f8153d62459bb0a6ad91d76a479812bbec9c7db182fdb58d6", + "addons/core/Prefabs/ERR/Prisoner.et": "e6d345ac98ad9ec742bf6c3b8686839c3256df9833d31bb4bdf605cd41d0ba5c", + "addons/core/Prefabs/ERR/Prisoner.et.meta": "974278ba49fafeed70b619a0067bbc70ae4d534be441c812ce2bef4a5fb3c022", + "addons/core/Prefabs/ERR/Radio.et": "99b1d80b49d1e7f9d30aa4283772a36fd66e962369f288f5fe329c7fc911bb1b", + "addons/core/Prefabs/ERR/Radio.et.meta": "bc91137b68ba550217914fb8892bb44d7e1920e4a1086f3e506623832a44212f", + "addons/core/Prefabs/ERR/RadioCamp.et": "5e6216bf8e6e160da5d9f966c8f7c2081bf6dd16dfac1b89d682a14b3b8a6716", + "addons/core/Prefabs/ERR/RadioCamp.et.meta": "3cb8d3d553ab6d8d111a74f721cf4e69ee1011cbb90248b83cdc7275697276bd", + "addons/core/Prefabs/ERR/ReconCrew.et": "33953379ceb725bcd0cbe505cd643c94adaed17dbbfa95dadd404d6b8e345715", + "addons/core/Prefabs/ERR/ReconCrew.et.meta": "2653e8c5b1cdb857de5badd5959ddd9826a6a298ab3c166628dbb835d7104d90", + "addons/core/Prefabs/ERR/ReconHelicopter.et": "c609ee59ab145e0a1936f95726b86df643f5cc425c43924029e9c1abc69ebbd0", + "addons/core/Prefabs/ERR/ReconHelicopter.et.meta": "d80f1c3d6a6f9104562b89729996c1c92ad240f08cdcf48d4c390a09d2047c08", + "addons/core/Prefabs/ERR/ReconPilotEast.et": "d961f329823b991eafed80d085de1bd89b44a4575be98d8dac385d6b77741ba2", + "addons/core/Prefabs/ERR/ReconPilotEast.et.meta": "d73df3c896359a54514d2efcd851f579feca7a2619b76c2a38f607649a31a4f1", + "addons/core/Prefabs/ERR/ReconPilotWest.et": "8f8221927269352e55c8026ee5b00e026c281c79af6684b386f24e7dc4675fb8", + "addons/core/Prefabs/ERR/ReconPilotWest.et.meta": "4785f23bb492c6af420ee7896ddbfc9767e0db1e06de95207df2dc949a1b1814", + "addons/core/Prefabs/ERR/RescueEscortION.et": "aa5cae65250936da239c865fe1070c68a1d20b49bcef37f6bd29021f22c274a9", + "addons/core/Prefabs/ERR/RescueEscortION.et.meta": "34eef8053198a49df774792322c6d3cba81f689e56b6a418aed5a14de0913537", + "addons/core/Prefabs/ERR/RescueEscortWest.et": "6f9671fb68e2716479d96be74b80ffb29fec535f1333c5e4dc4d7b684dca1ccb", + "addons/core/Prefabs/ERR/RescueEscortWest.et.meta": "45a87304712d6a18afb0ddc5b3ccbb70da395541e21aafe9d6d3641c392eaab3", + "addons/core/Prefabs/ERR/RescueHelicopter.et": "d9b3b01a0cdb6ed779f40f14f03dfd8fe732382931cdee02ea643f260783313c", + "addons/core/Prefabs/ERR/RescueHelicopter.et.meta": "f47b4a9a72faddcb5817e396dab7c115ac05a5455e5dc8ca4c6a3e9d5fc4093c", + "addons/core/Prefabs/ERR/RescueHelicopterAFRF.et": "cbd9c88b0a0c798096c78770cdef834cebfbdfd62fa12d96b56e74c5ac60dcb3", + "addons/core/Prefabs/ERR/RescueHelicopterAFRF.et.meta": "5cf8cdd7f34e2bffccedfab0cc0cf266d3c8a6856215e797874c531eb2a3ebf2", + "addons/core/Prefabs/ERR/RescueHelicopterFIA.et": "6aa11ddd56ebc9d0aefd06e1f55c7c61136d5c703b6eddc9cc84f3bcd8c8c26b", + "addons/core/Prefabs/ERR/RescueHelicopterFIA.et.meta": "ac0b32de9818aab77ff42d244cd5165112a7764eb6d289dcfe12ec77ea0c9b3b", + "addons/core/Prefabs/ERR/RescueHelicopterION.et": "a910548e7346439af90bb87e445f056aa1799572a69f87f45addbc17dc01fd85", + "addons/core/Prefabs/ERR/RescueHelicopterION.et.meta": "426a478d15718c0193490d5381f732e4c559348f2e5e9c7eb9473a4ca3017add", + "addons/core/Prefabs/ERR/RescueHelicopterWest.et": "dbf3827f58c27d957aac23f505a24c0af2a8f6c6555a82d0962a0ee5d47efb42", + "addons/core/Prefabs/ERR/RescueHelicopterWest.et.meta": "963b5b22139a2f3496e937855854c3ffe01604b86a93f1fb63b547bc1ba29068", + "addons/core/Prefabs/ERR/ScoutEast.et": "b4593aab494c4f21637757b3daff4f349da77b4c48ec5e8a4a2917a6a12cf811", + "addons/core/Prefabs/ERR/ScoutEast.et.meta": "7c72af632537f687e92f30b52d97fc7d70dadd39d33f37e0b1004f9596f55012", + "addons/core/Prefabs/ERR/ScoutGroup.et": "8dc0fce8ee3decc1c367411b00b6d8518df9d8458281cec0881464a2ce906af0", + "addons/core/Prefabs/ERR/ScoutGroup.et.meta": "62e43fd152ce57aa4ceb2187a49e4aecdeb9f428b3a1e8318f86bb83dbf8a259", + "addons/core/Prefabs/ERR/ScoutRadioPack.et": "f1c4911e2556c0e0caa8b896982190c5e7d0433e850b4610e19442caf9117607", + "addons/core/Prefabs/ERR/ScoutRadioPack.et.meta": "825506e3d814d30a52d5dbf64124470641c07a0453543ba71426841e6c783635", + "addons/core/Prefabs/ERR/ScoutWest.et": "2681b5a3f8621adb44195f0dbca6f3e79ef7decad8f9d0dd0e1a801ec2549627", + "addons/core/Prefabs/ERR/ScoutWest.et.meta": "22a6607b4b8fe606ca75a634a0b2ac3178e7bedae7f9ff01e7a4989f3b39a3da", + "addons/core/Prefabs/ERR/SpectatorCamera.et": "761d845fc1c086da58fd87c8933bf58d46d1ff5e17cca5950141743d07c8ce16", + "addons/core/Prefabs/ERR/SpectatorCamera.et.meta": "2a16558f93915e31908a674509e9c89976470f78a84a1174a64b94b68b8f77ad", + "addons/core/Prefabs/ERR/SupplyCache.et": "4c97b0514fe7e723046f40f0b396f6573b12eb9981ca5df70b9e8aa3cf29323e", + "addons/core/Prefabs/ERR/SupplyCache.et.meta": "2f5de3a4aab1e9d0e35521f48dc57b95bd6e449c383dec4f1c258e10295904cb", + "addons/core/Prefabs/ERR/SupplySite.et": "f33681281d26676fac88736091834d5a7b87fe0fa01883582542df964dcfc694", + "addons/core/Prefabs/ERR/SupplySite.et.meta": "b41944b12eb3528ff7a3ac94258b18c66ef161aa6a38894aee300a1ac4798630", + "addons/core/Prefabs/ERR/TerrainConnector.et": "cf9cb28f5a3f41931380864a29ac979aa1d5e843456f564ac7a7de78f88e1aa8", + "addons/core/Prefabs/ERR/TerrainConnector.et.meta": "9f37c979ae282aabca29ce84beb0db851ee8b9a19f7f3a64d387c175616127f3", + "addons/core/Prefabs/ERR/TerrainTakeover.et": "63e26db48c624056350f9ee123ae5b815bc2fad392060a9c26cfcd86c0af872a", + "addons/core/Prefabs/ERR/TerrainTakeover.et.meta": "64e953e378ab1d2a75ee7246f876f4ef93ae8a4e3fddb5002d7ba37268cb959d", + "addons/core/Prefabs/ERR/TerrainTakeoverWithChat.et": "aa857f7e8e0585839c7e9b2e3616702180779e68c6fb149d94a83c05bd52449b", + "addons/core/Prefabs/ERR/TerrainTakeoverWithChat.et.meta": "a213272011050a881c87fdd94b229e002ece24f26f25db5fe53824f8a5b147a8", + "addons/core/Prefabs/ERR/Vehicles/SupplyUAZ.et": "29f157849f413261494a63d4d5b0a97e8bbe61cb076efcd59d6c940653f23352", + "addons/core/Prefabs/ERR/Vehicles/SupplyUAZ.et.meta": "544c94cf1c26757a10654de7856cd6cc48b821ce3bdc02add13cfe6514269915", + "addons/core/Prefabs/ERR/Vehicles/SupplyUAZRearSeats.et": "cd4acdcecad299445952aea0adefc85f2b9e9b43f8186f100e0505c5621b3656", + "addons/core/Prefabs/ERR/Vehicles/SupplyUAZRearSeats.et.meta": "123955da03462befb90e293b3bb856d49cc761166611b38ff47cb48ec2fbe95c", + "addons/core/Prefabs/Items/Equipment/ERRM_AtlasLeaf.et": "11185bc813c00c5bda09c0afaad78849a1c81d89843e8f7f3a14c0776b8296a9", + "addons/core/Prefabs/Items/Equipment/ERRM_AtlasLeaf.et.meta": "8cdfeb51c3774ed790a223ce74872cfbc4588fb6812a0e98136eb607dcc17d9d", + "addons/core/Prefabs/Items/Equipment/ERRM_SharedFieldMap.et": "57dfbd215f28478a791b2e029832ddf3c363658d5dd5590cee4db9ec6a8b690a", + "addons/core/Prefabs/Items/Equipment/ERRM_SharedFieldMap.et.meta": "59093e9072c50d99c86da7609c87541016da5bcf92b98d99db4c524e1c8e0e8d", + "addons/core/Scripts/Game/ERR/00_Core/ERR_Actions.c": "cabb21413c90b252d29708b62228d0679c51c76e39ee0e483953ad7061b2b9d6", + "addons/core/Scripts/Game/ERR/00_Core/ERR_Actors.c": "7258b838f8365e4fa575c4f2ccc8532e22a33946405ebb490120534d5ca65607", + "addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c": "2978e44217db17f03e336088c733d42fe8c8d36479bba1fe40f1cfa55d9e5133", + "addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenuFields.c": "55466b735c5c8f1041ac600338d55847372a71af48ba1ffbde8cce4286e55b19", + "addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenuPages.c": "63e27e74a5e0b1cf1c82ca776b9166e713f59918da02a5a89fccfd50a1036e9e", + "addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenuView.c": "39f7b77029834403ad98ecd398a364bdd5c7c4473c67816240c23160e8dd4080", + "addons/core/Scripts/Game/ERR/00_Core/ERR_AdminPlayerTools.c": "56780d78bf423f54f482d8620f821b8d3eb0199194482b4f12e37f5ec3f55ef5", + "addons/core/Scripts/Game/ERR/00_Core/ERR_AdminProvider.c": "62e05aceb49da07fed7aa89fb6b2f8eb3efc373ac09eff286fba983c3ad570eb", + "addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c": "6fdca5f333332b7bb2349c4ecd1f11278201770829aeac486a4f2fa7cd4221b7", + "addons/core/Scripts/Game/ERR/00_Core/ERR_AdminSession.c": "b8f9df2cccd49d917389a1f872b1dba2704d3de657ac0b236d88f2ec0c4c43c1", + "addons/core/Scripts/Game/ERR/00_Core/ERR_AirfieldPlacement.c": "0601d8da570e1f63e43ea7df451d0e997872e4e163669fd8ad37bedd59de7b9b", + "addons/core/Scripts/Game/ERR/00_Core/ERR_BakedSpeech.c": "d11fff16f86fd3447126701af84c3800860d721263b3c2c16d3b91d686d8b2fc", + "addons/core/Scripts/Game/ERR/00_Core/ERR_BuryBodyAction.c": "adbf4cee5b5f5317b185f17738205f28b701a15364e643cc9fb443866a121109", + "addons/core/Scripts/Game/ERR/00_Core/ERR_CasualtyGroundSupport.c": "8010394e7731d3bc2ce436cf795a37819e16d4d605e8d152027cf391e811d37c", + "addons/core/Scripts/Game/ERR/00_Core/ERR_CasualtyLoadingFeedback.c": "ac554a946cd14f73a5bf79005b8a3f770c9caaf0a6ea409134077b8d96f9433f", + "addons/core/Scripts/Game/ERR/00_Core/ERR_CasualtyLoadingGameMode.c": "1e670937736330716a467157d0f45bace512047d7e87d8678f4ae0ee123969c3", + "addons/core/Scripts/Game/ERR/00_Core/ERR_CasualtyLoadingOwner.c": "3f430930c2598951b75cba157a57967189686ca7a86aff8ecedd20f181262bbb", + "addons/core/Scripts/Game/ERR/00_Core/ERR_CasualtyLoadingService.c": "6efcfb4bb1bdf0da84400eef28a24f6c61e7f446073af2c9667b6f26f0bc47b5", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ChatClient.c": "4bf937ff74e0483c17fa0103f73bfee3afd8f67a37c0c248b08772a17bfcc349", + "addons/core/Scripts/Game/ERR/00_Core/ERR_CommunityStatistics.c": "84f2615fac1375bbe3738edd13a413ef1d524dea5ce0195b05b3a957aae84435", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ContentDefinition.c": "bc1dd4ed27ec894cf4bd617fb226cb8140b544aaa9ef6d8654e815d81e7f6fd0", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ContentMenu.c": "4321499b0d0ac09142fd8ec013f5020d115b64a40a5648169cea9fbb60cca0c2", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ContentPolicy.c": "3a4987345d5a7159234a7e1a08bf5ac40dd5a23031e9b1953fc43a0501d9924b", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ContentSettings.c": "3e2734dfb935a4e93d79b66ca4a7c883723a1fc348e3595432dbf2bfca643923", + "addons/core/Scripts/Game/ERR/00_Core/ERR_CrashSiteResources.c": "832aebf6e754dba54ba28b8f480180d32ced968a2d2f192424c4007a862c2b43", + "addons/core/Scripts/Game/ERR/00_Core/ERR_DebugAI.c": "bc5529c60591a6c36c39cdb753b1af948690fc3c3c725f38a8d595b41a499235", + "addons/core/Scripts/Game/ERR/00_Core/ERR_DebugIntel.c": "a9f6e7e42a3214879a21052e897f9afc0653c12ff15d5bee7766e26a1af6ab17", + "addons/core/Scripts/Game/ERR/00_Core/ERR_DiscoveryMap.c": "8a8653b322c948c462002077272dae79e2f45a24e883767b3ad9c1f24caad237", + "addons/core/Scripts/Game/ERR/00_Core/ERR_DiscoveryService.c": "552d9e40f72a135f19d25a17fbecf475b8bba8d8b62ec6b6d055c65499a0b637", + "addons/core/Scripts/Game/ERR/00_Core/ERR_DocumentComponent.c": "885c8ab4fc9e64c3f8b40056536096c8c32b011cb340b25ca00353a96da15c69", + "addons/core/Scripts/Game/ERR/00_Core/ERR_EnvironmentService.c": "c0fae5bf51ccf507bc84d44adc2d1dff021cd83653c0468b7645a4f343e3fc8d", + "addons/core/Scripts/Game/ERR/00_Core/ERR_Epinephrine.c": "5fb23182223e7b951a63171959c8701ac439ec290755d7445b2768ccdde489c2", + "addons/core/Scripts/Game/ERR/00_Core/ERR_EscapeGameOverScreenInfo.c": "4746b0715c6e9a527038b47d54d2ff305becea219da6b736f6998d81901143c9", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ExtractionPlacement.c": "6d388fe21f08b25cbdb5dfa4e198d94a4c80c5092f8d17630a4654e18bf8b3f5", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ExtractionRadio.c": "ad7fd91d8bb96f05f3be04a03fd00c20ab9413adb12d649d3020c6f7994319e1", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ExtractionService.c": "c48e79a117990f83839704ae221af05fc7995b956af2d4b4f9ed2a6b91ec335a", + "addons/core/Scripts/Game/ERR/00_Core/ERR_FactionCatalog.c": "a5a8c53acf21259e14f59cb919535910f30dabff5a9179c664bc6a7a4d744d6e", + "addons/core/Scripts/Game/ERR/00_Core/ERR_FactionMenu.c": "1deab4c396c307fc17dbaa8731b2943ffb73c37dc0121a5cba5c239c34901ba3", + "addons/core/Scripts/Game/ERR/00_Core/ERR_FactionSelection.c": "04b8bd8a042cf743ab8a87ec3711f9758e7b0112995a9d363e81a63df4eabe5e", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ForceLedger.c": "0ecaf1c5a52568e751678b0b1236e91e6adb538f8fbf010d38b61a312d1bf253", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ForceSimulation.c": "2bbb8888c752a3ffc042f3059937526d25ae288af5b4db024cc683025a2605ce", + "addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c": "d26e665cf5b9c0e1dc6c5e2da200a99711ef9272718d330f5aa66b09cede40d1", + "addons/core/Scripts/Game/ERR/00_Core/ERR_GeneratedSites.c": "4943f761d0274e163a1da325ac4f750de9180947b05b79ee173e12141bfd6661", + "addons/core/Scripts/Game/ERR/00_Core/ERR_InitialGuardRules.c": "8c9d6b1dc4c2af96284e34f487eebb5a221091016ef108006dec60b4e3836558", + "addons/core/Scripts/Game/ERR/00_Core/ERR_Inventory.c": "b63e679d5b650b63a7a91dfc3f42a62f4be58b0338f6df3313e66d29a93bebf6", + "addons/core/Scripts/Game/ERR/00_Core/ERR_InventoryHitZoneCompat.c": "23c34f901311dce63a79aafe83c11e2fc25bd3fddb0cde38bd313eb90878ca90", + "addons/core/Scripts/Game/ERR/00_Core/ERR_InventoryLayout.c": "1af5df45a0f06d19a017954e9bf4b2d722e2d2389f8ffdeb1d024435b9a08be8", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ItemPreferences.c": "b22d75484cc671392f623fddd175882f9a430227247355f35c0aef4c68a7e0a2", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ItemStationLifecycle.c": "0feee55cc5ddf13e00fe73ae3926ec2cadd5ba861d793a939164287ccbe1861d", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ItemStationTeardown.c": "102e61216e86e47ad243cc19f6f66da1fcd8cc77f8799354ceaeb4a6dc74622e", + "addons/core/Scripts/Game/ERR/00_Core/ERR_JournalText.c": "1ad00a77cb0be741c7b6ef5dad2d8a8c1ff1fcca3b093df2e132ead0b69f74e1", + "addons/core/Scripts/Game/ERR/00_Core/ERR_LoadCasualtyAction.c": "1521af55a34110ce1c971b5226a618a9c6d931622abaae4946d9c0d27c05a273", + "addons/core/Scripts/Game/ERR/00_Core/ERR_LocalAdmission.c": "a076922d1ae966cf5cc67b198166ca6b1288358fb7f641207a4681e6a610a2ae", + "addons/core/Scripts/Game/ERR/00_Core/ERR_Location.c": "6323facd2d5b32382b466a2aa0140064632a8c1de0597e0738cb09a3fcfd7abb", + "addons/core/Scripts/Game/ERR/00_Core/ERR_MenuTheme.c": "90d9babfec7978b3d7f5baffedbbe5c1b54a9742f576f51e6232043e32d1cc9a", + "addons/core/Scripts/Game/ERR/00_Core/ERR_MissionRules.c": "17d1f48878b02a457eebbe5ff87c4267488d68d2a9b3121dda77a8bc8e69c8c6", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeAdminMenuProbe.c": "1d20d0ecd77849ce1cf93bc89516059e25a6cbd89bf4c0ffc098be71f3ccd76b", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeAdminSettingsProbe.c": "73e58561e08e50008d33f81eed606dd4e8109fb69a627e892d28ca468d641f7d", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeAggressionProbe.c": "dcda8528a64d35e6aa2f030b59503a49ea8a85ffcc33beaecdf8da00c003b50e", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeAirfieldMarkersProbe.c": "980025c07d042c2f109e2c99ccee0316b596806ba2c4fcd9446b6ad7580d1d1f", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c": "289efc599c52f184ce85f93baea2d5d269f9a7cd204013f9639cc4f0dcb98b41", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeCasualtyGroundProbe.c": "41f40db3735f8aa535c0061458d9892dcfe972222b8a17238e67b65633c46bc3", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeCasualtyLoadProbe.c": "94c533deea654ec546df3f1ebb7cd213e6cd79b1125d99bab5303ef8e87b018b", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeCasualtyRemoveProbe.c": "4cfc06f6d43b709b63caae714155ffc562502e884f49a9825623c6f46972ad36", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeCasualtyServiceProbe.c": "02a9d70d1f4ba20326df199ba41489cfbae12b7bb0aab441361421cee38ad9ea", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeContentProbe.c": "cb0ccfe1e1575f9ed80a8054058b471f3543378db059e6af2fed8eb42a262638", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeContractsProbe.c": "60aa767d9455f2f4958a6a47bd68bbd8ab0466adc8f1dd9ae433c40ae96c68ff", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeCoordinatedAIProbe.c": "9104a7a5154bf34906f135f00c5793229e343da6a1ed19dd572e3c1d9821d393", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeDefinitionProbe.c": "904d8d4c3e7b91d49dadfe495fc7b4f02f300e4aaf7efa348df5ca17ec4a6e8a", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeDiscoveryMapProbe.c": "9ed32d391d08d937ff4f9f33bd11c3a204ce9a2230c2d72b6761e0320b31f24f", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeEndProbe.c": "2dec80c7e247ec5c257407f95d9d5edd42fe0cc44d6af9f724f6ebd10d8a355f", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeEnemyLoadoutProbe.c": "0885ca69f6fa7f53414ce1792cfc3c49916886b8173783c634135c13c5d37429", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeEnvironmentProbe.c": "9e2e66ac5d9778a1c65445348e3300cbb90b68b3f669f3ddf1d2b78a858430eb", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeEnvironmentRestartProbe.c": "7a836a206381b8e8de24c92baf141beb30575723675dd6948d738169ed26b507", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeExtractionProbe.c": "4b09385f73206de004f4f3c38a6192fd658a0d2012482dc5a84dff1599ceb972", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeFactionCatalogProbe.c": "e3011fa47b67122d7b2c23af8f91b7a217599d6cc114c4f9de628744b9c948d3", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeFactionSelectionProbe.c": "946879d822e7be2d48e03a3140aa1aad3e3d44dd18507fa3cdedb4c6d7dd81b9", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeFlightFootprint.c": "a8862a04ac784426f431e2e9cd3f1f19f820b38a081674d29549b8b00277c72d", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeFlightInputDiagnostics.c": "e231dcbdaefe81beb3bd587cb73be8fb3d3184bc86d18dbaa30a43ca4ba5dcc4", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeGeneratedSitesProbe.c": "a3202ee4658e98539122926755caf689a693a79642e8c88d3146c247dd961138", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeGuardCancelProbe.c": "bfa1c65cb039e19c3d1891e4a3fd33469e974c97cc7f42128efc31a8f587d514", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeInfantryCombatProbe.c": "3e4c4382ab91f470357268927b954fd101685df6b2244df483f0422d6c0d7da2", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeInventoryHitZoneProbe.c": "604eacdc66cd52bc8a0115862ee7884a8b0f83bdb50609397de671eacd476543", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeItemStationTeardownProbe.c": "028588a49fa9207087cd27ec9396979cd932daa071e2490bd0262cd71dae30d9", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeLifecycleProbe.c": "b613a5dbe81c2032fd02d31070ae5631166d14d0b02cdbec3b63b782f0bbc115", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeLocalAdmissionProbe.c": "29c860ca3e51cdaa463f3c23a98ff45ee0499ce9bbc322046e0efbed587f1e09", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeMedicalCombatProbe.c": "59c5b04dd32370f8cf5c62fe6931d64209825522127a068b1957e9d1556274e1", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeMedicalProbe.c": "4b73e7cf35e9c8fddb089678a320ea58272e1b6611318d0a762754c171eff785", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeObjectiveLossProbe.c": "6b80fd317a8c6e8583bd4337870838a86251e3b8a681d0aa07e8b52e22abfc48", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeObjectiveMapProbe.c": "37e66177ee0f16fc33d3d18321211206bc4f095fccbd17b177d4b8cc1ba4e057", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeObjectiveProbe.c": "71fbd2479a70048f83d8b262ad1452f427be2c0afc8b071ef13bff477b9dc29b", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeOpeningToolsProbe.c": "0c5d6b96ed5a7cb1cf0d098d534dc7190449ed09339062e6b584729d53a61b4c", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeOptionalSiteProbe.c": "59d86432c19fd85da30a63087cf1f15375c8ee30178ed7e2765c1bda4cc4d035", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativePatrolProbe.c": "5a2af71cc4cfccc7dd7c23584398a38290eba1a8894cd46f6967252ff3a7087c", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativePoweredFlightProbe.c": "7ae5d6f85bbbebec214c66bfdf6f53b93c802f109fcd5bcfbad82c5d61d6cb9f", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativePreparationStatusProbe.c": "456f4d7c3272b89d5f4c6713617baa5596d846994152356fee9a659839bcd8b5", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativePrisonBreachProbe.c": "4889e0d3799a6a21b9f84bcb2f2ae78dce16269cccd1329f8d1fcc1a23e7c22c", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativePrisonerProbe.c": "e2d1ffe5d269add55124601ff0d1dc032a398c7d51010736e6f0e4cbeb38ae86", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeProgressionIdentityProbe.c": "1d3fe20bce761a1082964bca9bad9ce79d1fa8b04db9b5053bae98da380ae560", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeRadioCampProbe.c": "67f1a088f9b20e5f0f11ed3aa083ffa772bffd7c74d85b7a307344ac3d47e09a", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeRadioDefenceProbe.c": "ff1e27a0ce75b123a86e7df31354831880ca62ea90b6c5d7c60d3ba168e1fff0", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeRadioInteractionProbe.c": "7b27552d233813894f65c08eb9f04b547c4ca9ff96a2370a763cb94bd4a555c3", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeRecoveryProbe.c": "d6691f22432b3ce37d07d448aeb3ce7feb5cd09e852e0167b5e4676afd24bb21", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeRescueProbe.c": "78f4dc11a179a5b924c0ec1939f0fc1e61d4e6f6647729c48a9f126ee32d1714", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeResultProbe.c": "c9a89cae067f221b9f33965b4d3ab311b78ba79ea383ca65032c4001df44b472", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeRoadblocksProbe.c": "6f44725b049f2545a9ce3c94ae3e9134e16077fa017a438fe4f073b453292c1e", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeSarkaProbe.c": "511816c4b86c9f1392bc4a1d83ad11e082280bec16356d8ab24ac1f2094bba93", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeSiteGarrisonsProbe.c": "1a5ff8747d7501862085b5e9162043fee3b05e82c73c516ad8093aaada53dc60", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeSiteSurvey.c": "ac0203753ade0cdff6d483132797a80e4520f14546c3f9dea288281cd4cd9c4f", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeSpeechDownloadProbe.c": "5377da12cabd58b9105851a2348d7496316db371bfc4346a5fb3f84f245ec013", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeStationTeardownDiagnostic.c": "172248540932174341882861bb78a9720a6cf89981fcdf9dffe63f7fd4023f2c", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeTimeProbe.c": "4a2c6ffd947334a0a71eac083d10c853c524b00042e9ea0987497c4cee60c3d5", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeTransportProbe.c": "7a3fe04aa1c5ed1061ff63d9d5180e8b925932b62d76c1ffb49bd3dc23530993", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeTransportSurvey.c": "e5abdb69c0675a20f4287afb24b9e3c877e91b162c77c32d81ef61e2a8b72577", + "addons/core/Scripts/Game/ERR/00_Core/ERR_NativeVehicleSecurityProbe.c": "59bce863142e4fba4b6fb2f544bb8a58061f0e99b1a38e5ac291cea919563763", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ObjectiveDetails.c": "628ebd166dcb5544c2d18dce489619984887ee18296cf2446fc6d894f89dd5bf", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ObjectiveMapPresentation.c": "70339c05c550b72a4fd5890a871c5d8e6a9e574813d61696194c5fbb1475ac8f", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ObjectiveText.c": "0d1e84c7e20cfd60876330221929e33f920df31d8907413d5e2f6c04fb0a6026", + "addons/core/Scripts/Game/ERR/00_Core/ERR_Objectives.c": "b9cd5bb3b193c20401175c4328405be1b121e8a60b7b5999910b6a71f834af8a", + "addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c": "fd2104aeaee0a5314f1e215bc2c20c68f8ddf2ee2c5f070baece8ff01dd03798", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverPlayerView.c": "c287a1891451dce0836c6e859eb962c154aad9621a46e50400f44fb83f97d95e", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverScope.c": "97d29535a0ca1571ceb86996f58b68e5ba0944edfe30370455c52a321772f85e", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverTargets.c": "1dfd50307b5d74075b90b5f2f70bae2c9488fc12df03d82eb19350a724f910f6", + "addons/core/Scripts/Game/ERR/00_Core/ERR_OrdinaryGateObserver.c": "1e376ca83c0005cf532539e14d7f47363c47b7c5b03fe3d55405a59208705fd7", + "addons/core/Scripts/Game/ERR/00_Core/ERR_OrdinaryInputObserver.c": "808a46cc1e4b31b65dbbbcdd55493f594ebb3974bbec38b4a2402334c653c0ef", + "addons/core/Scripts/Game/ERR/00_Core/ERR_OrdinaryLookObserver.c": "d5a1b636938d65e9d11c78b31487f2924400c34d98e2ec812954a2a224da7dc0", + "addons/core/Scripts/Game/ERR/00_Core/ERR_OrdinaryScenarioLaunch.c": "8d8da4c1222dcc6adfee350490f4399b4129cfce7a42b45fd7fba52febfd3eda", + "addons/core/Scripts/Game/ERR/00_Core/ERR_PassengerHandbrake.c": "3078487e077aaed1567894f6df12386aae440b790b6c134118d3afc2c4da156d", + "addons/core/Scripts/Game/ERR/00_Core/ERR_Performance.c": "037ffa482e3765bb537629e34f10919d85d4b6bfb0abf56ccc7c0d44fce8872f", + "addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerAdminCommands.c": "eca9baa3b07da48a740851583540840f8bf5f65f7be854902a3778f76967faad", + "addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerEscapeResult.c": "3124ab4d2d6d471409a47aba0af528790734df53243c03fe0cdac3e407047c14", + "addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerGuide.c": "dc52f551a1d3705fceb64ea18d882009d9e9548c01f08ae1c7ef3083b19ab2d3", + "addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerMedical.c": "270454d50235bc28df9bba4fde2689934578874c4eca9560afb9f0f6324d7215", + "addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerReconnect.c": "478fd041e8a13132df25202088f42d0810d36680060dd7a0424d8f40a5b1ca44", + "addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerRoster.c": "dc785a8a1d78a285fe55ce76f87b78a9f522eeb9f7d9655dfe9b5c4348a26a18", + "addons/core/Scripts/Game/ERR/00_Core/ERR_PreparationFailureMenu.c": "5c31219740bbc26cb903a91f4fe22866c465b38220391093c72a3b094ca30f34", + "addons/core/Scripts/Game/ERR/00_Core/ERR_PreparationStatus.c": "c5cc9125986e2285282f3f7fa9068ca72f00d7e26465d00213e734039c0e34e4", + "addons/core/Scripts/Game/ERR/00_Core/ERR_PrisonAI.c": "67f8b444824634fc37fc253c60fd24b56d06d971f48e56a8d53095c97aaf0070", + "addons/core/Scripts/Game/ERR/00_Core/ERR_PrisonEscapeService.c": "ed180448cb516bc4ec521aedd366810a47787628c7f5ee9c9afdbd185d980751", + "addons/core/Scripts/Game/ERR/00_Core/ERR_PrisonPatrol.c": "284618391ee2da84dbcda4a99ca3c072eae3d67b8a9ef40a86827b9ea00810f9", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ProfileConfig.c": "b5aca526a860ed9ed609dc3fad99ebb34d2f6fbd537575a167390472efd4ba89", + "addons/core/Scripts/Game/ERR/00_Core/ERR_RadioDefenceService.c": "f5f0bf1352141f933d0edb5630e30541e001dc07e1d2c8529b29cc159c126b13", + "addons/core/Scripts/Game/ERR/00_Core/ERR_RadioFeedback.c": "545262e65e9b3645d9e514a0521e442fe08f0c2e4e7c83ba638c5105d09818db", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ReaperRHSCompat.c": "bcdf98c05ac6cdf0c7eaa854531b02e9ee4f53fbb346ae01d58114e572e7dd4f", + "addons/core/Scripts/Game/ERR/00_Core/ERR_RecoveryForecast.c": "0eda507fb58860c0eac1d957319d1e80e9576a870d22931c86081db748df44b6", + "addons/core/Scripts/Game/ERR/00_Core/ERR_RecoveryService.c": "182037828b74938746022ebe03bbd9cf849d52e31067c0b33f392f03969d6162", + "addons/core/Scripts/Game/ERR/00_Core/ERR_RescueLanding.c": "e6f63ee8860335de1125e0a62eae3b8406faf5a5ad2c8d9c6f9a6da077986e56", + "addons/core/Scripts/Game/ERR/00_Core/ERR_Resources.c": "d0169aac6fd0c38928c47a9002ded0fd8ba558ea3314a600e792d307ac1ca3ee", + "addons/core/Scripts/Game/ERR/00_Core/ERR_RoundStatistics.c": "60e6839cc50fa02df1795a00b93fd431160f9201bdbb6f1e3b9b50271306e9fb", + "addons/core/Scripts/Game/ERR/00_Core/ERR_RoundStatisticsLifecycle.c": "b813a9ccea9a61c9c6d7d4806cf18adfb323d483622ca1225930207ca2436d12", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c": "ca6b20d5c4599e31957e5cb464d21bb3a4bac517dd539711f9d4f380e48a0e15", + "addons/core/Scripts/Game/ERR/00_Core/ERR_SelectionStyle.c": "b9ea5158e51559dc13812cdfa8939ac36b58c79b9e4115c72465d869fdf264a8", + "addons/core/Scripts/Game/ERR/00_Core/ERR_SelfEpinephrine.c": "da5850374b51e89016c4557d06f3c7c087b6e6e36fa9734b22c7c6c50d757914", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ServerItemCatalog.c": "e4622ef82a3a3ecaf251c865786b97ff0f95bbeba147cde848c722708e151b2b", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ServerItemsMenu.c": "012cc0f3928d883b99a9922fd3b30cbafc6363a73ee27772e359116e0e6233f7", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ServerItemsWeightsMenu.c": "75ac61a9ba0d984a5c6d21ad6a020bb1562914ab09dafac8a80e822f0af77611", + "addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c": "fc5cbd7f52632228c071b9f14cce7979ef55298672e441f665f0efa97f083249", + "addons/core/Scripts/Game/ERR/00_Core/ERR_SiteDefinition.c": "d421d70b32bd2b55cd8e4966c04af3d5bc381ac811d3b4b2efa4bc0a8af8bd2c", + "addons/core/Scripts/Game/ERR/00_Core/ERR_SiteDiscoveryRules.c": "dcbce3b7aee89bfc47a50e9de89209758af1f1bb462b99af67a8ad7a26e9f229", + "addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c": "2b283ad19b046fd6b33245d466b197bd05b4c472e128a747e6b1e725f4a34c2a", + "addons/core/Scripts/Game/ERR/00_Core/ERR_SiteSquads.c": "183883783a4e8399cf62851a4cc65a5d091d25131bc53b4323cf17286315b2e0", + "addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnClientScene.c": "b56f99a91cdc53bdb93d320d10839339022d7e61e6b101eee8296271abd7d3cb", + "addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnFinalization.c": "6ec741f8d19ee316d3c67b0dd2216698584bf2cde3634a3d6a7c3cdc3a3935b8", + "addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnLogic.c": "6d9243f66b4ca8c53d53fedff9dea8986c815e90c1f97de58200cd759aa8b49f", + "addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnPlacement.c": "fc5b61f1877aeaee75fbfd6aed3cd0b4c5277ae8543bc38a0c09836353d54b27", + "addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnSceneInventory.c": "76b79d5bd9d1f367e1ed19724302b7d49f8b0ede85cf19e89c452582ef651f8f", + "addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnSceneLease.c": "20de7f96bacc9037580d88d4424664b4554b2c082d82e7d29913e9dfe390b594", + "addons/core/Scripts/Game/ERR/00_Core/ERR_SpeechMenu.c": "33f9ef80b4d6cdbf3646eab602d15ae0d05884bb20b3a7222b0648752339cda5", + "addons/core/Scripts/Game/ERR/00_Core/ERR_SpeechService.c": "74ea305ac0c9a9d145646a5c73cba58e4920ab472943ae73b3dbd71d8216f752", + "addons/core/Scripts/Game/ERR/00_Core/ERR_StartBackpack.c": "4a36799d3a3defd674822974cd96647c0accb2a854631dd5488c35e2ef697f7c", + "addons/core/Scripts/Game/ERR/00_Core/ERR_StartBackpackMenu.c": "07c90db1a84893759dae71f40b7ae3109827646e48fade542190d21598ac271e", + "addons/core/Scripts/Game/ERR/00_Core/ERR_StartRules.c": "ab7c21ef700343cbe769d8d51d91f167db37285b466d6af9873e121b3adf4ade", + "addons/core/Scripts/Game/ERR/00_Core/ERR_StartSetup.c": "58a6b617b22910d0ce9255f82091dc11cd1284417a66adf1cdaa7b661c1ed01c", + "addons/core/Scripts/Game/ERR/00_Core/ERR_Supplies.c": "752e8a56b0b8cde33cf532032cb0a203553017b686faac2e19bee06f7a9c0713", + "addons/core/Scripts/Game/ERR/00_Core/ERR_Telemetry.c": "b1dc3ecab89f5e6db1ff4ba613b43755a5ae44f3612249288cd134ad850719f6", + "addons/core/Scripts/Game/ERR/00_Core/ERR_TelemetryCredentials.c": "9835caf69d4615d6a3fc840d9dc3cb0f507a0c5a2a6490893c21761f9cb4a216", + "addons/core/Scripts/Game/ERR/00_Core/ERR_TelemetryLifecycle.c": "15d2d90d4457339dacd61c1bb23b619243b8af10731687c35ecc300125a642f5", + "addons/core/Scripts/Game/ERR/00_Core/ERR_TelemetryServerName.c": "21a146c5763b4d4c4dc71cb61aaf7cdd5a8fea670f5ce4f4d4fb2ddd0afbc6be", + "addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainCatalog.c": "ba2e68543a8c929492d06f8725a743d26849a630d6f8c09be0f23894f92d0990", + "addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainControllerInit.c": "8071e38812ac61f9b480d9e91f9ebc7712b360d431f5555da83027db6cdf2471", + "addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainMenu.c": "1da542ef60ff7c1897b1f12432c636ef6a08cc710bac520ef186589d506e9a54", + "addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainPreference.c": "93fc55f2c33e95adbcf2bead3430aa14bdb55154315760bcc9647f89dc8286ff", + "addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainSelection.c": "ee75cac068b99e7403f5eaabe556152513cf5d973602cc6e6b8967d25402f6ad", + "addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainSites.c": "4ac347d4d91951bcc7175a965ccea25f3782edb6e352354cce506d6b626e5ac0", + "addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainSupportStations.c": "c4fe4265963ec88e24379c143edae251452c84e43a3102f5da120205f9a39c9d", + "addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeover.c": "ac31af5ff2937f8a1fd89ac03732a8a473b27b09c83a07923f17986b0f9db841", + "addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverClient.c": "70f192d5d749b4211c5604575d2b9d7e39deca589b5841ffdd03f99baeca1280", + "addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverManagers.c": "ab1d64c8073e4daebf14f4ad2c745d874032675ebcac3fdb0fdae4626acb8dbf", + "addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverPresentation.c": "91d450215eb6ed2da74446b5fbb805356e3881e3cb43e37fe45308e30b22be9c", + "addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverSettings.c": "da22775930b13e41d4a71409e37af3bb99e4cc1d94bdea0e3b59fbdcd4d40132", + "addons/core/Scripts/Game/ERR/00_Core/ERR_TimeService.c": "63867760876d523a1794f619118a52c5cd387d5d92756cd286b2c16a348a6c7b", + "addons/core/Scripts/Game/ERR/00_Core/ERR_UnconsciousCameraProbe.c": "57def439d0293fca246703c72a03f1ba0ce3fa1fbf2d6c6f8c61ceb0b4b5c94a", + "addons/core/Scripts/Game/ERR/00_Core/ERR_UnconsciousWeapon.c": "24d31bc857486638e55c605283c17cde0cdbd3c5c6021eac1ec93611387b10d1", + "addons/core/Scripts/Game/ERR/00_Core/ERR_VehicleSecurityActions.c": "bd7d0aeb4abcc9d8044e4888f9a92f9e5fc97c098bdfa3bbad57e1a29697a779", + "addons/core/Scripts/Game/ERR/00_Core/ERR_VehicleSecurityComponent.c": "2ffec4e646584ec546147fc4b08249825cb32b531aba26467b586d760485adc3", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ViewDistance.c": "8514f72f8207f12570d2317e64094a45507a1647dfd45a1501499ef81e90189f", + "addons/core/Scripts/Game/ERR/00_Core/ERR_WelcomeIdentity.c": "8624bac67c1990f87b3fb799361e5facd31c2e03cb621563b9c73c4fe9d7f87b", + "addons/core/Scripts/Game/ERR/00_Core/ERR_WelcomeVoice.c": "a6a9a816dee1c766675990c158a757cf9ea8628a7b2b0de991af67ff4cd23e47", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ZBakedSpeechProbe.c": "533c89b6a258dccb61d7ff6abe0a43da58d52ffae60735a38c2545fbb4480afb", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ZSelfEpinephrineProbe.c": "a13e14d1ef525209d4609a2532c6d72135377ce058298937b632a91eeb6392fe", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ZServerNameProbe.c": "65f47a58c334c021f574e69c97a6b97a54fb4a2d161ab980d537b838a310939d", + "addons/core/Scripts/Game/ERR/00_Core/ERR_ZStartBackpackProbe.c": "6778cd98392f4e73dfe974782e626d98472251c1266698020d63edf8fe735bcf", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AIDriverHorn.c": "db9f100c6cc10efc6f5b86e8fe9adb75657ebabee6f8bf0c89b997c2e947750a", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AirAmmoMission.c": "0a471a40c8f7dfb797d8d35e41c5b1ff91e8d2ab13b43cf866e46c0e4ab4ed9d", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AirAssaultLoad.c": "de7b6c485167ab68ab0b37b96fe161ee821c2efd41362172b810448a02110e68", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AirAssaultSystem.c": "2eaf9816eee3e1cd2ac70744a6ef7ea5a90d0a3eebdf925e384028a7edde6a42", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AirReconSystem.c": "f8a0e3cf4552d8133efd33c2126ea15a7694676773a7047c15e00021c1d9721e", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AirSupportPolicy.c": "c1a7ac8759ca8ec8b43cbccf303255a2f0b69de447d0d991e44b6bd3db415d51", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AircraftAmmo.c": "2be282406d0c45ab2e35b0ced791b5e494c662588f536701a4e82e6f5fb7c913", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AircraftControlBridge.c": "b8f0141644d8f043c6db4c2d4bb93215c548e5c27ab5468c66e996d62bdf905a", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AircraftFieldEnvelope.c": "0f53c7d9ab167a804eb904d63725decebb53fd5796fdf27864a75abed5679838", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AircraftFieldFlight.c": "c04e264cff9963821852094c451502d31bf9c96d4deae2e8ff8a2373b53b5665", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AircraftFieldRules.c": "30be68f4ca2db7e169f6f0432815821179cddf9159f7b150341dc8eb479bbd17", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AircraftFlight.c": "aa25a8e67a30c48677bf528236402b7f8e5d815dc18df93b878520ad3d1fe8a7", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AircraftFlightEnvelope.c": "8125cef61fb283d0d7c4dd2df3c04b062b448f1101fd48dd53f4f4097df5fef3", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AircraftRouteEnvelope.c": "e8fcef844239c47a1f2f36be420afe943bc450091b05859435406b46c87fa511", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AircraftRouteFlight.c": "1d8d92bf6d97429d68a9f62018232e90cc124cc8d7445efeabe863706a31e2b9", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AircraftService.c": "3f05209227986b5c6bfb193f8f53783cfd4ef083bce898e22257c3e63daed913", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AirfieldSupport.c": "c6d88ede0b1b6bac5a7075b6e2e783ccfa8850210aabd28bfa4bcdb698cc5f87", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AssaultParachute.c": "a93c996059e7166988e9d1b0b498cbfd36f19a1cf8520cb4e9215a8884752e18", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AttackSpace.c": "e7be303231b5946d78c61480a5de3f5b37a63d0d2281a9a30a21e59f072904d0", + "addons/core/Scripts/Game/ERR/10_AI/ERR_AutomaticEquipment.c": "8658421f846fcd3402289103762edaccbe3ca26280d7d72df2846c32ae8a4cdc", + "addons/core/Scripts/Game/ERR/10_AI/ERR_BodyRecovery.c": "f5fc311ecac4533c2d053a686c51e71216e69130a34483f27146627172914c99", + "addons/core/Scripts/Game/ERR/10_AI/ERR_CessnaBridge.c": "f1adb0f9b92f6ed94a8e9897b64e2fccba0f28552525a93f1ab893faa4fc5da2", + "addons/core/Scripts/Game/ERR/10_AI/ERR_CivilianTraffic.c": "ee8e5f0483ad738f6254adcd66e71ad4872fd000c92f44405c4dcc0453a40308", + "addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyBoardingProgress.c": "328f8a5eabb8174b7727eee1d675db83a2eb0be7b8e834db4e950a645ca5d7a3", + "addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyBudget.c": "fc7e254056c1da4aadd26d4f56d51ee375d5d3331f5f7e5183581108c7474a10", + "addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyCombat.c": "5c4b7576b28d51edea868cbb91a0ab31a40e6ddb558f1b8a38e07e20fdb2969d", + "addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyMineStock.c": "4497d6b790a45df2b76d18541ddaf299b406def88b868caa29f096849026a42c", + "addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyPlacement.c": "af4799e3028448e1a73f944989313e5d10c6262521bd29a8cdd5236842717963", + "addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyRoadPlan.c": "fae404f85cae63e63976938a5fdb73efc988ba25c5846564f12edca04fb88551", + "addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c": "7e9a8034060187b1f9a0ad47b5b82586f48cfcc38308a9ca935ed5b770b43722", + "addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyStartup.c": "360228fd45aa6ecd9426e36120b5e637d2feee1e3a4a89aac8207526b871c87e", + "addons/core/Scripts/Game/ERR/10_AI/ERR_DroneBridge.c": "9ad23a763c5995b2ac6f68ed02def487b0ba887e8ca68f0e546d1b6812504cc9", + "addons/core/Scripts/Game/ERR/10_AI/ERR_DroneRecon.c": "dc685b57c884d733e0ff90cf7937f5664ec08545f4cbf59d63bc8a84383dc442", + "addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyEquipment.c": "dfb6a7aeda23adb8aff2292753b694770affc9855bff9fb11d50bb534f9219eb", + "addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyGunnerSkill.c": "adf8bac5a6f7e40b2f9caff5fe9be590c4e4ce2b2b387ba39e741a3c4f4cc564", + "addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyLoadout.c": "a48f32dbd7f209bf08fce2840aaffc1781c571fc679763be58987f6b082e776c", + "addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyMapMarking.c": "0f161fd3e08a54e3e1063679127549e482c40253a022e9d06cd9ee4d740b8625", + "addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyMedicine.c": "68b65c98214e1a27383e155727901a4f70e9c065bb0cf133bdcf7fe6d5a57a40", + "addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyOptics.c": "215e8a05594c541861b48e093520c00091708eb0f3635dcee5d0b5e1160230eb", + "addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyRadio.c": "3a97e5c3de9ff33873d97c6b2d84ae505cfc4f3146e047b928e76ec3b22c8789", + "addons/core/Scripts/Game/ERR/10_AI/ERR_EnemySkillAwareness.c": "00c450d033e43fc45c9e2cb64a1d81161d123cc594a4292cfc5f68eb6c17ee60", + "addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMine.c": "067bed625a3f4d0fa189f46476988e90528c59b2351ec0ea71916317b06efabb", + "addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMinePosts.c": "bcddb72845189b52e5ed71c687d7dd4b0b40b91b7306c3f6edf84a631002fc5b", + "addons/core/Scripts/Game/ERR/10_AI/ERR_EngineerMineSite.c": "d838f5693d4fa5d252d529e59777f560674c02532f1041a5598e938b3a850839", + "addons/core/Scripts/Game/ERR/10_AI/ERR_EquipmentLog.c": "df9eec7f94e4bf1b30b9463604e9b97ba3cbff36028a80b81327b0b021cbb2a9", + "addons/core/Scripts/Game/ERR/10_AI/ERR_FiniteMagazineReservation.c": "8278590ced1936d618905b0cbd0b5aebddda238d426a6a2a52e455fc36bbb502", + "addons/core/Scripts/Game/ERR/10_AI/ERR_GarageVehicles.c": "47e7c984814558d1c355ec15959ae3dae82285ab33f1363b7ab965069136d14f", + "addons/core/Scripts/Game/ERR/10_AI/ERR_GroundDepotHQ.c": "c51fccd0ecdb0a1d6ba99ae965e2e572fc1ea65a5d8dc2e1e2a058698dae45c3", + "addons/core/Scripts/Game/ERR/10_AI/ERR_GroundDepotLoading.c": "6abb278409527bb67db084138a773de240d99ba7f63f9ac740f8167615d53f1d", + "addons/core/Scripts/Game/ERR/10_AI/ERR_GroundDepotTransfer.c": "21daa9ed8cb5f2dc08a0fa85e305fa4b4f3e7b01e16c71fd1ca98d66a4e8b652", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c": "2334a96e1e29c70a9f3076f9fbef53949e39d5560e23d078639d73559cb0b823", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQAdminProvider.c": "8879febe59fd99584add06690b29853f7cf14b580bb15c9a955cf1a2ce033908", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoReporting.c": "95c024cbdd35c02d9b0c3d389d29bb042e5dda6ffd2f5f0d896078274b3adb35", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c": "8bb0ec1422887feaa340307d3c60bae50fc82d424fce6390876e580ff8b75e8b", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQBodyObserver.c": "f59421c1e0c83f0f480d7dd4d048b4b0a41193ed8fe8f2f627d2b6a696420ea1", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQCoordinated.c": "5b67ad624c4a6b0bbb715314ab9cf14e527018fccb7d38af479184b4952b8c49", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQCoverage.c": "0ef1ed718797114d95c636c8fa5a9e46a5f2a7ccbe3f432049b0c7d37f1eebd0", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQDeliveredCasualties.c": "1f738127e55e843ee2705007cedb5c08741f52f63c75eca7c80242177af82273", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQDoctrine.c": "6335728e89ed40ce11b811f00b61bfea76e94dd3379e4357cbfc438b05522bb0", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQExecution.c": "d286b8273faec0acf87366bf53ad2517d5b7d909741e981cdf283cd1f4f50314", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQHunterOperations.c": "b9200b6de3a3a61de94ad720aca94aca64db542acc64ba65e7b8061a5e4884e7", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQHypotheses.c": "060796bbbed604f9733d23e131c9836702870c6e25c81cc0b396c885eb06ade2", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQJournal.c": "53563ff54fc4e0656713d6eaec5e88ddf69fd4bfa8ef6a67cfa13380d24031e1", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQLocalAwareness.c": "124260280ac8eda0e920bfcfc86932c9b21df6b1f7a6aa8acfc43feea3da149f", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQLocalHandoff.c": "2dd700dae9d9b232d22ca63ff01af75a3f5cc213ac24b7c0a43ffa38bc0b9ffa", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQMemory.c": "ec2047a477fb4273e22ae6bb68e7365f321f55c583b5d2c48c4abf2ac38e3b7d", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQMineSampler.c": "2a0fdc4d27e947a1d77b02ae23f757c7fa074eb456f817b7df0766583d757f55", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedAmmoSampler.c": "97dfab7381385959b0906be13fa2575253ac43be91a271a5d41edaf4106366ac", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedDispatch.c": "12769b56019e9fa5b8141498a1c33814ba5262253193495e72f85972365961a7", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedIntercept.c": "5f45545c11de5e183969203cdf07e29089697ddc89752a9c5aea0fa2a1a33a97", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQNetwork.c": "0b73be993628a55e3b21c8670d540e39c938093dd5cb4d586e800ac530509b25", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQObservation.c": "57ad3f0b68d150791e30a7e4bd7dfedfb1ddd78a1c76f228e67b33ea9cb7144e", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQOperationSpeech.c": "6f133c104223ce23262c2927db9e16df0f76f1ee647085114baef1d51417f4f3", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQOperations.c": "b83b805c61c3960801d24ae54854ba7aa4dc914ea89e61db365b5ec99f305ae5", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c": "280e91feecc4e5b6d0886fc4201b12117b58c56857d4386b6c4cb963557274bf", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQReport.c": "4c0c1f0e33f621d1e02375cf4f56b0e8c4ba6fd851912d3d16081949bae46158", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c": "f87727aa1c722a180c7b71fa2d896601b6250978a21bd2d1e973501fee81373b", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQSquadJudgment.c": "12e9ccc9d42a858a998bd46708fbb04d05d48dcf460303a5ecc1629021051bb4", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQStatusSampler.c": "b937eaed913a5095afa8e678dc1c36b5c805541bf8cd5be927069533d6baed1c", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQTacticalGround.c": "eba9a9cabdaa80b936aa5e7025544e8b011fb597b96cbdb8d781583c025afbe8", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQTask.c": "29359f52734cbf3400e12696057bbb7aa54466a018fc88c42015b6eb9bb9990c", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HQWatch.c": "abe634cf6003ae920ec1ed18e305ed462920204960894520d3dd11f7e5ef1c33", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HelicopterAvailability.c": "60eaef2f6d4b2fd44861c5a4434ef97e732e40a5ff7401e2da451ed2dd9c2762", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HunterCloseContact.c": "56f22051566354ccdc61531af4869b62458a30d945e4d21de5aadf7079d2245b", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HunterCombat.c": "df1c319d10efd07ed6f971cf0e41d8d7e377b04ea7f0fe143bf94df733fd1d9c", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HunterCrewRecovery.c": "22e192ca222017d4d7a27210c6e42cf0f128827bca00b37924c9c8d6866b62a4", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HunterGunTeam.c": "0e66d82964ef8eeba29de11d5bb2af954ad72a8090855ac4c1b28fe1e66c0d69", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HunterOpeningPlan.c": "7791513b8b41f4d2ba1b593b5219d7f6e2bf036e352f8e4eb1a8029fc97ab7c8", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HunterSarkaRules.c": "8c8c96e2f985f553e303f7e459b73a2a5cd0051951b22aa0ed364bc2ae19e194", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c": "01007951eea6a4eb980c3d88f3aad10219ae54f8d4f9982c40922edaa9d55300", + "addons/core/Scripts/Game/ERR/10_AI/ERR_HunterTactics.c": "274ae619589b85cf64df1510631ab914aa9fe8c50fc2a76e6e8c5aa5a2c8d9ba", + "addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoAccess.c": "ab4262f78d54782c5ecbf5fff4ebe3da16d72329658d7e18c0c35387dc8b3c7d", + "addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c": "99fa26beb0c0869d0cd694500f3bfc0705c2c1cf674dea90d98fb93a59570ef7", + "addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryCombat.c": "5d7a8e579edf95003133ddb6e509202dba56ac9e68385247e1ac0256c1b319ec", + "addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryMineTransfer.c": "02c399413f80879c22b4891e8ec807c67a6235677095fc4b98bafdc360d6944c", + "addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c": "ca8eee2704e7758172e33014404bb1307c683f19b561b840d4592f6312051e62", + "addons/core/Scripts/Game/ERR/10_AI/ERR_InfantrySupplyPool.c": "c8e454573cf064ff392291d6feebf1f23be64390b5b8e944c03dd065f9f46b42", + "addons/core/Scripts/Game/ERR/10_AI/ERR_InterceptVehicleFootprint.c": "5dea67d14ebab67231c485e14bc92f34c005a5f1bf6d9c0943db28e9857e958a", + "addons/core/Scripts/Game/ERR/10_AI/ERR_KnownMines.c": "045468a052e485a91da601adcb42b504bb6e4ff13b096bc0666128e50a6aa9c6", + "addons/core/Scripts/Game/ERR/10_AI/ERR_MortarAimSolution.c": "59e8343d09f7a44871d8d3335d6ad1d02216abb5deed30d38b5ec9ba3f2c0649", + "addons/core/Scripts/Game/ERR/10_AI/ERR_MortarBattery.c": "157024afc8fd3c1ffeea6ae923ff091e5773b9ffcf5f5864f8040d001e689110", + "addons/core/Scripts/Game/ERR/10_AI/ERR_MortarCourierTransfer.c": "968e58ca700b4a11e5e04a5c159696f514a0b1bce531210172b913e495f7e2b7", + "addons/core/Scripts/Game/ERR/10_AI/ERR_MortarFiring.c": "2606ed8c19d5569815dacd3248f012072bb76834f429ce7557d24d884724f733", + "addons/core/Scripts/Game/ERR/10_AI/ERR_MortarLogistics.c": "b3624e4a00a2bdef16f6c100bbd79910e3bb8c2ae8ec5fe2ad5aa70bffecd303", + "addons/core/Scripts/Game/ERR/10_AI/ERR_MortarPrediction.c": "05863b2b4f9f70cdb288f91c722cbea163c13f2a2927dc485482e5bd4fa7d57f", + "addons/core/Scripts/Game/ERR/10_AI/ERR_MortarReadiness.c": "4c3799b3f2681e977669de0cbfaeb307d9a682b04365bfc9f59079e762965758", + "addons/core/Scripts/Game/ERR/10_AI/ERR_MortarRefresh.c": "52ae8d9d6cad92f42d68c634091b80a474d2666b6c06d63a8c30a3fab71494c4", + "addons/core/Scripts/Game/ERR/10_AI/ERR_MortarReportLedger.c": "42a96040b63e56677353e55164ae23031c90f70b4d6673a9f412ad7b19e636ae", + "addons/core/Scripts/Game/ERR/10_AI/ERR_MortarSalvo.c": "c128850c7a78a4d8fee00ad38f48b265f1fbd1d9b24b092ddc5c205b0f0ad6da", + "addons/core/Scripts/Game/ERR/10_AI/ERR_MortarSpotting.c": "39b3c223ade610ed9f1d73040423339ac65c90b153f30c6f5c42290978c57427", + "addons/core/Scripts/Game/ERR/10_AI/ERR_MortarStation.c": "cbbd7c86f722b664c85ab7c070ae1131d2ce5c41d1dacb4381bff9c1720a1866", + "addons/core/Scripts/Game/ERR/10_AI/ERR_MortarSupplyTransfer.c": "ad71084f507ad9c32d3055e7ec6844a1ec5ddf568c0216ab8391e902b56e417c", + "addons/core/Scripts/Game/ERR/10_AI/ERR_MortarSystem.c": "01bae0bab685e9f0c86c3a8939d18ce6f20dc0b8c1a389d179d91e93008c6957", + "addons/core/Scripts/Game/ERR/10_AI/ERR_MountedAmmoPickup.c": "5f7ca5b31f3b405c2a6ca32ff91042f5b4dac6ab9640ce67fb10d7a74236ba69", + "addons/core/Scripts/Game/ERR/10_AI/ERR_MountedAmmoTransfer.c": "0b195784497672fdaf651f336c14f7be98c257f98c85cfbeb4f96fe1a401999b", + "addons/core/Scripts/Game/ERR/10_AI/ERR_MountedMagazineSeed.c": "b42d0aac142907433df8bedc40de24c94813544016cf5f451846f1980250f995", + "addons/core/Scripts/Game/ERR/10_AI/ERR_MountedResupply.c": "c26ad43705765be1c20eca37fbe2978b8c647151f6620ed37141657a4b7dc312", + "addons/core/Scripts/Game/ERR/10_AI/ERR_NativeAirAssaultProbe.c": "c1d55b338b157b758e6ffb993d5f689029b28b13929797c91e5dcb15b5e561ea", + "addons/core/Scripts/Game/ERR/10_AI/ERR_NativeAirLogisticsProbe.c": "58fe863775d0ad575cb566c78cef696c9826691cf49ef9adde76376be7316db1", + "addons/core/Scripts/Game/ERR/10_AI/ERR_NativeAirReconProbe.c": "c1216e0512a0268e4dbb82429e228334401c0c3cac52ba4486ae6dc91e41576d", + "addons/core/Scripts/Game/ERR/10_AI/ERR_NativeAircraftControlProbe.c": "0cf3e72ccdc42e6ef20fb71d743f0e037a9e69b2e082c720fa705628cf790205", + "addons/core/Scripts/Game/ERR/10_AI/ERR_NativeAircraftFieldProbe.c": "b64907c7566a894ca2f3c49ede5d972d4087a938da1a0ca87bcbd7c2203b2c52", + "addons/core/Scripts/Game/ERR/10_AI/ERR_NativeAircraftRouteProbe.c": "8d0dca97e5106d1c84b64992c789e1af0689514beefc027e713ca6ab901fc470", + "addons/core/Scripts/Game/ERR/10_AI/ERR_NativeMortarCourierProbe.c": "740c8bebbd4128a9311b1167d1058549475d6b46f81c9b6c0d19650f4b50616f", + "addons/core/Scripts/Game/ERR/10_AI/ERR_NativeMortarFiringProbe.c": "ee570d21bede8baebefc799399712d9582b3e30ebda7299f6c941777f02ad2a2", + "addons/core/Scripts/Game/ERR/10_AI/ERR_NativeMortarSalvoProbe.c": "fb9ccc6b36247ab3ad828ffbb2556abf5c2838d93ea6cd1ad259c0a015142f5e", + "addons/core/Scripts/Game/ERR/10_AI/ERR_NativeMortarStationProbe.c": "a110f0abc753cb1d2f781e457e637dd5346ecd2e924eb5eb3a5c7dca3d1e2340", + "addons/core/Scripts/Game/ERR/10_AI/ERR_NativeMortarTransferProbe.c": "399c21940ef10777b46fdb3e905430ce43404ce1de2f4a6bb62f9c61c1a0689c", + "addons/core/Scripts/Game/ERR/10_AI/ERR_NativePatrolHQProbe.c": "9784b551cbb91f017004fb728ce08b5c36bead0ffc83aa7149c2c46a7c4dc495", + "addons/core/Scripts/Game/ERR/10_AI/ERR_NativeScoutProbe.c": "41b39b1abe5e51a5b8a428d533cccbfb0dda9d7a570228dc3489408c60543f17", + "addons/core/Scripts/Game/ERR/10_AI/ERR_NativeTownProbe.c": "28876e65f9f8a7d96d8056f446b61e12e97d9bbf1a5ac0d4c40edee44e6c4a21", + "addons/core/Scripts/Game/ERR/10_AI/ERR_ObjectiveDefence.c": "83ef91c2118521aedb4c1838b2e47909c80e0950086449b98ced168f1fd87080", + "addons/core/Scripts/Game/ERR/10_AI/ERR_OptionalMotorbikes.c": "87cf6544983386d29058a6f892aadbf242a517cfff4ed24348f56a72bcde8cd1", + "addons/core/Scripts/Game/ERR/10_AI/ERR_PlaneFlight.c": "14fa2328e1d31cf59fb89cbb53ba33224be6cfcdd2813525fa9585f8b2e237c7", + "addons/core/Scripts/Game/ERR/10_AI/ERR_PlaneRecon.c": "fe94c29fd9befd3c1bf3ab97b9b27458a8563697d4d46f72df43c6e642f3664e", + "addons/core/Scripts/Game/ERR/10_AI/ERR_ReconAirports.c": "7551347d776d6e0eec818d6468cacbca7ac53daee7317e0a1618ca3d39ebc762", + "addons/core/Scripts/Game/ERR/10_AI/ERR_ReconCrewLocks.c": "83b63c21574140208792aaf3d24e3d22b255a078871382122110bb48520d4541", + "addons/core/Scripts/Game/ERR/10_AI/ERR_ReconFlightComponent.c": "1386e2a2d2a635417ca517aa1d8ddd4f657dd415ff59be2465543cd8f601e7a9", + "addons/core/Scripts/Game/ERR/10_AI/ERR_RescueAssets.c": "fca1a4518baf09f0b2c97f3a51995001d9674849417fea9ca1233e8d1f578495", + "addons/core/Scripts/Game/ERR/10_AI/ERR_RescueFlightComponent.c": "60db8029bbfbbb362b5bce61f509740eabea5925b93bc975e7c2b44addcf03f3", + "addons/core/Scripts/Game/ERR/10_AI/ERR_RoadblockMining.c": "47d0212cd20c0b320a47123d17f2b89f7d60b44072a00c69944ada7bb83c4e5a", + "addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c": "879f78beeb284d1298ec49dbca9624c5f3899ffc6fcd376e748bf55e4347d4bf", + "addons/core/Scripts/Game/ERR/10_AI/ERR_ScoutSystem.c": "63287a252e4e7d5aefce840279f26ce3e1f286cb79cadb32596c37897cec7119", + "addons/core/Scripts/Game/ERR/10_AI/ERR_SettlementSafety.c": "e5c0ebc95521a9a33a3f7d9cca6c551dda862a08b7af20b017877aba9584795b", + "addons/core/Scripts/Game/ERR/10_AI/ERR_SoldierSpeech.c": "ca87494ecd355052b46b72e2faf13660e458f3d376285bb0e4c20fd0112ef2e2", + "addons/core/Scripts/Game/ERR/10_AI/ERR_SoldierSpeechAudio.c": "4734d614c5366ae3ff57717f514664c9dfdbe4408e16ba36f6b10d807361e313", + "addons/core/Scripts/Game/ERR/10_AI/ERR_SoldierSpeechContext.c": "d2baa94f036f7dde5cf24e004ad1fb9416f249cbf7d0c6c07c5898f7d21751be", + "addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyColumn.c": "bd898aabcac5724d60dbecc4ff5ab6f062c66cf7c735ff59127c588ff94f898e", + "addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyService.c": "5393efab9d663382e67b9b27a388063986a4f99c2607328c998f007588074ca4", + "addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoySystem.c": "f09aa817c27d6af0ccfb544f4ab1ae2afafb63d626641e03d87cfe159475d821", + "addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c": "d2fe073b0c23c877afaa65e246544751eecabce4febd72412fc9a4ec18eb1c8c", + "addons/core/Scripts/Game/ERR/10_AI/ERR_SupportScenario.c": "151eb6189701e559bd4f1359c91187f5a64deab1f2bacbd521c4f4cf3de6d878", + "addons/core/Scripts/Game/ERR/10_AI/ERR_TownGarrisons.c": "9b48331ff8bf7e5a111b498c59a57a5d0210125af983424535286a4aeda9a18d", + "addons/core/Scripts/Game/ERR/10_AI/ERR_VehicleAlarmPerception.c": "45ac71d25e10a057a26a5b0fc7128935baaf47f4c0f87208a4420fa52086fd98", + "addons/core/Scripts/Game/ERR/20_Intel/ERR_IntelAdminProvider.c": "40adf8497b64cc74b1390149fe5ba789ee2f14e6fb3c325af8f5ffa8c806878e", + "addons/core/Scripts/Game/ERR/20_Intel/ERR_IntelDiscoveryFeedback.c": "741de1dbadfd46c154905a0918aad41386ddc139d843553db55d20d60700c020", + "addons/core/Scripts/Game/ERR/20_Intel/ERR_IntelEvidence.c": "ff436bd3c7aa0fcd5c24e940430c1ffdda934889953f63e877ae418bc9557d26", + "addons/core/Scripts/Game/ERR/20_Intel/ERR_IntelReroll.c": "d8b04de9ea789732cc78ebce9aa60786c86fe46d05f671cf251eefa68a214b59", + "addons/core/Scripts/Game/ERR/20_Intel/ERR_IntelService.c": "2911325c79a7a372628b18ffa0e110d11919ce2c2cd7b19277bfd608fdb03dfc", + "addons/core/Scripts/Game/ERR/20_Intel/ERR_MineIntel.c": "3ce16cebaa9703b2f9803df7c11614f94de6460a9136dab8f80dcad41ae2b1c2", + "addons/core/Scripts/Game/ERR/30_Map/ERRM_AtlasPresentation.c": "f0b36472b16477b8c3b5b62f55cfb2a3b3a08304b0779bc39244359af1abe203", + "addons/core/Scripts/Game/ERR/30_Map/ERRM_NativeAtlasAnimationProbe.c": "845b39276bfdbadd6bfd9b41f445fce1a9d40231d38ef32adc7a6654868e17f2", + "addons/core/Scripts/Game/ERR/30_Map/ERRM_NativeHeldMapProbe.c": "46c0210a3aef40fc34e4c9682d9364b1cc1e15d4373d8aabfeac7d9b8e55c351", + "addons/core/Scripts/Game/ERR/30_Map/ERRM_NativeMapOwnershipProbe.c": "32418602f085e3edba7343f42a6a691630b533be262002f29248e8ae87a38254", + "addons/core/Scripts/Game/ERR/30_Map/ERRM_SharedMapComponent.c": "73298c94ecc8d227452b0eddfcaec7fe9df4748875fa7fafcc52544a43658f36", + "addons/core/Scripts/Game/ERR/30_Map/ERRM_SharedMapPages.c": "2286d327b87f127f052e06c22f553b30ea31d25127b860fc02a220aeb4ce3ea2", + "addons/core/Scripts/Game/ERR/30_Map/ERR_ScenarioMap.c": "75fa1d742129ef7f33c005cd48f56db7f49ee7bde3c5428532b0b501b032b0a3", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_AdminCommands.c": "282fe1463add7af8771aa58507b254e69d2d06f50274d4c77321db073f8394f0", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeAlarmHearingProbe.c": "5dbb6a48909b76cd141e3a79ef01d2e68cafee55153821c4471f85c761fff94f", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeConvoyCoverProbe.c": "d2450140cf9bdf09210473234605b4c0d818ea5b91f9532209166a35c5d13701", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeConvoyProbe.c": "87df652b339df4fe4239617752ac5fd9f47aa4396ba94742a0af39a32b54e784", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeCrashGeometryProbe.c": "7867ce1794aa0d1bbab72c32f60f5bd0c2bad4f5ff6224978c215e7828840178", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeCrashStartProbe.c": "957f40e36bfc025b8bc752b1f41dc177c8d0844c69ee5994321a47cf3ddea1ad", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeHQBodyProbe.c": "811244cc8297817efbfc3ee4edf01c868eae23c88127ba0c72e98ee7422da72f", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeHQMemoryProbe.c": "9bcd0b2e1007d40cb794aabe80368a2584ae58aad6a3ebbee070efc7b21a8147", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeHQResponseProof.c": "0f8b09fd8753234c91fd6baaca0e2f812e409ae1c4be8f340b648ae59b69c2f3", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeInfantryTransferProbe.c": "9b2de4dc22bdadd51b0271219a971dc8dc07f70ed3534f306acf03ed81b29424", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeLocalHandoffProbe.c": "0d4d61c424a16008595f86fa61f3b848caf9bd6e9ebbacea9c10254d88ba378e", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeMaintenanceProbe.c": "852b9b397efc3f919c97fec63bc3df1a850cd7833421af8441994afd191951d4", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeMortarProbe.c": "ddb892141500c192c0787b011d57dd8c07dd8c9a3ca5fdc407f6c271897bde0a", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeMountedAmmoProbe.c": "7ddb634444c79ad9291d53a754dc0ed383342e36720ddaa4b7aa4bd4e7ee656d", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeMountedCourierProbe.c": "12710e9e5548fe8856f7a52f3566338369a8a1eedf0bd92af16229271872ada5", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeMountedInterceptProbe.c": "34b0b1e38a59f14c4e83d93ec412779945388e6f4d697bda2f6cb90f519a6372", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeMountedSwapProbe.c": "a6721fcaaccadf8eddb06687c7d587d0a3e377d5d1bb0b8e72e630fede8690ac", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeRadioProbe.c": "a4bd5cd9fe0594e05238b2d6dcd80466773f8fa160dc60bbe1eda70e3acdbe79", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeRandomRadiusTrace.c": "7e946406067092e30b6b75cc6ef0c56db7f866249e02bd13648156933ee578d9", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c": "4dfb218419db4cd7a46d25cb1f2e578e75ad89efc43f99c6ca8ab8e591e139d7", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSupplyConvoyProbe.c": "89c2696071912500840f4bb91d754f695bf674335b51a7ee1d03727084fd4869", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSupplyTravelProbe.c": "32e6074e912491f2643a24cb44311b9fe5b069c76e4e94a45e7409ac82d36507", + "addons/core/Scripts/Game/ERR/40_Chat/ERR_OrdinaryScenarioLaunchChat.c": "a747af09599ce4d0c52051f18f00c4624d24d137e328341b580b82e896c6bf65", + "addons/core/Scripts/GameLib/ERR/00_Core/ERR_DiagnosticNoSplash.c": "0ea1b402f8927b6d68bf6f7e5caa2967f7e3a39147634faab147796b791933b5", + "addons/core/Scripts/WorkbenchGame/ERR/00_Core/ERR_BuildAtlasModel.c": "f6cb5e6f5afc9bb60a8a46d96b52c7506882faf0a880e0b20a01a2603b336d2c", + "addons/core/Scripts/WorkbenchGame/ERR/00_Core/ERR_ValidateBaseWorldPlugin.c": "49a38c37b51e5318c7dcbcb01543dd9a6300d053f0d39808b3974f4edc45b871", + "addons/core/Scripts/WorkbenchGame/ERR/40_Chat/ERR_ValidateWorldPlugin.c": "d1f3fc372d0d16062a5983f2a6cffb6e72f202cb93fea5b33b4d77e0c852ecc7", + "addons/core/Sounds/ERR/Speech/radio_01.acp": "cf63378bf3950f5e6c1c1f46c057ad38a371b14b945b1201777820384805c3af", + "addons/core/Sounds/ERR/Speech/radio_01.acp.meta": "d5966526f35aa64dc980238f72f4f93f9d0e6a360f10e3f6538e97b7d086aa47", + "addons/core/Sounds/ERR/Speech/radio_01.wav": "cfe4a00000bc9b629a9e8e282ed64e7177673f829c5657b1ae2b2cb9e661d2ae", + "addons/core/Sounds/ERR/Speech/radio_01.wav.meta": "eaf33fa344fccf9aacab626117d03b96b135fa819d3e63ae300fac544291b571", + "addons/core/Sounds/ERR/Speech/radio_02.acp": "9111d118594094a8e09acdf80d8b107cc7d5d890b6ab086274e0e1004039e345", + "addons/core/Sounds/ERR/Speech/radio_02.acp.meta": "76b55d9abf7f599c5db3269a5829546a6d5f92e6350b2fe987245f9cd6b599fb", + "addons/core/Sounds/ERR/Speech/radio_02.wav": "6f0c94ae0e501afe6cc1aba804ba6a0403ba4de931debc28bf22970f591d0798", + "addons/core/Sounds/ERR/Speech/radio_02.wav.meta": "06ec85166aa4c54a6fafa1fa2eb2cc32f4a36dc8eecde8f9ba9912229e5e00e0", + "addons/core/Sounds/ERR/Speech/radio_03.acp": "019f939d65396f4a334a024bbf53c41479a9f8ce806610945af2be13e7352c70", + "addons/core/Sounds/ERR/Speech/radio_03.acp.meta": "8aa2f4f26f04bc3085ae046f543c047cdadf9af9e06a38920f1e1fd8a3aa02fe", + "addons/core/Sounds/ERR/Speech/radio_03.wav": "415a206bd1de167342cf1b82d59c708441b8d802395f941d15097c064133802a", + "addons/core/Sounds/ERR/Speech/radio_03.wav.meta": "de1d81fb653146404de5b6a6018c1cd73e902571b82d544de5b6cfccf21f5410", + "addons/core/Sounds/ERR/Speech/radio_04.acp": "77bcff7cc7ce49036a440e829fb4eab1dbe5252b0fc9ea24c42e6f6b68e2698e", + "addons/core/Sounds/ERR/Speech/radio_04.acp.meta": "dd61ee2545776cf23403240424470661c737048d01432d90b24f2c9e75f45d82", + "addons/core/Sounds/ERR/Speech/radio_04.wav": "a225810c7fba048517248fd9f7e9f7ab8aad5bac35e59f22e11e69166551f7f3", + "addons/core/Sounds/ERR/Speech/radio_04.wav.meta": "0ae046e2ac7f9653661ba99646d40dd78d52f113ea6554e1dbfd6e578572a4f4", + "addons/core/Sounds/ERR/Speech/radio_05.acp": "7dbc2b3b3d251eb8d3e749b0a7f32186b0421a95fa5585be493cf04b45510e3d", + "addons/core/Sounds/ERR/Speech/radio_05.acp.meta": "cbf3b48ed3865e30ba5d838d96ec4f1ee950cdcd51368f7a9259460e4b78cc1b", + "addons/core/Sounds/ERR/Speech/radio_05.wav": "90f5abe221165ac5cd7d88c9b3e01cf6fd4d00b594c84d09c6baf101fdb776ad", + "addons/core/Sounds/ERR/Speech/radio_05.wav.meta": "d8e3de0e1ae8670a01bcc272a1adf4199ffcea514ce024ee3286820dedab4d83", + "addons/core/Sounds/ERR/Speech/radio_06.acp": "e14da41c1cd186f35a4f385fab71ad85ad6fe718e67299a65466339ae23a2922", + "addons/core/Sounds/ERR/Speech/radio_06.acp.meta": "efc810a60e93453942adba476b423121f2a7dc1133b25d8764c1bec546a3949b", + "addons/core/Sounds/ERR/Speech/radio_06.wav": "b3f86b8dde535bc1a63301376c8d42775446d1b6117168557841565391fe0a14", + "addons/core/Sounds/ERR/Speech/radio_06.wav.meta": "07f53131f4ad153cc09051ba1de07f7b43d38f69781aad24371c4eeb331b280b", + "addons/core/Sounds/ERR/Speech/radio_07.acp": "bf3a48af07ad6e5da2ca6ed6c581fc6232e1bd62ebb905b934188366f2bc3d77", + "addons/core/Sounds/ERR/Speech/radio_07.acp.meta": "60aab59223c0ea734f7c1e88f803de30164ccef1a909730abed3eb76b3378ca2", + "addons/core/Sounds/ERR/Speech/radio_07.wav": "f3fd18ddaebe9eb3d0db9ac73c89a7b7a7f18b76e4b66d404d544be660a6825e", + "addons/core/Sounds/ERR/Speech/radio_07.wav.meta": "ef6c72aa32c1c46f7133782536744bdaf4eee03bcdb75aecb87accef6d1c4f2d", + "addons/core/Sounds/ERR/Speech/radio_08.acp": "a82c5dbcd7094085324d93d539c6f829c53dcf3781f40b8d65a10b7cb4125bae", + "addons/core/Sounds/ERR/Speech/radio_08.acp.meta": "69fb3400109e19d4f4d0fb371d056edbecc0a96a0a923174d1aba95dfeac1014", + "addons/core/Sounds/ERR/Speech/radio_08.wav": "ce3c2aff20dbea83aa6545a383c454969c5caeb53d5a88155ec182f789047d4a", + "addons/core/Sounds/ERR/Speech/radio_08.wav.meta": "72c5e117ab7b605b8370fe90923edee73593df6071c39b86556731387814a014", + "addons/core/Sounds/ERR/Speech/radio_09.acp": "73725377def1832742c7620fec46dc0100400d92b9cb79add8b4d44e94c46c0a", + "addons/core/Sounds/ERR/Speech/radio_09.acp.meta": "95456fe20def274a1972867024e9d0eb572f3878eae19c63522fcefe401dcb07", + "addons/core/Sounds/ERR/Speech/radio_09.wav": "3eb0be6393c431b526dbddc723cb608a7681a2dcd8af6910d9f2234aa98c923c", + "addons/core/Sounds/ERR/Speech/radio_09.wav.meta": "724dd301d1b25e41318503a815e87b54e7283735c51fd122af55c070f26adb37", + "addons/core/Sounds/ERR/Speech/radio_10.acp": "f976a0fecf73b3379ffb9728e96c68ca79800712919320e7ff7f3484cd2c9724", + "addons/core/Sounds/ERR/Speech/radio_10.acp.meta": "5411a9ffa23e42c8a2e8d4eba75d6e87631bcf542128327dfefd603a64d5b6ec", + "addons/core/Sounds/ERR/Speech/radio_10.wav": "2b2dac499a45a7779e79465d628a67b87078c0951db7db3e051feeb4342f2816", + "addons/core/Sounds/ERR/Speech/radio_10.wav.meta": "c3ac10a092dcb26ec637be6c9264a85eae1b6da55b9d8b44a14766a70ddf2933", + "addons/core/Sounds/ERR/Speech/soldier_1001.acp": "1af0d303063f37811a9b1a25424d887fe5eed4b5c57da71cf36ba6985f8cf002", + "addons/core/Sounds/ERR/Speech/soldier_1001.acp.meta": "2fbdda4d9fa83c6945985bd2f2215aa9f745b3a18adb0e7edafac1b01c161ba5", + "addons/core/Sounds/ERR/Speech/soldier_1001.wav": "a8935a5aded028f2fb03734f4423e731ea4f5b17394526df47c0407a3deddfe7", + "addons/core/Sounds/ERR/Speech/soldier_1001.wav.meta": "aff7a92a727d8bd4df033be2961fb85cafdc5c45fd272c5ea718bc07fb8e5cce", + "addons/core/Sounds/ERR/Speech/soldier_1002.acp": "1444636949683a430efe20a7f296e85e669cb8e26799c326f73a00c4f8d32fa5", + "addons/core/Sounds/ERR/Speech/soldier_1002.acp.meta": "d8eb34135a3b76f0a4f889f9662d954b3b0b3638c10c0732e25ca29d7b2ef412", + "addons/core/Sounds/ERR/Speech/soldier_1002.wav": "8535643e93757dd916f4158b5b03671cefc599489dbccf6bb0f7abf294fbae88", + "addons/core/Sounds/ERR/Speech/soldier_1002.wav.meta": "e8c8813e5a1989a0d7e4d2a5c90622c4b0f909aafb929f4737c82e5710220d1f", + "addons/core/Sounds/ERR/Speech/soldier_1003.acp": "5ab123f150debd6fda0b822fee7ffca5a0bdc168f4d8d2e28b3982972db05613", + "addons/core/Sounds/ERR/Speech/soldier_1003.acp.meta": "f89fa8e7ac42e7f675d29f0b241d181df572413c6c41bf31cfb98fa205baa87e", + "addons/core/Sounds/ERR/Speech/soldier_1003.wav": "5910896661ad67333d2374a16ca44ce831a5820feb337655df99c02df2915d5a", + "addons/core/Sounds/ERR/Speech/soldier_1003.wav.meta": "5281a71be599a70aeb9972ea85364e5ef32f824db94c71953d8a1912fb2a27c2", + "addons/core/Sounds/ERR/Speech/soldier_1004.acp": "10ac847b206834168e63d8d0f7ad25d7542a58a49d331af50aecf13e7e070ac9", + "addons/core/Sounds/ERR/Speech/soldier_1004.acp.meta": "8c073328d3f5f06ca583d97e9ce2453d8167ec7bc5643ab90783748a4cd9b6bf", + "addons/core/Sounds/ERR/Speech/soldier_1004.wav": "76375e462ba518c76f752d4b194c9370dffcebd8ce0037865133b37af3ff9a9e", + "addons/core/Sounds/ERR/Speech/soldier_1004.wav.meta": "563aae8d93efeaa7ca098c12bf0004da279321607793bb32a93b2530d6832d06", + "addons/core/Sounds/ERR/Speech/soldier_1005.acp": "ea3d874f84dd85f09871444c348b1c7f4948fc043c59cba4d3aea8b8eaa8b86e", + "addons/core/Sounds/ERR/Speech/soldier_1005.acp.meta": "40cbfddfa9937e319d412f3ecb66b75e2c1c88f2823a64e43a81a2939fb9021a", + "addons/core/Sounds/ERR/Speech/soldier_1005.wav": "3e20728fb61863278738f5366343cddd6fac1b8b114a14867f91ebb5cac547d8", + "addons/core/Sounds/ERR/Speech/soldier_1005.wav.meta": "e330fc819872b0a97a280a42d936ff75e30d21ea3ac96891988b48ded36af7b0", + "addons/core/Sounds/ERR/Speech/soldier_1100.acp": "86dc8e90d826947672342f44a6ff8d2fd4a8f014db0b215c0865b3728b14ea88", + "addons/core/Sounds/ERR/Speech/soldier_1100.acp.meta": "1a41730365335ebfabf6c75ab1c67ecb1d0d6c85db56d1d85f688a780a3ecf15", + "addons/core/Sounds/ERR/Speech/soldier_1100.wav": "eaf1deb5381f6bac165e364a652920fcf78438c351c564ebe50a216f3f3ba5a0", + "addons/core/Sounds/ERR/Speech/soldier_1100.wav.meta": "dcd087d3e0bec53abdd4e42218a3bbb234b7ae3cd2da05dcee71a298632a8da9", + "addons/core/Sounds/ERR/Speech/soldier_1101.acp": "2680131b5364efe39f3dee44ce1605dbc9e064cea9baf8247a8c5b55dcfa8d99", + "addons/core/Sounds/ERR/Speech/soldier_1101.acp.meta": "4feaa2b8e069ee2e9576d4009329f585b1b5ade9e47816882b78075f4897646f", + "addons/core/Sounds/ERR/Speech/soldier_1101.wav": "4bca012ec25d8456cd38d26606312f54b480b674365b76a9c9f54e408314570b", + "addons/core/Sounds/ERR/Speech/soldier_1101.wav.meta": "6ac6bd7a06be3fa35c513137b4584e405c0cd7e32d0c6912f2d7f9b5a3087d36", + "addons/core/Sounds/ERR/Speech/soldier_1102.acp": "08002dc26a4c07d6bf488d5f7c8c6587787c1755ce374b4053dad582eb2d77cb", + "addons/core/Sounds/ERR/Speech/soldier_1102.acp.meta": "2133e516f162d6aaa7f0d3a191421964ca6df7b00d0a50fa42ffaba70864bef4", + "addons/core/Sounds/ERR/Speech/soldier_1102.wav": "1432bb3c9d99910314a84d9773d2afafe4f5fbb143afb291b0deed55d25ee6d0", + "addons/core/Sounds/ERR/Speech/soldier_1102.wav.meta": "a6ca33938733e22789434db35ca2cdfb0fc4c51716feadf4a28a60ee9d1044b2", + "addons/core/Sounds/ERR/Speech/soldier_1103.acp": "c93e1e75c272d31107fde86205b859cf505e2acf7d8776fbd81357bc8b3625b9", + "addons/core/Sounds/ERR/Speech/soldier_1103.acp.meta": "f3d41ab27e932b10d46c3a2aa63f826825403a70640381a089db608c1c00a257", + "addons/core/Sounds/ERR/Speech/soldier_1103.wav": "c294454907a229d521c2285eec26dbb82f9782f2a7eca764e11713ff63f0e004", + "addons/core/Sounds/ERR/Speech/soldier_1103.wav.meta": "968d93e9dee2104aee94407604d021d8849a83dc3d115484088e81289654c246", + "addons/core/Sounds/ERR/Speech/soldier_1104.acp": "cb94ed3c95670f11f78f6470f1f50492fff6d2f0ac94602d054d474d52400cec", + "addons/core/Sounds/ERR/Speech/soldier_1104.acp.meta": "92d43bf610784953d8d94322b73ae235f7a8aeb83ece72733d795c8034275052", + "addons/core/Sounds/ERR/Speech/soldier_1104.wav": "952b3d71366c26fc12bdbb9014665bfc0402f30944c22b1b2369d26a93db7104", + "addons/core/Sounds/ERR/Speech/soldier_1104.wav.meta": "939562214b39250feaa8f6f6c6bd10c3d4e0b4c529f338490a2dc234919c0d31", + "addons/core/Sounds/ERR/Speech/soldier_1105.acp": "62c5d3d230a8a15cf2a4de193620ccdf83ca9973b550ace4e8edf7cc49f52851", + "addons/core/Sounds/ERR/Speech/soldier_1105.acp.meta": "6774063562b9c83975bc394dd3c72aa2a0d8001d0990ab8ff870ae2876889f93", + "addons/core/Sounds/ERR/Speech/soldier_1105.wav": "20e9e82d9897b598497da4722432f83ae03a5e6bf9a1f7a77caab712e3fc3130", + "addons/core/Sounds/ERR/Speech/soldier_1105.wav.meta": "6f21652352edbd59d1244ce9c2dcd1ad4ab65f022222381d9709c28148121c42", + "addons/core/Sounds/ERR/Speech/soldier_1200.acp": "c27db60629a41145697e041224cc2dde21c72d6a883fa305c71ec9c5b86bd84e", + "addons/core/Sounds/ERR/Speech/soldier_1200.acp.meta": "ea228ec869a7a0266f5604266f68c4166abc6cfeaa29180cb3b4351464b71079", + "addons/core/Sounds/ERR/Speech/soldier_1200.wav": "5a1561e821ef33a574c1f9ef95ccf76e69b832bf0a20bcbbf914760345921859", + "addons/core/Sounds/ERR/Speech/soldier_1200.wav.meta": "ccf3b62fd2eea460d576fa68863a901eb0025f47b1b76729a4289162d0dfa0c8", + "addons/core/Sounds/ERR/Speech/soldier_1201.acp": "45f220c5b6bac64db61eebcf54bb52da572f9f8f1ea0872c5af9fd3238c25a4e", + "addons/core/Sounds/ERR/Speech/soldier_1201.acp.meta": "3be417d66b05e11e0b520af136dfc45c86c17d5e35b56a611efbcf38e0d35003", + "addons/core/Sounds/ERR/Speech/soldier_1201.wav": "f058f0482cfe4034fdf37d9b596f7ba8172fae1c57aeb387ce153f8041895e45", + "addons/core/Sounds/ERR/Speech/soldier_1201.wav.meta": "abaebe719eff7e009eb4d0222060280877451126c02154706df6d13e4c5d3e11", + "addons/core/Sounds/ERR/Speech/soldier_1202.acp": "c08625886d8c6dc405cf96c640c9dafd4510ba5cef2c22b037c8c7b3eb5bea39", + "addons/core/Sounds/ERR/Speech/soldier_1202.acp.meta": "cf345e968edad1b825dd33b87267b63f770c169250f8c51195bffc3161d396bd", + "addons/core/Sounds/ERR/Speech/soldier_1202.wav": "c875f4e798ff5e24c3eb3b48bdfa1b8af03de9326a19c671eca86660297ad737", + "addons/core/Sounds/ERR/Speech/soldier_1202.wav.meta": "dd2d6411abe914bce71c6688a1647c2699303ca248191ec01c6325b366e5d7fb", + "addons/core/Sounds/ERR/Speech/soldier_1203.acp": "f0237a51ffe0e7d463154d321117a9b19c6914f6ee680ebf07a0ff9ab51fc340", + "addons/core/Sounds/ERR/Speech/soldier_1203.acp.meta": "0e93332191ac74be950ce8139b96ed6fdfe3ddbe68de8949826dc801d12c97f2", + "addons/core/Sounds/ERR/Speech/soldier_1203.wav": "6e7857dc96dfb556f011ba89f2f4ea659cfd7b605b88790c69e754d5cc51398a", + "addons/core/Sounds/ERR/Speech/soldier_1203.wav.meta": "0f564169dc05a95f7b01f64f72bdabf6de9ed3f37a2f44b4751f89b38eb18cd8", + "addons/core/Sounds/ERR/Speech/soldier_1204.acp": "94dbf98b5e05f1ebf6960495032c46203caaa4440dfb1ffd3beda2679fe6bec5", + "addons/core/Sounds/ERR/Speech/soldier_1204.acp.meta": "7cc8c61f4bb210c0576fac350d170f8f54a6e55531db6102c1a0fa7cb7a8a809", + "addons/core/Sounds/ERR/Speech/soldier_1204.wav": "bd170ca015b017114e42078dcc5c49d5626aef17d22e4a12b8282e9ee4908ae6", + "addons/core/Sounds/ERR/Speech/soldier_1204.wav.meta": "1c6ddb11c22a14d9d498074e2fb2864783f91afc552c4cd671452fa8df483de0", + "addons/core/Sounds/ERR/Speech/soldier_1205.acp": "47180a3445a265335d682eac6fa51fedca51d76f329adc39b1802092b311118b", + "addons/core/Sounds/ERR/Speech/soldier_1205.acp.meta": "c34346f4b10149b426ebb4e94db8c29883c9d0e36f2b42d07d41570eb8932554", + "addons/core/Sounds/ERR/Speech/soldier_1205.wav": "769c457805376e9c0172ddb525ead5f1d8774e15bd1f3f3b218a19f9d77b6883", + "addons/core/Sounds/ERR/Speech/soldier_1205.wav.meta": "31a2a7314b44afb3f0031376b2ad6df22649e8089bf1a3bbeb64cdc41156cff7", + "addons/core/Sounds/ERR/Speech/soldier_clear.acp": "f33f03f3d24182a95db7d02058f2dd6ae52098e3b5142a33ede833ee881d54ed", + "addons/core/Sounds/ERR/Speech/soldier_clear.acp.meta": "7d3643c9de2d81ab0f1b4898e781341c1962bd8505c20882233ef7c40159b23b", + "addons/core/Sounds/ERR/Speech/soldier_clear.wav": "d6527b8136aa720b721463210420f47de0dbc45acc4e4c390b6a44968c5f0acd", + "addons/core/Sounds/ERR/Speech/soldier_clear.wav.meta": "d9f9a921a85fe275c6cce649421a19946ff6aec49559796725c3858d9fe9b261", + "addons/core/Sounds/ERR/Speech/soldier_covering.acp": "7d4109edbd6c20f9e2af0b394fa12e8b3b40fc18bf2b5ac118beebb1fa4a79b1", + "addons/core/Sounds/ERR/Speech/soldier_covering.acp.meta": "320908bf7decea55b8d8b4a89b30e84e7d96a033d4a9cd1bd84912dce013f0d8", + "addons/core/Sounds/ERR/Speech/soldier_covering.wav": "f51e0086eaf0eece8b4061711088a413a2dbeae263deab7ea30fb1852dafda69", + "addons/core/Sounds/ERR/Speech/soldier_covering.wav.meta": "7bdf2d2c5f9cf741bae523e1ab0b9acc99b38ccd1228ebfbcba92f2b9a5cba9d", + "addons/core/Sounds/ERR/Speech/soldier_engaging.acp": "9f052e87edaf182030edb656821621cdeabbf13d65bc181308357530c9d93a74", + "addons/core/Sounds/ERR/Speech/soldier_engaging.acp.meta": "9e487fa3746c46b2993f1dfd1f0513f375571728704533674bee62586e25b72b", + "addons/core/Sounds/ERR/Speech/soldier_engaging.wav": "ac0f2d7d698732e3d7c270186e7aa2ab7e6bd4c908f36b07fb6f66a3cbe75ccf", + "addons/core/Sounds/ERR/Speech/soldier_engaging.wav.meta": "d571b9ebce196cc209078a83e4bde8f6a5f4b6d28c2945e2b47953ccb6889e97", + "addons/core/Sounds/ERR/Speech/soldier_moving.acp": "05503ca7af13a7ad03ed2a3aa562cc58504fdfff3148477f8d81b5a01abdf37c", + "addons/core/Sounds/ERR/Speech/soldier_moving.acp.meta": "a2474c87e4e1587226919dfdbd03739d938803d34f4a4f441889b0d8015513bd", + "addons/core/Sounds/ERR/Speech/soldier_moving.wav": "a61dda64e6fc2a11eac3e6008ee14b89f360bd67e258dce1191a71d9e2e26fad", + "addons/core/Sounds/ERR/Speech/soldier_moving.wav.meta": "bd87cd72ccb819584f9da09fb0d6973ffd6eb0bb5572c562978500e2af467217", + "addons/core/Sounds/ERR/Speech/soldier_no_ammo.acp": "c821c61afb2ddc12d3ecdb552a66d2a9ff89a5258a567ff2a99fed4f45a33520", + "addons/core/Sounds/ERR/Speech/soldier_no_ammo.acp.meta": "65a27b81a5b437163befd4dc566785a9dda4def1798495ca53c8f768a5d7a73f", + "addons/core/Sounds/ERR/Speech/soldier_no_ammo.wav": "ffe6ef5fdf30411749e0cb73186e3005b1ce6a52fcb7c836caeb1bca422ed3ed", + "addons/core/Sounds/ERR/Speech/soldier_no_ammo.wav.meta": "25b5680fd2a313c18e15ce6e0d3f709d78352b93206c22c7881be19b81faddd5", + "addons/core/Sounds/ERR/Speech/soldier_reloading.acp": "51c5cb464bab6637cf9e3b1880a010e05774d76a92298daf40b1d84970845be7", + "addons/core/Sounds/ERR/Speech/soldier_reloading.acp.meta": "ca351e809b1ba7c69b066d2c1d0a5d2afdcaa833c1163e6d13a0396437e98cf6", + "addons/core/Sounds/ERR/Speech/soldier_reloading.wav": "15b2749f79bc73fd0784bcd6e6076e1bc50da835e0c434cd43bd185d0735c732", + "addons/core/Sounds/ERR/Speech/soldier_reloading.wav.meta": "161c9ebee1d0496c7aa9501d00eba646094bf1ce89cda4efde6e3a0229a49e4f", + "addons/core/Sounds/ERR/Speech/soldier_target_down.acp": "77acc89c4ef0b912b969a2e5f6c41e071e04d10cfe89666fbc1296878dab62bf", + "addons/core/Sounds/ERR/Speech/soldier_target_down.acp.meta": "fe456d354ace93b0f2261de483e86ccc92991011b1738c5703d6d7bc0daa49a6", + "addons/core/Sounds/ERR/Speech/soldier_target_down.wav": "e2e60e8b83a24a548c90b84c5e1137adb7e792cf1d4ba6232ddfb1012fa78892", + "addons/core/Sounds/ERR/Speech/soldier_target_down.wav.meta": "064588b86589ac9437fbccafedeefae04bec3331dd8a229d1c6d27b5e524a9e9", + "addons/core/Sounds/ERR/Speech/soldier_target_lost.acp": "5ffd3ff57550296274e9e93fb64efdf2fb06a51161d6deb221c1f3eb408d1459", + "addons/core/Sounds/ERR/Speech/soldier_target_lost.acp.meta": "366a3c9e20f27c0f73bdff5827576df8924e99241061f933e72bd342e5bc7119", + "addons/core/Sounds/ERR/Speech/soldier_target_lost.wav": "bbcf454d48adea47478d410b1baf14df3a5d018237272e70ae33ee62823f822f", + "addons/core/Sounds/ERR/Speech/soldier_target_lost.wav.meta": "d1ac64242280521a7e40de51ae32a007878d7801401020e7a074a48e0f8ce62e", + "addons/core/Sounds/ERR/Speech/soldier_under_fire.acp": "5af1c949d4662c2f33320165d2efdbd69cb183af1101aa246fe538bf0d91a33e", + "addons/core/Sounds/ERR/Speech/soldier_under_fire.acp.meta": "aa7799e8a790539166a1d3a9c691416e7b2f6589d1674f5cf4b3e47523f15259", + "addons/core/Sounds/ERR/Speech/soldier_under_fire.wav": "e4bd5e69fda289593fc6e6b8c2fae9a271a10d0a9bae877f18ba02dbbf75d1fc", + "addons/core/Sounds/ERR/Speech/soldier_under_fire.wav.meta": "25a988c31e4fc3ae6540920df3d4548307a4a1e3b96b1303e9b7bdb4ac5eea27", + "addons/core/Sounds/ERR/Speech/welcome.acp": "f8a7b6cdd08b57b4b227a2d5b480a6c76857d7fc8ace3434478aec01c0de5a83", + "addons/core/Sounds/ERR/Speech/welcome.acp.meta": "d3ac98b02c0725d347b30d0ac7586cb22904d46b65a145e4a1131222484a6929", + "addons/core/Sounds/ERR/Speech/welcome.wav": "01de068469bd6746dba7634fb6b5b85b150621d3359c17fd01bcb916b34b1720", + "addons/core/Sounds/ERR/Speech/welcome.wav.meta": "bf33edad44bbb04df852501154f08a9a9900ce503248abc698127a51e9eec934", + "addons/core/UI/Textures/ERR/SiteMarkers.edds": "611ec6c68c2456c2af017c47135134d73122461017b315f64ed7ca9891f20c21", + "addons/core/UI/Textures/ERR/SiteMarkers.edds.meta": "90f40b38e52a79951b72da2960e30887d5ee8bbcefbada0c0ac950c1bfcf847f", + "addons/core/UI/Textures/ERR/SiteMarkers.imageset": "3f947d03b3492ada541032539cdb3f34a03d68ae83c0ef8796dc7fd091b5911d", + "addons/core/UI/Textures/ERR/SiteMarkers.imageset.meta": "1dc5285abd0062af288fb40b2f22b5b7c3925cf7270a096862335db250ac1453", + "addons/core/UI/Textures/ERR/SiteMarkers.png": "76b472982234bd27050a9430e6a53f4c14fa38f7388db3706ebef506eab4e660", + "addons/core/UI/layouts/ERR/AdminMenu.layout": "a9afb8957e26738df5ca48a6de0ebd57dd14a10046fd136b5cb9c858ef79e584", + "addons/core/UI/layouts/ERR/AdminMenu.layout.meta": "9deeece996f43b1f1fed6b5ec89279645c7b6424250200a42580cc088142e9bf", + "addons/core/UI/layouts/ERR/AdminValue.layout": "cd811af6d2fb187f01c74e20ea07ad85669546b0ad70efae2ce52921ce26cbca", + "addons/core/UI/layouts/ERR/AdminValue.layout.meta": "56b3d2335db400c1308e07c4a35d278b23ef32013dfd6dc82951866327428d08", + "addons/core/UI/layouts/ERR/ContentRow.layout": "942e33bf2e08550446b40bcac31fb4504b7d8c717a42c196f54bf4025c72706c", + "addons/core/UI/layouts/ERR/ContentRow.layout.meta": "8c4bc13b41e7aee38e0ca640a97813ed1a6964a00e2cff9e1fe4ebd71fc2fdc6", + "addons/core/UI/layouts/ERR/ItemRow.layout": "6d364ce48158b90abd2185ca87ee27b095d0f7a5c8d59a0c65940e4be5488ac3", + "addons/core/UI/layouts/ERR/ItemRow.layout.meta": "33eaef9c403ea3a030e58ec17f78e46af457048243571ad5856fff04101fb60b", + "addons/core/UI/layouts/ERR/ItemWeightRow.layout": "dfa6b87420efc683cb278178553350daa7753004d64672741a3dc13b4316810d", + "addons/core/UI/layouts/ERR/ItemWeightRow.layout.meta": "eb4b28587abdf6100092fd6eee1695663fa6e1548d5e53017173c4a5f7320240", + "addons/core/UI/layouts/ERR/PauseModsButton.layout": "d21d436291e2c1b262e33dc228b0d60a1d188dc9e85cfda6d2373f8281431b79", + "addons/core/UI/layouts/ERR/PauseModsButton.layout.meta": "9ed6adabd8f53eb9a562d7186789876bba5c4202155ca894c1af866ca47354f3", + "addons/core/UI/layouts/ERR/PreparationFailure.layout": "b7c4a2e6aa2be0992b478bb4f969f80355753573fffbc15be44670f3b136ea2e", + "addons/core/UI/layouts/ERR/PreparationFailure.layout.meta": "17ca2c9a12f691680883cec368ff06f511c996e5da5c25498f5f0955887867b0", + "addons/core/UI/layouts/ERR/PreparationStatus.layout": "ec3369f4030e593d34043e70a311b0963606cf024cb452648671556f25f5b52a", + "addons/core/UI/layouts/ERR/PreparationStatus.layout.meta": "75b12176a443bb911fc4ad446982d762201fda42ed38736b79bc5439fca31ccc", + "addons/core/UI/layouts/ERR/RoundResults.layout": "8550ebc009024f619827a43fd766aff8b7fbf8bc87ef7593db0fd7f4d778c615", + "addons/core/UI/layouts/ERR/RoundResults.layout.meta": "7509e050b3910ee5730dd25c60aacf405110e943f4ab46e3fc2186b3b15a3cf9", + "addons/core/UI/layouts/ERRMap/ObjectiveMapElement.layout": "82d9ae77f076f945583791dfa0b01f1c925e6ff8aaebcf382fd56089fe61a115", + "addons/core/UI/layouts/ERRMap/ObjectiveMapElement.layout.meta": "8691fe2eaf46568bca4a9f8128b247a8da810bb87a5546aa7ca3334e740270d6", + "addons/core/UI/layouts/ERRMap/SharedMapRenderTarget.layout": "aef5d565301046ae902033ec4eb4799ff1bdd9a37bc507898781d8790f04fc2a", + "addons/core/UI/layouts/ERRMap/SharedMapRenderTarget.layout.meta": "5c846445cc089e2e08899ba703059d4adb805ff30ab5a16409a3cd59718cbd54", + "addons/core/Worlds/ERR_Everon.ent": "22324bc7eb0d80dec8a1f3bd61a9df9201b72f749f4e751766b0c9d0b7d46b87", + "addons/core/Worlds/ERR_Everon.ent.meta": "19d1903ca72baa326b4724a6791d0da1104a1e53f3f31dc1ca41a856efd68e83", + "addons/core/Worlds/ERR_Everon_Layers/default.layer": "6bcb7fbb27b6282719dae8a37486e518d4cbaea385f2ea9af1918b36bb28b175", + "addons/core/addon.gproj": "73e973a1f1c5910393e16f86a3a40d792031bb2fa3fc7f4dd87a06a16c3e34a4" +} diff --git a/docs/evidence/telemetry-server-name-2026-11-01/native-checks.log b/docs/evidence/telemetry-server-name-2026-11-01/native-checks.log new file mode 100644 index 0000000..1e87979 --- /dev/null +++ b/docs/evidence/telemetry-server-name-2026-11-01/native-checks.log @@ -0,0 +1,15 @@ +19:26:53.303 RESOURCES (E): Wrong GUID/name for resource @"{0AFDD005CD669892}Language/rhs_localization.st" in property "StringTableSource" +19:26:53.304 RESOURCES (E): Wrong GUID/name for resource @"{41FA2A08B6971D4C}Language/wcs_armament_localization.st" in property "StringTableSource" +19:26:53.304 RESOURCES (E): Wrong GUID/name for resource @"{84270E09F7A4A7A0}Language/wcs_spacecore_localization.st" in property "StringTableSource" +19:26:53.305 RESOURCES (E): Wrong GUID/name for resource @"{E50E557B468C7073}Language/wcs_bmp1_localization.st" in property "StringTableSource" +19:27:32.910 WORLD (E): Unknown keyword/data 'Parent' at offset 35955(0x8c73) +19:27:32.910 WORLD (E): Unknown keyword/data 'Parent' at offset 36915(0x9033) +19:27:32.912 WORLD (E): Unknown keyword/data 'Parent' at offset 42646(0xa696) +19:27:32.912 WORLD (E): Unknown keyword/data 'Parent' at offset 43645(0xaa7d) +19:27:32.912 WORLD (E): Unknown keyword/data 'Parent' at offset 44617(0xae49) +19:27:32.912 WORLD (E): Unknown keyword/data 'Parent' at offset 45590(0xb216) +19:27:33.042 WORLD (E): Unknown keyword/data 'SlidingTrackMaterial' at offset 19441(0x4bf1) +19:27:40.567 WORLD (E): Unknown keyword/data 'm_HardLandingVelocity' at offset 133(0x85) +19:27:40.567 WORLD (E): Unknown keyword/data 'm_DeathLandingVelocity' at offset 163(0xa3) +19:27:48.902 SCRIPT : ERR SMOKE PASS: native server-name API and override; serverConfigPresent=0 resolvedLength=0 +19:27:48.902 SCRIPT : ERR SMOKE COMPLETE: server-name diff --git a/docs/evidence/telemetry-server-name-2026-11-01/native-probe.c b/docs/evidence/telemetry-server-name-2026-11-01/native-probe.c new file mode 100644 index 0000000..a2f59b1 --- /dev/null +++ b/docs/evidence/telemetry-server-name-2026-11-01/native-probe.c @@ -0,0 +1,29 @@ +#ifdef WORKBENCH +modded class ERR_GameMode +{ + protected override void StartScenario() {} + protected override void EOnInit(IEntity owner) + { + super.EOnInit(owner); + if (!SCR_Global.IsEditMode()) GetGame().GetCallqueue().CallLater(ServerNameProbe, 8000, false); + } + void ServerNameProbe() + { + if (ERR_TelemetryServerName.Resolve(" Manual name ") != "Manual name") { Print("ERR SMOKE FAIL: server name override", LogLevel.ERROR); return; } + string expected; + ServerConfig config = ServerLobbyApi.GetServerConfig(); + if (config) expected = config.GetName(); + expected.TrimInPlace(); + if (expected.IsEmpty()) + { + ServerInfo info = GetGame().GetServerInfo(); + if (info) expected = info.GetName(); + expected.TrimInPlace(); + } + if (expected.Length() > 120) expected = expected.Substring(0, 120); + if (ERR_TelemetryServerName.Resolve("") != expected) { Print("ERR SMOKE FAIL: native server metadata resolution", LogLevel.ERROR); return; } + PrintFormat("ERR SMOKE PASS: native server-name API and override; serverConfigPresent=%1 resolvedLength=%2", config != null, expected.Length()); + Print("ERR SMOKE COMPLETE: server-name"); + } +} +#endif diff --git a/docs/evidence/telemetry-server-name-2026-11-01/runner.json b/docs/evidence/telemetry-server-name-2026-11-01/runner.json new file mode 100644 index 0000000..dee6f05 --- /dev/null +++ b/docs/evidence/telemetry-server-name-2026-11-01/runner.json @@ -0,0 +1,34 @@ +{ + "stopped_unix": 1793557676.2040396, + "console_sha256": "be222c877a00082b926346f792d60123bc5a9a32e2453e1d67e8086a55a7a8b8", + "end_source_sha256": "4231ef21dc76eee5a8b985429a24f9bd54df8244df8c71f1c605a18445728d49", + "source_unchanged_after_run": true, + "source_provenance": "launch_snapshot", + "launch_id": "afae555b-934f-4c27-a8ea-93e9eca2bcac", + "launch_source_manifest": "launch-source-sha256.json", + "launch_source_metadata": "launch-source.json", + "launch_source_sha256": "4231ef21dc76eee5a8b985429a24f9bd54df8244df8c71f1c605a18445728d49", + "external_addons_unchanged_after_run": true, + "external_addon_count": 7, + "external_addon_inputs": "external-addons-inputs.json", + "started_unix": 1793557609.7395656, + "result": "smoke-completed", + "result_before_cleanup": "smoke-completed", + "cleanup_confirmed": true, + "cleanup_report": { + "cleanup_confirmed": true, + "idle_verified": true, + "errors": [], + "orphan_cleanup": { + "attempted": false, + "reason": "noncanonical-private-prefix" + } + }, + "runner_error": null, + "session": "/tmp/erri-server-name-run2/logs/logs_2026-11-01_19-26-51", + "rounds_required": 1, + "project": "core", + "display": "headless Gamescope 1280x720/30Hz", + "dedicated_server": false, + "audio": "silent sink and per-test-stream mute" +} diff --git a/docs/evidence/telemetry-server-name-2026-11-01/setup.py b/docs/evidence/telemetry-server-name-2026-11-01/setup.py new file mode 100644 index 0000000..3105f59 --- /dev/null +++ b/docs/evidence/telemetry-server-name-2026-11-01/setup.py @@ -0,0 +1,16 @@ +from pathlib import Path +import sys,shutil +root=Path('/home/jakub/devel/escapistsrereintroduced');sys.path.insert(0,str(root/'tools')) +from source_manifest import addon_manifest +from headless_game import assert_prefix_idle +assert_prefix_idle(root/'.local/playtest-compat-20260920') +target=Path('/tmp/erri-server-name-source');target.mkdir(exist_ok=True) +for rel in addon_manifest(root): + p=target/rel;p.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(root/rel,p) +shutil.copytree(root/'tools',target/'tools',ignore=shutil.ignore_patterns('__pycache__'),dirs_exist_ok=True) +(target/'docs').mkdir(exist_ok=True);shutil.copy2(root/'docs/external-addons.json',target/'docs/external-addons.json') +core=target/'addons/core/Scripts/Game/ERR/00_Core' +(core/'ERR_ZServerNameProbe.c').write_text(Path('/tmp/erri-server-name-probe.c').read_text()) +p=core/'ERR_NativeBaseSmoke.c';p.write_text(p.read_text().replace('array independentFixtures = {','array independentFixtures = {"server-name", ')) +p=target/'tools/headless_workbench.py';s=p.read_text().replace("choices=['coordinated-ai',","choices=['server-name', 'coordinated-ai',").replace("a.smoke not in {'coordinated-ai',","a.smoke not in {'server-name', 'coordinated-ai',");p.write_text(s) +print(target) diff --git a/docs/evidence/telemetry-server-name-2026-11-01/verification.json b/docs/evidence/telemetry-server-name-2026-11-01/verification.json new file mode 100644 index 0000000..745a255 --- /dev/null +++ b/docs/evidence/telemetry-server-name-2026-11-01/verification.json @@ -0,0 +1,12 @@ +{ + "workerVersion": "3f33aae9-ba97-4b7e-a680-30483bbc8ad2", + "migration": "0004_reported_server_names.sql", + "apiAndClientTests": 21, + "bundledClientTests": 2, + "native": "compiled; explicit override and absent metadata safe fallback passed; no configured dedicated-server name in Workbench", + "scriptFailures": 0, + "inheritedEngineErrors": 13, + "firstAttempt": "GameLib ServerInfo and Game ServerInfo are distinct native types. Corrected fallback to GetGame().GetServerInfo(); rerun passed.", + "liveDashboard": "HTML and JS match tested bundle; manual refresh retained", + "historicalData": "10 servers; 0 named servers; 0 stored events with a name at inspection" +} diff --git a/docs/telemetry.md b/docs/telemetry.md index 16e21cb..1d081d4 100644 --- a/docs/telemetry.md +++ b/docs/telemetry.md @@ -243,3 +243,37 @@ kills. Downed means an unconscious episode, while a death is a fatal loss. Recoveries include self-recovery and are not healer-attributed revives. Local result tracking continues when telemetry is disabled; sending still follows the existing telemetry setting and endpoint configuration. + +## Browser dashboard + +The receiver serves a private statistics dashboard at +`https://co10escape-telemetry.zacekjakub.workers.dev/dashboard`. Use the receiver’s +read-only token or admin token to view server records and last receipt times, +mission outcomes, player counters, mods, settings and raw event history. Tokens +are held only in browser-tab memory. See the [receiver guide](../services/telemetry-worker/README.md#statistics-dashboard) +for deployment, pagination and freshness semantics. This requires only the Worker +update and receipt index migration; it does not require a new mission release. + +### Automatic server names + +New events resolve the reporting server’s name from `ServerLobbyApi.GetServerConfig().GetName()`, +falling back to lobby metadata if unavailable. The optional profile `serverName` +remains an explicit override. Names are trimmed and bounded to 120 characters. +Resolution repeats for each new event so startup metadata delays do not permanently +leave a server unnamed. Neither scenario titles nor account identities are used. +The installation UUID and queued historical events remain unchanged. + +The receiver updates automatic server labels from nonblank accepted reports, +using server-reported event time to prevent older queued names from replacing a +newer one. As with all telemetry timestamps, bad server clocks can affect that +ordering. Managed labels remain operator-owned. Mission rows use the historical +reported name where present and the current installation label otherwise. +Automatic discovery needs a new Workshop release; existing blank records only +acquire names after an updated server reports. + +Validation: [2026-11-01 evidence](evidence/telemetry-server-name-2026-11-01/verification.json) +records 21 receiver/client tests plus two bundled-client checks. Native compilation, +explicit override and absent-metadata fallback pass without script failures; the +13 inherited resource/world errors remain. The local Workbench fixture has no +configured server name, so the real dedicated-server name must still be confirmed +from the first report after publishing. diff --git a/services/telemetry-worker/README.md b/services/telemetry-worker/README.md index db8e5f7..5e4a145 100644 --- a/services/telemetry-worker/README.md +++ b/services/telemetry-worker/README.md @@ -69,13 +69,57 @@ set, administration and detailed reads fail closed. Public aggregate summaries remain available. Public ingestion enrolls servers independently of this admin credential. Wrangler prints the Worker URL, for example `https://co10escape-telemetry.YOUR-SUBDOMAIN.workers.dev`. `/health` is a public liveness -check, not a database readiness test. The service does not enable CORS; a future -browser dashboard should use a backend to keep API credentials private. +check, not a database readiness test. The service does not enable CORS. The dashboard is served from the same Worker +and uses your entered read credential without embedding any service secret. Deployment uses Cloudflare's documented [D1 migrations](https://developers.cloudflare.com/d1/reference/migrations/), [Worker secrets](https://developers.cloudflare.com/workers/configuration/secrets/), and [transactional D1 batches](https://developers.cloudflare.com/d1/worker-api/d1-database/#batch). +## Statistics dashboard + +Open `https://co10escape-telemetry.zacekjakub.workers.dev/dashboard` after deploying +this Worker. Enter the existing `READ_TOKEN` (recommended) or `ADMIN_TOKEN`. +The static login page is public; every detailed data request is authenticated. +Tokens remain in tab memory, never in URLs, browser storage or bundled assets. +Refreshing the page or signing out clears the token. There are no third-party +scripts, fonts or analytics and no server-administration buttons. + +- **Overview:** total servers/reports/missions, wins, losses, win rate, outcome + breakdown and terrain records. Counts cover all stored records, not just the page. +- **Servers:** per-installation mission totals, last stored report and its event + type, latest reported terrain/player count, registration and ingestion status. + Search by saved name or installation ID. Unnamed servers retain their UUID. + Updated missions automatically report the configured game-server name; automatic + installation labels follow the newest nonblank reported name. Blank/older queued + reports cannot erase it. Explicitly managed registration labels remain unchanged. +- **Missions:** page through history, filter by server/outcome/exact terrain, + and open players, mods, numeric settings, coverage flags, selected snapshot JSON + and paginated raw event history. Download the selected snapshot as JSON. +- **Players:** personal escapes and combat totals, including counter coverage. + Coverage counts are not counts of missions with friendly-fire incidents. + +Selecting a server scopes Overview, Missions and Players. The Servers tab returns +to all installations. Overview shows at most 100 terrains with an explicit notice +if truncated; all missions remain accessible through filters and pagination. +Lists show 25 records per page. Automatic refresh is disabled. Use **Refresh** +to update the current view; navigation and filters fetch their requested records. + +**Last received** is the collector's timestamp of the latest newly stored event. +It includes late queued events that do not replace a completed mission snapshot; +idempotent retries do not advance it. The separately shown server-reported time +may have clock skew. Game heartbeats normally arrive every five minutes. Neither +fresh receipts nor silence prove online/offline status. A server's Enabled/Revoked +label controls ingestion and is not an online indicator. No-report servers show +“No reports yet”; absent counters and undefined win rates show a dash. + +Run `npm run migrate:remote` before `npm run deploy`. Migration 0003 adds a +receipt lookup index; 0004 maintains automatic server labels from accepted events +and recovers any existing reported names without rewriting event history. +The webpage requires no mission update. Automatic name discovery requires the +updated mission release; no operator configuration is needed. Until such a server +reports, blank historical names cannot be reconstructed from its installation UUID. + ## Automatic community-server enrollment The updated addon saves a random server ID in its telemetry state and generates @@ -179,18 +223,20 @@ they are not invented as zero. This feature requires the updated Workshop build. ## API -All responses are JSON with `Cache-Control: no-store`. Administrative endpoints +API responses are JSON with `Cache-Control: no-store`. Administrative endpoints require `Authorization: Bearer ADMIN_TOKEN`. Read endpoints accept ADMIN_TOKEN or the optional READ_TOKEN. Public summaries and `/health` do not require a token; ingestion uses each installation's own credential. | Method and path | Purpose | | --- | --- | +| `GET /dashboard` | Browser statistics dashboard; details require a read/admin token | +| `GET /api/dashboard` | Authenticated overview, optional `serverId`; totals, outcomes and up to 100 terrains | | `GET /health` | Public liveness check | | `GET /`, `GET /api/summary`, `GET /api/mission-events` | Public map win/failure summaries; optional `terrain` and `serverId` filters | | `POST /api/mission-events` | Ingest `{ "token": "...", "event": {...} }`; first valid event enrolls an unknown server | | `POST /api/servers` | Register `{serverId, name}` and return a new token | -| `GET /api/servers` | List server labels, IDs, registration origin, creation times and active state; never token hashes | +| `GET /api/servers` | Read/admin access: labels, IDs, registration, mission totals, last received report and active ingestion state; optional `q` name/ID search; never token hashes | | `POST /api/servers/:serverId/token` | Rotate token and reactivate server | | `DELETE /api/servers/:serverId` | Revoke ingestion; retain history | | `GET /api/missions` | Mission summaries, newest-created first | @@ -200,7 +246,7 @@ ingestion uses each installation's own credential. | `GET /api/players` | Authenticated participants' attempts, personal escapes and combat totals | `/api/missions`, `/api/stats` and `/api/players` accept an optional `serverId` filter. -`/api/missions` also accepts `outcome` (`victory`, `defeat`, `preparation_failed`, +`/api/missions` also accepts an exact, case-insensitive `terrain` filter and `outcome` (`victory`, `defeat`, `preparation_failed`, `restarted_empty`, `interrupted`, or `unfinished`). Mission, server and event lists accept `limit` (1–100, default 50) and `before` from the preceding `nextBefore`. Player lists sort by stable account ID and use `after` from `nextAfter`, plus @@ -251,3 +297,19 @@ policy should preserve deduplication tombstones if raw payloads are removed. Keep read credentials private: mission details contain player names and account IDs. Worker request logging is disabled in the supplied configuration. Cloudflare platform logging/settings are managed separately in the account. + +## Dashboard verification (2026-11-01) + +20 local tests pass, including real Miniflare/D1 ingestion and aggregate queries, +read-only authorization, late receipt/final-result separation, pagination and +client auth/navigation/detail/error behavior using a minimal DOM boundary. +`npm run check` also exercises the exact Wrangler-bundled browser script; keep +`keep_names: false` so function serialization has no Worker-only helper references. + +Migration 0003 was applied and Worker version +`8fa09cd0-f148-43d4-a6d8-685528d73618` deployed. Live dashboard HTML/JS/CSS returned +200 and matched the tested bundle byte-for-byte; health/public summary returned +200 and unauthenticated detailed routes returned 401. No production test events +were submitted. Native browser visual/accessibility verification was unavailable +in this session; the DOM tests do not establish layout quality. Authenticated +production data rendering was not exercised with the operator's private token. diff --git a/services/telemetry-worker/migrations/0003_dashboard_receipts.sql b/services/telemetry-worker/migrations/0003_dashboard_receipts.sql new file mode 100644 index 0000000..ef54383 --- /dev/null +++ b/services/telemetry-worker/migrations/0003_dashboard_receipts.sql @@ -0,0 +1,3 @@ +-- Bounded per-server lookup of the last stored report, including late events +-- that did not change the authoritative mission snapshot. +CREATE INDEX events_server_receipt ON events(server_id, received_at DESC, id DESC); diff --git a/services/telemetry-worker/migrations/0004_reported_server_names.sql b/services/telemetry-worker/migrations/0004_reported_server_names.sql new file mode 100644 index 0000000..4c6e4e0 --- /dev/null +++ b/services/telemetry-worker/migrations/0004_reported_server_names.sql @@ -0,0 +1,24 @@ +-- Names describe an installation; its existing UUID remains the identity. +-- Operator-managed labels remain under operator control. +ALTER TABLE servers ADD COLUMN name_reported_at INTEGER; + +UPDATE servers SET + name = (SELECT trim(json_extract(payload, '$.serverName')) FROM events + WHERE events.server_id = servers.server_id AND trim(json_extract(payload, '$.serverName')) != '' + ORDER BY json_extract(payload, '$.occurredAt') DESC, id DESC LIMIT 1), + name_reported_at = (SELECT json_extract(payload, '$.occurredAt') FROM events + WHERE events.server_id = servers.server_id AND trim(json_extract(payload, '$.serverName')) != '' + ORDER BY json_extract(payload, '$.occurredAt') DESC, id DESC LIMIT 1) +WHERE registration = 'automatic' AND EXISTS ( + SELECT 1 FROM events WHERE events.server_id = servers.server_id + AND trim(json_extract(payload, '$.serverName')) != ''); + +-- This runs only for a newly accepted, authenticated event in its transaction. +-- Blank reports and old queued events must not erase/roll back a known name. +CREATE TRIGGER project_server_name AFTER INSERT ON events +WHEN trim(json_extract(NEW.payload, '$.serverName')) != '' BEGIN + UPDATE servers SET name = trim(json_extract(NEW.payload, '$.serverName')), + name_reported_at = json_extract(NEW.payload, '$.occurredAt') + WHERE server_id = NEW.server_id AND registration = 'automatic' + AND (name_reported_at IS NULL OR json_extract(NEW.payload, '$.occurredAt') >= name_reported_at); +END; diff --git a/services/telemetry-worker/package.json b/services/telemetry-worker/package.json index a7e4b42..24dfdfc 100644 --- a/services/telemetry-worker/package.json +++ b/services/telemetry-worker/package.json @@ -9,7 +9,7 @@ "scripts": { "test": "node --test test/*.test.js", "dev": "wrangler dev", - "check": "wrangler deploy --dry-run --outdir dist", + "check": "wrangler deploy --dry-run --outdir dist && DASHBOARD_BUNDLE=1 node --test test/dashboard-client.test.js", "deploy": "wrangler deploy", "migrate:local": "wrangler d1 migrations apply DB --local", "migrate:remote": "wrangler d1 migrations apply DB --remote" diff --git a/services/telemetry-worker/src/dashboard-api.js b/services/telemetry-worker/src/dashboard-api.js new file mode 100644 index 0000000..31b2adf --- /dev/null +++ b/services/telemetry-worker/src/dashboard-api.js @@ -0,0 +1,44 @@ +// Receipt times come from the receiver, never the game server's clock. A late +// queued report is fresh receipt evidence, not proof that the server is online. +export async function dashboardOverview(db, serverId) { + const where = serverId ? ' WHERE server_id = ?' : ''; + const values = serverId ? [serverId] : []; + const results = await db.batch([ + db.prepare(`SELECT COUNT(*) AS missions, COALESCE(SUM(outcome = 'victory'), 0) AS wins, + COALESCE(SUM(outcome = 'defeat'), 0) AS losses, COALESCE(SUM(terminal = 0), 0) AS unfinished, + COALESCE(SUM(partial), 0) AS partialMissions FROM missions${where}`).bind(...values), + db.prepare(`SELECT COUNT(*) AS servers FROM servers${where}`).bind(...values), + db.prepare(`SELECT COUNT(*) AS events, MAX(received_at) AS lastReceivedAt FROM events${where}`).bind(...values), + db.prepare(`SELECT CASE WHEN terminal = 1 THEN outcome ELSE 'unfinished' END AS outcome, + COUNT(*) AS missions FROM missions${where} GROUP BY terminal, outcome`).bind(...values), + db.prepare(`SELECT lower(json_extract(payload, '$.terrain')) AS terrain, COUNT(*) AS missions, + SUM(outcome = 'victory') AS wins, SUM(outcome = 'defeat') AS losses, + SUM(terminal = 0) AS unfinished FROM missions${where} + GROUP BY lower(json_extract(payload, '$.terrain')) ORDER BY missions DESC, terrain LIMIT 101`).bind(...values), + ]); + const totals = {...results[0].results[0], ...results[1].results[0], ...results[2].results[0]}; + const completed = totals.wins + totals.losses; + totals.winRate = completed ? Math.round(1000 * totals.wins / completed) / 10 : null; + return {serverId, generatedAt: Math.floor(Date.now() / 1000), totals, + outcomes: results[3].results, maps: results[4].results.slice(0, 100), mapsTruncated: results[4].results.length > 100}; +} + +export async function dashboardServers(db, {limit, before}, query) { + const result = await db.prepare(`WITH page AS ( + SELECT rowid AS cursor, * FROM servers WHERE rowid < ? + AND (? = '' OR instr(lower(name), lower(?)) > 0 OR instr(server_id, lower(?)) > 0) + ORDER BY rowid DESC LIMIT ? + ) SELECT s.cursor, s.server_id AS serverId, s.name, s.active, s.registration, s.created_at AS createdAt, + e.received_at AS lastReceivedAt, json_extract(e.payload, '$.occurredAt') AS lastReportedAt, + json_extract(e.payload, '$.type') AS lastEventType, e.mission_id AS lastMissionId, + json_extract(e.payload, '$.terrain') AS terrain, + json_extract(e.payload, '$.connectedPlayers') AS lastReportedPlayers, + COUNT(m.id) AS missions, COALESCE(SUM(m.outcome = 'victory'), 0) AS wins, + COALESCE(SUM(m.outcome = 'defeat'), 0) AS losses, + COALESCE(SUM(m.terminal = 0), 0) AS unfinished, COALESCE(SUM(m.partial), 0) AS partialMissions + FROM page s LEFT JOIN events e ON e.id = ( + SELECT id FROM events WHERE server_id = s.server_id ORDER BY received_at DESC, id DESC LIMIT 1) + LEFT JOIN missions m ON m.server_id = s.server_id + GROUP BY s.cursor ORDER BY s.cursor DESC`).bind(before, query, query, query, limit).all(); + return {servers: result.results, nextBefore: result.results.length === limit ? result.results.at(-1).cursor : null}; +} diff --git a/services/telemetry-worker/src/dashboard-client.js b/services/telemetry-worker/src/dashboard-client.js new file mode 100644 index 0000000..8346a5f --- /dev/null +++ b/services/telemetry-worker/src/dashboard-client.js @@ -0,0 +1,209 @@ +// Serialized as a standalone same-origin browser script. Never interpolate +// telemetry into HTML; names, reasons and arbitrary settings are text nodes. +export function dashboardApp() { + const $ = id => document.getElementById(id); + const state = {token: '', view: 'overview', server: '', serverName: '', outcome: '', terrain: '', query: '', cursors: [null], page: 0}; + let controller, detailController; + const labels = {victory: 'Victory', defeat: 'Defeat', unfinished: 'Unfinished', preparation_failed: 'Preparation failed', restarted_empty: 'Empty restart', interrupted: 'Interrupted'}; + const phases = ['Preparing', 'Prison', 'Radio search', 'Radio defence', 'Extraction', 'Victory', 'Defeat']; + const number = value => value == null ? '—' : Number(value).toLocaleString(); + const date = value => value == null ? 'Never received' : new Date(value * 1000).toLocaleString(); + const age = value => { + if (value == null) return 'No reports yet'; + const seconds = Math.max(0, Math.floor(Date.now() / 1000 - value)); + if (seconds < 60) return 'Just now'; + if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; + if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; + return `${Math.floor(seconds / 86400)}d ago`; + }; + const duration = value => value == null ? '—' : `${Math.floor(value / 3600)}h ${Math.floor(value % 3600 / 60)}m ${Math.floor(value % 60)}s`; + function el(tag, text, className) { + const node = document.createElement(tag); + if (text != null) node.textContent = String(text); + if (className) node.className = className; + return node; + } + function button(text, action, className = 'link') { + const node = el('button', text, className); node.type = 'button'; node.addEventListener('click', action); return node; + } + function stack(text, sub, mono = false) { + const node = el('div', text); node.append(el('span', sub, mono ? 'sub mono' : 'sub')); return node; + } + function timestamp(value) { + const node = stack(age(value), date(value)); + if (value != null) node.title = new Date(value * 1000).toISOString(); + return node; + } + function badge(outcome) { + return el('span', labels[outcome] || outcome, `badge ${outcome === 'victory' ? 'good' : outcome === 'defeat' ? 'bad' : 'warn'}`); + } + function table(headers, rows) { + const wrap = el('div', null, 'table-wrap'), node = el('table'), head = el('thead'), tr = el('tr'), body = el('tbody'); + headers.forEach(h => tr.append(el('th', h))); head.append(tr); node.append(head, body); wrap.append(node); + rows.forEach(cells => { const row = el('tr'); cells.forEach(value => { const cell = el('td'); cell.append(value instanceof Node ? value : document.createTextNode(value == null ? '—' : String(value))); row.append(cell); }); body.append(row); }); + if (!rows.length) { const cell = el('td', 'No records match this selection.', 'empty'); cell.colSpan = headers.length; const row = el('tr'); row.append(cell); body.append(row); } + return wrap; + } + function params(extra = {}) { + const query = new URLSearchParams(); + if (state.server) query.set('serverId', state.server); + Object.entries(extra).forEach(([key, value]) => { if (value != null && value !== '') query.set(key, value); }); + return `?${query}`; + } + async function api(path, signal) { + const response = await fetch(path, {headers: {Authorization: `Bearer ${state.token}`}, cache: 'no-store', signal}); + if (response.status === 401) throw new Error('Token rejected. Sign out and enter a valid read or administrator token.'); + if (!response.ok) { const data = await response.json().catch(() => ({})); throw new Error(data.error || `Request failed (${response.status}).`); } + return response.json(); + } + function resetPages() { state.page = 0; state.cursors = [null]; } + function selectServer(serverId, name) { + state.server = serverId; state.serverName = name || serverId; state.view = 'missions'; resetPages(); refresh(); + } + function renderMetrics(data) { + const t = data.totals, cards = [ + ['Reporting servers', number(t.servers), `${number(t.events)} stored reports`], + ['Missions recorded', number(t.missions), `${number(t.unfinished)} without a final result`], + ['Successful escapes', number(t.wins), `${number(t.losses)} failed missions`], + ['Win rate', t.winRate == null ? '—' : `${t.winRate}%`, 'Completed gameplay missions only'], + ]; + $('metrics').replaceChildren(...cards.map(([label, value, note]) => { const card = el('div', null, 'metric'); card.append(el('div', label, 'metric-label'), el('div', value, 'metric-value'), el('div', note, 'metric-note')); return card; })); + } + function renderOverview(data) { + const grid = el('div', null, 'overview-grid'), left = el('div'), right = el('div'); + left.append(el('h2', 'Mission outcomes'), el('p', 'One result per mission. Startup failures and empty restarts remain separate.', 'small')); + for (const outcome of Object.keys(labels)) { + const count = data.outcomes.find(row => row.outcome === outcome)?.missions || 0; + const row = el('div', null, 'outcome-row'), label = el('div', null, 'outcome-label'), bar = el('div', null, 'bar'), fill = el('i', null, outcome === 'victory' ? 'good' : outcome === 'defeat' ? 'bad' : ''); + label.append(el('span', labels[outcome]), el('span', number(count))); fill.style.width = `${data.totals.missions ? 100 * count / data.totals.missions : 0}%`; bar.append(fill); row.append(label, bar); left.append(row); + } + left.append(el('h3', 'Latest report received'), timestamp(data.totals.lastReceivedAt), el('p', `${number(data.totals.partialMissions)} missions have incomplete telemetry coverage.`, 'small')); + right.append(el('h2', 'By terrain'), el('p', 'Select a terrain to explore its missions. Historical world paths are preserved as reported.', 'small')); + right.append(table(['Terrain', 'Missions', 'Won', 'Failed', 'Unfinished'], data.maps.map(map => [button(map.terrain || 'Unknown terrain', () => { state.terrain = map.terrain || ''; state.view = 'missions'; resetPages(); refresh(); }), number(map.missions), number(map.wins), number(map.losses), number(map.unfinished)]))); + if (data.mapsTruncated) right.append(el('p', 'Showing the 100 terrains with the most missions. Use the terrain filter in Missions to find others.', 'notice')); + grid.append(left, right); $('content').replaceChildren(grid); + } + function pager(next) { + const node = el('div', null, 'pager'), controls = el('div'); + const previous = button('← Previous', () => { state.page--; refresh(); }, 'secondary'); previous.disabled = state.page === 0; + const forward = button('Next →', () => { state.cursors[state.page + 1] = next; state.page++; refresh(); }, 'secondary'); forward.disabled = next == null; + controls.append(previous, forward); node.append(el('span', `Page ${state.page + 1} · Up to 25 records per page`), controls); return node; + } + function toolbar(title, description) { + const node = el('div', null, 'toolbar'), heading = el('div'); heading.append(el('h2', title), el('span', description, 'small muted')); node.append(heading); return node; + } + function textFilter(label, value) { + const node = el('label', label), input = el('input'); input.value = value; node.append(input); return {node, input}; + } + function renderServers(data) { + const top = toolbar('Reporting servers', 'Registration is permission to submit data, not an online status.'), form = el('form', null, 'filters'); + const query = textFilter('Server name or ID', state.query); query.input.maxLength = 120; + const submit = el('button', 'Search', 'secondary'); form.append(query.node, submit); form.addEventListener('submit', e => { e.preventDefault(); state.query = query.input.value.trim(); resetPages(); refresh(); }); top.append(form); + const rows = data.servers.map(server => { + const name = el('div'); name.append(button(server.name || 'Unnamed server', () => selectServer(server.serverId, server.name)), el('span', server.serverId, 'sub mono')); + const registration = stack(server.active ? 'Enabled' : 'Revoked', server.registration); + const results = stack(`${number(server.wins)} won / ${number(server.losses)} failed`, `${number(server.missions)} missions · ${number(server.unfinished)} unfinished`); + const last = timestamp(server.lastReceivedAt); last.append(el('span', server.lastEventType || 'No event', 'sub')); + const latest = server.lastMissionId ? button(server.terrain || 'Unknown terrain', () => showMission(server.serverId, server.lastMissionId)) : '—'; + return [name, results, last, stack(number(server.lastReportedPlayers), 'at last report'), latest, registration]; + }); + $('content').replaceChildren(top, table(['Server', 'Mission record', 'Last received', 'Players', 'Latest reported mission', 'Ingestion'], rows), pager(data.nextBefore)); + } + function missionServer(m) { + const node = el('div'); + node.append(button(m.serverName || 'Unnamed server', () => selectServer(m.serverId, m.serverName)), el('span', m.serverId, 'sub mono')); + return node; + } + function renderMissions(data) { + const top = toolbar('Mission history', 'Newest created first. Select a mission to inspect its complete report.'), form = el('form', null, 'filters'); + const outcomeLabel = el('label', 'Outcome'), select = el('select'); + for (const [value, label] of [['', 'All outcomes'], ...Object.entries(labels)]) { const opt = el('option', label); opt.value = value; select.append(opt); } + select.value = state.outcome; outcomeLabel.append(select); + const terrain = textFilter('Exact terrain / world path', state.terrain); terrain.input.maxLength = 1024; + form.append(outcomeLabel, terrain.node, el('button', 'Apply', 'secondary')); form.addEventListener('submit', e => { e.preventDefault(); state.outcome = select.value; state.terrain = terrain.input.value.trim(); resetPages(); refresh(); }); top.append(form); + const rows = data.missions.map(m => { + const title = el('div'); title.append(button(m.terrain || 'Unknown terrain', () => showMission(m.serverId, m.missionId)), el('span', m.missionId, 'sub mono')); + const flags = [m.partial ? 'Partial coverage' : '', m.playersTruncated ? 'Player list truncated' : '', m.droppedEvents ? `${m.droppedEvents} dropped reports` : ''].filter(Boolean).join(' · '); + return [title, badge(m.outcome), missionServer(m), duration(m.elapsedSeconds), number(m.peakPlayers), timestamp(m.receivedAt), flags || 'Complete report']; + }); + $('content').replaceChildren(top, table(['Terrain / mission', 'Outcome', 'Server', 'Duration', 'Peak players', 'Selected snapshot received', 'Coverage'], rows), pager(data.nextBefore)); + } + function counter(p, key, coverage) { + return stack(number(p[key]), `${number(p[coverage])}/${number(p.attempts)} missions reported this counter`); + } + function renderPlayers(data) { + const top = toolbar('Player records', 'Authenticated participants in completed gameplay missions. Names are available in individual mission reports.'); + const rows = data.players.map(p => [el('span', p.playerId, 'mono'), number(p.attempts), stack(number(p.escapes), `${number(p.didNotEscape)} did not escape`), duration(p.bestEscapeSeconds), counter(p, 'kills', 'missionsWithKills'), counter(p, 'friendlyKills', 'missionsWithFriendlyKills'), counter(p, 'deaths', 'missionsWithDeaths'), counter(p, 'downed', 'missionsWithDowned'), counter(p, 'recoveries', 'missionsWithRecoveries'), `${number(p.partialMissions)} partial / ${number(p.truncatedMissions)} truncated`]); + $('content').replaceChildren(top, table(['Player ID', 'Attempts', 'Escapes', 'Best escape', 'Kills', 'Friendly kills', 'Deaths', 'Downed', 'Recoveries', 'Coverage'], rows), el('p', 'Counter coverage means the field was present, not that a kill or injury happened. A dash means no recorded value.', 'small muted'), pager(data.nextAfter)); + } + async function refresh() { + controller?.abort(); const run = new AbortController(); controller = run; $('refresh').disabled = true; + $('error').hidden = true; $('updated').textContent = 'Fetching reports…'; + $('scope-label').textContent = state.server ? `Server: ${state.serverName}` : 'All reporting servers'; $('clear-scope').hidden = !state.server; + document.querySelectorAll('#tabs button').forEach(node => { if (node.dataset.view === state.view) node.setAttribute('aria-current', 'page'); else node.removeAttribute('aria-current'); }); + // Do not leave another server's rows visible beneath a newly selected scope. + $('content').replaceChildren(el('p', 'Loading records…', 'empty')); $('metrics').replaceChildren(); + try { + const cursor = state.cursors[state.page]; + const paths = {servers: `/api/servers${params({limit: 25, before: cursor, q: state.query})}`, missions: `/api/missions${params({limit: 25, before: cursor, outcome: state.outcome, terrain: state.terrain})}`, players: `/api/players${params({limit: 25, after: cursor})}`}; + const [summary, data] = await Promise.all([api(`/api/dashboard${params()}`, run.signal), state.view === 'overview' ? Promise.resolve(null) : api(paths[state.view], run.signal)]); + if (run.signal.aborted) return; + renderMetrics(summary); + if (state.view === 'overview') renderOverview(summary); + else if (state.view === 'servers') renderServers(data); + else if (state.view === 'missions') renderMissions(data); + else renderPlayers(data); + $('updated').textContent = `Refreshed ${date(summary.generatedAt)} · latest stored report ${age(summary.totals.lastReceivedAt)}`; + } catch (error) { + if (run.signal.aborted) return; + $('error').textContent = error.message; $('error').hidden = false; $('updated').textContent = 'Refresh failed. No current data shown.'; + $('content').replaceChildren(el('p', 'Could not load records. Use Refresh to try again.', 'empty')); + } finally { if (controller === run) { $('refresh').disabled = false; } } + } + function rawDetails(label, data) { const node = el('details'); node.append(el('summary', label), el('pre', JSON.stringify(data, null, 2))); return node; } + function download(data, name) { const url = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'})); const a = el('a'); a.href = url; a.download = name; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); } + async function showMission(server, mission) { + detailController?.abort(); const run = new AbortController(); detailController = run; + const dialog = $('mission-dialog'); if (!dialog.open) dialog.showModal(); $('mission-body').replaceChildren(el('p', 'Loading mission…')); $('mission-heading').textContent = 'Mission details'; + const path = `/api/missions/${encodeURIComponent(server)}/${encodeURIComponent(mission)}`; + try { + const snapshot = await api(path, run.signal); if (run.signal.aborted) return; + const e = snapshot.event, body = $('mission-body'); $('mission-heading').textContent = e.terrain || 'Unknown terrain'; + const facts = el('div', null, 'facts'); + for (const [label, value] of [['Server', e.serverName?.trim() || snapshot.serverName || server], ['Server ID', server], ['Mission ID', mission], ['Result', labels[e.outcome] || 'Unfinished'], ['Phase', phases[e.phase] || e.phase], ['Duration', duration(e.elapsedSeconds)], ['Snapshot received', date(snapshot.receivedAt)], ['Server-reported time', date(e.occurredAt)], ['Connected / peak players', `${e.connectedPlayers} / ${e.peakPlayers}`], ['Game version', e.gameVersion || 'Unknown'], ['Scenario', e.scenario || 'Unknown'], ['Previous outcome', e.previousOutcome || 'None reported']]) { const item = el('div', null, 'fact'); item.append(el('span', label), el('strong', value)); facts.append(item); } + body.replaceChildren(facts, el('p', e.reason || 'No reason reported.'), el('p', `Coverage: ${e.partial ? 'partial mission' : 'complete mission report'} · ${e.droppedEvents} dropped reports${e.playersTruncated ? ' · players truncated' : ''}${e.modsTruncated ? ' · mods truncated' : ''}`, 'notice')); + if (e.preparationError) body.append(el('p', e.preparationError, 'error')); + body.append(button('Download snapshot JSON', () => download(snapshot, `mission-${mission}.json`), 'secondary'), el('h3', 'Players in this snapshot')); + body.append(table(['Player', 'Connected', 'Participated', 'Escaped', 'Time connected', 'Kills', 'Friendly kills', 'Deaths', 'Downed', 'Recoveries'], e.players.map(p => [stack(p.name || 'Unnamed player', `${p.id}${p.authenticated ? '' : ' · unauthenticated'}`, true), p.connected ? 'Yes' : 'No', p.participated ? 'Yes' : 'No', p.escaped ? 'Yes' : 'No', duration(p.connectedSeconds), ...['kills', 'friendlyKills', 'deaths', 'downed', 'recoveries'].map(k => number(p[k]))]))); + const mods = el('details'); mods.append(el('summary', `Mods (${e.mods.length}${e.modsTruncated ? ', truncated' : ''})`), table(['Name', 'Version', 'ID'], e.mods.map(m => [m.name, m.version, m.id]))); body.append(mods); + const settings = el('details'); settings.append(el('summary', `Mission settings (${Object.keys(e.settings).length})`), table(['Setting', 'Value'], Object.entries(e.settings).sort(([a], [b]) => a.localeCompare(b)))); body.append(settings, rawDetails('Complete selected snapshot', snapshot), el('h3', 'Event history')); + const history = el('div'); body.append(history); let before = null; const prior = []; + async function loadEvents() { + history.replaceChildren(el('p', 'Loading events…')); + try { + const data = await api(`${path}/events?limit=25${before ? `&before=${before}` : ''}`, run.signal); if (run.signal.aborted) return; + const rows = data.events.map(row => [timestamp(row.receivedAt), date(row.event.occurredAt), row.event.type, phases[row.event.phase], duration(row.event.elapsedSeconds), rawDetails(row.event.outcome || 'View JSON', row.event)]); + const controls = el('div', null, 'pager'), prev = button('← Newer', () => { before = prior.pop(); loadEvents(); }, 'secondary'), next = button('Older →', () => { prior.push(before); before = data.nextBefore; loadEvents(); }, 'secondary'); prev.disabled = !prior.length; next.disabled = data.nextBefore == null; controls.append(prev, el('span', `Page ${prior.length + 1}`), next); + history.replaceChildren(el('p', 'Newest received first. Queued events can arrive late; the selected final result remains authoritative.', 'small'), table(['Received by collector', 'Reported by server', 'Event', 'Phase', 'Elapsed', 'Payload'], rows), controls); + } catch (error) { if (!run.signal.aborted) history.replaceChildren(el('p', error.message, 'error'), button('Retry events', loadEvents, 'secondary')); } + } + await loadEvents(); + } catch (error) { if (!run.signal.aborted) $('mission-body').replaceChildren(el('p', error.message, 'error'), button('Retry mission', () => showMission(server, mission), 'secondary')); } + } + $('login-form').addEventListener('submit', async event => { + event.preventDefault(); const submit = event.target.querySelector('button'); submit.disabled = true; $('login-error').textContent = ''; + state.token = $('token').value.trim(); $('token').value = ''; + try { await api('/api/dashboard'); $('login').hidden = true; $('app').hidden = false; await refresh(); } + catch (error) { state.token = ''; $('login-error').textContent = error.message; } + finally { submit.disabled = false; } + }); + $('logout').addEventListener('click', () => { + controller?.abort(); detailController?.abort(); state.token = ''; state.server = ''; state.serverName = ''; state.view = 'overview'; state.query = ''; state.terrain = ''; state.outcome = ''; resetPages(); + $('mission-dialog').close(); $('mission-body').replaceChildren(); $('app').hidden = true; $('login').hidden = false; $('metrics').replaceChildren(); $('content').replaceChildren(); $('token').focus(); + }); + $('tabs').addEventListener('click', event => { const view = event.target.closest('button')?.dataset.view; if (view) { state.view = view; if (view === 'servers') { state.server = ''; state.serverName = ''; } resetPages(); refresh(); } }); + $('refresh').addEventListener('click', refresh); + $('clear-scope').addEventListener('click', () => { state.server = ''; state.serverName = ''; resetPages(); refresh(); }); + $('close-dialog').addEventListener('click', () => $('mission-dialog').close()); + $('mission-dialog').addEventListener('close', () => detailController?.abort()); +} diff --git a/services/telemetry-worker/src/dashboard.js b/services/telemetry-worker/src/dashboard.js new file mode 100644 index 0000000..d7a5b85 --- /dev/null +++ b/services/telemetry-worker/src/dashboard.js @@ -0,0 +1,36 @@ +import {dashboardApp} from './dashboard-client.js'; + +const html = ` + +Escape Observatory · Mission statistics + +
CO10 ESCAPEOBSERVATORYCOMMUNITY MISSION TELEMETRY
+
+ +
+
MISSION DOSSIER

Mission details

+`; +const css = ` +:root{color-scheme:dark;--bg:#0d1419;--panel:#141e25;--line:#293740;--text:#edf2ee;--muted:#9eafb8;--accent:#e5bb70;--green:#8ed6b1;--red:#f29f95;font-family:Inter,ui-sans-serif,system-ui,sans-serif;background:var(--bg);color:var(--text)} +*{box-sizing:border-box}body{margin:0}button,input,select{font:inherit}button,a,input,select{touch-action:manipulation}button{cursor:pointer;border:1px solid transparent;border-radius:7px;padding:10px 16px;background:var(--accent);color:#172129;font-weight:650}button:hover{filter:brightness(1.12)}button:disabled{cursor:default;opacity:.4}button.secondary,input,select{background:#19262e;border:1px solid #455660;color:var(--text)}button.quiet{background:transparent;color:var(--muted)}button.link{padding:2px 0;background:none;color:var(--accent);text-align:left;font-weight:550}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--accent);outline-offset:4px}input,select{padding:10px;border-radius:6px;max-width:100%}input[type=checkbox]{accent-color:var(--accent)}a{color:var(--accent)}[hidden]{display:none!important}.masthead{max-width:1440px;margin:auto;padding:25px 40px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between}.brand{display:flex;align-items:center;gap:13px;color:var(--text);text-decoration:none;font-weight:800;letter-spacing:2px;font-size:14px}.brand small{display:block;font-size:10px;letter-spacing:4px;color:var(--accent);margin-top:5px}.mark{font-size:32px;background:var(--accent);color:var(--bg);width:43px;height:43px;text-align:center;border-radius:4px}.eyebrow{font-size:10px;letter-spacing:2px;font-weight:750;color:var(--muted)}main{max-width:1440px;margin:auto;padding:38px 40px}h1{font-size:clamp(30px,4vw,43px);line-height:1.12;letter-spacing:-1.4px;margin:12px 0}h2{font-size:21px;margin:0 0 9px;letter-spacing:-.5px}h3{font-size:15px;margin:26px 0 12px}p{line-height:1.6;color:var(--muted)}.small{font-size:12px}.muted{color:var(--muted)}.panel{border:1px solid var(--line);background:var(--panel);border-radius:10px;padding:26px}.login{max-width:720px;margin:6vh auto;padding:42px}.login h1{font-size:clamp(36px,5vw,58px);margin:20px 0}.login form{margin-top:30px}.login label{display:block;font-size:13px;margin-bottom:10px}.login-fields{display:flex;gap:10px}.login input{flex:1;min-width:0}.page-heading{display:flex;gap:20px;align-items:center;justify-content:space-between}.page-heading p{margin:10px 0 0}.actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap}.status-line{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:22px 0;font-size:12px;color:var(--muted);min-height:25px}.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-bottom:28px}.metric{padding:22px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}.metric-label{font-size:11px;letter-spacing:1.2px;text-transform:uppercase;color:var(--muted)}.metric-value{font-size:36px;font-weight:650;margin:8px 0;letter-spacing:-1px;font-variant-numeric:tabular-nums}.metric-note{font-size:12px;color:var(--muted)}nav{display:flex;gap:10px;border-bottom:1px solid var(--line);margin-bottom:22px;overflow:auto}nav button{border-radius:0;background:transparent;color:var(--muted);padding:14px 18px;border-bottom:2px solid transparent}nav button[aria-current=page]{color:var(--accent);border-bottom-color:var(--accent)}.toolbar{display:flex;gap:12px;align-items:center;justify-content:space-between;margin-bottom:22px;flex-wrap:wrap}.filters{display:flex;gap:12px;align-items:end;flex-wrap:wrap}.filters label{font-size:11px;color:var(--muted);display:grid;gap:6px}.table-wrap{overflow:auto}table{border-collapse:collapse;width:100%;font-size:13px;text-align:left}th{color:var(--muted);font-size:10px;letter-spacing:1px;text-transform:uppercase;white-space:nowrap;font-weight:650;padding:11px 13px;border-bottom:1px solid var(--line)}td{padding:15px 13px;border-bottom:1px solid var(--line);font-variant-numeric:tabular-nums;max-width:330px;overflow-wrap:anywhere;vertical-align:top}tbody tr:hover{background:#19262d}tr:last-child td{border-bottom:none}.sub{display:block;color:var(--muted);font-size:11px;margin-top:5px;overflow-wrap:anywhere}.mono{font-family:ui-monospace,monospace;font-size:11px}.badge{display:inline-block;padding:4px 8px;border-radius:4px;background:#293740;color:var(--muted);font-size:11px;white-space:nowrap}.badge.good{color:var(--green);background:#20382e}.badge.bad{color:var(--red);background:#3b292b}.badge.warn{color:var(--accent);background:#393325}.pager{display:flex;justify-content:space-between;align-items:center;gap:15px;margin-top:22px;font-size:12px;color:var(--muted)}.pager div{display:flex;gap:8px}.empty{padding:42px 10px;text-align:center;color:var(--muted)}.overview-grid{display:grid;grid-template-columns:1fr 2fr;gap:25px}.outcome-row{margin:18px 0}.outcome-label{display:flex;justify-content:space-between;font-size:13px;margin-bottom:8px}.bar{height:5px;border-radius:3px;background:#293740;overflow:hidden}.bar i{display:block;height:100%;background:var(--accent)}.bar i.good{background:var(--green)}.bar i.bad{background:var(--red)}footer{font-size:11px;line-height:1.8;color:var(--muted);margin:24px 0}.error,#login-error{color:var(--red)}.error{padding:15px;background:#35282b;border:1px solid #634346;border-radius:7px}dialog{background:var(--panel);color:var(--text);border:1px solid #455660;border-radius:12px;width:min(1180px,94vw);max-height:90vh;padding:30px}dialog::backdrop{background:#000b}.dialog-top{display:flex;justify-content:space-between;gap:20px;align-items:center;margin-bottom:24px}.facts{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:15px;padding:20px 0}.fact span{display:block;color:var(--muted);font-size:11px;margin-bottom:6px}.fact strong{font-size:13px;font-weight:550;overflow-wrap:anywhere}details{margin:18px 0}summary{cursor:pointer;color:var(--accent);font-size:13px}pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:11px;background:var(--bg);padding:18px;max-height:380px;overflow:auto}.notice{font-size:12px;color:var(--accent)}.wide-text{max-width:500px} +@media(max-width:850px){.masthead,main{padding:22px}.masthead>.eyebrow{display:none}.page-heading{align-items:start;flex-direction:column}.metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.overview-grid{grid-template-columns:1fr}.facts{grid-template-columns:repeat(2,minmax(0,1fr))}.panel{padding:18px}.login{padding:26px;margin-top:20px}.login-fields{flex-direction:column}} +@media(max-width:480px){main{padding:18px 12px}.metric{padding:15px}.metric-value{font-size:29px}.facts{grid-template-columns:1fr}dialog{padding:18px}nav{gap:0}nav button{padding:13px}.actions{gap:6px}} +`; +export function dashboardAsset(path) { + const script = path.endsWith('/app.js'), style = path.endsWith('/style.css'); + return new Response(script ? `(${dashboardApp.toString()})();` : style ? css : html, {headers: { + 'Content-Type': script ? 'text/javascript; charset=utf-8' : style ? 'text/css; charset=utf-8' : 'text/html; charset=utf-8', + 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer', + 'Content-Security-Policy': "default-src 'none'; script-src 'self'; style-src 'self'; style-src-attr 'unsafe-inline'; connect-src 'self'; img-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'", + }}); +} diff --git a/services/telemetry-worker/src/index.js b/services/telemetry-worker/src/index.js index fca6cd8..8234d23 100644 --- a/services/telemetry-worker/src/index.js +++ b/services/telemetry-worker/src/index.js @@ -1,3 +1,5 @@ +import {dashboardOverview, dashboardServers} from './dashboard-api.js'; +import {dashboardAsset} from './dashboard.js'; import {HttpError, requireValue, object, string, uuid, outcomes, validateEvent, canonical, readJson} from './validation.js'; const json = (value, status = 200) => new Response(JSON.stringify(value), { @@ -140,12 +142,11 @@ async function ingest(request, env) { } async function servers(request, env, url) { - await authorize(request, env); + await authorize(request, env, request.method === 'GET'); if (request.method === 'GET') { - const {limit, before} = paging(url); - const result = await env.DB.prepare(`SELECT rowid AS cursor, server_id AS serverId, name, active, registration, created_at AS createdAt - FROM servers WHERE rowid < ? ORDER BY rowid DESC LIMIT ?`).bind(before, limit).all(); - return json({servers: result.results, nextBefore: result.results.length === limit ? result.results.at(-1).cursor : null}); + const query = url.searchParams.get('q') ?? ''; + requireValue(string(query, 120), 'Invalid server search'); + return json(await dashboardServers(env.DB, paging(url), query)); } method(request, 'POST'); const body = await readJson(request, 4096); @@ -174,15 +175,20 @@ async function serverCredential(request, env, serverId, rotate) { async function missions(request, env, url) { method(request, 'GET'); await authorize(request, env, true); const {limit, before} = paging(url), serverId = serverFilter(url); + const terrain = url.searchParams.get('terrain'); + requireValue(terrain === null || string(terrain, 1024, 1), 'Invalid terrain'); const outcome = url.searchParams.get('outcome'); requireValue(outcome === null || [...outcomes, 'unfinished'].includes(outcome), 'Invalid outcome filter'); const conditions = ['id < ?'], values = [before]; if (serverId) { conditions.push('server_id = ?'); values.push(serverId); } + if (terrain) { conditions.push("lower(json_extract(payload, '$.terrain')) = lower(?)"); values.push(terrain); } if (outcome) { conditions.push('outcome = ?'); values.push(outcome === 'unfinished' ? '' : outcome); } const rows = await env.DB.prepare(`SELECT id AS cursor, server_id AS serverId, mission_id AS missionId, CASE WHEN terminal = 1 THEN outcome ELSE 'unfinished' END AS outcome, elapsed_seconds AS elapsedSeconds, occurred_at AS occurredAt, received_at AS receivedAt, partial, peak_players AS peakPlayers, dropped_events AS droppedEvents, + COALESCE(NULLIF(trim(json_extract(payload, '$.serverName')), ''), + (SELECT name FROM servers WHERE servers.server_id = missions.server_id), '') AS serverName, json_extract(payload, '$.terrain') AS terrain, json_extract(payload, '$.playersTruncated') AS playersTruncated FROM missions WHERE ${conditions.join(' AND ')} ORDER BY id DESC LIMIT ?`).bind(...values, limit).all(); return json({missions: rows.results, nextBefore: rows.results.length === limit ? rows.results.at(-1).cursor : null}); @@ -198,10 +204,10 @@ async function missionDetails(request, env, serverId, missionId, events, url) { return json({events: rows.results.map(row => ({cursor: row.id, receivedAt: row.received_at, event: JSON.parse(row.payload)})), nextBefore: rows.results.length === limit ? rows.results.at(-1).id : null}); } - const row = await env.DB.prepare('SELECT received_at, payload FROM missions WHERE server_id = ? AND mission_id = ?') + const row = await env.DB.prepare('SELECT received_at, payload, (SELECT name FROM servers WHERE servers.server_id = missions.server_id) AS server_name FROM missions WHERE server_id = ? AND mission_id = ?') .bind(serverId, missionId).first(); if (!row) throw new HttpError(404, 'Mission not found'); - return json({receivedAt: row.received_at, event: JSON.parse(row.payload)}); + return json({receivedAt: row.received_at, serverName: row.server_name || '', event: JSON.parse(row.payload)}); } async function stats(request, env, url) { @@ -240,6 +246,13 @@ export default { async fetch(request, env) { try { const url = new URL(request.url), path = url.pathname; + if (['/dashboard', '/dashboard/', '/dashboard/app.js', '/dashboard/style.css'].includes(path)) { + method(request, 'GET'); return dashboardAsset(path); + } + if (path === '/api/dashboard') { + method(request, 'GET'); await authorize(request, env, true); + return json(await dashboardOverview(env.DB, serverFilter(url))); + } if (path === '/health') { method(request, 'GET'); return json({ok: true, service: 'co10escape-telemetry'}); } if (path === '/' || path === '/api/summary' || (path === '/api/mission-events' && request.method === 'GET')) { return await publicSummary(request, env, url); diff --git a/services/telemetry-worker/test/dashboard-client.test.js b/services/telemetry-worker/test/dashboard-client.test.js new file mode 100644 index 0000000..8be3c6a --- /dev/null +++ b/services/telemetry-worker/test/dashboard-client.test.js @@ -0,0 +1,73 @@ +import {test} from 'node:test'; +import assert from 'node:assert/strict'; +import {runInNewContext} from 'node:vm'; +import {dashboardApp} from '../src/dashboard-client.js'; +const clientScript = process.env.DASHBOARD_BUNDLE + ? await (await (await import('../dist/index.js')).default.fetch(new Request('https://telemetry.example/dashboard/app.js'), {})).text() + : `(${dashboardApp.toString()})();`; + +// Minimal DOM boundary for client behavior; not a browser layout test. +class Element { + constructor(tag = 'div') { this.tagName=tag.toUpperCase(); this.children=[]; this.listeners={}; this.dataset={}; this.attributes={}; this.hidden=false; this.value=''; this.open=false; this._text=''; } + set textContent(value) { this._text=String(value); this.children=[]; } + get textContent() { return this._text+this.children.map(x=>x.textContent).join(' '); } + set innerHTML(_) { throw new Error('HTML interpolation forbidden'); } + append(...nodes) { this.children.push(...nodes); } + replaceChildren(...nodes) { this._text=''; this.children=nodes; } + addEventListener(type, handler) { this.listeners[type]=handler; } + setAttribute(key,value) { this.attributes[key]=value; } + removeAttribute(key) { delete this.attributes[key]; } + closest(selector) { return selector==='button'&&this.tagName==='BUTTON'?this:null; } + querySelector(selector) { return descendants(this).find(x=>x.tagName.toLowerCase()===selector)||null; } + showModal() { this.open=true; } + close() { this.open=false; this.listeners.close?.(); } + focus() {} +} +const descendants = node => node.children.flatMap(child=>[child,...descendants(child)]); +const tick = async () => { for(let i=0;i<8;i++) await new Promise(resolve=>setImmediate(resolve)); }; +function fixture() { + const ids=Object.fromEntries(['token','login','login-form','login-error','app','metrics','content','updated','error','scope-label','clear-scope','tabs','refresh','logout','mission-dialog','mission-heading','mission-body','close-dialog'].map(id=>[id,new Element()])); + ids['login-form'].append(new Element('button')); ids.app.hidden=true; + for(const view of ['overview','servers','missions','players']) { const node=new Element('button'); node.dataset.view=view; ids.tabs.append(node); } + const sid='11111111-1111-4111-8111-111111111111',mid='22222222-2222-4222-8222-222222222222',malicious=''; + const summary={generatedAt:1793361000,totals:{servers:1,missions:1,events:3,wins:1,losses:0,unfinished:0,partialMissions:0,winRate:100,lastReceivedAt:1793361000},outcomes:[{outcome:'victory',missions:1}],maps:[{terrain:'Everon',missions:1,wins:1,losses:0,unfinished:0}]}; + const event={serverId:sid,missionId:mid,serverName:malicious,terrain:'Everon',outcome:'victory',phase:5,elapsedSeconds:120,occurredAt:1793360000,connectedPlayers:1,peakPlayers:1,gameVersion:'1.6',scenario:'Escape',previousOutcome:'',reason:malicious,partial:false,droppedEvents:0,mods:[{name:malicious,version:'1',id:'mod'}],settings:{roadblockCount:4},players:[{id:sid,name:malicious,authenticated:true,connected:true,participated:true,escaped:true,connectedSeconds:120,kills:2,friendlyKills:0}]}; + const requests=[]; let fail=false; + const document={getElementById:id=>ids[id],createElement:tag=>new Element(tag),createTextNode:text=>{const node=new Element('#text');node.textContent=text;return node;},querySelectorAll:()=>ids.tabs.children,hidden:false}; + const fetch=async(path,options)=>{ + requests.push({path,options}); + if(fail)return{ok:false,status:503,json:async()=>({error:'Temporarily unavailable'})}; + if(options.headers.Authorization!=='Bearer valid-read-token-0123456789abcdef')return{ok:false,status:401,json:async()=>({})}; + let data; + if(path.startsWith('/api/dashboard'))data=summary; + else if(path.startsWith('/api/servers'))data={servers:[{serverId:sid,name:malicious,wins:1,losses:0,missions:1,unfinished:0,lastReceivedAt:1793361000,lastEventType:'heartbeat',lastReportedPlayers:1,lastMissionId:mid,terrain:'Everon',active:1,registration:'automatic'}],nextBefore:7}; + else if(path.includes('/events?'))data={events:[{receivedAt:1793361000,event}],nextBefore:null}; + else if(path.startsWith(`/api/missions/${sid}/`))data={receivedAt:1793361000,event}; + else if(path.startsWith('/api/missions?'))data={missions:[{...event,receivedAt:1793361000}],nextBefore:null}; + else if(path.startsWith('/api/players'))data={players:[{playerId:sid,attempts:1,escapes:1,didNotEscape:0,bestEscapeSeconds:120,kills:2,missionsWithKills:1,friendlyKills:0,missionsWithFriendlyKills:1}],nextAfter:null}; + else throw new Error('Unexpected path '+path); + return{ok:true,status:200,json:async()=>data}; + }; + runInNewContext(clientScript,{document,Node:Element,fetch,AbortController,URLSearchParams,Date,setInterval:()=>{throw new Error("Automatic refresh must remain disabled");},setTimeout,URL,Blob}); + const click=async(node,target=node)=>{node.listeners.click?.({target});await tick();}; + const button=(root,text)=>{const node=descendants(root).find(x=>x.tagName==='BUTTON'&&x.textContent===text);assert.ok(node,`Button ${text}`);return node;}; + const login=async token=>{ids.token.value=token;await ids['login-form'].listeners.submit({preventDefault(){},target:ids['login-form']});await tick();}; + return{ids,requests,sid,malicious,click,button,login,tab:async view=>click(ids.tabs,ids.tabs.children.find(x=>x.dataset.view===view)),fail:()=>{fail=true;}}; +} + +test('client login, scope, pagination, mission details, safe text and logout',async()=>{ + const f=fixture();await f.login('wrong-token');assert.match(f.ids['login-error'].textContent,/Token rejected/);assert.equal(f.ids.app.hidden,true); + await f.login('valid-read-token-0123456789abcdef');assert.equal(f.ids.app.hidden,false);assert.equal(f.ids.token.value,'');assert.match(f.ids.metrics.textContent,/100%/); + await f.tab('servers');assert.match(f.ids.content.textContent,/Last received/);assert.ok(f.ids.content.textContent.includes(f.malicious));assert.equal(descendants(f.ids.content).some(x=>x.tagName==='IMG'),false); + await f.click(f.button(f.ids.content,'Next →'));assert.ok(f.requests.some(r=>r.path.includes('before=7'))); + await f.click(f.button(f.ids.content,f.malicious));assert.ok(f.requests.some(r=>r.path.startsWith('/api/missions?')&&r.path.includes(`serverId=${f.sid}`))); + await f.click(f.button(f.ids.content,'Everon'));assert.equal(f.ids['mission-dialog'].open,true);assert.match(f.ids['mission-body'].textContent,/Mission settings/);assert.match(f.ids['mission-body'].textContent,/Event history/);assert.ok(f.ids['mission-body'].textContent.includes(f.malicious)); + await f.click(f.ids['close-dialog']);assert.equal(f.ids['mission-dialog'].open,false); + await f.tab('players');assert.match(f.ids.content.textContent,/1\/1 missions reported this counter/); + await f.click(f.ids.logout);assert.equal(f.ids.app.hidden,true);assert.equal(f.ids.content.textContent,'');assert.equal(f.ids['mission-body'].textContent,''); + for(const request of f.requests)assert.ok(!request.path.includes('token')); +}); +test('failed refresh removes stale records and explains retry',async()=>{ + const f=fixture();await f.login('valid-read-token-0123456789abcdef');f.fail();await f.click(f.ids.refresh); + assert.equal(f.ids.error.hidden,false);assert.match(f.ids.updated.textContent,/Refresh failed/);assert.equal(f.ids.metrics.textContent,'');assert.match(f.ids.content.textContent,/Use Refresh/); +}); diff --git a/services/telemetry-worker/test/worker.test.js b/services/telemetry-worker/test/worker.test.js index cb49881..fcdef98 100644 --- a/services/telemetry-worker/test/worker.test.js +++ b/services/telemetry-worker/test/worker.test.js @@ -41,7 +41,7 @@ async function detail(e) { } before(async () => { mf = new Miniflare(convertV4MiniflareOptions({host: '127.0.0.1', port: 0, workers: [{name: 'telemetry', - modules: ['index.js', 'validation.js'].map(name => ({type: 'ESModule', path: new URL(`../src/${name}`, import.meta.url).pathname})), + modules: ['index.js', 'validation.js', 'dashboard-api.js', 'dashboard.js', 'dashboard-client.js'].map(name => ({type: 'ESModule', path: new URL(`../src/${name}`, import.meta.url).pathname})), compatibilityDate: '2026-10-23', d1Databases: ['DB'], ratelimits: { REQUEST_LIMITER: {namespace_id: '1001', simple: {limit: 300, period: 60}}, @@ -302,3 +302,99 @@ test('map records count victories and defeats across servers, excluding other ma const row = maps.maps.find(m => m.terrain === terrain.toLowerCase()); assert.deepEqual(row, {terrain: terrain.toLowerCase(), completed: 2, wins: 1, losses: 1}); }); + +test('dashboard shell is public, same-origin and contains no telemetry or credentials', async () => { + for (const path of ['/dashboard', '/dashboard/', '/dashboard/app.js', '/dashboard/style.css']) { + const response = await call(path); + assert.equal(response.status, 200); + assert.match(response.headers.get('content-security-policy'), /frame-ancestors 'none'/); + assert.equal(response.headers.get('cache-control'), 'no-store'); + const content = await response.text(); + assert.ok(!content.includes(ADMIN) && !content.includes(READ) && !content.includes(token)); + } + assert.equal((await call('/dashboard', {method: 'POST'})).status, 405); + assert.equal((await call('/api/dashboard')).status, 401); + assert.equal((await call('/api/servers')).status, 401); + assert.equal((await call('/api/dashboard', {headers: {authorization: `Bearer ${token}`}})).status, 401); + assert.equal((await call('/api/servers', {headers: readAuth})).status, 200); + assert.equal((await call(`/api/servers/${serverId}`, {method: 'DELETE', headers: readAuth})).status, 401); +}); + +test('dashboard receipt freshness uses raw arrivals, independently of terminal snapshot selection', async () => { + const sid = crypto.randomUUID(), writeToken = 'dashboard-installation-secret-0123456789'; + const complete = event({serverId: sid, serverName: '', terrain: 'Everon dashboard', players: []}); + assert.equal((await ingest(complete, writeToken)).status, 201); + const finalReceipt = (await db.prepare('SELECT received_at FROM missions WHERE server_id = ?').bind(sid).first()).received_at; + const delayed = {...complete, eventId: crypto.randomUUID(), type: 'heartbeat', phase: 1, outcome: '', occurredAt: 1, elapsedSeconds: 10}; + assert.equal((await ingest(delayed, writeToken)).status, 201); + await db.prepare('UPDATE events SET received_at = ? WHERE server_id = ? AND event_id = ?').bind(finalReceipt + 40, sid, delayed.eventId).run(); + const rows = await (await call(`/api/servers?q=${sid}`, {headers: readAuth})).json(); + assert.equal(rows.servers.length, 1); + const server = rows.servers[0]; + assert.equal(server.lastReceivedAt, finalReceipt + 40); + assert.equal(server.lastReportedAt, 1); + assert.equal(server.lastEventType, 'heartbeat'); + assert.equal(server.wins, 1); assert.equal(server.losses, 0); assert.equal(server.unfinished, 0); + assert.equal(server.lastMissionId, complete.missionId); + assert.equal(server.name, complete.serverName); // Stored literally; browser renders as text. + assert.ok(!JSON.stringify(rows).includes('token_hash')); + const snapshot = await (await call(`/api/missions/${sid}/${complete.missionId}`, {headers: readAuth})).json(); + assert.equal(snapshot.receivedAt, finalReceipt); assert.equal(snapshot.event.outcome, 'victory'); + const summary = await (await call(`/api/dashboard?serverId=${sid}`, {headers: readAuth})).json(); + assert.equal(summary.totals.missions, 1); assert.equal(summary.totals.events, 2); + assert.equal(summary.totals.wins, 1); assert.equal(summary.totals.winRate, 100); + assert.equal(summary.totals.servers, 1); assert.equal(summary.totals.lastReceivedAt, finalReceipt + 40); + assert.deepEqual(summary.maps.map(m => m.terrain), ['everon dashboard']); + assert.equal((await ingest(delayed, writeToken)).status, 200); + const duplicateSummary = await (await call(`/api/dashboard?serverId=${sid}`, {headers: readAuth})).json(); + assert.equal(duplicateSummary.totals.events, 2); + const empty = await (await call(`/api/dashboard?serverId=${crypto.randomUUID()}`, {headers: readAuth})).json(); + assert.equal(empty.totals.lastReceivedAt, null); assert.equal(empty.totals.winRate, null); + assert.equal(empty.totals.missions, 0); assert.deepEqual(empty.maps, []); +}); + +test('dashboard server pagination, never-reporting servers and exact terrain filters', async () => { + const sid = crypto.randomUUID(); + assert.equal((await post('/api/servers', {serverId: sid, name: 'Never reported'}, auth)).status, 201); + const first = await (await call('/api/servers?limit=1', {headers: readAuth})).json(); + assert.equal(first.servers[0].serverId, sid); + assert.equal(first.servers[0].lastReceivedAt, null); assert.equal(first.servers[0].missions, 0); + const next = await (await call(`/api/servers?limit=1&before=${first.nextBefore}`, {headers: readAuth})).json(); + assert.notEqual(next.servers[0].serverId, sid); + const matched = await (await call('/api/missions?terrain=EVERON%20DASHBOARD', {headers: readAuth})).json(); + assert.equal(matched.missions.length, 1); assert.equal(matched.missions[0].outcome, 'victory'); + const empty = await (await call('/api/missions?terrain=Missing%20island', {headers: readAuth})).json(); + assert.equal(empty.missions.length, 0); + assert.equal((await call('/api/servers?q=' + 'a'.repeat(121), {headers: readAuth})).status, 400); + assert.equal((await call('/api/dashboard?serverId=bad', {headers: readAuth})).status, 400); +}); + +test('automatic server names arrive later, follow renames, and survive blanks, retries and old queued reports', async () => { + const sid = crypto.randomUUID(), secret = 'server-name-test-credential-0123456789'; + const original = event({serverId: sid, serverName: '', players: [], occurredAt: 100}); + assert.equal((await ingest(original, secret)).status, 201); + const name = async () => (await db.prepare('SELECT name FROM servers WHERE server_id = ?').bind(sid).first()).name; + assert.equal(await name(), ''); + const report = (serverName, occurredAt) => event({serverId: sid, serverName, occurredAt, players: []}); + const first = report(' Community Escape ', 200); + assert.equal((await ingest(first, secret)).status, 201); assert.equal(await name(), 'Community Escape'); + const renamed = report('New name', 300); + assert.equal((await ingest(renamed, secret)).status, 201); assert.equal(await name(), 'New name'); + assert.equal((await ingest(report(' ', 400), secret)).status, 201); assert.equal(await name(), 'New name'); + assert.equal((await ingest(report('Old name', 150), secret)).status, 201); assert.equal(await name(), 'New name'); + assert.equal((await ingest(first, secret)).status, 200); assert.equal(await name(), 'New name'); + assert.equal((await ingest({...renamed, serverName: 'Conflict'}, secret)).status, 409); assert.equal(await name(), 'New name'); + assert.equal((await ingest(report('Impersonated', 500), 'wrong-credential-0123456789abcdef123456')).status, 401); + assert.equal(await name(), 'New name'); + const listing = await (await call(`/api/servers?q=${encodeURIComponent('New name')}`, {headers: readAuth})).json(); + assert.equal(listing.servers[0].serverId, sid); + const missions = await (await call(`/api/missions?serverId=${sid}`, {headers: readAuth})).json(); + assert.equal(missions.missions.find(m => m.missionId === original.missionId).serverName, 'New name'); + assert.equal(missions.missions.find(m => m.missionId === first.missionId).serverName, 'Community Escape'); + const snapshot = await (await call(`/api/missions/${sid}/${original.missionId}`, {headers: readAuth})).json(); + assert.equal(snapshot.serverName, 'New name'); assert.equal(snapshot.event.serverName, ''); // No historical payload rewriting. + const managed = crypto.randomUUID(); + const registered = await (await post('/api/servers', {serverId: managed, name: 'Operator label'}, auth)).json(); + assert.equal((await ingest(event({serverId: managed, serverName: 'Game name', players: []}), registered.token)).status, 201); + assert.equal((await db.prepare('SELECT name FROM servers WHERE server_id = ?').bind(managed).first()).name, 'Operator label'); +}); diff --git a/services/telemetry-worker/wrangler.jsonc b/services/telemetry-worker/wrangler.jsonc index 5347e85..df697c9 100644 --- a/services/telemetry-worker/wrangler.jsonc +++ b/services/telemetry-worker/wrangler.jsonc @@ -2,6 +2,9 @@ "$schema": "node_modules/wrangler/config-schema.json", "name": "co10escape-telemetry", "main": "src/index.js", + // The dashboard serializes its self-contained browser function. Avoid + // injecting references to Worker-only function-name helpers into that code. + "keep_names": false, "compatibility_date": "2026-10-23", "workers_dev": true, "d1_databases": [{ diff --git a/tests/test_telemetry_server_name.py b/tests/test_telemetry_server_name.py new file mode 100644 index 0000000..2e3d7b2 --- /dev/null +++ b/tests/test_telemetry_server_name.py @@ -0,0 +1,39 @@ +"""Exercise the production name resolver with native metadata boundaries doubled.""" +from policy_harness import ROOT, body, run +p = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_TelemetryServerName.c' +method = body(p, 'static string Resolve(') +for a, b in [('ServerConfig config', 'ServerConfig* config'), ('ServerInfo info', 'ServerInfo* info'), + ('GetGame().GetServerInfo()', 'ServerLobbyApi::GetServerInfo()'), ('ServerLobbyApi.', 'ServerLobbyApi::'), ('config.GetName()', 'config->GetName()'), ('info.GetName()', 'info->GetName()')]: + method = method.replace(a, b) +run(r''' +#include +#include +struct string:std::string { + using std::string::string;using std::string::operator=; + string(std::string v):std::string(v){} + bool IsEmpty(){return empty();}int Length(){return size();} + string Substring(int start,int count){return substr(start,count);} + void TrimInPlace(){auto a=find_first_not_of(" \t\r\n");if(a==npos){clear();return;}*this=substr(a,find_last_not_of(" \t\r\n")-a+1);} +}; +struct ServerConfig {string name=" Community Escape ";string GetName(){return name;}}; +struct ServerInfo {string name="Listed server";string GetName(){return name;}}; +namespace ServerLobbyApi {ServerConfig cfg;ServerInfo info;bool configPresent=true,infoPresent=true; + ServerConfig* GetServerConfig(){return configPresent?&cfg:nullptr;}ServerInfo* GetServerInfo(){return infoPresent?&info:nullptr;}} +struct Names { +''' + method + r''' +}; +int main(){ + assert(Names::Resolve("")=="Community Escape"); + assert(Names::Resolve(" Manual label ")=="Manual label"); + assert(Names::Resolve(" ")=="Community Escape"); + ServerLobbyApi::configPresent=false;assert(Names::Resolve("")=="Listed server"); + ServerLobbyApi::infoPresent=false;assert(Names::Resolve("")==""); + ServerLobbyApi::configPresent=true;ServerLobbyApi::cfg.name="Changed name"; + assert(Names::Resolve("")=="Changed name"); + assert(Names::Resolve(string(160,'x')).Length()==120); + ServerLobbyApi::cfg.name=" ";ServerLobbyApi::infoPresent=true;assert(Names::Resolve("")=="Listed server"); +} +''') +telemetry = p.with_name('ERR_Telemetry.c') +assert 'ERR_TelemetryServerName.Resolve(m_Config.ServerName)' in body(telemetry, 'protected void Event(') +print('PASS: automatic server config/lobby name, explicit override, startup absence, rename and payload bound') diff --git a/tools/check.py b/tools/check.py index a85cac2..f347746 100644 --- a/tools/check.py +++ b/tools/check.py @@ -16,6 +16,7 @@ def check(): subprocess.run([sys.executable,str(ROOT/'tests/test_ai_driver_horn.py')],check=True) subprocess.run([sys.executable,str(ROOT/'tests/test_telemetry.py')],check=True) subprocess.run([sys.executable,str(ROOT/'tests/test_telemetry_credentials.py')],check=True) + subprocess.run([sys.executable,str(ROOT/'tests/test_telemetry_server_name.py')],check=True) subprocess.run([sys.executable,str(ROOT/'tests/test_community_statistics.py')],check=True) subprocess.run([sys.executable,str(ROOT/'tests/test_terrain_controller_init.py')],check=True) subprocess.run([sys.executable,str(ROOT/'tests/test_terrain_chat_retention.py')],check=True)