Compare commits

...
49 Commits
Author SHA1 Message Date
jakub 828bb0a1a7 Add own-body spectator camera and player names; record Workshop 0.0.139
continuous-integration/drone/push Build is passing
2026-11-03 21:16:05 +01:00
jakub 4f02ad64b4 Read telemetry names from running dedicated config; record Workshop 0.0.138
continuous-integration/drone/push Build is passing
2026-11-01 19:49:47 +01:00
jakub 6ba3036de6 Add telemetry dashboard and automatic server names; record Workshop 0.0.137
continuous-integration/drone/push Build is passing
2026-11-01 19:29:54 +01:00
jakub b8dfa9184e Add configurable main-road checkpoints and reconnect replacements; record Workshop 0.0.136
continuous-integration/drone/push Build is passing
2026-11-01 18:22:38 +01:00
jakub 0e1e7790f9 Show Escape mission text after terrain takeover; record Workshop 0.0.135
continuous-integration/drone/push Build is passing
2026-10-31 15:52:27 +01:00
jakub 2c2158702d Finish airborne extraction after 30 seconds and hold rescue aircraft; record Workshop 0.0.134
continuous-integration/drone/push Build is passing
2026-10-31 15:01:51 +01:00
jakub d21adb90d5 Forecast all-down recovery and preserve self-rescue; record Workshop 0.0.133
continuous-integration/drone/push Build is passing
2026-10-31 14:32:30 +01:00
jakub 6738eda748 Add front-passenger handbrake control; record Workshop 0.0.132
continuous-integration/drone/push Build is passing
2026-10-30 20:21:27 +01:00
jakub 2dcbe3107d Add configurable nearby garage vehicles and fix roadblocks and fuel pumps; record Workshop 0.0.131
continuous-integration/drone/push Build is passing
2026-10-30 18:56:57 +01:00
jakub 3ec882dc4e Fix telemetry delivery and report loaded island names; record Workshop 0.0.130
continuous-integration/drone/push Build is passing
2026-10-30 17:13:11 +01:00
jakub 08e5cbf5ea Limit repeated AI driver honking; record Workshop 0.0.129
continuous-integration/drone/push Build is passing
2026-10-30 15:38:44 +01:00
jakub 681d5e5eae Extend results countdown and stabilize spectator hints; record Workshop 0.0.128
continuous-integration/drone/push Build is passing
2026-10-30 15:20:21 +01:00
jakub 213c31dfb2 Fix blank mission results panel; record Workshop 0.0.127
continuous-integration/drone/push Build is passing
2026-10-30 14:48:26 +01:00
jakub 5453159867 Add community map records and telemetry API; record Workshop 0.0.126
continuous-integration/drone/push Build is passing
2026-10-30 14:19:39 +01:00
jakub c6ee764c6c Add after-mission statistics; record Workshop 0.0.125
continuous-integration/drone/push Build is passing
2026-10-30 12:59:32 +01:00
jakub 970dd338f5 Allow off-airport extraction across terrains; record Workshop 0.0.124
continuous-integration/drone/push Build is passing
2026-10-28 15:19:50 +01:00
jakub 95d21b06ff Add ground starting supplies and mission telemetry; record Workshop 0.0.123
continuous-integration/drone/push Build is passing
2026-10-28 14:43:43 +01:00
jakub 9233320fd1 Allow leaving failed-start screens; record Workshop 0.0.122
continuous-integration/drone/push Build is passing
2026-10-25 14:55:36 +01:00
jakub 6397fe89b6 Fix low-terrain startup and extraction placement; record Workshop 0.0.121
continuous-integration/drone/push Build is passing
2026-10-25 14:01:49 +01:00
jakub 3768ce7ed8 Improve small-terrain placement and show startup failure causes; record Workshop 0.0.120
continuous-integration/drone/push Build is passing
2026-10-24 03:03:10 +02:00
jakub 253dde90cd Gate radio intel behind configurable site discovery; record Workshop 0.0.119
continuous-integration/drone/push Build is passing
2026-10-21 16:36:22 +02:00
jakub ac285b5c97 Add recon drones and body burial; stabilize spectator camera
continuous-integration/drone/push Build is passing
2026-10-21 15:28:32 +02:00
jakub f41d8ba2f9 Add configurable anonymous site markers and record Workshop 117
continuous-integration/drone/push Build is passing
2026-10-20 18:35:22 +02:00
jakub 87d85ff02e Preserve stolen intel evidence and safely clean up spent folders
continuous-integration/drone/push Build is passing
2026-10-20 16:13:00 +02:00
jakub ae2aedd131 Fix stalled hunter recovery and document intel cleanup
continuous-integration/drone/push Build is passing
2026-10-20 15:47:18 +02:00
aussie cc322572b4 Merge pull request 'Update addons/core/Scripts/Game/ERR/00_Core/ERR_DiscoveryService.c' (#1) from aussie/EscapistsReintroduced:main into main
continuous-integration/drone/push Build is passing
Reviewed-on: #1
2026-10-19 19:25:16 +00:00
jakub 372fca7a68 Configure anger points per Sarka instead of cars per point
continuous-integration/drone/push Build is passing
2026-10-17 18:58:35 +02:00
jakub 82b52ae1fc Fix scrolling reports and duplicate Sarka status
continuous-integration/drone/push Build is passing
2026-10-17 18:44:01 +02:00
jakub d5464f7438 Reorganize mission menu and separate design from logic
continuous-integration/drone/push Build is passing
2026-10-17 18:27:31 +02:00
jakub 0e71ef1fa7 Make Sarka special-forces cars per anger point configurable
continuous-integration/drone/push Build is passing
2026-10-17 17:42:41 +02:00
jakub 38fd8f7dd2 Record Workshop versions in Drone changelogs and retire local sending 2026-10-17 17:42:39 +02:00
jakub 8f2e1f210d Add Drone Discord changelog pipeline and test notification
continuous-integration/drone/push Build is passing
2026-10-17 17:07:01 +02:00
aussie 7a7ac59edf Update addons/core/Scripts/Game/ERR/00_Core/ERR_DiscoveryService.c
Delete intel folder from player's inventory :)
2026-10-17 01:12:53 +00:00
jakub 96f471ef4a Expand roadblocks with defensive bays and checkpoint equipment 2026-10-16 17:34:11 +02:00
jakub f15c915e54 Fix atlas resource handling and suppression AI serialization 2026-10-16 16:36:46 +02:00
jakub a66aca35bf Add anger-scaled Sarka hunter waves every ten minutes 2026-10-16 16:36:38 +02:00
jakub ae9aaafaae try to fix roadblocks 2026-10-15 21:38:20 +02:00
jakub 009b2fb591 Bundle mission speech with an admin mute option and fix live audio downloads 2026-10-15 16:53:39 +02:00
jakub 07c4d70de5 Show discovered mission markers in the handheld atlas 2026-10-15 16:53:21 +02:00
jakub a2106cfa26 Configure shared starting backpack contents from the admin menu 2026-10-15 14:36:18 +02:00
jakub 0a7102cd51 Add admin-controlled self epinephrine for unconscious players 2026-10-15 12:39:19 +02:00
jakub 3b920e0715 Build realistic roadblocks with staggered barriers and guarded positions 2026-10-15 12:39:04 +02:00
jakub 7b13231f29 Add changelog-only Discord notifications after successful pushes 2026-10-14 19:48:29 +02:00
jakub fda19a5200 Complete roadblocks in background and fix session regressions
Continue safe roadblock placement after admission and prison unlock, build posts incrementally, and clean up incomplete transactions. Add bounded stuck-patrol dismount recovery, defer deployment initialization during terrain takeover, reset deferred site search timing on first player arrival, and fix atlas page serialization and terrain resource lookup.

Validation: 30 native Everon posts with 120 living guards after unlock; seven affected regression suites pass; two final restart smoke rounds complete with 21 assertions and no script/runtime failures. Existing dependency asset errors remain.
2026-10-14 19:48:29 +02:00
jakub 01994ed344 Preserve native chat during terrain takeover with fallback for missing chat 2026-10-14 14:39:55 +02:00
jakub e4ca37dfc1 Make empty-server restarts configurable in the mission menu
Add persistent last-disconnect and idle-interval controls with enabled/five-hour defaults. Apply saved settings immediately, reset idle time while occupied, and cover migration, routing, layout and restart behavior.
2026-10-14 14:05:46 +02:00
jakub 0048527fe2 Restart empty missions after the last disconnect and every five hours
Use connected player counts to protect loading players and spectators, and elapsed real time for idle restarts. Reuse native mission termination and cover restart timing, authority and lifecycle guards with targeted regressions.
2026-10-14 13:56:56 +02:00
jakub bf85f62dca Raise the configurable roadblock limit to 30
Align runtime validation, settings import, admin UI, help and documentation. Update boundary regression coverage; keep the default at four.
2026-10-13 22:52:54 +02:00
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00
8677 changed files with 2219498 additions and 2518 deletions
+31
View File
@@ -0,0 +1,31 @@
kind: pipeline
type: docker
name: discord-changelog
clone:
depth: 0
steps:
- name: test-changelog
image: python:3.13-alpine
commands:
- apk add --no-cache git
- python tools/build_admin_menu.py --check
- python -m unittest discover -s tests -p test_admin_menu_sources.py
- python -m unittest discover -s tests -p test_discord_changelog_ci.py
- python -m unittest discover -s tests -p test_workshop_changelog.py
- name: notify-discord
image: python:3.13-alpine
environment:
DISCORD_WEBHOOK_URL:
from_secret: discord_webhook_url
commands:
- apk add --no-cache git
- python tools/discord_changelog_ci.py
trigger:
branch:
- main
event:
- push
+32
View File
@@ -0,0 +1,32 @@
# Push changelogs
Drone is the sole Discord changelog sender. Its first delivery was verified in
build #1 on 2026-10-17, and the user confirmed receipt and disabled local sending.
Do not run a local/manual Discord sender after pushes or as an automatic fallback.
`tools/discord_changelog.py` is disabled and cannot send messages.
For player-facing changes, maintain the newest entry in `CHANGELOG.md` before
pushing to `origin/main`. Keep it concise and describe only fixes/features, without
server, deployment, Git push status, operational disclaimers or footers. Do not
backfill old changes unless requested. Internal workflow-only changes need no
announcement.
After a confirmed successful Workbench Workshop upload, run
`python3 tools/workshop_changelog.py --published` before committing the changelog.
This reads the local publishing manifest and records the version in the changelog
preamble. Compilation/bundling alone is not confirmation of publication; check
the successful upload result first. With no flag the helper only displays the
bundled version. Drone includes the recorded version with new changelog entries;
a version-only edit does not announce an old entry again.
The `.drone.yml` pipeline posts the latest changed entry using Drone's
`discord_webhook_url` repository secret. See `docs/discord-changelog-ci.md`.
Check the Drone result and notification log when available; a successful Git push
alone does not prove delivery. Report delivery failures honestly. Inspect Discord
before rerunning an uncertain delivery or rebuilding the same push, since builds
do not share delivery state and retries can produce duplicates.
The saved local webhook in `.local/discord-webhook.url` is no longer used for
post-push notifications. Never print it, commit it, or put it into changelogs or
command-line arguments. Do not configure an additional Discord push webhook
alongside Drone.
+163
View File
@@ -0,0 +1,163 @@
# Changelog
Latest published Workshop version: **0.0.139**
## 2026-11-03 — Spectator body camera and player names
- Spectator cycling now includes your own unconscious body, with ground-level framing and orbit controls.
- The statistics dashboard now shows player names alongside their account IDs.
## 2026-11-01 — Dedicated-server names
- Server-name telemetry now reads the running dedicated-server configuration before checking lobby metadata.
## 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
- Added larger checkpoints with eight guards, extra sandbags and longer barrier approaches on wide roads.
- Added separate small/large checkpoint counts and a road-width cutoff under Forces → Checkpoints.
- Fixed saving mission settings on fresh profiles.
- Returning players receive a fresh soldier when their reserved body is gone, until extraction begins.
## 2026-10-31 — Terrain mission text
- Terrain takeover now shows Escape’s mission description on the loading screen instead of the original terrain scenario’s text.
## 2026-10-31 — Extraction finish
- Escape statistics open after 30 seconds of airborne evacuation instead of a long flight across the escape boundary.
- Rescue helicopters remain safely in place while the results screen is open.
## 2026-10-31 — Recovery forecast
- All-down rounds now check whether anyone can wake within five minutes before starting the defeat countdown, while preserving usable self-epinephrine opportunities.
## 2026-10-30 — Passenger handbrake
- The front passenger can hold Space to apply the handbrake and release it to let go.
## 2026-10-30 — Roadblocks, fuel pumps and garage vehicles
- Added civilian cars and optional motorbikes to garages near exploring players, with gradual spawning, retention of used vehicles, and menu controls for count, chance, radius and spawn delay.
- Reduced roadside checkpoint width and length to fit more locations while retaining four guards and a clear vehicle passage.
- Fixed existing petrol-station pumps losing their refuelling service when Escape starts on a terrain scenario.
## 2026-10-30 — Community statistics delivery
- Fixed rejected mission reports so wins, defeats and queued results can update community statistics.
- Game servers now report the loaded island name in statistics instead of the Game Master scenario path.
## 2026-10-30 — Quieter AI traffic
- Limited AI drivers to short horn beeps with a 30-second cooldown to reduce repeated honking.
## 2026-10-30 — Results reading time and spectator hints
- Kept mission results open for two minutes before automatic restart.
- Prevented objective hints from interrupting unconscious spectators and stabilized recovery messages to reduce repeated switching.
## 2026-10-30 — Results panel visibility
- Fixed the blank end-of-mission panel so victory, failure and mission statistics are visible before restart.
## 2026-10-30 — Community mission statistics
- Mission statistics now connect automatically on community servers without manual registration.
- Server administrators can disable reporting under Mission → Statistics.
- Victory and failure screens show the current map's wins, failures and win rate on this server and across all reporting servers.
## 2026-10-30 — Mission results
- Added an after-mission summary with duration, kills, friendly kills, deaths, times downed and recoveries for each participant.
- Results show who escaped and retain the records of players who disconnected.
## 2026-10-28 — Extraction landing zones
- Extraction can use suitable clear ground anywhere on the terrain, including original Everon, without prioritizing airports.
- Landing zones retain terrain, obstacle, aircraft-space and escape-boundary checks.
## 2026-10-28 — Starting supplies and mission statistics
- Oversized starting weapons and other items remain available beside the backpack instead of delaying preparation and being removed.
- Choose whether each starting item spawns inside the backpack or on the ground beside it, with separate quantities for each destination.
- Added optional mission statistics reporting for starts, restarts, terrain, mods, player participation and mission results.
- Statistics are enabled by default and can be disabled under Mission → Statistics; player escape records distinguish actual escapees from spectators.
## 2026-10-25 — Startup error screen
- Failed mission starts now show an interactive error screen with a Return to main menu button.
- Esc opens the normal pause menu from the error screen, so leaving no longer requires closing Arma.
## 2026-10-25 — Small-terrain startup
- Fixed low-lying dry terrain being rejected during site placement, player spawning and mortar setup; water checks now use the actual ground surface.
- Helicopter landing sites can use clear ground near roads and use the landing-pad flatness limit instead of the stricter camp limit.
- Fixed aircraft clearance checks using the wrong ground height during extraction placement.
## 2026-10-24 — Placement on smaller terrains
- Improved mission placement on small maps with shorter airport separation for prisons and support for terrain bounds down to 1 km.
- Extraction searches can find a clear landing zone elsewhere when an airport has no suitable space.
- Difficult terrain searches try more positions for longer before giving up.
- Startup failures show their cause directly on the loading screen, including placement rejection reasons.
## 2026-10-21 — Radio intel progression
- Radio intel unlocks after five distinct non-radio sites are fully identified; finding and using radios in person remains unrestricted.
- Configure the requirement under Intel → Radio intel, with live progress and a zero option to disable it for the next round.
- Early ordinary folders reveal other sites; fixed radio documents stay available and are read automatically after unlocking.
## 2026-10-21 — Recon drones, body burial and spectator camera
- Players holding a shovel can bury dead bodies and their remaining equipment.
- Added optional enemy reconnaissance drones using Realistic Combat Drones, responding to recent HQ sightings.
- Configure the active drone limit and launch cooldown under HQ → Recon drones; disabled by default.
- Buffered the full first-person spectator camera position to reduce shaking from remote character movement corrections.
## 2026-10-20 — Optional site map hints
- Added an Intel menu option to show sites as question marks until intel identifies their types.
## 2026-10-20 — Safe intel cleanup
- Removing used intel folders preserves enemies' ability to discover stolen documents.
- Used ordinary folders are cleaned up even when no new locations remain to reveal.
- Failed inventory cleanup retries safely without granting extra intel or deleting objective documents.
## 2026-10-20 — Hunter recovery and intel cleanup
- Hunters with a blocked driver seat or failed reboarding can abandon the vehicle and continue on foot.
- Hunters remember failed approach positions and pause before attempting another coordinated manoeuvre.
- Ordinary intel folders are removed after revealing a location; objective documents are retained.
## 2026-10-17 — Fewer Šárka hunters
- Set how many anger points are needed per Šárka in the mission menu. Higher values mean fewer cars.
- Default: one car per three anger points, rounded down. Zero disables new waves.
## 2026-10-17 — Mission status readability
- Fixed clipped status and journal text so long reports can be scrolled.
- Fixed duplicated Šárka wave details and restored the hunting-patrol summary.
## 2026-10-17 — Mission menu navigation
- Reorganized the mission menu with main tabs across the top and related submenus on the left.
## 2026-10-17 — Configurable Šárka hunters
- Added a mission-menu setting for the number of Šárka special-forces cars per anger point.
- Set cars per anger point to zero to disable new waves. Deployed teams remain.
## 2026-10-17 — Discord changelog test
- Test notification for automatic mission changelogs.
## 2026-10-17 — Changelog notifications
- Added automatic Discord changelogs for new mission updates.
+33 -3
View File
@@ -6,7 +6,9 @@ An independently authored replacement for the abandoned Escapists base mission,
The current base supplies prison spawning, two independent scavenging/clue routes, finite medical and outpost stock, an optional ground vehicle, physical radio documents, named discovery markers, native objectives, a separate radio-defence service, capacity-checked helicopter extraction, identity-based partial results, early helicopter-loss defeat, saved mission time/speed, defeat/restart, identity-based lives and local spectator following. Authored site definitions and an injected vanilla faction/asset catalog own the initial physical population. Core provides administration and startup diagnostics; the AI, Intel, Chat and Map modules extend its interfaces inside one addon. HQ coordinates enemy responses through physical radios and available forces. The current base supplies prison spawning, two independent scavenging/clue routes, finite medical and outpost stock, an optional ground vehicle, physical radio documents, named discovery markers, native objectives, a separate radio-defence service, capacity-checked helicopter extraction, identity-based partial results, early helicopter-loss defeat, saved mission time/speed, defeat/restart, identity-based lives and local spectator following. Authored site definitions and an injected vanilla faction/asset catalog own the initial physical population. Core provides administration and startup diagnostics; the AI, Intel, Chat and Map modules extend its interfaces inside one addon. HQ coordinates enemy responses through physical radios and available forces.
The [local development playtest handoff](docs/playtest-build.md) contains the earlier `5e74269` checkpoint: five native addon packages, matching source and separate manual launch scripts. It does not contain the newer HQ memory or RHS/WCS/BMP integration. Two scripted Core escape/restart rounds pass616 assertions; packaged Core and full-companion startup both pass. Full human completion and multiplayer remain to be tested. The current working tree passes **326 registered regression programs** after the [5 October validation repairs](docs/evidence/review-audit-2026-10-05/review.json). These offline checks do not establish native gameplay or multiplayer correctness.
The [4 October development playtest handoff](.build/playtest-2026-10-04-ui-bmp/README.md) includes the inventory callback and BMP ammunition-accounting corrections. Its 439 authored files match 378 PAK payloads and three runtime files. At that archived checkpoint, code/tests/tools bound to 309 passing regression programs; exact-source normal escape and ordinary packaged startup complete with retained engine errors. It remains an **unqualified development candidate**. Existing missions and prior packages are preserved. See [current status](docs/current-status.md) for exact scope and limits.
The [AI migration checkpoint](docs/ai-migration-progress.md) adds delivered-report memory, bounded trajectory prediction, physical infantry pursuit, status checks, body observations and finite equipment. Native memory and actual-sighting response fixtures pass their assertions, with four retained upstream localization errors. See [current development status](docs/current-status.md) for source-bound checkpoints. The earlier five-addon checkpoint shows the [real prisoner view and starting objective](docs/evidence/game-2026-09-19-125906-integrated-prison/ordinary-prison-view.png). Native tests cover scripted escape/restart, finite mortar support, physical radio delivery, shared-map ownership, and the reusable aircraft controller's vertical flight, airborne cancellation and an isolated 20 m fixed route. Archived five-addon packages pass ordinary Chat startup without loose successor source and compile/prepare the empty mission on an isolated Linux server. The latter retains four installed-asset errors reproduced by a vanilla control. The [AI migration checkpoint](docs/ai-migration-progress.md) adds delivered-report memory, bounded trajectory prediction, physical infantry pursuit, status checks, body observations and finite equipment. Native memory and actual-sighting response fixtures pass their assertions, with four retained upstream localization errors. See [current development status](docs/current-status.md) for source-bound checkpoints. The earlier five-addon checkpoint shows the [real prisoner view and starting objective](docs/evidence/game-2026-09-19-125906-integrated-prison/ordinary-prison-view.png). Native tests cover scripted escape/restart, finite mortar support, physical radio delivery, shared-map ownership, and the reusable aircraft controller's vertical flight, airborne cancellation and an isolated 20 m fixed route. Archived five-addon packages pass ordinary Chat startup without loose successor source and compile/prepare the empty mission on an isolated Linux server. The latter retains four installed-asset errors reproduced by a vanilla control.
@@ -18,7 +20,12 @@ Later ordinary input checks demonstrate walking, clean inventory interaction and
Open **`addons/core/addon.gproj`**, titled **Co10 Escape Reintroduced**. This is the Open **`addons/core/addon.gproj`**, titled **Co10 Escape Reintroduced**. This is the
single project to develop, package and eventually publish as one Workshop mod. single project to develop, package and eventually publish as one Workshop mod.
Use **`Missions/ERR_Everon.conf`** (`{C1E20667DCE324ED}`) for the full scenario. Select **Co10 Escape Reintroduced — Automatic terrain**,
**`Missions/ERR_Everon.conf`** (`{C1E20667DCE324ED}`), as the universal starting
scenario. It opens Escape on Everon by default; a saved choice in the admin
**Terrains** menu loads another installed terrain after restart. The existing
resource ID is unchanged. The public browser may keep the generic Automatic
terrain title after switching maps; it no longer labels this entry as Everon.
The Core directory and addon GUID `2528466DE220D91C` are retained for continuity. The Core directory and addon GUID `2528466DE220D91C` are retained for continuity.
Scripts are organized under `addons/core/Scripts/Game/ERR/`: Scripts are organized under `addons/core/Scripts/Game/ERR/`:
@@ -32,16 +39,39 @@ Scripts are organized under `addons/core/Scripts/Game/ERR/`:
The prefixes preserve Enforce script extension order. All modules load together. The prefixes preserve Enforce script extension order. All modules load together.
Resources retain their existing paths and GUIDs within the combined project. Resources retain their existing paths and GUIDs within the combined project.
Menu appearance and navigation are maintained in [ui/admin_menu](ui/admin_menu/README.md):
one theme, a navigation manifest, and separate page layouts. Run
`python3 tools/build_admin_menu.py` after changing those sources; the generated
native layouts remain in the addon for Workbench and publishing.
Register only this project and its external dependencies in Workbench. Remove stale Register only this project and its external dependencies in Workbench. Remove stale
AI/Intel/Map/Chat project entries if you previously added them. RHS: Status Quo, AI/Intel/Map/Chat project entries if you previously added them. RHS: Status Quo,
WCS Armaments and WCS BMP-1 (including their dependencies) remain separate, WCS Armaments and WCS BMP-1 (including their dependencies) remain separate,
author-supplied mods; see [external dependencies](docs/external-addons.md). author-supplied mods; see [external dependencies](docs/external-addons.md).
Their assets are referenced, never redistributed. Their assets are referenced, never redistributed.
With a shovel equipped, look at a dead body and hold **Bury body** for eight
seconds. This removes the corpse and its remaining equipment; loot it first.
Living and unconscious characters cannot be buried. Existing HQ casualty reports
remain valid after burial.
Optional [enemy reconnaissance drones](docs/drone-reconnaissance.md) use Realistic
Combat Drones. Configure their limit and cooldown in **HQ → Recon drones**;
they are disabled by default.
See the [single-mod migration and current commands](docs/single-mod.md). See the [single-mod migration and current commands](docs/single-mod.md).
Earlier five-addon build reports below are historical evidence, not validation of Earlier five-addon build reports below are historical evidence, not validation of
the merged project. Nothing has been published to Workshop. the merged project. Nothing has been published to Workshop.
[Automatic terrain takeover](docs/terrain-takeover.md) starts Escape when an admin
loads Escape alongside a terrain and selects that terrain's existing scenario.
No connector, custom startup flag or airport coordinates are required. The map
must provide usable navigation and safe placement space. Published authenticated
multiplayer validation remains outstanding; see the linked test scope.
Terrain authors can optionally use the [shared connector prefab](docs/terrain-adapters.md)
for an authored Escape scenario; [Nogova setup](docs/nogova-publishing.md) describes
both routes.
See the [player route playtest guide](docs/player-route.md), [player lifecycle](docs/player-lifecycle.md), [manual gameplay gaps](docs/base-gameplay-gaps.md), [release qualification](docs/release-qualification.md), [performance comparison tooling](docs/performance-comparison.md), [configuration and commands](docs/configuration.md), [site authoring](docs/site-authoring.md), [architecture](docs/architecture.md) and [migration priorities](docs/base-replacement.md). See the [player route playtest guide](docs/player-route.md), [player lifecycle](docs/player-lifecycle.md), [manual gameplay gaps](docs/base-gameplay-gaps.md), [release qualification](docs/release-qualification.md), [performance comparison tooling](docs/performance-comparison.md), [configuration and commands](docs/configuration.md), [site authoring](docs/site-authoring.md), [architecture](docs/architecture.md) and [migration priorities](docs/base-replacement.md).
## Validation ## Validation
@@ -60,4 +90,4 @@ The [development source bundle tool](docs/development-source-bundle.md) previews
Random field hospitals and camps, their permanent/mobile guard split, finite supplies and authoring options are documented in [Field sites](docs/field-sites.md). Random field hospitals and camps, their permanent/mobile guard split, finite supplies and authoring options are documented in [Field sites](docs/field-sites.md).
Town population and its 700-metre spawning rule: [town garrisons](docs/town-garrisons.md). Town population, configurable factory guards and safe tower/roof posts: [town and object garrisons](docs/town-garrisons.md).
@@ -0,0 +1,6 @@
MatPBRBasic {
Color 0.48 0.018 0.012 1
MetalnessScale 0
RoughnessScale 0.75
BCRMap "{A64A8C63E0FBAE87}system/textures/defWhite.edds"
}
@@ -0,0 +1,11 @@
MetaFileClass {
Name "{F6575DAA717C6693}Assets/Items/ERR_Atlas/AtlasCover.emat"
Configurations {
ResourceClass PC {}
ResourceClass HEADLESS : PC {}
ResourceClass XBOX_ONE : PC {}
ResourceClass XBOX_SERIES : PC {}
ResourceClass PS4 : PC {}
ResourceClass PS5 : PC {}
}
}
@@ -0,0 +1,6 @@
MatPBRBasic {
Color 0.73 0.70 0.62 1
MetalnessScale 0
RoughnessScale 0.75
BCRMap "{A64A8C63E0FBAE87}system/textures/defWhite.edds"
}
@@ -0,0 +1,11 @@
MetaFileClass {
Name "{C029F1CCABDA2C3E}Assets/Items/ERR_Atlas/AtlasPaper.emat"
Configurations {
ResourceClass PC {}
ResourceClass HEADLESS : PC {}
ResourceClass XBOX_ONE : PC {}
ResourceClass XBOX_SERIES : PC {}
ResourceClass PS4 : PC {}
ResourceClass PS5 : PC {}
}
}
@@ -0,0 +1,7 @@
MatPBRBasic {
Color 0.882 0.847 0.792 1
AlphaTest 0.5
AllowUserAlphaBias 1
BCRMap "$rendertarget"
OpacityMap "{A64A8C63E0FBAE87}system/textures/defWhite.edds" "{A64A8C63E0FBAE87}system/textures/defWhite.edds" anim( 0 )
}
@@ -0,0 +1,11 @@
MetaFileClass {
Name "{CE2D59FD9280F47D}Assets/Items/ERR_Atlas/AtlasPrint.emat"
Configurations {
ResourceClass PC {}
ResourceClass HEADLESS : PC {}
ResourceClass XBOX_ONE : PC {}
ResourceClass XBOX_SERIES : PC {}
ResourceClass PS4 : PC {}
ResourceClass PS5 : PC {}
}
}
@@ -0,0 +1,64 @@
AnimSrcGraphFile {
Sheets {
AnimSrcGraphSheet Atlas {
Nodes {
AnimSrcNodeStateMachine ERRAtlasControl {
states {
AnimSrcNodeState Idle { Child "MasterControl" IsExit 1 }
AnimSrcNodeState Turn { Child "ERRAtlasHandIK" TimeStorage "Real Time" }
AnimSrcNodeState TurnBack { Child "ERRAtlasBackHandIK" TimeStorage "Real Time" }
}
transitions {
AnimSrcNodeTransition { FromState "Idle" ToState "Turn" Duration "0.25" Condition "IsCommand(CMD_HealSelf) && GetCommandI(CMD_HealSelf) == 1" }
AnimSrcNodeTransition { FromState "Idle" ToState "TurnBack" Duration "0.25" Condition "IsCommand(CMD_HealSelf) && GetCommandI(CMD_HealSelf) == 2" }
AnimSrcNodeTransition { FromState "TurnBack" ToState "Idle" Duration "0.25" Condition "GetStateTime() > 0.95 || GetCommandI(CMD_HealSelf) == -1" }
AnimSrcNodeTransition { FromState "Turn" ToState "Idle" Duration "0.25" Condition "GetStateTime() > 0.95 || GetCommandI(CMD_HealSelf) == -1" }
}
}
AnimSrcNodeIK2Target ERRAtlasHandTarget {
Child "MasterControl"
Bones {
AnimSrcNodeIK2BoneTarget { IkTarget "ERRAtlasRightHand" Bone "LeftHandProp" }
}
}
AnimSrcNodeProcTransform ERRAtlasHandOffset {
Child "ERRAtlasHandTarget"
Expression "1"
IkTargets {
AnimSrcNodeProcTrIkTargetItem { Operation Translate Axis X Amount "-0.068 + 0.21*cos(3.141593*curve_s(clamp((GetUpperRTime()-0.25)/0.65,0,1)))" Space Local IkTarget "ERRAtlasRightHand" }
AnimSrcNodeProcTrIkTargetItem { Operation Translate Axis Y Amount "0.02 + 0.055*sin(3.141593*curve_s(clamp((GetUpperRTime()-0.25)/0.65,0,1)))" Space Local IkTarget "ERRAtlasRightHand" }
AnimSrcNodeProcTrIkTargetItem { Operation Translate Axis Z Amount "-0.065" Space Local IkTarget "ERRAtlasRightHand" }
}
}
AnimSrcNodeIK2 ERRAtlasHandIK {
Tags { "TagLItemAction" }
Child "ERRAtlasHandOffset"
Weight "1"
Chains {
AnimSrcIkBinding { IkTarget "ERRAtlasRightHand" IkChain "ERRAtlasRightArm" }
}
Solver AnimSrcNodeIK2TwoBoneSolver { SnapRotation 0 AllowStretching 0 }
Version 1
}
AnimSrcNodeProcTransform ERRAtlasBackHandOffset {
Child "ERRAtlasHandTarget"
Expression "1"
IkTargets {
AnimSrcNodeProcTrIkTargetItem { Operation Translate Axis X Amount "-0.068 + 0.21*cos(3.141593*(1-curve_s(clamp((GetUpperRTime()-0.25)/0.65,0,1))))" Space Local IkTarget "ERRAtlasRightHand" }
AnimSrcNodeProcTrIkTargetItem { Operation Translate Axis Y Amount "0.02 + 0.055*sin(3.141593*(1-curve_s(clamp((GetUpperRTime()-0.25)/0.65,0,1))))" Space Local IkTarget "ERRAtlasRightHand" }
AnimSrcNodeProcTrIkTargetItem { Operation Translate Axis Z Amount "-0.065" Space Local IkTarget "ERRAtlasRightHand" }
}
}
AnimSrcNodeIK2 ERRAtlasBackHandIK {
Tags { "TagLItemAction" }
Child "ERRAtlasBackHandOffset"
Weight "1"
Chains {
AnimSrcIkBinding { IkTarget "ERRAtlasRightHand" IkChain "ERRAtlasRightArm" }
}
Solver AnimSrcNodeIK2TwoBoneSolver { SnapRotation 0 AllowStretching 0 }
Version 1
}
}
}
}
@@ -0,0 +1,4 @@
MetaFileClass {
Name "{B428122ACA3850BC}Assets/Items/ERR_Atlas/AtlasTurn.agf"
Configurations { ResourceClass PC {} ResourceClass HEADLESS : PC {} ResourceClass XBOX_ONE : PC {} ResourceClass XBOX_SERIES : PC {} ResourceClass PS4 : PC {} ResourceClass PS5 : PC {} }
}
@@ -0,0 +1,70 @@
AnimSrcGraph {
AnimSetTemplate "{91953B0B3D746630}Assets/Items/Equipment/workspaces/paper_map.ast"
ControlTemplate AnimSrcGCT "{55567D08724BC61D}" {
Variables {
AnimSrcGCTVarFloat AimY {
MinValue -90
MaxValue 90
}
AnimSrcGCTVarInt Stance {
DefaultValue 0
MaxValue 2
}
AnimSrcGCTVarBool Low {
DefaultValue 0
}
AnimSrcGCTVarFloat AimX {
MinValue -90
MaxValue 90
}
AnimSrcGCTVarBool Temp {
}
AnimSrcGCTVarInt WeaponType {
MaxValue 20
}
AnimSrcGCTVarBool FreeLook {
}
}
Commands { AnimSrcGCTCmd CMD_HealSelf { } }
IkChains {
AnimSrcGCTIkChain ERRAtlasRightArm {
Joints { "rightarm" "rightarmtwist" "rightforearm" "rightforearmtwist" "righthand" }
MiddleJoint "rightforearm"
ChainAxis "+y"
}
AnimSrcGCTIkChain Head {
Joints {
"neck2"
"neck3"
"head"
}
MiddleJoint "neck3"
ChainAxis "+y"
}
AnimSrcGCTIkChain LeftArm {
Joints {
"leftarm"
"leftarmtwist"
"leftforearm"
"leftforearmtwist"
"lefthand"
}
MiddleJoint "leftforearm"
ChainAxis "+y"
}
}
GlobalTags {
"ITEM"
"GADGET"
"MAP"
"PaperMap01"
}
}
Debug AnimSrcGD "{55567D08724BC783}" {
}
GraphFilesResourceNames {
"{B428122ACA3850BC}Assets/Items/ERR_Atlas/AtlasTurn.agf"
"{6F87679E2512527D}Assets/Items/Equipment/workspaces/map.agf"
}
DefaultRunNode "ERRAtlasControl"
}
@@ -0,0 +1,4 @@
MetaFileClass {
Name "{4959464B088CCCA4}Assets/Items/ERR_Atlas/AtlasTurn.agr"
Configurations { ResourceClass PC {} ResourceClass HEADLESS : PC {} ResourceClass XBOX_ONE : PC {} ResourceClass XBOX_SERIES : PC {} ResourceClass PS4 : PC {} ResourceClass PS5 : PC {} }
}
@@ -0,0 +1,6 @@
MatPBRBasic {
Color 0.8 0.8 0.78 1
MetalnessScale 0.65
RoughnessScale 0.75
BCRMap "{A64A8C63E0FBAE87}system/textures/defWhite.edds"
}
@@ -0,0 +1,11 @@
MetaFileClass {
Name "{993AED2484A6829B}Assets/Items/ERR_Atlas/AtlasWire.emat"
Configurations {
ResourceClass PC {}
ResourceClass HEADLESS : PC {}
ResourceClass XBOX_ONE : PC {}
ResourceClass XBOX_SERIES : PC {}
ResourceClass PS4 : PC {}
ResourceClass PS5 : PC {}
}
}
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
@@ -0,0 +1,85 @@
MetaFileClass {
Name "{51ABB9E2E838FC9E}Assets/Items/ERR_Atlas/ERR_Atlas.xob"
Configurations {
FBXResourceClass PC {
MaterialAssigns {
MaterialAssignClass "{6A9727841D4273B0}" {
SourceMaterial "AtlasPrint"
AssignedMaterial "{CE2D59FD9280F47D}Assets/Items/ERR_Atlas/AtlasPrint.emat"
}
MaterialAssignClass "{6A9727841D427046}" {
SourceMaterial "AtlasPaper"
AssignedMaterial "{C029F1CCABDA2C3E}Assets/Items/ERR_Atlas/AtlasPaper.emat"
}
MaterialAssignClass "{6A9727841D427075}" {
SourceMaterial "AtlasCover"
AssignedMaterial "{F6575DAA717C6693}Assets/Items/ERR_Atlas/AtlasCover.emat"
}
MaterialAssignClass "{6A9727841D427065}" {
SourceMaterial "AtlasWire"
AssignedMaterial "{993AED2484A6829B}Assets/Items/ERR_Atlas/AtlasWire.emat"
}
}
MeshParams {
MeshParam BackCover {
}
MeshParam FrontCover {
}
MeshParam PageBlock {
}
MeshParam PageEdge_00 {
}
MeshParam PageEdge_01 {
}
MeshParam PageEdge_02 {
}
MeshParam PageEdge_03 {
}
MeshParam PageEdge_04 {
}
MeshParam PageEdge_05 {
}
MeshParam PageEdge_06 {
}
MeshParam PageEdge_07 {
}
MeshParam PageEdge_08 {
}
MeshParam PageEdge_09 {
}
MeshParam PageEdge_10 {
}
MeshParam PageEdge_11 {
}
MeshParam PrintedSheet {
}
MeshParam SpiralWire {
}
}
GeometryParams {
GeometryParam UBX_Atlas {
LayerPreset "ItemFireView"
SurfaceProperties {
"{AC1A1CDEE58FEE27}Common/Materials/Game/paper.gamemat"
}
Mass 0.35
}
}
Common TXOCommonClass "{6A9727841D4270C8}" : "{0877E7C4BB2B2C9A}Configs/System/ResourceTypes/PC/MeshObjectCommon.conf" {
}
ApplicationVendor "Blender Foundation"
ApplicationName "Blender (stable FBX IO)"
ApplicationVersion "4.5.3 LTS"
}
FBXResourceClass HEADLESS : PC {
}
FBXResourceClass XBOX_ONE : PC {
}
FBXResourceClass XBOX_SERIES : PC {
}
FBXResourceClass PS4 : PC {
}
FBXResourceClass PS5 : PC {
}
}
}
Binary file not shown.
@@ -0,0 +1,374 @@
$object "" {
#version 1.0
#tag MergeMeshes "1"
#tag Coords "XZY"
#tag Scale "1.000000"
#tag SrcFile_AppVendor "Blender Foundation"
#tag SrcFile_AppName "Blender (stable FBX IO)"
#tag SrcFile_AppVersion "4.5.3 LTS"
$materials {
$material "AtlasPaper" {
#UVset 0
}
$material "AtlasPrint" {
#UVset 0
}
}
$lod 0 {
#lodFactor 0.5
$mesh "LeafBack" {
#texCoords 1
$faces {
0 4 0 1 2 3
0 4 4 5 6 7
0 4 8 9 10 11
0 4 12 13 14 15
0 4 16 17 18 19
0 4 20 21 22 23
0 4 24 25 26 27
0 4 28 29 30 31
0 4 32 33 34 35
0 4 36 37 38 39
0 4 40 41 42 43
0 4 44 45 46 47
0 4 48 49 50 51
0 4 52 53 54 55
0 4 56 57 58 59
0 4 60 61 62 63
}
$faceVerts {
0 0 0
1 0 1
3 0 2
2 0 3
2 1 3
3 1 2
5 1 4
4 1 5
4 2 5
5 2 4
7 2 6
6 2 7
6 3 7
7 3 6
9 3 8
8 3 9
8 4 9
9 4 8
11 4 10
10 4 11
10 5 11
11 5 10
13 5 12
12 5 13
12 6 13
13 6 12
15 6 14
14 6 15
14 7 15
15 7 14
17 7 16
16 7 17
16 8 17
17 8 16
19 8 18
18 8 19
18 9 19
19 9 18
21 9 20
20 9 21
20 10 21
21 10 20
23 10 22
22 10 23
22 11 23
23 11 22
25 11 24
24 11 25
24 12 25
25 12 24
27 12 26
26 12 27
26 13 27
27 13 26
29 13 28
28 13 29
28 14 29
29 14 28
31 14 30
30 14 31
30 15 31
31 15 30
33 15 32
32 15 33
}
$verts {
0 -0 0.1485 0
0 0 -0.1485 0
0.011937 0.00078 0.1485 0
0.011937 0.00078 -0.1485 0
0.023875 0.001531 0.1485 0
0.023875 0.001531 -0.1485 0
0.035813 0.002222 0.1485 0
0.035813 0.002222 -0.1485 0
0.04775 0.002828 0.1485 0
0.04775 0.002828 -0.1485 0
0.059687 0.003326 0.1485 0
0.059687 0.003326 -0.1485 0
0.071625 0.003696 0.1485 0
0.071625 0.003696 -0.1485 0
0.083563 0.003923 0.1485 0
0.083563 0.003923 -0.1485 0
0.0955 0.004 0.1485 0
0.0955 0.004 -0.1485 0
0.107437 0.003923 0.1485 0
0.107437 0.003923 -0.1485 0
0.119375 0.003696 0.1485 0
0.119375 0.003696 -0.1485 0
0.131313 0.003326 0.1485 0
0.131313 0.003326 -0.1485 0
0.14325 0.002828 0.1485 0
0.14325 0.002828 -0.1485 0
0.155188 0.002222 0.1485 0
0.155188 0.002222 -0.1485 0
0.167125 0.001531 0.1485 0
0.167125 0.001531 -0.1485 0
0.179063 0.00078 0.1485 0
0.179063 0.00078 -0.1485 0
0.191 -0 0.1485 0
0.191 0 -0.1485 0
}
$texCoords {
0.10753 0
0.10753 1
0.16331 1
0.16331 0
0.21909 1
0.21909 0
0.27487 1
0.27487 0
0.33065 1
0.33065 0
0.38642 1
0.38642 0
0.4422 1
0.4422 0
0.49798 1
0.49798 0
0.55376 1
0.55376 0
0.60954 1
0.60954 0
0.66532 1
0.66532 0
0.7211 1
0.7211 0
0.77688 1
0.77688 0
0.83266 1
0.83266 0
0.88844 1
0.88844 0
0.94422 1
0.94422 0
1 1
1 0
}
$normals {
0.0652 -0.9979 -0
0.0627 -0.998 -0
0.0578 -0.9983 -0
0.0507 -0.9987 -0
0.0416 -0.9991 -0
0.0309 -0.9995 -0
0.0191 -0.9998 -0
0.0064 -1 -0
-0.0064 -1 -0
-0.0191 -0.9998 -0
-0.0309 -0.9995 -0
-0.0416 -0.9991 -0
-0.0507 -0.9987 -0
-0.0578 -0.9983 -0
-0.0627 -0.998 -0
-0.0652 -0.9979 -0
}
}
$mesh "TurningLeaf" {
#texCoords 1
$faces {
1 4 0 1 2 3
1 4 4 5 6 7
1 4 8 9 10 11
1 4 12 13 14 15
1 4 16 17 18 19
1 4 20 21 22 23
1 4 24 25 26 27
1 4 28 29 30 31
1 4 32 33 34 35
1 4 36 37 38 39
1 4 40 41 42 43
1 4 44 45 46 47
1 4 48 49 50 51
1 4 52 53 54 55
1 4 56 57 58 59
1 4 60 61 62 63
}
$faceVerts {
2 0 0
3 0 1
1 0 2
0 0 3
4 1 4
5 1 5
3 1 1
2 1 0
6 2 6
7 2 7
5 2 5
4 2 4
8 3 8
9 3 9
7 3 7
6 3 6
10 4 10
11 4 11
9 4 9
8 4 8
12 5 12
13 5 13
11 5 11
10 5 10
14 6 14
15 6 15
13 6 13
12 6 12
16 7 16
17 7 17
15 7 15
14 7 14
18 8 18
19 8 19
17 8 17
16 8 16
20 9 20
21 9 21
19 9 19
18 9 18
22 10 22
23 10 23
21 10 21
20 10 20
24 11 24
25 11 25
23 11 23
22 11 22
26 12 26
27 12 27
25 12 25
24 12 24
28 13 28
29 13 29
27 13 27
26 13 26
30 14 30
31 14 31
29 14 29
28 14 28
32 15 32
33 15 33
31 15 31
30 15 30
}
$verts {
0 -0 0.1485 0
0 0 -0.1485 0
0.011937 0.00078 0.1485 0
0.011937 0.00078 -0.1485 0
0.023875 0.001531 0.1485 0
0.023875 0.001531 -0.1485 0
0.035813 0.002222 0.1485 0
0.035813 0.002222 -0.1485 0
0.04775 0.002828 0.1485 0
0.04775 0.002828 -0.1485 0
0.059687 0.003326 0.1485 0
0.059687 0.003326 -0.1485 0
0.071625 0.003696 0.1485 0
0.071625 0.003696 -0.1485 0
0.083563 0.003923 0.1485 0
0.083563 0.003923 -0.1485 0
0.0955 0.004 0.1485 0
0.0955 0.004 -0.1485 0
0.107437 0.003923 0.1485 0
0.107437 0.003923 -0.1485 0
0.119375 0.003696 0.1485 0
0.119375 0.003696 -0.1485 0
0.131313 0.003326 0.1485 0
0.131313 0.003326 -0.1485 0
0.14325 0.002828 0.1485 0
0.14325 0.002828 -0.1485 0
0.155188 0.002222 0.1485 0
0.155188 0.002222 -0.1485 0
0.167125 0.001531 0.1485 0
0.167125 0.001531 -0.1485 0
0.179063 0.00078 0.1485 0
0.179063 0.00078 -0.1485 0
0.191 -0 0.1485 0
0.191 0 -0.1485 0
}
$texCoords {
0.16331 0
0.16331 1
0.10753 1
0.10753 0
0.21909 0
0.21909 1
0.27487 0
0.27487 1
0.33065 0
0.33065 1
0.38642 0
0.38642 1
0.4422 0
0.4422 1
0.49798 0
0.49798 1
0.55376 0
0.55376 1
0.60954 0
0.60954 1
0.66532 0
0.66532 1
0.7211 0
0.7211 1
0.77688 0
0.77688 1
0.83266 0
0.83266 1
0.88844 0
0.88844 1
0.94422 0
0.94422 1
1 0
1 1
}
$normals {
-0.0652 0.9979 0
-0.0627 0.998 0
-0.0578 0.9983 0
-0.0507 0.9987 0
-0.0416 0.9991 0
-0.0309 0.9995 0
-0.0191 0.9998 0
-0.0064 1 0
0.0064 1 0
0.0191 0.9998 0
0.0309 0.9995 0
0.0416 0.9991 0
0.0507 0.9987 0
0.0578 0.9983 0
0.0627 0.998 0
0.0652 0.9979 0
}
}
}
}
Binary file not shown.
@@ -0,0 +1,38 @@
MetaFileClass {
Name "{76D738EEB0CA431F}Assets/Items/ERR_Atlas/ERR_AtlasLeaf.xob"
Configurations {
FBXResourceClass PC {
MaterialAssigns {
MaterialAssignClass "{6A9727841D4273B0}" {
SourceMaterial "AtlasPrint"
AssignedMaterial "{CE2D59FD9280F47D}Assets/Items/ERR_Atlas/AtlasPrint.emat"
}
MaterialAssignClass "{6A9727841D427046}" {
SourceMaterial "AtlasPaper"
AssignedMaterial "{C029F1CCABDA2C3E}Assets/Items/ERR_Atlas/AtlasPaper.emat"
}
}
MeshParams {
MeshParam LeafBack {
}
MeshParam TurningLeaf {
}
}
Common TXOCommonClass "{6A9727841D4270C8}" : "{0877E7C4BB2B2C9A}Configs/System/ResourceTypes/PC/MeshObjectCommon.conf" {
}
ApplicationVendor "Blender Foundation"
ApplicationName "Blender (stable FBX IO)"
ApplicationVersion "4.5.3 LTS"
}
FBXResourceClass HEADLESS : PC {
}
FBXResourceClass XBOX_ONE : PC {
}
FBXResourceClass XBOX_SERIES : PC {
}
FBXResourceClass PS4 : PC {
}
FBXResourceClass PS5 : PC {
}
}
}
+5
View File
@@ -1,5 +1,10 @@
MenuManager { MenuManager {
MenuPresets { MenuPresets {
MenuPreset ERR_PreparationFailure {
Layout "{28DD8E27B1CF40C5}UI/layouts/ERR/PreparationFailure.layout"
ActionContext "MenuContext"
Class "ERR_PreparationFailureMenu"
}
MenuPreset ERR_AdminPanel { MenuPreset ERR_AdminPanel {
Layout "{AE761C0439D25FB8}UI/layouts/ERR/AdminMenu.layout" Layout "{AE761C0439D25FB8}UI/layouts/ERR/AdminMenu.layout"
ActionContext "MenuContext" ActionContext "MenuContext"
+18 -2
View File
@@ -30,12 +30,22 @@ ERR_ScenarioDefinition {
GuardOffset 18 0 0 GuardOffset 18 0 0
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et" Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
ContainerOffset 0 0 0 ContainerOffset 0 0 0
Transport ERR_SiteTransport {
Optional 1
Prefab "{FA65287C19DE304B}Prefabs/ERR/Vehicles/SupplyUAZ.et"
Offset 0 0 -12
Heading 180
ClearanceRadius 4
MaximumTerrainRange 1
}
Stock { Stock {
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 8 } ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 8 }
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 16 } ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 16 }
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 12 } ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 12 }
ERR_SiteStock { Prefab "{13772C903CB5E4F7}Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et" Count 1 } ERR_SiteStock { Prefab "{13772C903CB5E4F7}Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et" Count 1 }
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" } ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
ERR_SiteStock { Prefab "{C09C7C97EFA34909}Prefabs/ERR/Items/EmergencyRepairKit.et" Count 1 }
ERR_SiteStock { Prefab "{12D5AD21E383B768}Prefabs/Items/Fuel/Jerrycan_01/Jerrycan_01_item.et" Count 2 }
} }
RevealOnEscape 1 RevealOnEscape 1
} }
@@ -60,12 +70,13 @@ ERR_ScenarioDefinition {
} }
ERR_SiteDefinition { ERR_SiteDefinition {
Id "everon-extraction-1" Id "everon-extraction-1"
Label "Initial helicopter pickup site" Label "Extraction landing zone"
Kind 2 Kind 2
Prefab "{033503110AC34EAC}Prefabs/ERR/Extraction.et" Prefab "{033503110AC34EAC}Prefabs/ERR/Extraction.et"
Position 4800.953 0 11487.412 Position 4800.953 0 11487.412
AircraftOffset 0 0 0 AircraftOffset 0 0 0
RequireAirfield 1 RequireAirfield 0
RandomClear 1
Guarded 1 Guarded 1
GuardOffset 30 0 0 GuardOffset 30 0 0
} }
@@ -121,6 +132,7 @@ ERR_ScenarioDefinition {
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 } ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
} }
Stock { Stock {
ERR_SiteStock { Prefab "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et" Count 2 }
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 } ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 } ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 } ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
@@ -244,6 +256,7 @@ ERR_ScenarioDefinition {
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 } ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
} }
Stock { Stock {
ERR_SiteStock { Prefab "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et" Count 2 }
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 } ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 } ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 } ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
@@ -273,6 +286,7 @@ ERR_ScenarioDefinition {
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 } ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
} }
Stock { Stock {
ERR_SiteStock { Prefab "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et" Count 2 }
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 } ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 } ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 } ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
@@ -302,6 +316,7 @@ ERR_ScenarioDefinition {
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 } ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
} }
Stock { Stock {
ERR_SiteStock { Prefab "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et" Count 2 }
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 } ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 } ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 } ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
@@ -331,6 +346,7 @@ ERR_ScenarioDefinition {
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 } ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
} }
Stock { Stock {
ERR_SiteStock { Prefab "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et" Count 2 }
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 } ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 } ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 } ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
+2 -2
View File
@@ -2,7 +2,7 @@ SCR_GameOverScreenConfig : "{651583FFFB7FC765}Configs/GameOverScreen/BaseGameOve
m_aGameOverScreens { m_aGameOverScreens {
ERR_EscapeGameOverScreenInfo "{6B193DC8DDC63507}" { ERR_EscapeGameOverScreenInfo "{6B193DC8DDC63507}" {
m_GameOverScreenId VICTORY m_GameOverScreenId VICTORY
m_GameOverContentLayout "{C4507D07F3C54831}UI/layouts/HUD/GameOver/EndScreen/EndScreenContent_NoImage.layout" m_GameOverContentLayout "{C63B4D1E09A572F8}UI/layouts/ERR/RoundResults.layout"
m_OptionalParams SCR_BaseGameOverScreenInfoOptional "{4984A5C1BFEFC04E}" { m_OptionalParams SCR_BaseGameOverScreenInfoOptional "{4984A5C1BFEFC04E}" {
m_sTitle "Escape complete" m_sTitle "Escape complete"
m_sAudioOneShot "SOUND_ONVICTORY" m_sAudioOneShot "SOUND_ONVICTORY"
@@ -10,7 +10,7 @@ SCR_GameOverScreenConfig : "{651583FFFB7FC765}Configs/GameOverScreen/BaseGameOve
} }
ERR_FailureGameOverScreenInfo "{E66F29A7A8AD7438}" { ERR_FailureGameOverScreenInfo "{E66F29A7A8AD7438}" {
m_GameOverScreenId LOOSE m_GameOverScreenId LOOSE
m_GameOverContentLayout "{C4507D07F3C54831}UI/layouts/HUD/GameOver/EndScreen/EndScreenContent_NoImage.layout" m_GameOverContentLayout "{C63B4D1E09A572F8}UI/layouts/ERR/RoundResults.layout"
m_OptionalParams SCR_BaseGameOverScreenInfoOptional "{3DBF0BDE715945FA}" { m_OptionalParams SCR_BaseGameOverScreenInfoOptional "{3DBF0BDE715945FA}" {
m_sTitle "Escape failed" m_sTitle "Escape failed"
m_sDebriefing "No surviving prisoners can complete the escape, or the extraction helicopter has been lost." m_sDebriefing "No surviving prisoners can complete the escape, or the extraction helicopter has been lost."
+104
View File
@@ -1,7 +1,111 @@
ActionManager : "{795184CF9AD764DB}Configs/System/chimeraInputCommon.conf" { ActionManager : "{795184CF9AD764DB}Configs/System/chimeraInputCommon.conf" {
Contexts { Contexts {
ActionContext ERR_PassengerBrakeContext {
Priority 60
Flags 0xa 0
Actions {
Action ERR_PassengerBrake {
InputSource InputSourceValue "{A7B19F416E541030}" {
Input "keyboard:KC_SPACE"
}
}
}
}
ActionContext ERR_FieldMapContext {
Priority 60
Flags 0xa 0
Actions {
Action ERR_AtlasDigit0 {
InputSource InputSourceValue "{A7B19F416E541020}" {
Input "keyboard:KC_NUMPAD0"
}
}
Action ERR_AtlasDigit1 {
InputSource InputSourceValue "{A7B19F416E541021}" {
Input "keyboard:KC_NUMPAD1"
}
}
Action ERR_AtlasDigit2 {
InputSource InputSourceValue "{A7B19F416E541022}" {
Input "keyboard:KC_NUMPAD2"
}
}
Action ERR_AtlasDigit3 {
InputSource InputSourceValue "{A7B19F416E541023}" {
Input "keyboard:KC_NUMPAD3"
}
}
Action ERR_AtlasDigit4 {
InputSource InputSourceValue "{A7B19F416E541024}" {
Input "keyboard:KC_NUMPAD4"
}
}
Action ERR_AtlasDigit5 {
InputSource InputSourceValue "{A7B19F416E541025}" {
Input "keyboard:KC_NUMPAD5"
}
}
Action ERR_AtlasDigit6 {
InputSource InputSourceValue "{A7B19F416E541026}" {
Input "keyboard:KC_NUMPAD6"
}
}
Action ERR_AtlasDigit7 {
InputSource InputSourceValue "{A7B19F416E541027}" {
Input "keyboard:KC_NUMPAD7"
}
}
Action ERR_AtlasDigit8 {
InputSource InputSourceValue "{A7B19F416E541028}" {
Input "keyboard:KC_NUMPAD8"
}
}
Action ERR_AtlasDigit9 {
InputSource InputSourceValue "{A7B19F416E541029}" {
Input "keyboard:KC_NUMPAD9"
}
}
Action ERR_AtlasEnter {
InputSource InputSourceSum "{A7B19F416E54102B}" {
Sources {
InputSourceValue "{A7B19F416E54102A}" {
Input "keyboard:KC_NUMPADENTER"
}
InputSourceValue "{A7B19F416E54102C}" {
Input "keyboard:KC_RETURN"
}
}
}
}
Action ERR_FieldMapLeft {
InputSource InputSourceValue "{A7B19F416E541010}" {
Input "keyboard:KC_LEFT"
}
}
Action ERR_FieldMapRight {
InputSource InputSourceValue "{A7B19F416E541011}" {
Input "keyboard:KC_RIGHT"
}
}
Action ERR_FieldMapUp {
InputSource InputSourceValue "{A7B19F416E541012}" {
Input "keyboard:KC_UP"
}
}
Action ERR_FieldMapDown {
InputSource InputSourceValue "{A7B19F416E541013}" {
Input "keyboard:KC_DOWN"
}
}
}
}
ActionContext ERR_ObserverContext : "{C538807EEE48BED8}Configs/System/ActionContext/ManualCameraContext.conf" { ActionContext ERR_ObserverContext : "{C538807EEE48BED8}Configs/System/ActionContext/ManualCameraContext.conf" {
Actions { Actions {
Action ERR_SelfEpinephrine {
InputSource InputSourceValue "{A7B19F416E541020}" {
Input "keyboard:KC_SPACE"
}
}
Action ERR_ObserverPrevious { Action ERR_ObserverPrevious {
InputSource InputSourceValue "{A7B19F416E541001}" { InputSource InputSourceValue "{A7B19F416E541001}" {
Input "keyboard:KC_LEFT" Input "keyboard:KC_LEFT"
+3
View File
@@ -0,0 +1,3 @@
ERR_TelemetryConfig {
Endpoint "https://co10escape-telemetry.zacekjakub.workers.dev/api/mission-events"
}
@@ -0,0 +1,11 @@
MetaFileClass {
Name "{E72B130684FA492C}Configs/ERR/Telemetry.conf"
Configurations {
CONFResourceClass PC { }
CONFResourceClass HEADLESS : PC { }
CONFResourceClass XBOX_ONE : PC { }
CONFResourceClass XBOX_SERIES : PC { }
CONFResourceClass PS4 : PC { }
CONFResourceClass PS5 : PC { }
}
}
@@ -3,5 +3,11 @@ MatPBRBasic {
AlphaTest 0.5 AlphaTest 0.5
AllowUserAlphaBias 1 AllowUserAlphaBias 1
BCRMap "$rendertarget" BCRMap "$rendertarget"
GlobalMapsUVTransform MatUVTransform {
TilingU 1.5
TilingV 2
Rotation 270
OffsetU 1
}
OpacityMap "{A64A8C63E0FBAE87}system/textures/defWhite.edds" "{A64A8C63E0FBAE87}system/textures/defWhite.edds" anim( 0 ) OpacityMap "{A64A8C63E0FBAE87}system/textures/defWhite.edds" "{A64A8C63E0FBAE87}system/textures/defWhite.edds" anim( 0 )
} }
+3 -2
View File
@@ -1,9 +1,10 @@
SCR_MissionHeader { SCR_MissionHeader {
World "{B6C516F86302C679}Worlds/ERR_Everon.ent" World "{B6C516F86302C679}Worlds/ERR_Everon.ent"
SystemsConfig "{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf" SystemsConfig "{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf"
m_sName "Co10 Escape Reintroduced — Everon" m_sName "Co10 Escape Reintroduced — Automatic terrain"
m_sAuthor "Jakub Jim Žáček" m_sAuthor "Jakub Jim Žáček"
m_sDescription "Independent escape prototype. Development scenario; runtime validation in progress." m_sDescription "Escape from captivity, find equipment and intelligence, and arrange helicopter extraction while enemy forces hunt you."
m_sDetails "Universal Escape starting scenario. Starts on Everon unless an administrator has saved another terrain in the Escape mission menu. Add terrain mods to the server, select their scenario in the Terrains page, save and restart. No terrain connector is required. The server browser may keep this Automatic terrain title while the selected map is running."
m_sGameMode "Escape" m_sGameMode "Escape"
m_iPlayerCount 12 m_iPlayerCount 12
m_eSaveTypes 0 m_eSaveTypes 0
-11
View File
@@ -1,11 +0,0 @@
SCR_MissionHeader {
World "{B6C516F86302C679}Worlds/ERR_Everon.ent"
SystemsConfig "{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf"
m_sName "Co10 Escape Reintroduced — Everon"
m_sAuthor "Jakub Jim Žáček"
m_sDescription "Legacy diagnostic header for the integrated Co10 Escape Reintroduced mod."
m_sGameMode "Escape"
m_iPlayerCount 12
m_eSaveTypes 0
m_bShowInScenarioMenu 0
}
@@ -1,9 +0,0 @@
MetaFileClass {
Name "{AD62322D5213C440}Missions/ERR_Everon_Base.conf"
Configurations {
ResourceClass PC {
}
ResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,17 @@
SCR_ChimeraCharacter {
ID "520EC961A090B1EE"
components {
ActionsManagerComponent "{520EA1D2F659CE02}" {
additionalActions {
ERR_BuryBodyAction "{A62CB789905D4011}" {
ParentContextList { "Chest" "LowerTorso" }
UIInfo SCR_ActionUIInfo "{A62CB789905D4012}" {
Name "Bury body"
Description "Bury the body and its remaining equipment. Loot anything you want to keep first."
}
Duration 8
}
}
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{37578B1666981FCE}Prefabs/Characters/Core/Character_Base.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS : PC {}
}
}
@@ -0,0 +1,23 @@
GenericEntity : "{0D9A5DCF89AE7AA9}Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et" {
ID "59DAB68B83510861"
components {
InventoryItemComponent "{5222EB4D0C73006B}" {
Attributes SCR_ItemAttributeCollection "{5222EB4D0A2B466B}" {
ItemDisplayName SCR_InventoryUIInfo "{5222EB4D07D865FA}" {
Name "Combat epinephrine (strong)"
Description "Stronger field injector carried by special troops and medics. Stops bleeding, restores consciousness, boosts dangerously low blood and partially heals injuries. Cannot revive the dead."
m_aItemHints {}
}
}
}
SCR_ConsumableItemComponent "{59E68C74C5CEE74F}" {
m_ConsumableEffect ERR_ConsumableCombatEpinephrine "{59E68C74C1F3910E}" {
m_fItemRegenerationSpeed 0
m_fItemRegenerationDuration 0
m_aDamageEffectsToLoad {
ERR_CombatEpinephrineEffect "{6122D60861B541E7}" {}
}
}
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{792634D9359AED21}Prefabs/ERR/CombatEpinephrine.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS : PC {}
}
}
+7
View File
@@ -0,0 +1,7 @@
GenericEntity {
ID "46B3FAD3A89A4EA3"
components {
ERR_LocationComponent "{606DF66235E34CA9}" { m_Kind 0 }
RplComponent "{B6AB8FE43FE143B4}" { Streamable Disabled }
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{174211DDE2F6422B}Prefabs/ERR/CrashSite.et"
Configurations {
ResourceClass PC {}
ResourceClass HEADLESS : PC {}
}
}
+9
View File
@@ -0,0 +1,9 @@
GenericEntity : "{74DCE1AEC6832E18}Prefabs/Props/Wrecks/UH1H_wreck/UH1H_wreck_static.et" {
ID "BDBAE020F10A41DF"
components {
Hierarchy "{BD920DE465D04435}" {}
RplComponent "{7E4852E4567E49D6}" { Streamable Disabled }
}
coords 0 0 0
angles 0 0 0
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{486DDD0B811047EE}Prefabs/ERR/CrashWreck.et"
Configurations {
ResourceClass PC {}
ResourceClass HEADLESS : PC {}
}
}
@@ -2,6 +2,9 @@ SCR_FactionManager : "{A242612865F2A48E}Prefabs/MP/Managers/Factions/FactionMana
ID "9D8B772E349A5ED5" ID "9D8B772E349A5ED5"
m_bCanChangeFactionsPlayable 1 m_bCanChangeFactionsPlayable 1
Factions { Factions {
SCR_Faction : "{3FA20B01D950D31F}Configs/Factions/CIV.conf" {
m_bIsPlayable 0
}
SCR_Faction : "{8053DB656DD18B14}Configs/Factions/FIA.conf" { SCR_Faction : "{8053DB656DD18B14}Configs/Factions/FIA.conf" {
m_aFriendlyFactionsIds { } m_aFriendlyFactionsIds { }
} }
+1
View File
@@ -11,5 +11,6 @@ ERR_GameMode : "{0F307326459A1395}Prefabs/MP/Modes/GameMode_Base.et" {
m_SpawnLogic ERR_SpawnLogic "{79AD722B2EFD595F}" {} m_SpawnLogic ERR_SpawnLogic "{79AD722B2EFD595F}" {}
} }
} }
m_fAutoReloadTime 120
m_bUseSpawnPreload 1 m_bUseSpawnPreload 1
} }
+8
View File
@@ -91,6 +91,14 @@ GenericEntity {
coords 0 0 -6 coords 0 0 -6
angles 0 0 0 angles 0 0 0
} }
StaticModelEntity : "{CAE1588B6265A2AB}Prefabs/Structures/Walls/BarbedWire/BarbedTape_01/BarbedTape_01_Short_noStake.et" {
ID "F23719AD8B46C015"
components {
Hierarchy "{B86B0198EF40D762}" {}
}
coords 0 2.95 -6
angles 0 90 0
}
DecalEntity : "{4AD6EB80A8B1FA3A}Prefabs/Decals/Dirt/Decal_Dirt_Patch_03.et" { DecalEntity : "{4AD6EB80A8B1FA3A}Prefabs/Decals/Dirt/Decal_Dirt_Patch_03.et" {
ID "FCFBA935CC13E016" ID "FCFBA935CC13E016"
components { components {
+10
View File
@@ -6,4 +6,14 @@ GenericEntity {
RigidBody "{4F20A90EDBD625A2}" { ModelGeometry 1 Static 1 } RigidBody "{4F20A90EDBD625A2}" { ModelGeometry 1 Static 1 }
RplComponent "{4F20A90EDBD625A3}" {} RplComponent "{4F20A90EDBD625A3}" {}
} }
{
StaticModelEntity : "{46904AF5D473FB75}Prefabs/Structures/Walls/BarbedWire/BarbedTape_01/BarbedTape_01_Long_noStake.et" {
ID "A32066F0B56D0EA9"
components {
Hierarchy "{F93ED27DEB1E2490}" {}
}
coords 1.5 2.95 0
angles 0 90 0
}
}
} }
@@ -0,0 +1,27 @@
Vehicle : "{90B3A9E658D47B30}Prefabs/ERR/RescueEscortWest.et" {
ID "90B3A9E658D47B71"
components {
MeshObject "{51DAA09FEFBFC0E7}" {
Materials {
MaterialAssignClass "{6168D22F500C3196}" {
SourceMaterial "UH1H_Text_Decals_Ext_30EC8A12786836F9"
AssignedMaterial "{E244CE4CE64A92D8}Assets/Vehicles/Helicopters/UH1H/Data/UH1H_Text_Decals_Ext_CIV.emat"
}
MaterialAssignClass "{6168D22F500C31B3}" {
SourceMaterial "UH_1H_Body01_6224CA051369DE45"
AssignedMaterial "{FFDE0DE516E4816F}Assets/Vehicles/Helicopters/UH1H/Data/UH_1H_Body01_CIV.emat"
}
}
}
SCR_VehicleFactionAffiliationComponent "{5882CBD9AC741CEC}" {
"faction affiliation" "RHS_ION"
}
SlotManagerComponent "{55BCE45E438E4CFF}" {
Slots {
RegisteringComponentSlotInfo USArmyDecal {
Enabled 0
}
}
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{90B3A9E658D47B70}Prefabs/ERR/RescueEscortION.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS {}
}
}
@@ -0,0 +1,14 @@
Vehicle : "{DDDD9B51F1234DF3}Prefabs/Vehicles/Helicopters/UH1H/UH1H_armed.et" {
ID "90B3A9E658D47B31"
components {
ERR_RescueFlightComponent "{90B3A9E658D47B32}" {
}
VehicleHelicopterSimulation "{51FAEE57F7DA270E}" {
Simulation Helicopter "{51FAED9EF37A2D29}" {
Engine HelicopterEngine "{51FAED9EF37A2D2B}" {
StartUpTime 0
}
}
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{90B3A9E658D47B30}Prefabs/ERR/RescueEscortWest.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS {}
}
}
@@ -0,0 +1,14 @@
Vehicle : "{7BD282AF716ED639}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_armed.et" {
ID "90B3A9E658D47B11"
components {
ERR_RescueFlightComponent "{90B3A9E658D47B12}" {
}
VehicleHelicopterSimulation "{51FAEE57F7DA270E}" {
Simulation Helicopter "{51FAED9EF37A2D29}" {
Engine HelicopterEngine "{51FAED9EF37A2D2B}" {
StartUpTime 0
}
}
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{90B3A9E658D47B10}Prefabs/ERR/RescueHelicopter.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS {}
}
}
@@ -0,0 +1,14 @@
Vehicle : "{5BF04078A1EC66D8}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MTT-A_Black_no_pylons.et" {
ID "90B3A9E658D47B51"
components {
ERR_RescueFlightComponent "{90B3A9E658D47B52}" {
}
VehicleHelicopterSimulation "{51FAEE57F7DA270E}" {
Simulation Helicopter "{51FAED9EF37A2D29}" {
Engine HelicopterEngine "{51FAED9EF37A2D2B}" {
StartUpTime 0
}
}
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{90B3A9E658D47B50}Prefabs/ERR/RescueHelicopterAFRF.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS {}
}
}
@@ -0,0 +1,16 @@
Vehicle : "{90B3A9E658D47B10}Prefabs/ERR/RescueHelicopter.et" {
ID "90B3A9E658D47B41"
components {
MeshObject "{51DAA09FEFBFC0E7}" {
Materials {
MaterialAssignClass "{6168E1F20FDD15C6}" {
SourceMaterial "Flag_decals_01_0418C5226701B4E1"
AssignedMaterial "{35D747E785C4D90F}Assets/Vehicles/Helicopters/Mi8/Data/MI8_Flag_decals_CIV_01.emat"
}
}
}
SCR_VehicleFactionAffiliationComponent "{5882CBD9AC741CEC}" {
"faction affiliation" "FIA"
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{90B3A9E658D47B40}Prefabs/ERR/RescueHelicopterFIA.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS {}
}
}
@@ -0,0 +1,27 @@
Vehicle : "{90B3A9E658D47B20}Prefabs/ERR/RescueHelicopterWest.et" {
ID "90B3A9E658D47B61"
components {
MeshObject "{51DAA09FEFBFC0E7}" {
Materials {
MaterialAssignClass "{6168D22F500C3196}" {
SourceMaterial "UH1H_Text_Decals_Ext_30EC8A12786836F9"
AssignedMaterial "{E244CE4CE64A92D8}Assets/Vehicles/Helicopters/UH1H/Data/UH1H_Text_Decals_Ext_CIV.emat"
}
MaterialAssignClass "{6168D22F500C31B3}" {
SourceMaterial "UH_1H_Body01_6224CA051369DE45"
AssignedMaterial "{FFDE0DE516E4816F}Assets/Vehicles/Helicopters/UH1H/Data/UH_1H_Body01_CIV.emat"
}
}
}
SCR_VehicleFactionAffiliationComponent "{5882CBD9AC741CEC}" {
"faction affiliation" "RHS_ION"
}
SlotManagerComponent "{55BCE45E438E4CFF}" {
Slots {
RegisteringComponentSlotInfo USArmyDecal {
Enabled 0
}
}
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{90B3A9E658D47B60}Prefabs/ERR/RescueHelicopterION.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS {}
}
}
@@ -0,0 +1,24 @@
Vehicle : "{70BAEEFC2D3FEE64}Prefabs/Vehicles/Helicopters/UH1H/UH1H.et" {
ID "90B3A9E658D47B21"
components {
ERR_RescueFlightComponent "{90B3A9E658D47B22}" {
}
VehicleHelicopterSimulation "{51FAEE57F7DA270E}" {
Simulation Helicopter "{51FAED9EF37A2D29}" {
Engine HelicopterEngine "{51FAED9EF37A2D2B}" {
StartUpTime 0
}
}
}
SlotManagerComponent "{55BCE45E438E4CFF}" {
Slots {
RegisteringComponentSlotInfo Seat_Gunners {
Prefab "{67030528FA4FA725}Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_seats_gunners.et"
}
RegisteringComponentSlotInfo Seat_Cargo {
Prefab "{205599C7074F1E02}Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_seats_cargo.et"
}
}
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{90B3A9E658D47B20}Prefabs/ERR/RescueHelicopterWest.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS {}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
SCR_ManualCamera { ERR_ObserverCamera {
ID "634904BCEDA1A285" ID "634904BCEDA1A285"
m_bInputEnabled 1 m_bInputEnabled 1
m_sActionContext "ERR_ObserverContext" m_sActionContext "ERR_ObserverContext"
@@ -0,0 +1,26 @@
ERR_GameMode : "{978E045E53EF36C8}Prefabs/ERR/GameMode.et" {
ID "D12F4DF2625CA3AC"
components {
Hierarchy "{3DC8FB32DE3E1EBD}" {}
ERR_ScenarioComponent "{B78E910E88DE30D2}" {
m_TerrainIndependent 1
}
}
{
PerceptionManager Perception : "{028DAEAD63E056BE}Prefabs/World/Game/PerceptionManager.et" {
ID "1CD3AA95056E2D65"
}
SCR_FactionManager Factions : "{5414041A15DC4B54}Prefabs/ERR/FactionManager.et" {
ID "9FE86AC6892BF013"
}
SCR_LoadoutManager Loadouts : "{97EF4C1A41F63DDB}Prefabs/MP/Managers/Loadouts/LoadoutManager_USxUSSR.et" {
ID "588AD55F7655278B"
}
RadioManagerEntity RadioManager : "{B8E09FAB91C4ECCD}Prefabs/Systems/Radio/RadioManager.et" {
ID "5F87F7A144BDDE95"
}
ScriptedChatEntity Chat : "{F69BC912AC8236F9}Prefabs/MP/ScriptedChatEntity.et" {
ID "85ED17B0836DB4A2"
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{8FB1559AE3F29722}Prefabs/ERR/TerrainConnector.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS : PC {}
}
}
@@ -0,0 +1,20 @@
ERR_TerrainTakeoverMode : "{978E045E53EF36C8}Prefabs/ERR/GameMode.et" {
ID "F39C0AB761E582D4"
components {
Hierarchy "{3DC8FB32DE3E1EBD}" {}
ERR_ScenarioComponent "{B78E910E88DE30D2}" {
m_TerrainIndependent 1
}
}
{
PerceptionManager Perception : "{028DAEAD63E056BE}Prefabs/World/Game/PerceptionManager.et" {
ID "1CD3AA95056E2D65"
}
SCR_FactionManager Factions : "{5414041A15DC4B54}Prefabs/ERR/FactionManager.et" {
ID "9FE86AC6892BF013"
}
SCR_LoadoutManager Loadouts : "{97EF4C1A41F63DDB}Prefabs/MP/Managers/Loadouts/LoadoutManager_USxUSSR.et" {
ID "588AD55F7655278B"
}
}
}
@@ -0,0 +1,4 @@
MetaFileClass {
Name "{9D182739ABEF405C}Prefabs/ERR/TerrainTakeover.et"
Configurations { ResourceClass PC {} ResourceClass HEADLESS : PC {} }
}
@@ -0,0 +1,9 @@
ERR_TerrainTakeoverMode : "{9D182739ABEF405C}Prefabs/ERR/TerrainTakeover.et" {
ID "CE902374A658BDF1"
{
ScriptedChatEntity Chat : "{F69BC912AC8236F9}Prefabs/MP/ScriptedChatEntity.et" {
ID "85ED17B0836DB4A2"
components { RplComponent "{CA456708D69D73BA}" { Streamable Disabled } }
}
}
}
@@ -0,0 +1,4 @@
MetaFileClass {
Name "{DA8E364BC095712F}Prefabs/ERR/TerrainTakeoverWithChat.et"
Configurations { ResourceClass PC {} ResourceClass HEADLESS : PC {} }
}
@@ -0,0 +1,6 @@
GenericEntity {
ID "E83CA269A0D647B1"
components {
MeshObject { Object "{76D738EEB0CA431F}Assets/Items/ERR_Atlas/ERR_AtlasLeaf.xob" InheritVisibility 1 }
}
}
@@ -0,0 +1,4 @@
MetaFileClass {
Name "{385C5228D90F456B}Prefabs/Items/Equipment/ERRM_AtlasLeaf.et"
Configurations { EntityPrefabResourceClass PC {} EntityPrefabResourceClass HEADLESS : PC {} EntityPrefabResourceClass XBOX_ONE : PC {} EntityPrefabResourceClass XBOX_SERIES : PC {} EntityPrefabResourceClass PS4 : PC {} EntityPrefabResourceClass PS5 : PC {} }
}
@@ -4,8 +4,8 @@ GameEntity : "{C95E11C60810F432}Prefabs/Items/Core/Item_Base.et" {
InventoryItemComponent "{5222EB4D0C73006B}" { InventoryItemComponent "{5222EB4D0C73006B}" {
Attributes SCR_ItemAttributeCollection "{5222EB4D0A2B466B}" { Attributes SCR_ItemAttributeCollection "{5222EB4D0A2B466B}" {
ItemDisplayName UIInfo "{7236A6DB18BD574C}" { ItemDisplayName UIInfo "{7236A6DB18BD574C}" {
Name "Shared Field Map" Name "OCRAM OLOP Autoatlas"
Description "A field map whose visible surface can be read by nearby players." Description "Terrain atlas with an overview and numbered detail pages. Arrow keys turn sheets; numpad page number + Enter jumps directly; 0 + Enter returns to the index. Double-tap M opens the interactive map. No player marker."
} }
ItemPhysAttributes ItemPhysicalAttributes PhysicalAttributes { ItemPhysAttributes ItemPhysicalAttributes PhysicalAttributes {
Weight 0.1 Weight 0.1
@@ -19,8 +19,9 @@ GameEntity : "{C95E11C60810F432}Prefabs/Items/Core/Item_Base.et" {
ItemAnimationAttributes ItemAnimationAttributes "{4AC999738D39BA9A}" { ItemAnimationAttributes ItemAnimationAttributes "{4AC999738D39BA9A}" {
AnimationAttachments { AnimationAttachments {
AnimationAttachmentInfo "{1D66E5B861D2F00A}" { AnimationAttachmentInfo "{1D66E5B861D2F00A}" {
AnimGraph "{83675D0B0AB1B2C3}Assets/Items/Equipment/workspaces/paper_map.agr" AnimGraph "{4959464B088CCCA4}Assets/Items/ERR_Atlas/AtlasTurn.agr"
AnimInstance "{56794534F6B8C3D7}Assets/Items/Equipment/workspaces/player_map.asi" AnimInstance "{56794534F6B8C3D7}Assets/Items/Equipment/workspaces/player_map.asi"
StartNode "ERRAtlasControl"
BindingName "Gadget" BindingName "Gadget"
} }
} }
@@ -46,13 +47,7 @@ GameEntity : "{C95E11C60810F432}Prefabs/Items/Core/Item_Base.et" {
} }
} }
MeshObject "{E66421D681D64AB7}" { MeshObject "{E66421D681D64AB7}" {
Object "{E443663E5241622A}Assets/Items/Equipment/Maps/Map_Paper_01/Map_Paper_01_folded.xob" Object "{51ABB9E2E838FC9E}Assets/Items/ERR_Atlas/ERR_Atlas.xob"
Materials {
MaterialAssignClass "{667CE655A40B0E50}" {
SourceMaterial "01__Default"
AssignedMaterial "{03D6DE777C8E411C}Materials/ERRM_MapRenderTarget.emat"
}
}
InheritVisibility 1 InheritVisibility 1
} }
RigidBody "{B29D8B091D657415}" { RigidBody "{B29D8B091D657415}" {
@@ -78,6 +78,15 @@ class ERR_Actors
return position; return position;
} }
// Map altitude is not water depth. Query the terrain surface itself, without
// Ground's 10cm spawn lift, so even shallow water remains unsuitable.
static bool DryGround(vector position)
{
position[1] = GetGame().GetWorld().GetSurfaceY(position[0], position[2]);
if (!ERR_ForceRules.Finite(position[1])) return false;
return !ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), position);
}
static void Players(out array<IEntity> players, bool conscious = false) static void Players(out array<IEntity> players, bool conscious = false)
{ {
players.Clear(); players.Clear();
@@ -3,60 +3,6 @@ modded enum ChimeraMenuPreset
ERR_AdminPanel ERR_AdminPanel
} }
// Presentation rules only: all mutations still use the authenticated command service.
class ERR_AdminMenuFields
{
static array<string> Keys()
{
return {"radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount", "antiAirChancePercent", "antiPersonnelMineChancePercent"};
}
static bool Available(string key, bool hq, bool intel)
{
if (key == "aiIntelChancePercent") return intel;
if (key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent" || key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres") return hq;
return Keys().Contains(key);
}
// A short journal has no older off-screen lines, even if its slider is zero.
static bool FollowJournal(bool empty, float slider, float height, float viewHeight, bool latest)
{
return latest || empty || slider >= 0.98 || height <= viewHeight;
}
// TextWidget interprets hash-prefixed tokens as localization IDs. Keep the
// server audit and retained entries intact; render their numeric IDs literally.
static string JournalText(string line, bool details = false)
{
return ERR_JournalText.Menu(line, details);
}
static string Display(string key, string value)
{
if (value == "-1" || value == "-2") return ERR_Settings.SelectionText(value.ToInt());
if (key == "startTimeMinutes") return ERR_TimeService.Clock(value.ToInt());
if (key == "startDateCode") return ERR_EnvironmentService.Date(value.ToInt());
if (key == "weatherProfile") return ERR_EnvironmentService.ProfileName(value.ToInt());
return value;
}
static string Command(string key, string value)
{
string prefix;
if (key == "radioDefenceSeconds") prefix = "mission radio";
else if (key == "aiIntelChancePercent") prefix = "intel carrying";
else if (key == "intelRevealChancePercent") prefix = "intel reveal";
else if (key == "startTimeMinutes") prefix = "mission time";
else if (key == "timeMultiplier") prefix = "mission speed";
else if (key == "startDateCode") prefix = "mission date";
else if (key == "weatherProfile") prefix = "mission weather";
else if (key == "reportIntervalSeconds") prefix = "hq interval";
else if (key == "supportSquadLimit") prefix = "hq squads";
else if (key == "responseRadiusMetres") prefix = "hq radius";
else if (key == "antiAirChancePercent") prefix = "hq aa";
else if (key == "antiPersonnelMineChancePercent") prefix = "hq mines";
else if (key == "convoyCount") prefix = "hq convoy count";
value = value.Trim();
if (prefix.IsEmpty() || value.IsEmpty() || value.Length() > 32) return "";
return prefix + " set " + value;
}
}
// Own mission dialog. ChimeraMenuBase owns focus, cursor and input-context lifetime. // Own mission dialog. ChimeraMenuBase owns focus, cursor and input-context lifetime.
class ERR_AdminMenu : ChimeraMenuBase class ERR_AdminMenu : ChimeraMenuBase
{ {
@@ -100,12 +46,15 @@ class ERR_AdminMenu : ChimeraMenuBase
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController()); m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
m_Mode = ERR_GameMode.Cast(GetGame().GetGameMode()); m_Mode = ERR_GameMode.Cast(GetGame().GetGameMode());
if (!m_Controller) { Close(); return; } if (!m_Controller) { Close(); return; }
array<string> names = {"Close", "OpenSettings", "OpenWorld", "OpenFactions", "OpenHQ", "OpenJournal", "PauseJournal", "JournalDetails", "LatestJournal", "KeepWeaponOn", "KeepWeaponOff", "IntelSummary", "RerollIntel", "OpenMods", "OpenIntel", "OpenStatus"}; array<string> names = {"TelemetryOn", "TelemetryOff","SitesOn", "SitesOff", "PlaneOn", "PlaneOff", "Close", "PauseJournal", "JournalDetails", "LatestJournal", "KeepWeaponOn", "KeepWeaponOff", "SelfEpiOn", "SelfEpiOff", "IntelSummary", "RerollIntel", "RandomDay_startTimeMinutes", "Default_viewDistanceMetres", "RestartEmptyOn", "RestartEmptyOff", "RecommendedPlane", "RecommendedWeapons", "RecommendedDrones"};
names.Insert("RandomDay_startTimeMinutes"); foreach (string mainButton : ERR_AdminMenuPages.MainButtons()) names.Insert(mainButton);
foreach (string pageButton : Navigation()) names.Insert(pageButton);
for (int density = 1; density <= 4; density++) names.Insert("GuardEvery_" + density.ToString());
for (int policy = 0; policy <= 2; policy++) names.Insert("GuardHQ_" + policy.ToString());
foreach (string key : ERR_AdminMenuFields.Keys()) foreach (string key : ERR_AdminMenuFields.Keys())
{ {
names.Insert("Apply_" + key); names.Insert("Apply_" + key);
names.Insert("Random_" + key); if (ERR_Settings.Randomizable(key)) names.Insert("Random_" + key);
} }
foreach (string name : names) foreach (string name : names)
{ {
@@ -123,7 +72,7 @@ class ERR_AdminMenu : ChimeraMenuBase
m_Ready = false; m_Age = 0; m_Poll = 0; m_Ready = false; m_Age = 0; m_Poll = 0;
GetGame().GetInputManager().AddActionListener(UIConstants.MENU_ACTION_BACK, EActionTrigger.PRESSED, OnBack); GetGame().GetInputManager().AddActionListener(UIConstants.MENU_ACTION_BACK, EActionTrigger.PRESSED, OnBack);
m_Bound = true; m_Bound = true;
ShowPage(0); ShowPage(ERR_AdminPage.MISSION);
UpdateActions(); UpdateActions();
RequestSnapshot(); RequestSnapshot();
} }
@@ -150,7 +99,7 @@ class ERR_AdminMenu : ChimeraMenuBase
UpdateActions(); UpdateActions();
} }
if (IsFocused() && m_Poll >= 2) RequestSnapshot(); if (IsFocused() && m_Poll >= 2) RequestSnapshot();
if (m_Page != 2) return; if (m_Page != ERR_AdminPage.JOURNAL) return;
m_LogPoll += tDelta; m_LogPoll += tDelta;
if (m_LogPending && m_LogPoll > 5) m_LogPending = false; if (m_LogPending && m_LogPoll > 5) m_LogPending = false;
if (IsFocused() && m_Ready && m_HQ && !m_LogPaused && !m_LogPending && m_LogPoll >= 1) if (IsFocused() && m_Ready && m_HQ && !m_LogPaused && !m_LogPending && m_LogPoll >= 1)
@@ -186,7 +135,7 @@ class ERR_AdminMenu : ChimeraMenuBase
SCR_EditBoxComponent edit = SCR_EditBoxComponent.GetEditBoxComponent(key, GetRootWidget()); SCR_EditBoxComponent edit = SCR_EditBoxComponent.GetEditBoxComponent(key, GetRootWidget());
if (edit && edit.IsInWriteMode()) return; if (edit && edit.IsInWriteMode()) return;
} }
if (m_Page != 0) ShowPage(0); if (m_Page != ERR_AdminPage.MISSION) ShowPage(ERR_AdminPage.MISSION);
else Close(); else Close();
} }
protected void SetText(string name, string value) protected void SetText(string name, string value)
@@ -194,43 +143,58 @@ class ERR_AdminMenu : ChimeraMenuBase
TextWidget widget = TextWidget.Cast(GetRootWidget().FindAnyWidget(name)); TextWidget widget = TextWidget.Cast(GetRootWidget().FindAnyWidget(name));
if (widget) widget.SetText(value); if (widget) widget.SetText(value);
} }
// Stable page order is also used by the shared navigation bar and native probe. // Stable IDs are generated from the navigation manifest; extensions use named pages.
static array<string> Pages() { return {"SettingsPage", "HQPage", "JournalPage", "ModsPage", "FactionsPage", "WorldPage", "IntelPage", "StatusPage"}; } static array<string> Pages() { return ERR_AdminMenuPages.Pages(); }
static array<string> Navigation() { return {"OpenSettings", "OpenHQ", "OpenJournal", "OpenMods", "OpenFactions", "OpenWorld", "OpenIntel", "OpenStatus"}; } static array<string> Navigation() { return ERR_AdminMenuPages.Navigation(); }
protected void ShowPage(int page) protected void ShowPage(int page)
{ {
array<string> pages = Pages(); if (!ERR_AdminMenuView.ShowPage(GetRootWidget(), page)) return;
array<string> navigation = Navigation();
if (page < 0 || page >= pages.Count()) return;
m_Page = page; m_Page = page;
for (int i = 0; i < pages.Count(); i++) if (page == ERR_AdminPage.JOURNAL) m_LogPoll = 1;
{
GetRootWidget().FindAnyWidget(pages[i]).SetVisible(i == page);
GetRootWidget().FindAnyWidget("Selected_" + navigation[i]).SetVisible(i == page);
}
if (page == 2) m_LogPoll = 1;
} }
protected int m_KeepWeaponState = -1;
protected int m_SelfEpiState = -1;
protected int m_SitesState = -1;
protected int m_RestartEmptyState = -1;
protected int m_TelemetryState = -1;
protected void UpdateActions() protected void UpdateActions()
{ {
ERR_SelectionStyle.Pair(GetRootWidget(), "TelemetryOn", "TelemetryOff", m_TelemetryState);
ERR_SelectionStyle.Pair(GetRootWidget(), "SitesOn", "SitesOff", m_SitesState);
ERR_SelectionStyle.Pair(GetRootWidget(), "SelfEpiOn", "SelfEpiOff", m_SelfEpiState);
ERR_SelectionStyle.Pair(GetRootWidget(), "KeepWeaponOn", "KeepWeaponOff", m_KeepWeaponState);
ERR_SelectionStyle.Pair(GetRootWidget(), "RestartEmptyOn", "RestartEmptyOff", m_RestartEmptyState);
foreach (SCR_ButtonBaseComponent button : m_Buttons) foreach (SCR_ButtonBaseComponent button : m_Buttons)
{ {
string name = button.GetRootWidget().GetName(); string name = button.GetRootWidget().GetName();
if (name.IndexOf("Apply_") == 0 || name.IndexOf("Random_") == 0 || name.IndexOf("RandomDay_") == 0) if (name.IndexOf("Apply_") == 0 || name.IndexOf("Random_") == 0 || name.IndexOf("RandomDay_") == 0 || name.IndexOf("Default_") == 0)
{ {
int prefix = name.IndexOf("_") + 1; int prefix = name.IndexOf("_") + 1;
string key = name.Substring(prefix, name.Length() - prefix); string key = name.Substring(prefix, name.Length() - prefix);
button.SetEnabled(m_Ready && m_AvailableFields.Contains(key) && ERR_AdminMenuFields.Available(key, m_HQ, m_Intel)); button.SetEnabled(m_Ready && m_AvailableFields.Contains(key) && ERR_AdminMenuFields.Available(key, m_HQ, m_Intel));
} }
else if (name == "PlaneOn" || name == "PlaneOff") button.SetEnabled(m_Ready && m_HQ && m_AvailableFields.Contains("planeReconEnabled"));
else if (name.IndexOf("GuardEvery_") == 0) button.SetEnabled(m_Ready && m_HQ && m_AvailableFields.Contains("objectGuardEvery"));
else if (name.IndexOf("GuardHQ_") == 0) button.SetEnabled(m_Ready && m_HQ && m_AvailableFields.Contains("objectGuardHQMode"));
else if (name == "RerollIntel" || name == "IntelSummary") button.SetEnabled(m_Ready && m_Intel); else if (name == "RerollIntel" || name == "IntelSummary") button.SetEnabled(m_Ready && m_Intel);
else if (name == "LatestJournal") button.SetEnabled(m_Ready && m_HQ); else if (name == "LatestJournal") button.SetEnabled(m_Ready && m_HQ);
else if (name == "KeepWeaponOn" || name == "KeepWeaponOff") button.SetEnabled(m_Ready); else if (name == "TelemetryOn") button.SetEnabled(m_Ready && m_TelemetryState == 0);
else if (name == "TelemetryOff") button.SetEnabled(m_Ready && m_TelemetryState == 1);
else if (name == "RestartEmptyOn") button.SetEnabled(m_Ready && m_RestartEmptyState == 0);
else if (name == "RestartEmptyOff") button.SetEnabled(m_Ready && m_RestartEmptyState == 1);
else if (name == "SitesOn") button.SetEnabled(m_Ready && m_SitesState == 0);
else if (name == "SitesOff") button.SetEnabled(m_Ready && m_SitesState == 1);
else if (name == "SelfEpiOn") button.SetEnabled(m_Ready && m_SelfEpiState == 0);
else if (name == "SelfEpiOff") button.SetEnabled(m_Ready && m_SelfEpiState == 1);
else if (name == "KeepWeaponOn") button.SetEnabled(m_Ready && m_KeepWeaponState == 0);
else if (name == "KeepWeaponOff") button.SetEnabled(m_Ready && m_KeepWeaponState == 1);
} }
} }
void ReceiveSnapshot(bool authorized, array<string> values, array<string> status) void ReceiveSnapshot(bool authorized, array<string> values, array<string> status)
{ {
if (!authorized) { Close(); return; } if (!authorized) { Close(); return; }
if (!values) return; if (!values) return;
m_HQ = false; m_Intel = false; m_AvailableFields.Clear(); m_HQ = false; m_Intel = false; m_AvailableFields.Clear(); m_KeepWeaponState = -1; m_SelfEpiState = -1; m_RestartEmptyState = -1; m_TelemetryState = -1;
foreach (string entry : values) foreach (string entry : values)
{ {
if (entry == "supports=hq") { m_HQ = true; continue; } if (entry == "supports=hq") { m_HQ = true; continue; }
@@ -238,23 +202,87 @@ class ERR_AdminMenu : ChimeraMenuBase
int divider = entry.IndexOf("="); int divider = entry.IndexOf("=");
if (divider <= 0) continue; if (divider <= 0) continue;
string key = entry.Substring(0, divider); string key = entry.Substring(0, divider);
if (key == "radioIntelProgress") { SetText("RadioIntelProgress", entry.Substring(divider + 1, entry.Length() - divider - 1)); continue; }
if (key == "droneRuntime") { SetText("DroneRuntime", entry.Substring(divider + 1, entry.Length() - divider - 1)); continue; }
if (key == "recommendedBikeRuntime") { SetText("RecommendedBikeRuntime", entry.Substring(divider + 1, entry.Length() - divider - 1)); continue; }
if (key.IndexOf("recommended_") == 0)
{
SetText(key, entry.Substring(divider + 1, entry.Length() - divider - 1)); continue;
}
if (key.IndexOf("recommendedReady_") == 0)
{
string addonKey = key.Substring(17, key.Length() - 17);
Widget state = GetRootWidget().FindAnyWidget("recommended_" + addonKey);
if (state)
{
if (entry.Substring(divider + 1, entry.Length() - divider - 1) == "1") state.SetColor(Color.FromInt(ERR_MenuTheme.READY_TEXT));
else state.SetColor(Color.FromInt(ERR_MenuTheme.UNAVAILABLE_TEXT));
}
continue;
}
if (key.IndexOf("resolved_") == 0) if (key.IndexOf("resolved_") == 0)
{ {
string resolvedKey = key.Substring(9, key.Length() - 9); string resolvedKey = key.Substring(9, key.Length() - 9);
if (ERR_AdminMenuFields.Keys().Contains(resolvedKey)) SetText("Saved_" + resolvedKey, "Saved: Random (using " + entry.Substring(divider + 1, entry.Length() - divider - 1) + ")"); if (ERR_AdminMenuFields.Keys().Contains(resolvedKey)) SetText("Saved_" + resolvedKey, "Saved: Random (using " + entry.Substring(divider + 1, entry.Length() - divider - 1) + ")");
continue; continue;
} }
if (key == "planeReconEnabled")
{
int planeState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
ERR_SelectionStyle.Pair(GetRootWidget(), "PlaneOn", "PlaneOff", planeState);
m_AvailableFields.Insert(key); continue;
}
if (key == "telemetryStatus") { SetText("TelemetryStatus", entry.Substring(divider + 1, entry.Length() - divider - 1)); continue; }
if (key == "telemetryEnabled")
{
m_TelemetryState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
string telemetryState = "Disabled";
if (m_TelemetryState == 1) telemetryState = "Enabled";
SetText("Saved_telemetryEnabled", "Saved: " + telemetryState);
continue;
}
if (key == "restartOnEmpty")
{
m_RestartEmptyState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
string restartState = "Disabled";
if (m_RestartEmptyState == 1) restartState = "Enabled";
SetText("Saved_restartOnEmpty", "Saved: " + restartState);
continue;
}
if (key == "showAllSites")
{
m_SitesState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
continue;
}
if (key == "selfEpinephrine")
{
m_SelfEpiState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
continue;
}
if (key == "keepWeaponOnUnconscious") if (key == "keepWeaponOnUnconscious")
{ {
m_KeepWeaponState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
string retention = "Disabled: held guns drop on unconsciousness."; string retention = "Disabled: held guns drop on unconsciousness.";
if (entry.Substring(divider + 1, entry.Length() - divider - 1) == "1") retention = "Enabled: retain your gun and equip it after revival."; if (entry.Substring(divider + 1, entry.Length() - divider - 1) == "1") retention = "Enabled: retain your gun and equip it after revival.";
SetText("Saved_keepWeaponOnUnconscious", retention); SetText("Saved_keepWeaponOnUnconscious", retention);
continue; continue;
} }
if (!ERR_AdminMenuFields.Keys().Contains(key)) continue; if (!ERR_AdminMenuFields.Keys().Contains(key)) continue;
string value = ERR_AdminMenuFields.Display(key, entry.Substring(divider + 1, entry.Length() - divider - 1)); string saved = entry.Substring(divider + 1, entry.Length() - divider - 1);
if (ERR_Settings.Randomizable(key)) ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText("Random_" + key, GetRootWidget()), saved == "-1");
if (key == "startTimeMinutes") ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText("RandomDay_startTimeMinutes", GetRootWidget()), saved == "-2");
if (key == "viewDistanceMetres") ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText("Default_viewDistanceMetres", GetRootWidget()), saved == "0");
if (key == "objectGuardEvery")
for (int density = 1; density <= 4; density++)
ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText("GuardEvery_" + density.ToString(), GetRootWidget()), saved.ToInt() == density);
if (key == "objectGuardHQMode")
for (int policy = 0; policy <= 2; policy++)
ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText("GuardHQ_" + policy.ToString(), GetRootWidget()), saved.ToInt() == policy);
string value = ERR_AdminMenuFields.Display(key, saved);
m_AvailableFields.Insert(key); m_AvailableFields.Insert(key);
SetText("Saved_" + key, "Saved: " + value); if (key == "startMode") SetText("Saved_" + key, ERR_AdminMenuFields.StartSummary(entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt(), m_Mode.StartMode()));
else if (key == "aiDoctrine" || key == "aiShadow") SetText("Saved_" + key, "Next round: " + value);
else SetText("Saved_" + key, "Saved: " + value);
// Existing fields retain all draft text, including a temporarily empty edit. // Existing fields retain all draft text, including a temporarily empty edit.
if (!m_InitializedFields.Contains(key)) if (!m_InitializedFields.Contains(key))
{ {
@@ -272,7 +300,7 @@ class ERR_AdminMenu : ChimeraMenuBase
} }
if (summary.IsEmpty()) summary = "No mission status is available yet."; if (summary.IsEmpty()) summary = "No mission status is available yet.";
SetText("Status", summary); SetText("Status", summary);
string connection = "Connected to server. Editing a value does not save it: press Apply. Unsaved edits are lost on close."; string connection = "Connected to server. Use Apply for edited values. Selection buttons save immediately. Unsaved edits are lost on close.";
if (!m_HQ || !m_Intel) connection = "Connected. Controls for companion systems absent from this mission are disabled."; if (!m_HQ || !m_Intel) connection = "Connected. Controls for companion systems absent from this mission are disabled.";
SetText("Connection", connection); SetText("Connection", connection);
m_Ready = true; m_Age = 0; UpdateActions(); m_Ready = true; m_Age = 0; UpdateActions();
@@ -324,6 +352,11 @@ class ERR_AdminMenu : ChimeraMenuBase
if (!button || !IsFocused()) return; if (!button || !IsFocused()) return;
string name = button.GetRootWidget().GetName(); string name = button.GetRootWidget().GetName();
if (name == "Close") { Close(); return; } if (name == "Close") { Close(); return; }
int section = ERR_AdminMenuPages.MainButtons().Find(name);
if (section >= 0) { ShowPage(ERR_AdminMenuPages.MainPage(section)); return; }
if (name == "RecommendedDrones") { ShowPage(ERR_AdminPage.DRONES); return; }
if (name == "RecommendedPlane") { ShowPage(ERR_AdminPage.PLANE); return; }
if (name == "RecommendedWeapons") { ShowPage(ERR_AdminPage.ITEMS); return; }
int page = Navigation().Find(name); int page = Navigation().Find(name);
if (page >= 0) { ShowPage(page); return; } if (page >= 0) { ShowPage(page); return; }
if (name == "JournalDetails") if (name == "JournalDetails")
@@ -354,6 +387,51 @@ class ERR_AdminMenu : ChimeraMenuBase
return; return;
} }
if (!m_Controller || !m_Ready || m_Age > 6 || m_ClickDelay > 0) return; if (!m_Controller || !m_Ready || m_Age > 6 || m_ClickDelay > 0) return;
if (name.IndexOf("GuardEvery_") == 0 || name.IndexOf("GuardHQ_") == 0)
{
string guardKey = "objectGuardEvery";
if (name.IndexOf("GuardHQ_") == 0) guardKey = "objectGuardHQMode";
if (!m_HQ || !m_AvailableFields.Contains(guardKey)) return;
int guardPrefix = name.IndexOf("_") + 1;
SCR_EditBoxComponent guardEdit = SCR_EditBoxComponent.GetEditBoxComponent(guardKey, GetRootWidget());
if (guardEdit) guardEdit.SetValue(name.Substring(guardPrefix, name.Length() - guardPrefix));
SetText("Feedback", "Choice selected. Press Apply to save for next round. Green marks the saved choice.");
return;
}
if (name == "PlaneOn" || name == "PlaneOff")
{
if (!m_HQ || !m_AvailableFields.Contains("planeReconEnabled")) return;
string enabled = "0"; if (name == "PlaneOn") enabled = "1";
m_ClickDelay = 0.3; SetText("Feedback", "Saving reconnaissance plane selection for next round...");
m_Controller.ERR_AdminSubmit("mission plane set " + enabled, true); return;
}
if (name == "TelemetryOn" || name == "TelemetryOff")
{
if (m_TelemetryState < 0) return;
string enabled = "0"; if (name == "TelemetryOn") enabled = "1";
m_Controller.ERR_AdminSubmit("mission telemetry set " + enabled, true); return;
}
if (name == "RestartEmptyOn" || name == "RestartEmptyOff")
{
if (m_RestartEmptyState < 0) return;
string restartEnabled = "0"; if (name == "RestartEmptyOn") restartEnabled = "1";
m_ClickDelay = 0.3; SetText("Feedback", "Saving restart after last disconnect...");
m_Controller.ERR_AdminSubmit("mission restartempty set " + restartEnabled, true); return;
}
if (name == "SitesOn" || name == "SitesOff")
{
if (m_SitesState < 0) return;
string sitesEnabled = "0"; if (name == "SitesOn") sitesEnabled = "1";
m_ClickDelay = 0.3; SetText("Feedback", "Saving site map visibility...");
m_Controller.ERR_AdminSubmit("intel sites set " + sitesEnabled, true); return;
}
if (name == "SelfEpiOn" || name == "SelfEpiOff")
{
if (m_SelfEpiState < 0) return;
m_ClickDelay = 0.3; SetText("Feedback", "Saving self epinephrine permission...");
string selfEpi = "0"; if (name == "SelfEpiOn") selfEpi = "1";
m_Controller.ERR_AdminSubmit("mission selfepi set " + selfEpi, true); return;
}
if (name == "KeepWeaponOn" || name == "KeepWeaponOff") if (name == "KeepWeaponOn" || name == "KeepWeaponOff")
{ {
string retain = "0"; if (name == "KeepWeaponOn") retain = "1"; string retain = "0"; if (name == "KeepWeaponOn") retain = "1";
@@ -370,7 +448,7 @@ class ERR_AdminMenu : ChimeraMenuBase
if (pause) pause.SetText("Pause live"); if (pause) pause.SetText("Pause live");
return; return;
} }
if (name.IndexOf("Random_") == 0 || name.IndexOf("RandomDay_") == 0) if (name.IndexOf("Random_") == 0 || name.IndexOf("RandomDay_") == 0 || name == "Default_viewDistanceMetres")
{ {
int prefix = name.IndexOf("_") + 1; int prefix = name.IndexOf("_") + 1;
string key = name.Substring(prefix, name.Length() - prefix); string key = name.Substring(prefix, name.Length() - prefix);
@@ -379,6 +457,7 @@ class ERR_AdminMenu : ChimeraMenuBase
if (edit) if (edit)
{ {
string choice = "Random"; string choice = "Random";
if (name == "Default_viewDistanceMetres") choice = "default";
if (name.IndexOf("RandomDay_") == 0) choice = "Random day"; if (name.IndexOf("RandomDay_") == 0) choice = "Random day";
edit.SetValue(choice); edit.SetValue(choice);
SetText("Feedback", choice + " selected. Press Apply to save."); SetText("Feedback", choice + " selected. Press Apply to save.");
@@ -0,0 +1,95 @@
// Presentation rules only: all mutations still use the authenticated command service.
class ERR_AdminMenuFields
{
static array<string> Keys()
{
return {"aiDoctrine", "aiShadow", "automaticEquipmentChancePercent", "sarkaAngerPerCar", "motorbikeCount", "droneReconCount", "droneCooldownSeconds", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "largeRoadblockCount", "mainRoadWidthMetres", "civilianTrafficCount", "garageVehicleCount", "garageChancePercent", "garageRadiusMetres", "garageSpawnIntervalSeconds", "emptyRestartHours", "startMode", "radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "radioIntelSiteCount", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount", "antiAirChancePercent", "antiPersonnelMineChancePercent", "heliTimerMinutes"};
}
static bool Available(string key, bool hq, bool intel)
{
if (key == "aiDoctrine" || key == "aiShadow") return hq;
if (key == "sarkaAngerPerCar" || key == "droneReconCount" || key == "droneCooldownSeconds") return hq;
if (key == "aiIntelChancePercent") return intel;
if (key == "automaticEquipmentChancePercent" || key == "motorbikeCount" || key == "planeAltitudeMetres" || key == "planeReplacementSeconds" || key == "planeSectorSeconds" || key == "planeReconEnabled" || key == "objectGuardCount" || key == "objectGuardEvery" || key == "objectGuardHQMode" || key == "hunterSpawnDistanceMetres" || key == "heliTimerMinutes" || key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent" || key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres") return hq;
return Keys().Contains(key);
}
// A short journal has no older off-screen lines, even if its slider is zero.
static bool FollowJournal(bool empty, float slider, float height, float viewHeight, bool latest)
{
return latest || empty || slider >= 0.98 || height <= viewHeight;
}
// TextWidget interprets hash-prefixed tokens as localization IDs. Keep the
// server audit and retained entries intact; render their numeric IDs literally.
static string JournalText(string line, bool details = false)
{
return ERR_JournalText.Menu(line, details);
}
static string Display(string key, string value)
{
if (key == "aiDoctrine") { if (value == "1") return "Coordinated"; return "Current"; }
if (key == "aiShadow") { if (value == "1") return "On"; return "Off"; }
if (value == "-1" || value == "-2") return ERR_Settings.SelectionText(value.ToInt());
if (key == "startMode") { if (value == "1") return "Crash"; return "Prison"; }
if (key == "startTimeMinutes") return ERR_TimeService.Clock(value.ToInt());
if (key == "startDateCode") return ERR_EnvironmentService.Date(value.ToInt());
if (key == "weatherProfile") return ERR_EnvironmentService.ProfileName(value.ToInt());
if (key == "viewDistanceMetres" && value == "0") return "default";
return value;
}
static string StartSummary(int saved, int current)
{
return "Next start: " + Display("startMode", saved.ToString()) + " | Current round: " + Display("startMode", current.ToString());
}
static string Command(string key, string value)
{
string prefix;
if (key == "aiDoctrine") prefix = "mission doctrine";
else if (key == "aiShadow") prefix = "mission aishadow";
else if (key == "automaticEquipmentChancePercent") prefix = "hq equipment";
else if (key == "sarkaAngerPerCar") prefix = "mission sarkapoints";
else if (key == "motorbikeCount") prefix = "mission motorbikes";
else if (key == "droneReconCount") prefix = "mission drones";
else if (key == "droneCooldownSeconds") prefix = "mission dronecooldown";
else if (key == "planeReconEnabled") prefix = "mission plane";
else if (key == "planeAltitudeMetres") prefix = "mission planealtitude";
else if (key == "planeReplacementSeconds") prefix = "mission planereplacement";
else if (key == "planeSectorSeconds") prefix = "mission planesector";
else if (key == "startWeaponCount") prefix = "mission startweapons";
else if (key == "startMagazineCount") prefix = "mission startammo";
else if (key == "objectGuardEvery") prefix = "mission objectevery";
else if (key == "objectGuardHQMode") prefix = "mission objecthq";
else if (key == "objectGuardCount") prefix = "mission objectguards";
else if (key == "hunterSpawnDistanceMetres") prefix = "mission hunterdistance";
else if (key == "fieldCampCount") prefix = "mission camps";
else if (key == "fieldHospitalCount") prefix = "mission hospitals";
else if (key == "roadblockCount") prefix = "mission roadblocks";
else if (key == "largeRoadblockCount") prefix = "mission largeroadblocks";
else if (key == "mainRoadWidthMetres") prefix = "mission mainroadwidth";
else if (key == "emptyRestartHours") prefix = "mission emptyhours";
else if (key == "garageChancePercent") prefix = "mission garagechance";
else if (key == "garageRadiusMetres") prefix = "mission garageradius";
else if (key == "garageSpawnIntervalSeconds") prefix = "mission garageinterval";
else if (key == "garageVehicleCount") prefix = "mission garagevehicles";
else if (key == "civilianTrafficCount") prefix = "mission traffic";
else if (key == "startMode") prefix = "mission start";
else if (key == "radioDefenceSeconds") prefix = "mission radio";
else if (key == "aiIntelChancePercent") prefix = "intel carrying";
else if (key == "intelRevealChancePercent") prefix = "intel reveal";
else if (key == "radioIntelSiteCount") prefix = "intel radiosites";
else if (key == "startTimeMinutes") prefix = "mission time";
else if (key == "timeMultiplier") prefix = "mission speed";
else if (key == "startDateCode") prefix = "mission date";
else if (key == "weatherProfile") prefix = "mission weather";
else if (key == "viewDistanceMetres") prefix = "mission viewdistance";
else if (key == "reportIntervalSeconds") prefix = "hq interval";
else if (key == "supportSquadLimit") prefix = "hq squads";
else if (key == "responseRadiusMetres") prefix = "hq radius";
else if (key == "antiAirChancePercent") prefix = "hq aa";
else if (key == "antiPersonnelMineChancePercent") prefix = "hq mines";
else if (key == "heliTimerMinutes") prefix = "hq heli timer";
else if (key == "convoyCount") prefix = "hq convoy count";
value = value.Trim();
if (prefix.IsEmpty() || value.IsEmpty() || value.Length() > 32) return "";
return prefix + " set " + value;
}
}
@@ -0,0 +1,51 @@
// Generated by tools/build_admin_menu.py; edit ui/admin_menu/navigation.json.
enum ERR_AdminPage
{
MISSION = 0,
HQ = 1,
JOURNAL = 2,
MODS = 3,
FACTIONS = 4,
WORLD = 5,
INTEL = 6,
STATUS = 7,
TERRAINS = 8,
ITEMS = 9,
SPEECH = 10,
FORCES = 11,
GUARDS = 12,
SITES = 13,
PLANE = 14,
RECOMMENDATIONS = 15,
WEIGHTS = 16,
ALLOWED_ITEMS = 17,
ITEM_PRESETS = 18,
AI = 19,
STARTER = 20,
SARKA = 21,
DRONES = 22,
RADIO_INTEL = 23,
TELEMETRY = 24,
VEHICLES = 25,
CHECKPOINTS = 26,
}
class ERR_AdminMenuPages
{
static array<string> Pages() { return {"SettingsPage", "HQPage", "JournalPage", "ModsPage", "FactionsPage", "WorldPage", "IntelPage", "StatusPage", "TerrainsPage", "ItemsPage", "TTSPage", "ForcesPage", "GuardsPage", "SitesPage", "PlanePage", "RecommendationsPage", "WeightsPage", "AllowedItemsPage", "ItemPresetsPage", "AIPage", "StarterPage", "SarkaPage", "DronesPage", "RadioIntelPage", "TelemetryPage", "VehiclesPage", "CheckpointsPage"}; }
static array<string> Navigation() { return {"OpenSettings", "OpenHQ", "OpenJournal", "OpenMods", "OpenFactions", "OpenWorld", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems", "OpenTTS", "OpenForces", "OpenGuards", "OpenSites", "OpenPlane", "OpenRecommendations", "OpenWeights", "OpenAllowedItems", "OpenItemPresets", "OpenAI", "OpenStarter", "OpenSarka", "OpenDrones", "OpenRadioIntel", "OpenTelemetry", "OpenVehicles", "OpenCheckpoints"}; }
static array<string> MainButtons() { return {"Main_Mission", "Main_Forces", "Main_HQ", "Main_Intel", "Main_Items", "Main_Mods", "Main_Audio", "Main_Status"}; }
static array<string> Sidebars() { return {"Sidebar_Mission", "Sidebar_Forces", "Sidebar_HQ", "Sidebar_Intel", "Sidebar_Items", "Sidebar_Mods", "Sidebar_Audio", "Sidebar_Status"}; }
static int Group(int page)
{
array<int> groups = {0, 2, 7, 5, 1, 0, 3, 7, 0, 4, 6, 1, 1, 1, 2, 5, 4, 4, 4, 2, 4, 1, 2, 3, 0, 0, 1};
if (page < 0 || page >= groups.Count()) return -1;
return groups[page];
}
static int MainPage(int group)
{
array<int> pages = {ERR_AdminPage.MISSION, ERR_AdminPage.FORCES, ERR_AdminPage.HQ, ERR_AdminPage.INTEL, ERR_AdminPage.ITEMS, ERR_AdminPage.MODS, ERR_AdminPage.SPEECH, ERR_AdminPage.STATUS};
if (group < 0 || group >= pages.Count()) return -1;
return pages[group];
}
}
@@ -0,0 +1,26 @@
// Widget presentation only. No settings writes, authorization or transport here.
class ERR_AdminMenuView
{
static bool ShowPage(Widget root, int page)
{
int group = ERR_AdminMenuPages.Group(page);
if (!root || group < 0) return false;
array<string> pages = ERR_AdminMenuPages.Pages();
array<string> navigation = ERR_AdminMenuPages.Navigation();
for (int i = 0; i < pages.Count(); i++)
{
root.FindAnyWidget(pages[i]).SetVisible(i == page);
root.FindAnyWidget("Selected_" + navigation[i]).SetVisible(i == page);
ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText(navigation[i], root), i == page);
}
array<string> buttons = ERR_AdminMenuPages.MainButtons();
array<string> sidebars = ERR_AdminMenuPages.Sidebars();
for (int section = 0; section < buttons.Count(); section++)
{
root.FindAnyWidget(sidebars[section]).SetVisible(section == group);
root.FindAnyWidget("Selected_" + buttons[section]).SetVisible(section == group);
ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText(buttons[section], root), section == group);
}
return true;
}
}
@@ -4,18 +4,58 @@ class ERR_CommandArguments
{ {
static string CoreSetting(string section, string option) static string CoreSetting(string section, string option)
{ {
if (section == "mission" && option == "telemetry") return "telemetryEnabled";
if (section == "mission" && option == "doctrine") return "aiDoctrine";
if (section == "mission" && option == "aishadow") return "aiShadow";
if (section == "mission" && option == "sarkapoints") return "sarkaAngerPerCar";
if (section == "mission" && option == "motorbikes") return "motorbikeCount";
if (section == "mission" && option == "planealtitude") return "planeAltitudeMetres";
if (section == "mission" && option == "planereplacement") return "planeReplacementSeconds";
if (section == "mission" && option == "planesector") return "planeSectorSeconds";
if (section == "mission" && option == "drones") return "droneReconCount";
if (section == "mission" && option == "dronecooldown") return "droneCooldownSeconds";
if (section == "mission" && option == "plane") return "planeReconEnabled";
if (section == "mission" && option == "camps") return "fieldCampCount";
if (section == "mission" && option == "hospitals") return "fieldHospitalCount";
if (section == "mission" && option == "startweapons") return "startWeaponCount";
if (section == "mission" && option == "startammo") return "startMagazineCount";
if (section == "mission" && option == "objectevery") return "objectGuardEvery";
if (section == "mission" && option == "objecthq") return "objectGuardHQMode";
if (section == "mission" && option == "objectguards") return "objectGuardCount";
if (section == "mission" && option == "hunterdistance") return "hunterSpawnDistanceMetres";
if (section == "mission" && option == "roadblocks") return "roadblockCount";
if (section == "mission" && option == "largeroadblocks") return "largeRoadblockCount";
if (section == "mission" && option == "mainroadwidth") return "mainRoadWidthMetres";
if (section == "mission" && option == "garagechance") return "garageChancePercent";
if (section == "mission" && option == "garageradius") return "garageRadiusMetres";
if (section == "mission" && option == "garageinterval") return "garageSpawnIntervalSeconds";
if (section == "mission" && option == "garagevehicles") return "garageVehicleCount";
if (section == "mission" && option == "traffic") return "civilianTrafficCount";
if (section == "mission" && option == "start") return "startMode";
if (section == "mission" && option == "selfepi") return "selfEpinephrine";
if (section == "mission" && option == "keepweapon") return "keepWeaponOnUnconscious"; if (section == "mission" && option == "keepweapon") return "keepWeaponOnUnconscious";
if (section == "mission" && option == "restartempty") return "restartOnEmpty";
if (section == "mission" && option == "emptyhours") return "emptyRestartHours";
if (section == "mission" && option == "radio") return "radioDefenceSeconds"; if (section == "mission" && option == "radio") return "radioDefenceSeconds";
if (section == "intel" && option == "sites") return "showAllSites";
if (section == "intel" && option == "radiosites") return "radioIntelSiteCount";
if (section == "intel" && option == "reveal") return "intelRevealChancePercent"; if (section == "intel" && option == "reveal") return "intelRevealChancePercent";
return ""; return "";
} }
static bool Selection(string key, string text, out int value) static bool Selection(string key, string text, out int value)
{ {
if (key == "aiDoctrine" && text == "current") { value = 0; return true; }
if (key == "aiDoctrine" && text == "coordinated") { value = 1; return true; }
if ((key == "aiShadow" || key == "restartOnEmpty" || key == "showAllSites") && text == "off") { value = 0; return true; }
if ((key == "aiShadow" || key == "restartOnEmpty" || key == "showAllSites") && text == "on") { value = 1; return true; }
if (key == "startMode" && text == "prison") { value = 0; return true; }
if (key == "startMode" && text == "crash") { value = 1; return true; }
if (text == "random" && ERR_Settings.Randomizable(key)) { value = -1; return true; } if (text == "random" && ERR_Settings.Randomizable(key)) { value = -1; return true; }
if ((text == "random-day" || text == "random day") && key == "startTimeMinutes") { value = -2; return true; } if ((text == "random-day" || text == "random day") && key == "startTimeMinutes") { value = -2; return true; }
if (key == "startTimeMinutes") return ERR_TimeService.ParseClock(text, value); if (key == "startTimeMinutes") return ERR_TimeService.ParseClock(text, value);
if (key == "startDateCode") return ERR_EnvironmentService.ParseDate(text, value); if (key == "startDateCode") return ERR_EnvironmentService.ParseDate(text, value);
if (key == "weatherProfile") return ERR_EnvironmentService.ParseProfile(text, value); if (key == "weatherProfile") return ERR_EnvironmentService.ParseProfile(text, value);
if (key == "viewDistanceMetres" && text == "default") { value = 0; return true; }
return Integer(text, value); return Integer(text, value);
} }
static bool Integer(string text, out int value) static bool Integer(string text, out int value)
@@ -81,6 +121,37 @@ class ERR_AdminService
} }
return handled; return handled;
} }
protected bool ViewDistanceCommand(ERR_GameMode mode, array<string> parts, array<string> replies)
{
if (parts.Count() < 2 || parts[0] != "mission" || parts[1] != "viewdistance") return false;
if (parts.Count() == 3 && parts[2] == "show")
{
replies.Insert("Mission view distance: " + ERR_AdminMenuFields.Display("viewDistanceMetres", mode.Settings().Get("viewDistanceMetres").ToString()) + ". Server limit: " + GetGame().GetViewDistanceServerLimit().ToString() + " m. Platform limits also apply.");
return true;
}
int value;
if (parts.Count() == 4 && parts[2] == "set" && ERR_CommandArguments.Selection("viewDistanceMetres", parts[3], value) && mode.Settings().Set("viewDistanceMetres", value))
{
mode.PublishViewDistance();
replies.Insert("Saved mission view distance: " + ERR_AdminMenuFields.Display("viewDistanceMetres", value.ToString()) + ". Applies now when players close menus, and to later joins; server/platform limits apply.");
}
else replies.Insert("Use /esc mission viewdistance show|set 500..10000|default. Invalid value or save failure leaves the previous setting unchanged.");
return true;
}
protected bool StartCommand(ERR_GameMode mode, array<string> parts, array<string> replies)
{
if (parts.Count() < 2 || parts[0] != "mission" || parts[1] != "start") return false;
if (parts.Count() == 3 && parts[2] == "show")
{
replies.Insert(ERR_AdminMenuFields.StartSummary(mode.Settings().Get("startMode"), mode.StartMode()));
return true;
}
int value;
if (parts.Count() == 4 && parts[2] == "set" && ERR_CommandArguments.Selection("startMode", parts[3], value) && mode.Settings().Set("startMode", value))
replies.Insert(ERR_AdminMenuFields.StartSummary(value, mode.StartMode()) + ". Saved for next round; current start and players are unchanged.");
else replies.Insert("Use /esc mission start show|set prison|crash (or 0|1). No random start. Invalid value or save failure leaves the previous selection unchanged.");
return true;
}
protected bool TimeCommand(ERR_GameMode mode, array<string> parts, array<string> replies) protected bool TimeCommand(ERR_GameMode mode, array<string> parts, array<string> replies)
{ {
if (parts.Count() < 2 || parts[0] != "mission" || (parts[1] != "time" && parts[1] != "speed")) return false; if (parts.Count() < 2 || parts[0] != "mission" || (parts[1] != "time" && parts[1] != "speed")) return false;
@@ -107,21 +178,65 @@ class ERR_AdminService
} }
protected string SettingHelp(string key) protected string SettingHelp(string key)
{ {
if (key == "aiDoctrine") return "Current (0) or Coordinated (1); default Current. Saved for next round.";
if (key == "aiShadow") return "On (1) records coordinated proposals without issuing their orders; Off (0) uses the selected doctrine. Saved for next round.";
if (key == "startWeaponCount") return "0..12 pistols in the shared prison/crash supply backpack; default 5. Applies next round.";
if (key == "startMagazineCount") return "0..48 spare pistol magazines in the shared prison/crash supply backpack; default 8. Applies next round.";
if (key == "sarkaAngerPerCar") return "1-30 anger points per car; 0 disables new waves. Default 3. Whole thresholds only: 5 means one car at 5 anger, two at 10. Four special-forces crew per car. Applies to new waves; existing teams remain.";
if (key == "motorbikeCount") return "0-6; default 2; 0 disabled. Optional WCS Motorbikes, two response specialists per bike. Applies next round; losses are not replaced.";
if (key == "droneReconCount") return "0-3; default 0. Applies next round.";
if (key == "droneCooldownSeconds") return "60-1800; default 300. Applies next round.";
if (key == "planeAltitudeMetres") return "150-500; default 250. Applies next round.";
if (key == "planeReplacementSeconds") return "30-1800; default 60. Applies next round.";
if (key == "planeSectorSeconds") return "30-600; default 120. Applies next round.";
if (key == "planeReconEnabled") return "0 disabled, 1 enabled if Cessna 172 and Propeller Flight Core are loaded. Applies next round. A clear airport departure area is required.";
if (key == "fieldCampCount") return "1..16 field camps; default 12. Applies next round. Counts are requested: unsafe optional sites may be omitted.";
if (key == "fieldHospitalCount") return "1..8 field hospitals; default 6. Applies next round. The required radio-clue hospital is retained.";
if (key == "objectGuardEvery") return "Guard every 1st, 2nd, 3rd or 4th eligible site; default every third. Nearby buildings share one site. Next round.";
if (key == "objectGuardHQMode") return "0: all hold their posts; 1: half hold, half respond to HQ; 2: all may respond to HQ. All report contacts. Next round.";
if (key == "objectGuardCount") return "0..16 soldiers per factory/industrial, military or infrastructure site; default 8, zero off. Next round. HQ participation is configured separately; all report. Towers and chimneys use suitable platforms. Lost guards are not replaced.";
if (key == "hunterSpawnDistanceMetres") return "Opening UAZ and troop-truck patrol placement. Applies next round.";
if (key == "largeRoadblockCount") return "Set 0..30 main-road checkpoints (default 4; 0 disables large posts). Eight guards per post. Applies next round; unsuitable sites are skipped.";
if (key == "mainRoadWidthMetres") return "Set main-road minimum width to 5..12 metres (default 6). Wider roads use large checkpoints; narrower roads use small ones. Applies next round.";
if (key == "roadblockCount") return "Set 0..30 small-road checkpoints (default 4; 0 disables small posts). Four guards per post. Applies next round; unsuitable sites are skipped.";
if (key == "telemetryEnabled") return "Default on. Sends mission results and player identities to the configured statistics service. Disabling clears unsent events immediately.";
if (key == "restartOnEmpty") return "On (1) restarts after the last disconnect; Off (0) preserves the round for reconnects. Default On. Applies immediately; normal round endings still apply.";
if (key == "emptyRestartHours") return "0 disables idle restarts; 1..168 real hours continuously empty. Default 5. Applies immediately, measured from mission start or the last disconnect. Connected players, including spectators, prevent idle restarts.";
if (key == "garageChancePercent") return "Garage selection chance (0-100%). Default 70%; 100 selects every suitable garage. Next round.";
if (key == "garageRadiusMetres") return "Spawn radius (250-1500 metres). Distance around players; default 1000 m. Next round.";
if (key == "garageSpawnIntervalSeconds") return "Time between spawns (5-60 seconds). Default 5 s. Increase to spread out spawning. Next round.";
if (key == "garageVehicleCount") return "0..24 unclaimed garage vehicles near players; default 16. Used vehicles are kept; distant unused vehicles retire. Optional bikes. Applies next round.";
if (key == "civilianTrafficCount") return "Set 0..6 civilian cars (default 3). Zero disables new traffic. Unclaimed cars retire only far from all players and out of sight; player-entered cars are preserved.";
if (key == "showAllSites") return "0 disabled (default), 1 enabled. Show prepared sites at their positions as question marks; recovered intel identifies their types. Applies immediately to everyone.";
if (key == "selfEpinephrine") return "0 disabled (default), 1 enabled. Unconscious players can press Space to use a carried epinephrine injector. Applies immediately; consumes the item and keeps its normal medical effect.";
if (key == "keepWeaponOnUnconscious") return "Set 1 to retain the held firearm during unconsciousness and equip it after recovery; 0 keeps normal dropping. Applies to the next knockout."; if (key == "keepWeaponOnUnconscious") return "Set 1 to retain the held firearm during unconsciousness and equip it after recovery; 0 keeps normal dropping. Applies to the next knockout.";
if (key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent") if (key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent")
return "Set 0..100 percent or Random. Applies to newly equipped enemy soldiers; existing loadouts are not rerolled or replenished."; return "Set 0..100 percent or Random. Applies to newly equipped enemy soldiers; existing loadouts are not rerolled or replenished.";
if (key == "radioIntelSiteCount") return "Sites required before radio intel: 0..20, default 5; 0 disables. Fully identify distinct non-radio sites; starting reveals do not count. Applies next round. Finding and using radios in person remains unrestricted.";
if (key == "intelRevealChancePercent") if (key == "intelRevealChancePercent")
return "Use /esc intel reveal set N (0-100) or random. This is the chance that a first document identifies its location exactly; otherwise it gives a rough location. A second document for that location always identifies it. Changes affect newly recovered documents immediately; known locations and spent documents remain unchanged."; return "Use /esc intel reveal set N (0-100) or random. This is the chance that a first document identifies its location exactly; otherwise it gives a rough location. A second document for that location always identifies it. With all sites shown, the first document always identifies the type. Changes affect newly recovered documents immediately; known locations and spent documents remain unchanged.";
return "";
}
protected string RoadblockTiming(string key)
{
if (key == "radioIntelSiteCount" || key == "garageVehicleCount" || key == "garageChancePercent" || key == "garageRadiusMetres" || key == "garageSpawnIntervalSeconds") return " for next round";
if (key == "aiDoctrine" || key == "aiShadow" || key == "droneReconCount" || key == "droneCooldownSeconds") return " for next round";
if (key == "motorbikeCount" || key == "planeAltitudeMetres" || key == "planeReplacementSeconds" || key == "planeSectorSeconds" || key == "planeReconEnabled" || key == "fieldCampCount" || key == "fieldHospitalCount" || key == "startWeaponCount" || key == "startMagazineCount" || key == "objectGuardCount" || key == "objectGuardEvery" || key == "objectGuardHQMode" || key == "hunterSpawnDistanceMetres" || key == "roadblockCount" || key == "largeRoadblockCount" || key == "mainRoadWidthMetres") return " for next round";
return ""; return "";
} }
protected void SettingCommand(ERR_Settings settings, string key, array<string> parts, array<string> replies) protected void SettingCommand(ERR_Settings settings, string key, array<string> parts, array<string> replies)
{ {
if (parts.Count() == 3 && parts[2] == "show") { replies.Insert(key + "=" + ERR_Settings.SelectionText(settings.Get(key))); return; } if (parts.Count() == 3 && parts[2] == "show")
{
string displayed = ERR_Settings.SelectionText(settings.Get(key));
if (key == "aiDoctrine" || key == "aiShadow") displayed = ERR_AdminMenuFields.Display(key, settings.Get(key).ToString()) + " (next round)";
replies.Insert(key + "=" + displayed); return;
}
string help = SettingHelp(key); string help = SettingHelp(key);
if (parts.Count() == 3 && parts[2] == "help" && !help.IsEmpty()) { replies.Insert(help); return; } if (parts.Count() == 3 && parts[2] == "help" && !help.IsEmpty()) { replies.Insert(help); return; }
int value; int value;
if (parts.Count() == 4 && parts[2] == "set" && ERR_CommandArguments.Selection(key, parts[3], value) && settings.Set(key, value)) if (parts.Count() == 4 && parts[2] == "set" && ERR_CommandArguments.Selection(key, parts[3], value) && settings.Set(key, value))
replies.Insert(key + "=" + ERR_Settings.SelectionText(value) + " saved"); replies.Insert(key + "=" + ERR_Settings.SelectionText(value) + " saved" + RoadblockTiming(key));
else else
{ {
string failure = "Invalid value or settings could not be saved; previous setting retained."; string failure = "Invalid value or settings could not be saved; previous setting retained.";
@@ -136,12 +251,19 @@ class ERR_AdminService
if (topic == "time" || topic == "mission") if (topic == "time" || topic == "mission")
{ {
replies.Insert("/esc mission radio show|set N|random | mission abandon"); replies.Insert("/esc mission radio show|set N|random | mission abandon");
replies.Insert("/esc mission selfepi show|set 0|1; allow unconscious self-injection with Space. Disabled by default.");
replies.Insert("/esc mission restartempty show|set on|off | mission emptyhours show|set 0..168; applies now, zero hours disables idle restarts.");
replies.Insert("/esc mission doctrine show|set current|coordinated | mission aishadow show|set off|on; next round only.");
replies.Insert("/esc mission start show|set prison|crash: save the next round start; current round unchanged.");
replies.Insert("/esc mission startweapons show|set 0..12 | mission startammo show|set 0..48; shared backpack, next round.");
replies.Insert("/esc mission viewdistance show|set 500..10000|default; applies immediately after closing menus.");
replies.Insert("/esc time show|set HH:MM|random|random-day | mission speed show|set N|random; saved changes apply next round."); replies.Insert("/esc time show|set HH:MM|random|random-day | mission speed show|set N|random; saved changes apply next round.");
replies.Insert("/esc mission date show|set YYYY-MM-DD|authored|random | mission weather show|set authored|clear|overcast|rain|random"); replies.Insert("/esc mission date show|set YYYY-MM-DD|authored|random | mission weather show|set authored|clear|overcast|rain|random");
return; return;
} }
if (topic == "intel") if (topic == "intel")
{ {
replies.Insert("/esc intel sites show|set on|off; show anonymous site positions and let intel identify their types.");
replies.Insert("/esc intel reveal show|set N|help (0-100); affects newly recovered documents immediately."); replies.Insert("/esc intel reveal show|set N|help (0-100); affects newly recovered documents immediately.");
replies.Insert("AI carrying and reroll commands require the Intel addon."); replies.Insert("AI carrying and reroll commands require the Intel addon.");
return; return;
@@ -153,7 +275,8 @@ class ERR_AdminService
} }
replies.Insert("/esc menu | status | settings | help mission|time|intel|hq|cheat|god|killais"); replies.Insert("/esc menu | status | settings | help mission|time|intel|hq|cheat|god|killais");
replies.Insert("/esc mission abandon | mission radio show|set N|random | time show|set HH:MM|random|random-day"); replies.Insert("/esc mission abandon | mission radio show|set N|random | time show|set HH:MM|random|random-day");
replies.Insert("/esc mission date|weather|speed show|set VALUE | intel reveal show|set N|help"); replies.Insert("/esc mission date|weather|speed show|set VALUE | mission viewdistance show|set 500..10000|default | intel reveal show|set N|help");
replies.Insert("/esc mission start show|set prison|crash; next round only.");
replies.Insert("/esc spectate next|previous; optional systems add their own command topics."); replies.Insert("/esc spectate next|previous; optional systems add their own command topics.");
} }
protected bool Unavailable(array<string> parts, array<string> replies) protected bool Unavailable(array<string> parts, array<string> replies)
@@ -193,6 +316,8 @@ class ERR_AdminService
foreach (string key : ERR_Settings.Keys()) foreach (string key : ERR_Settings.Keys())
{ {
string line = key + "=" + mode.Settings().Get(key).ToString(); string line = key + "=" + mode.Settings().Get(key).ToString();
if (key == "startMode") line = ERR_AdminMenuFields.StartSummary(mode.Settings().Get(key), mode.StartMode());
if (key == "aiDoctrine" || key == "aiShadow") line = key + "=" + ERR_AdminMenuFields.Display(key, mode.Settings().Get(key).ToString()) + " (next round)";
if (key == "automaticConvoyEnabled") line += " (saved for next round; current startup unchanged)"; if (key == "automaticConvoyEnabled") line += " (saved for next round; current startup unchanged)";
replies.Insert(line); replies.Insert(line);
} }
@@ -219,13 +344,16 @@ class ERR_AdminService
return; return;
} }
if (ERR_EnvironmentCommands.Execute(mode.Settings(), mode.Environment(), parts, replies)) return; if (ERR_EnvironmentCommands.Execute(mode.Settings(), mode.Environment(), parts, replies)) return;
if (ViewDistanceCommand(mode, parts, replies)) return;
if (TimeCommand(mode, parts, replies)) return; if (TimeCommand(mode, parts, replies)) return;
if (StartCommand(mode, parts, replies)) return;
if (parts.Count() == 3 || parts.Count() == 4) if (parts.Count() == 3 || parts.Count() == 4)
{ {
string key = Setting(parts[0], parts[1]); string key = Setting(parts[0], parts[1]);
if (!key.IsEmpty()) if (!key.IsEmpty())
{ {
SettingCommand(mode.Settings(), key, parts, replies); SettingCommand(mode.Settings(), key, parts, replies);
if (key == "showAllSites") mode.PublishSiteVisibility();
return; return;
} }
} }
@@ -9,6 +9,8 @@ class ERR_AdminSession
protected bool m_Stopped; protected bool m_Stopped;
protected bool m_Live; protected bool m_Live;
protected int m_Cursor; protected int m_Cursor;
protected string m_ItemAddon;
protected int m_ItemPage;
protected float m_LastRequest = -1000; protected float m_LastRequest = -1000;
protected float m_LastSnapshot = -1000; protected float m_LastSnapshot = -1000;
protected float m_LastLog = -1000; protected float m_LastLog = -1000;
@@ -66,11 +68,70 @@ class ERR_AdminSession
return; return;
} }
array<string> replies = {}; array<string> replies = {};
if (!JournalCommand(arguments, replies) && !m_Tools.Execute(m_Mode, m_Controller, arguments, replies)) if (!StartBackpackCommand(arguments, replies) && !ItemCatalogCommand(arguments, replies) && !JournalCommand(arguments, replies) && !m_Tools.Execute(m_Mode, m_Controller, arguments, replies))
m_Mode.Admin().Execute(m_Mode, true, arguments, replies); m_Mode.Admin().Execute(m_Mode, true, arguments, replies);
foreach (string reply : replies) m_Controller.ERR_AdminDeliverReply(m_Token, reply, menu); foreach (string reply : replies) m_Controller.ERR_AdminDeliverReply(m_Token, reply, menu);
if (menu) Snapshot(true); if (menu) Snapshot(true);
} }
protected bool StartBackpackCommand(string arguments, array<string> replies)
{
array<string> parts = {}; arguments.Split(" ", parts, true);
if (parts.IsEmpty() || parts[0] != "starter") return false;
ERR_StartBackpack supplies = GetGame().ERR_StartingSupplies();
bool saved;
if (parts.Count() == 3 && parts[1] == "mode" && (parts[2] == "default" || parts[2] == "custom")) saved = supplies.SetMode(parts[2] == "custom");
else if ((parts.Count() == 3 || parts.Count() == 4) && parts[1] == "remove")
{
string removeDestination = "backpack"; if (parts.Count() == 4) removeDestination = parts[3];
saved = supplies.Remove(parts[2], removeDestination);
}
else if ((parts.Count() == 5 || parts.Count() == 6) && parts[1] == "set")
{
ERR_ServerItemCatalog catalog = GetGame().ERR_ServerItems(); catalog.Start(m_Mode);
int count;
if (catalog.Contains(parts[2], parts[3]) && ERR_CommandArguments.Integer(parts[4], count))
{
ERR_ServerItem item = catalog.Item(parts[3]);
string destination = "backpack"; if (parts.Count() == 6) destination = parts[5];
if (item) saved = supplies.Set(item.Prefab, count, destination);
}
}
if (saved) replies.Insert("Starting supplies saved for next round. Custom contents are used only when Custom is selected.");
else replies.Insert("Not saved. Choose a catalog item and quantity 0-48; maximum 32 entries and 64 items across both destinations. Check configuration if read-only.");
return true;
}
// Per-session paging: another administrator cannot change this menu's selection.
protected bool ItemCatalogCommand(string arguments, array<string> replies)
{
array<string> parts = {}; arguments.Split(" ", parts, true);
if (parts.IsEmpty() || parts[0] != "items") return false;
int page;
ERR_ServerItemCatalog catalog = GetGame().ERR_ServerItems(); catalog.Start(m_Mode);
if (parts.Count() == 4 && parts[1] == "preset")
{
if (GetGame().ERR_ItemSelections().Preset(parts[2], parts[3])) replies.Insert("Preset " + parts[2] + " completed: " + parts[3] + ". Future random equipment uses the saved selection.");
else replies.Insert("Preset not saved/applied. Use an existing name to load/delete, or save a name of 1-24 letters, digits, _ or -. Maximum 16 presets. Previous selection retained.");
return true;
}
if (parts.Count() == 5 && parts[1] == "set" && catalog.Find(parts[2]) && (parts[3] == "*" || catalog.Contains(parts[2], parts[3])) && (parts[4] == "0" || parts[4] == "1"))
{
if (GetGame().ERR_ItemSelections().Set(parts[2], parts[3], parts[4] == "1")) replies.Insert("Item selection saved. Applies to future random equipment; existing inventories remain unchanged.");
else replies.Insert("Could not save item selection. Previous selection retained.");
return true;
}
if (parts.Count() == 5 && parts[1] == "weight" && catalog.Find(parts[2]) && (parts[3] == "*" || catalog.Contains(parts[2], parts[3])))
{
int weight;
if (!ERR_CommandArguments.Integer(parts[4], weight) || weight < 0 || weight > 100) replies.Insert("Relative weight must be 0-100; 1 is normal, 0 excludes from extra equipment.");
else if (GetGame().ERR_ItemSelections().SetWeight(parts[2], parts[3], weight)) replies.Insert("Probability weight saved. Applies to future extra-equipment rolls; disabled items remain disabled.");
else replies.Insert("Could not save weight. Previous settings retained.");
return true;
}
if (parts.Count() != 4 || parts[1] != "list" || !catalog.Find(parts[2]) || !ERR_CommandArguments.Integer(parts[3], page) || page < 0 || page > 2730)
{ replies.Insert("Choose a loaded server mod in Items, then press Show items."); return true; }
m_ItemAddon = parts[2]; m_ItemPage = page;
return true;
}
protected bool JournalCommand(string arguments, array<string> replies) protected bool JournalCommand(string arguments, array<string> replies)
{ {
if (arguments != "hq log" && arguments != "hq log show" && arguments != "hq log next" && arguments != "hq log set on" && arguments != "hq log set off") return false; if (arguments != "hq log" && arguments != "hq log show" && arguments != "hq log next" && arguments != "hq log set on" && arguments != "hq log set off") return false;
@@ -98,7 +159,11 @@ class ERR_AdminSession
if (key == "convoyCount" || key == "startTimeMinutes" || key == "timeMultiplier" || key == "startDateCode" || key == "weatherProfile") continue; if (key == "convoyCount" || key == "startTimeMinutes" || key == "timeMultiplier" || key == "startDateCode" || key == "weatherProfile") continue;
if (m_Mode.Settings().Get(key) == -1) values.Insert("resolved_" + key + "=" + m_Mode.Settings().Runtime(key).ToString()); if (m_Mode.Settings().Get(key) == -1) values.Insert("resolved_" + key + "=" + m_Mode.Settings().Runtime(key).ToString());
} }
if (m_Mode.Discovery()) values.Insert("radioIntelProgress=" + m_Mode.Discovery().RadioIntelStatus());
m_Mode.Admin().Capabilities(values); m_Mode.Admin().Capabilities(values);
ERR_RecommendedAddons.Snapshot(m_Mode, values);
GetGame().ERR_ServerItems().Snapshot(m_Mode, values, m_ItemAddon, m_ItemPage);
GetGame().ERR_StartingSupplies().Snapshot(values);
m_Mode.Admin().Execute(m_Mode, true, "status", status); m_Mode.Admin().Execute(m_Mode, true, "status", status);
m_Controller.ERR_AdminDeliverSnapshot(m_Token, true, values, status); m_Controller.ERR_AdminDeliverSnapshot(m_Token, true, values, status);
} }
@@ -3,24 +3,58 @@
class ERR_AirfieldPlacement class ERR_AirfieldPlacement
{ {
protected bool m_NearAirport; protected bool m_NearAirport;
// Optional terrain/adapter anchors; positions are centres, not trusted pads.
// Numbered names allow several airports without matching unrelated map labels.
static void AuthoredAirports(array<vector> positions)
{
if (!GetGame().GetWorldEntity()) return;
vector low, high;
GetGame().GetWorldEntity().GetWorldBounds(low, high);
for (int index = -1; index < 16; index++)
{
string name = "ERR_ReconAirport";
if (index >= 0) name = "ERR_Airfield_" + index.ToString();
IEntity marker = GetGame().GetWorld().FindEntityByName(name);
if (!marker) continue;
vector position = ERR_Actors.Ground(marker.GetOrigin());
if (position[0] < low[0] || position[0] > high[0] || position[2] < low[2] || position[2] > high[2]) continue;
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), position)) continue;
positions.Insert(position);
}
}
static bool AirportPrefab(string path)
{
path.ToLower();
return path.Contains("prefabs/structures/") && (path.Contains("/runway") || path.Contains("/lightrunway_") || path.Contains("/aircontroltower") || path.Contains("/airportcontroltower") || path.Contains("/controltower"));
}
protected bool Airport(IEntity entity) protected bool Airport(IEntity entity)
{ {
if (!entity || !entity.GetPrefabData()) return true; if (!entity || !entity.GetPrefabData()) return true;
string path = entity.GetPrefabData().GetPrefabName(); path.ToLower(); string path = entity.GetPrefabData().GetPrefabName(); path.ToLower();
if (path.Contains("/runway") || path.Contains("/lightrunway_") || path.Contains("/aircontroltower") || path.Contains("/airportcontroltower") || path.Contains("/locations/eden/airport.et")) m_NearAirport = true; if (AirportPrefab(path) || path.Contains("/locations/eden/airport.et")) m_NearAirport = true;
return !m_NearAirport; return !m_NearAirport;
} }
static bool Clear(vector position) static bool Clear(vector position)
{ {
if (!Replication.IsServer() || position[1] < 2 || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), position)) return false; if (!Replication.IsServer() || !ERR_Actors.DryGround(position)) return false;
ERR_AirfieldPlacement survey = new ERR_AirfieldPlacement(); ERR_AirfieldPlacement survey = new ERR_AirfieldPlacement();
GetGame().GetWorld().QueryEntitiesBySphere(position, 600, survey.Airport, null, EQueryEntitiesFlags.ALL); array<vector> authored = {};
AuthoredAirports(authored);
foreach (vector centre : authored)
if (vector.DistanceXZ(centre, position) <= 600) survey.m_NearAirport = true;
if (!survey.m_NearAirport) GetGame().GetWorld().QueryEntitiesBySphere(position, 600, survey.Airport, null, EQueryEntitiesFlags.ALL);
if (!survey.m_NearAirport) return false; if (!survey.m_NearAirport) return false;
return ClearGround(position);
}
// Same aircraft footprint/vertical sweep, without requiring airport scenery.
static bool ClearGround(vector position)
{
if (!Replication.IsServer() || !ERR_Actors.DryGround(position)) return false;
for (int edge = 0; edge < 4; edge++) for (int edge = 0; edge < 4; edge++)
{ {
float side = edge * Math.PI / 2; float side = edge * Math.PI / 2;
vector sample = position + Vector(Math.Cos(side) * 14, 0, Math.Sin(side) * 14); vector sample = position + Vector(Math.Cos(side) * 14, 0, Math.Sin(side) * 14);
if (Math.AbsFloat(SCR_TerrainHelper.GetTerrainY(sample) - position[1]) > 0.8) return false; if (!ERR_Actors.DryGround(sample) || Math.AbsFloat(SCR_TerrainHelper.GetTerrainY(sample) - position[1]) > 0.8) return false;
} }
if (!SCR_WorldTools.TraceCilinderUtil(position + "0 2.5 0", 40, 4, TraceFlags.ENTS)) return false; if (!SCR_WorldTools.TraceCilinderUtil(position + "0 2.5 0", 40, 4, TraceFlags.ENTS)) return false;
TraceBox clearance = new TraceBox(); TraceBox clearance = new TraceBox();
@@ -0,0 +1,102 @@
// Generated by tools/build_bundled_speech.py from docs/bundled-speech.json.
// Only these packaged graphs are accepted as local speech sources.
class ERR_BakedSpeech
{
static array<string> Sources() { return {"baked:welcome", "baked:soldier_1001", "baked:soldier_1002", "baked:soldier_1003", "baked:soldier_1004", "baked:soldier_1005", "baked:soldier_1100", "baked:soldier_1101", "baked:soldier_1102", "baked:soldier_1103", "baked:soldier_1104", "baked:soldier_1105", "baked:soldier_1200", "baked:soldier_1201", "baked:soldier_1202", "baked:soldier_1203", "baked:soldier_1204", "baked:soldier_1205", "baked:soldier_clear", "baked:soldier_moving", "baked:soldier_covering", "baked:soldier_reloading", "baked:soldier_under_fire", "baked:soldier_no_ammo", "baked:soldier_engaging", "baked:soldier_target_down", "baked:soldier_target_lost", "baked:radio_01", "baked:radio_02", "baked:radio_03", "baked:radio_04", "baked:radio_05", "baked:radio_06", "baked:radio_07", "baked:radio_08", "baked:radio_09", "baked:radio_10"}; }
static string Welcome() { return "baked:welcome"; }
static string WelcomeText() { return "Welcome to Escape Reintroduced. Work together, stay alert, and find your way off the island. Good luck."; }
static ResourceName Graph(string source)
{
switch (source)
{
case "baked:welcome": return "{3B55F836F9B6D20C}Sounds/ERR/Speech/welcome.acp";
case "baked:soldier_1001": return "{3DF753B659A184FB}Sounds/ERR/Speech/soldier_1001.acp";
case "baked:soldier_1002": return "{B1B78A9E50D41FD5}Sounds/ERR/Speech/soldier_1002.acp";
case "baked:soldier_1003": return "{FEE47650B627484E}Sounds/ERR/Speech/soldier_1003.acp";
case "baked:soldier_1004": return "{2FA4DC16F80F04FE}Sounds/ERR/Speech/soldier_1004.acp";
case "baked:soldier_1005": return "{5F3C22AA392DAE36}Sounds/ERR/Speech/soldier_1005.acp";
case "baked:soldier_1100": return "{AD2DC2C52E226AA7}Sounds/ERR/Speech/soldier_1100.acp";
case "baked:soldier_1101": return "{AE2E3AA19209018D}Sounds/ERR/Speech/soldier_1101.acp";
case "baked:soldier_1102": return "{74617A1D01426F95}Sounds/ERR/Speech/soldier_1102.acp";
case "baked:soldier_1103": return "{6B5037B1DEEB3FCB}Sounds/ERR/Speech/soldier_1103.acp";
case "baked:soldier_1104": return "{0C8AE411366B20EB}Sounds/ERR/Speech/soldier_1104.acp";
case "baked:soldier_1105": return "{3E1B0B6406E16734}Sounds/ERR/Speech/soldier_1105.acp";
case "baked:soldier_1200": return "{86FD331E3D1EB134}Sounds/ERR/Speech/soldier_1200.acp";
case "baked:soldier_1201": return "{A840837CC23E4A32}Sounds/ERR/Speech/soldier_1201.acp";
case "baked:soldier_1202": return "{3AF9ED4087FE442C}Sounds/ERR/Speech/soldier_1202.acp";
case "baked:soldier_1203": return "{97397F4FD27C4371}Sounds/ERR/Speech/soldier_1203.acp";
case "baked:soldier_1204": return "{EB394FAC323A0CA0}Sounds/ERR/Speech/soldier_1204.acp";
case "baked:soldier_1205": return "{69750949590AECF1}Sounds/ERR/Speech/soldier_1205.acp";
case "baked:soldier_clear": return "{ECF3257A4BFF2F53}Sounds/ERR/Speech/soldier_clear.acp";
case "baked:soldier_moving": return "{BAB1942B5DE85A52}Sounds/ERR/Speech/soldier_moving.acp";
case "baked:soldier_covering": return "{78DCA194DA126513}Sounds/ERR/Speech/soldier_covering.acp";
case "baked:soldier_reloading": return "{C51337F1692C21A7}Sounds/ERR/Speech/soldier_reloading.acp";
case "baked:soldier_under_fire": return "{8A05B1A08D5F23CC}Sounds/ERR/Speech/soldier_under_fire.acp";
case "baked:soldier_no_ammo": return "{D363E6B37331E416}Sounds/ERR/Speech/soldier_no_ammo.acp";
case "baked:soldier_engaging": return "{A700E14970CCA3E2}Sounds/ERR/Speech/soldier_engaging.acp";
case "baked:soldier_target_down": return "{388F2800E5683289}Sounds/ERR/Speech/soldier_target_down.acp";
case "baked:soldier_target_lost": return "{DA42EA7B286B20BD}Sounds/ERR/Speech/soldier_target_lost.acp";
case "baked:radio_01": return "{29F6A1A8A691488D}Sounds/ERR/Speech/radio_01.acp";
case "baked:radio_02": return "{0B1147F331AC4EDA}Sounds/ERR/Speech/radio_02.acp";
case "baked:radio_03": return "{BC0C902F48008D93}Sounds/ERR/Speech/radio_03.acp";
case "baked:radio_04": return "{E9EB0183C1ED1906}Sounds/ERR/Speech/radio_04.acp";
case "baked:radio_05": return "{4995CB641455140B}Sounds/ERR/Speech/radio_05.acp";
case "baked:radio_06": return "{E4473DD1140515F9}Sounds/ERR/Speech/radio_06.acp";
case "baked:radio_07": return "{3D3AA49576443D5D}Sounds/ERR/Speech/radio_07.acp";
case "baked:radio_08": return "{207C5D973D7EBE55}Sounds/ERR/Speech/radio_08.acp";
case "baked:radio_09": return "{DCAF67D5B901D45F}Sounds/ERR/Speech/radio_09.acp";
case "baked:radio_10": return "{8CC6CCBA6BDF1568}Sounds/ERR/Speech/radio_10.acp";
}
return "";
}
static string Soldier(int kind)
{
switch (kind)
{
case 1001: return "baked:soldier_1001";
case 1002: return "baked:soldier_1002";
case 1003: return "baked:soldier_1003";
case 1004: return "baked:soldier_1004";
case 1005: return "baked:soldier_1005";
case 1100: return "baked:soldier_1100";
case 1101: return "baked:soldier_1101";
case 1102: return "baked:soldier_1102";
case 1103: return "baked:soldier_1103";
case 1104: return "baked:soldier_1104";
case 1105: return "baked:soldier_1105";
case 1200: return "baked:soldier_1200";
case 1201: return "baked:soldier_1201";
case 1202: return "baked:soldier_1202";
case 1203: return "baked:soldier_1203";
case 1204: return "baked:soldier_1204";
case 1205: return "baked:soldier_1205";
case ECommunicationType.REPORT_CLEAR: return "baked:soldier_clear";
case ECommunicationType.REPORT_MOVING: return "baked:soldier_moving";
case ECommunicationType.REPORT_COVERING: return "baked:soldier_covering";
case ECommunicationType.REPORT_RELOADING: return "baked:soldier_reloading";
case ECommunicationType.REPORT_UNDER_FIRE: return "baked:soldier_under_fire";
case ECommunicationType.REPORT_NO_AMMO: return "baked:soldier_no_ammo";
case ECommunicationType.REPORT_ENGAGING: return "baked:soldier_engaging";
case ECommunicationType.REPORT_TARGET_DOWN: return "baked:soldier_target_down";
case ECommunicationType.REPORT_TARGET_LOST: return "baked:soldier_target_lost";
}
return "";
}
static string Radio(string text)
{
switch (text)
{
case "Escapists, this is rescue command. We are preparing your escape route and assembling transports with armed escort helicopters. Hold the radio position until preparations are complete. Keep your smoke grenades ready and stand by for pickup coordinates.": return "baked:radio_01";
case "Your escape route is ready. The exact pickup point is marked on your map. Gather your team there and throw a smoke grenade within 25 metres of the mark. We will send the transport and armed escorts when we see your smoke.": return "baked:radio_02";
case "Rescue command cannot establish a safe approach. Extraction has failed.": return "baked:radio_03";
case "Rescue command could not assemble the required transport capacity. Extraction has failed.": return "baked:radio_04";
case "Signal received. Launching the rescue flight. Keep the landing point clear.": return "baked:radio_05";
case "A rescue transport is down or unable to continue. Extraction has failed.": return "baked:radio_06";
case "Rescue transports are on the ground. Everyone aboard, including your wounded. Use either rescue transport if there are two. Escorts are covering the pickup. We leave when all surviving prisoners are inside.": return "baked:radio_07";
case "All survivors aboard. Lifting now. Stay inside the transport; escorts are taking you to safety.": return "baked:radio_08";
case "Smoke confirmed. Rescue transport and armed cover are inbound. Clear the marked landing point. Bring your wounded; we wait for all surviving prisoners to board.": return "baked:radio_09";
case "Smoke confirmed. Two rescue transports and armed cover are inbound. The helicopters will land separately near the marker. Use either transport and bring your wounded; both wait until everyone is aboard.": return "baked:radio_10";
}
return "";
}
}
@@ -0,0 +1,91 @@
// The native action supplies held progress/cancellation. Authority rechecks the
// current player, shovel and corpse immediately before replicated deletion.
class ERR_Burial
{
static bool Dead(IEntity entity)
{
ChimeraCharacter character = ChimeraCharacter.Cast(entity);
return character && character.GetCharacterController()
&& character.GetCharacterController().GetLifeState() == ECharacterLifeState.DEAD;
}
static bool ShovelHeld(IEntity user)
{
ChimeraCharacter character = ChimeraCharacter.Cast(user);
if (!character || !character.GetCharacterController()) return false;
CharacterControllerComponent controller = character.GetCharacterController();
// Gadgets occupy the left-hand slot; GetCurrentItemInHands alone only finds
// weapons. Inventory ownership and a selected quickslot are not sufficient.
IEntity item = controller.GetAttachedGadgetAtLeftHandSlot();
if (!item) item = controller.GetCurrentItemInHands();
return item && item.FindComponent(SCR_CampaignBuildingGadgetToolComponent) != null;
}
static bool Reach(float distance)
{
return distance >= 0 && distance <= 2.5;
}
static bool Clear(IEntity user, IEntity body)
{
TraceParam trace = new TraceParam();
trace.Start = user.GetOrigin() + Vector(0, 1.2, 0);
trace.End = body.GetOrigin() + Vector(0, 0.25, 0);
trace.Exclude = user; trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
if (GetGame().GetWorld().TraceMove(trace, null) >= 1) return true;
IEntity hit = trace.TraceEnt;
for (int depth = 0; hit && depth < 16; depth++)
{
if (hit == body) return true;
hit = hit.GetParent();
}
return false;
}
static string Rejection(IEntity user, IEntity body)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!mode || !mode.Active()) return "Mission is not active";
if (!ERR_Actors.Player(user) || !ERR_Actors.CanFight(user)) return "You must be alive and conscious";
if (user == body || !Dead(body)) return "Only dead bodies can be buried";
if (!ShovelHeld(user)) return "Take a shovel into your hands";
if (CompartmentAccessComponent.GetVehicleIn(user) || CompartmentAccessComponent.GetVehicleIn(body)) return "Move yourself and the body out of the vehicle";
if (!Reach(vector.Distance(user.GetOrigin(), body.GetOrigin()))) return "Move closer to the body";
if (!Clear(user, body)) return "The body is obstructed";
return string.Empty;
}
static bool Permitted(IEntity user, IEntity body)
{
if (!Replication.IsServer()) return false;
ERR_GameMode mode = ERR_GameMode.Get();
return mode && mode.Players() && mode.Players().CurrentActor(user) && Rejection(user, body).IsEmpty();
}
static void Complete(IEntity user, IEntity body)
{
if (!Permitted(user, body)) return;
// Corpses and their remaining inventory disappear on every peer. Already
// queued HQ casualty reports retain their immutable attribution and position.
SCR_EntityHelper.DeleteEntityAndChildren(body);
}
}
class ERR_BuryBodyAction : ScriptedUserAction
{
override bool HasLocalEffectOnlyScript() { return false; }
override bool CheckOnServerFirstScript() { return true; }
override bool CanBroadcastScript() { return false; }
override bool CanBeShownScript(IEntity user)
{
ERR_GameMode mode = ERR_GameMode.Get();
return mode && mode.Active() && user != GetOwner() && ERR_Burial.Dead(GetOwner()) && ERR_Burial.ShovelHeld(user);
}
override bool CanBePerformedScript(IEntity user)
{
string reason = ERR_Burial.Rejection(user, GetOwner());
SetCannotPerformReason(reason);
return reason.IsEmpty();
}
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
{
if (pOwnerEntity != GetOwner() || !ERR_Burial.Permitted(pUserEntity, pOwnerEntity)) return;
// Let the native action finish before destroying its owning manager. The
// deferred callback revalidates, so duplicate completions are harmless.
GetGame().GetCallqueue().CallLater(ERR_Burial.Complete, 1, false, pUserEntity, pOwnerEntity);
}
}
@@ -1,5 +1,5 @@
// Conservative fallback for a sleeping, engine-off Mi8 whose native simulation // Conservative fallback for audited sleeping, engine-off rescue aircraft whose
// has not yet populated contact flags. Reads physical support; never wakes it. // native simulation has not populated contact flags. Reads support; never wakes it.
class ERR_CasualtyGroundSupport class ERR_CasualtyGroundSupport
{ {
static bool SupportHit(float fraction, float normalY, bool selfHit, bool dynamicHit) static bool SupportHit(float fraction, float normalY, bool selfHit, bool dynamicHit)
@@ -59,14 +59,37 @@ class ERR_CasualtyGroundSupport
} }
return supported; return supported;
} }
static int LandingGearKind(string prefab)
{
if (prefab.Contains("/Mi8MT/") || prefab.EndsWith("Prefabs/ERR/RescueHelicopter.et") || prefab.EndsWith("Prefabs/ERR/RescueHelicopterFIA.et") || prefab.EndsWith("Prefabs/ERR/RescueHelicopterAFRF.et")) return 1;
if (prefab.Contains("/UH1H/") || prefab.EndsWith("Prefabs/ERR/RescueHelicopterWest.et") || prefab.EndsWith("Prefabs/ERR/RescueHelicopterION.et")) return 2;
return 0;
}
static bool SkidsSupported(Vehicle aircraft, bool diagnostics = false)
{
vector leftFront, rightFront, leftRear, rightRear;
if (!WheelCenter(aircraft, "v_ground_contact_L01", leftFront, diagnostics) || !WheelCenter(aircraft, "v_ground_contact_R01", rightFront, diagnostics) || !WheelCenter(aircraft, "v_ground_contact_L02", leftRear, diagnostics) || !WheelCenter(aircraft, "v_ground_contact_R02", rightRear, diagnostics)) return false;
// Installed UH1H_base.et defines these four skid pivots without wheel radii.
// Both skids and both ends need direct static support. Missing/collapsed
// pivots and a helicopter suspended above the ground fail closed.
if (vector.DistanceXZ(leftFront, rightFront) < 1 || vector.DistanceXZ(leftRear, rightRear) < 1 || vector.DistanceXZ(leftFront, leftRear) < 1 || vector.DistanceXZ(rightFront, rightRear) < 1) return false;
bool a = WheelSupported(aircraft, leftFront, 0, diagnostics);
bool b = WheelSupported(aircraft, rightFront, 0, diagnostics);
bool c = WheelSupported(aircraft, leftRear, 0, diagnostics);
bool d = WheelSupported(aircraft, rightRear, 0, diagnostics);
return a && b && c && d;
}
static bool Parked(Vehicle aircraft, bool diagnostics = false) static bool Parked(Vehicle aircraft, bool diagnostics = false)
{ {
if (!aircraft || !aircraft.GetPrefabData() || !aircraft.GetPrefabData().GetPrefabName().Contains("/Mi8MT/")) return false; if (!aircraft || !aircraft.GetPrefabData()) return false;
int kind = LandingGearKind(aircraft.GetPrefabData().GetPrefabName());
if (!kind) return false;
Physics physics = aircraft.GetPhysics(); Physics physics = aircraft.GetPhysics();
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(aircraft.FindComponent(HelicopterControllerComponent)); HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(aircraft.FindComponent(HelicopterControllerComponent));
if (!physics || !controller || !controller.GetSimulation()) return false; if (!physics || !controller || !controller.GetSimulation()) return false;
if (diagnostics) PrintFormat("ERR PARKED SUPPORT: physicsAwake=%1 engineOn=%2 nativeContact=%3", physics.IsActive(), controller.GetSimulation().EngineIsOn(), controller.GetSimulation().HasAnyGroundContact()); if (diagnostics) PrintFormat("ERR PARKED SUPPORT: physicsAwake=%1 engineOn=%2 nativeContact=%3", physics.IsActive(), controller.GetSimulation().EngineIsOn(), controller.GetSimulation().HasAnyGroundContact());
if (physics.IsActive() || controller.GetSimulation().EngineIsOn()) return false; if (physics.IsActive() || controller.GetSimulation().EngineIsOn()) return false;
if (kind == 2) return SkidsSupported(aircraft, diagnostics);
vector front, left, right; vector front, left, right;
if (!WheelCenter(aircraft, "v_wheel_F", front, diagnostics) || !WheelCenter(aircraft, "v_wheel_bL", left, diagnostics) || !WheelCenter(aircraft, "v_wheel_bR", right, diagnostics)) return false; if (!WheelCenter(aircraft, "v_wheel_F", front, diagnostics) || !WheelCenter(aircraft, "v_wheel_bL", left, diagnostics) || !WheelCenter(aircraft, "v_wheel_bR", right, diagnostics)) return false;
// Fail closed if native pivot resolution collapses to an owner origin or // Fail closed if native pivot resolution collapses to an owner origin or
@@ -4,7 +4,7 @@ class ERR_CasualtyFeedback
static bool Visible(ERR_GameMode mode, IEntity caller, IEntity casualty) static bool Visible(ERR_GameMode mode, IEntity caller, IEntity casualty)
{ {
if (!mode || !mode.Active() || mode.Phase() != ERR_Phase.EXTRACTION || !caller || caller == casualty || !ERR_CasualtyLoadingRules.Down(casualty)) return false; if (!mode || !mode.Active() || mode.Phase() != ERR_Phase.EXTRACTION || !caller || caller == casualty || !ERR_CasualtyLoadingRules.Down(casualty)) return false;
Vehicle aircraft = mode.CasualtyLoadingAircraft(); Vehicle aircraft = mode.CasualtyLoadingAircraftFor(caller, casualty);
Faction faction = mode.PlayerFaction(); Faction faction = mode.PlayerFaction();
return aircraft && faction && SCR_FactionManager.SGetFaction(caller) == faction && SCR_FactionManager.SGetFaction(casualty) == faction && ERR_CasualtyLoadingRules.Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin())); return aircraft && faction && SCR_FactionManager.SGetFaction(caller) == faction && SCR_FactionManager.SGetFaction(casualty) == faction && ERR_CasualtyLoadingRules.Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin()));
} }
@@ -14,7 +14,7 @@ class ERR_CasualtyFeedback
if (!caller || caller == casualty || !ERR_Actors.CanFight(caller)) return "A living, conscious rescuer is required"; if (!caller || caller == casualty || !ERR_Actors.CanFight(caller)) return "A living, conscious rescuer is required";
if (CompartmentAccessComponent.GetVehicleIn(caller)) return "Leave the helicopter before loading a casualty"; if (CompartmentAccessComponent.GetVehicleIn(caller)) return "Leave the helicopter before loading a casualty";
if (!ERR_CasualtyLoadingRules.GroundSettled(casualty)) return "Place the living, unconscious casualty on stable ground"; if (!ERR_CasualtyLoadingRules.GroundSettled(casualty)) return "Place the living, unconscious casualty on stable ground";
Vehicle aircraft = mode.CasualtyLoadingAircraft(); Vehicle aircraft = mode.CasualtyLoadingAircraftFor(caller, casualty);
if (!aircraft || !ERR_CasualtyLoadingRules.Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin()))) return "Stay beside the casualty and within twelve metres of the extraction helicopter"; if (!aircraft || !ERR_CasualtyLoadingRules.Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin()))) return "Stay beside the casualty and within twelve metres of the extraction helicopter";
if (!ERR_CasualtyLoadingRules.AircraftReady(aircraft)) return "The helicopter must be upright, grounded and stationary"; if (!ERR_CasualtyLoadingRules.AircraftReady(aircraft)) return "The helicopter must be upright, grounded and stationary";
if (!ERR_CasualtyLoadingRules.Nearby(mode, caller, casualty)) return "Keep clear access to a fellow prisoner's body"; if (!ERR_CasualtyLoadingRules.Nearby(mode, caller, casualty)) return "Keep clear access to a fellow prisoner's body";
@@ -1,9 +1,11 @@
// Round-owned server service; only the aircraft reference is replicated for action visibility. // Round-owned server service; only registered transport references are replicated.
modded class ERR_GameMode modded class ERR_GameMode
{ {
protected ref ERR_CasualtyLoadingService m_ERRCasualtyLoading; protected ref ERR_CasualtyLoadingService m_ERRCasualtyLoading;
[RplProp()] [RplProp()]
protected RplId m_ERRCasualtyAircraftId = RplId.Invalid(); protected RplId m_ERRCasualtyAircraftId = RplId.Invalid();
[RplProp()]
protected RplId m_ERRCasualtySecondAircraftId = RplId.Invalid();
ERR_CasualtyLoadingService CasualtyLoading() { return m_ERRCasualtyLoading; } ERR_CasualtyLoadingService CasualtyLoading() { return m_ERRCasualtyLoading; }
Vehicle CasualtyLoadingAircraft() Vehicle CasualtyLoadingAircraft()
{ {
@@ -12,17 +14,61 @@ modded class ERR_GameMode
if (!component) return null; if (!component) return null;
return Vehicle.Cast(component.GetEntity()); return Vehicle.Cast(component.GetEntity());
} }
int CasualtyLoadingAircrafts(out array<Vehicle> aircrafts)
{
aircrafts.Clear();
if (Replication.IsServer())
{
if (Extraction()) return Extraction().Vehicles(aircrafts);
return 0;
}
Vehicle first = CasualtyLoadingAircraft();
if (first) aircrafts.Insert(first);
Vehicle second = Vehicle.Cast(ERR_CasualtyLoadingRules.Entity(m_ERRCasualtySecondAircraftId));
if (second && second != first) aircrafts.Insert(second);
return aircrafts.Count();
}
bool CasualtyLoadingTransport(Vehicle aircraft)
{
if (!aircraft) return false;
if (Replication.IsServer()) return Extraction() && Extraction().IsTransport(aircraft);
return aircraft == CasualtyLoadingAircraft() || (m_ERRCasualtySecondAircraftId != RplId.Invalid() && ERR_CasualtyLoadingRules.EntityId(aircraft) == m_ERRCasualtySecondAircraftId);
}
// Prefer a usable nearby transport; retain the nearest ineligible aircraft for
// a useful denial reason. An existing casualty reservation wins over distance.
Vehicle CasualtyLoadingAircraftFor(IEntity caller, IEntity casualty)
{
if (!caller || !casualty) return null;
array<Vehicle> aircrafts = {}; CasualtyLoadingAircrafts(aircrafts);
Vehicle nearest, available;
float nearestDistance = float.MAX, availableDistance = float.MAX;
foreach (Vehicle aircraft : aircrafts)
{
if (!ERR_Actors.Alive(aircraft)) continue;
float distance = vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin());
if (distance < nearestDistance) { nearest = aircraft; nearestDistance = distance; }
if (!ERR_CasualtyLoadingRules.Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), distance) || !ERR_CasualtyLoadingRules.AircraftReady(aircraft)) continue;
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(aircraft.FindComponent(BaseCompartmentManagerComponent));
if (!manager) continue;
array<BaseCompartmentSlot> seats = {}; manager.GetCompartments(seats);
foreach (BaseCompartmentSlot seat : seats)
{
if (!ERR_CasualtyLoadingRules.TransportBench(seat, aircraft) || seat.GetOccupant() || !seat.IsCompartmentAccessible() || seat.IsGetInLockedFor(casualty)) continue;
if (seat.IsReservedBy(casualty)) return aircraft;
if (!seat.IsReserved() && distance < availableDistance) { available = aircraft; availableDistance = distance; }
}
}
if (available) return available;
return nearest;
}
protected void ERR_UpdateCasualtyAircraft(float now) protected void ERR_UpdateCasualtyAircraft(float now)
{ {
Vehicle aircraft = ExtractionVehicle(); array<Vehicle> aircrafts = {}; CasualtyLoadingAircrafts(aircrafts);
if (!aircraft) RplId first = RplId.Invalid(), second = RplId.Invalid();
{ if (!aircrafts.IsEmpty()) first = ERR_CasualtyLoadingRules.EntityId(aircrafts[0]);
if (m_ERRCasualtyAircraftId != RplId.Invalid()) { m_ERRCasualtyAircraftId = RplId.Invalid(); Replication.BumpMe(); } if (aircrafts.Count() > 1) second = ERR_CasualtyLoadingRules.EntityId(aircrafts[1]);
return; if (first == m_ERRCasualtyAircraftId && second == m_ERRCasualtySecondAircraftId) return;
} m_ERRCasualtyAircraftId = first; m_ERRCasualtySecondAircraftId = second;
RplComponent component = RplComponent.Cast(aircraft.FindComponent(RplComponent));
if (!component || m_ERRCasualtyAircraftId == component.Id()) return;
m_ERRCasualtyAircraftId = component.Id();
Replication.BumpMe(); Replication.BumpMe();
} }
protected void ERR_StopCasualtyLoading() protected void ERR_StopCasualtyLoading()
@@ -52,7 +52,7 @@ modded class SCR_PlayerController
IEntity caller = ERR_CasualtyLoadingRules.Entity(callerId); IEntity caller = ERR_CasualtyLoadingRules.Entity(callerId);
Vehicle vehicle = Vehicle.Cast(ERR_CasualtyLoadingRules.Entity(vehicleId)); Vehicle vehicle = Vehicle.Cast(ERR_CasualtyLoadingRules.Entity(vehicleId));
IEntity owner = ERR_CasualtyLoadingRules.Entity(ownerId); IEntity owner = ERR_CasualtyLoadingRules.Entity(ownerId);
if (lease && mode && ERR_CasualtyLoadingRules.EntityId(mode) == modeId && ERR_CasualtyLoadingRules.EntityId(actor) == actorId && vehicle == mode.CasualtyLoadingAircraft() && ERR_CasualtyLoadingRules.Nearby(mode, caller, actor) && owner) if (lease && mode && ERR_CasualtyLoadingRules.EntityId(mode) == modeId && ERR_CasualtyLoadingRules.EntityId(actor) == actorId && mode.CasualtyLoadingTransport(vehicle) && ERR_CasualtyLoadingRules.NearbyAircraft(mode, caller, actor, vehicle) && owner)
{ {
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(owner.FindComponent(BaseCompartmentManagerComponent)); BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(owner.FindComponent(BaseCompartmentManagerComponent));
SCR_CompartmentAccessComponent access = SCR_CompartmentAccessComponent.Cast(actor.FindComponent(SCR_CompartmentAccessComponent)); SCR_CompartmentAccessComponent access = SCR_CompartmentAccessComponent.Cast(actor.FindComponent(SCR_CompartmentAccessComponent));
@@ -1,5 +1,5 @@
// First release supports only the registered Mi8 extraction aircraft's physical // Only registered rescue transports and audited native child benches are eligible.
// child transport benches. Nothing changes health, ownership, or seat classes. // Nothing changes health, ownership, or seat classes.
class ERR_CasualtyLoadingRules class ERR_CasualtyLoadingRules
{ {
static int RoundAction(int phase) static int RoundAction(int phase)
@@ -86,20 +86,30 @@ class ERR_CasualtyLoadingRules
return false; return false;
} }
static bool Nearby(ERR_GameMode mode, IEntity caller, IEntity casualty) static bool Nearby(ERR_GameMode mode, IEntity caller, IEntity casualty)
{
if (!mode) return false;
return NearbyAircraft(mode, caller, casualty, mode.CasualtyLoadingAircraftFor(caller, casualty));
}
static bool NearbyAircraft(ERR_GameMode mode, IEntity caller, IEntity casualty, Vehicle aircraft)
{ {
if (!mode || !mode.Active() || mode.Phase() != ERR_Phase.EXTRACTION || !caller || caller == casualty) return false; if (!mode || !mode.Active() || mode.Phase() != ERR_Phase.EXTRACTION || !caller || caller == casualty) return false;
CharacterControllerComponent rescuer = CharacterControllerComponent.Cast(caller.FindComponent(CharacterControllerComponent)); CharacterControllerComponent rescuer = CharacterControllerComponent.Cast(caller.FindComponent(CharacterControllerComponent));
if (!ERR_Actors.CanFight(caller) || !rescuer || rescuer.GetLifeState() != ECharacterLifeState.ALIVE || !GroundSettled(casualty) || CompartmentAccessComponent.GetVehicleIn(caller)) return false; if (!ERR_Actors.CanFight(caller) || !rescuer || rescuer.GetLifeState() != ECharacterLifeState.ALIVE || !GroundSettled(casualty) || CompartmentAccessComponent.GetVehicleIn(caller)) return false;
Vehicle aircraft = mode.CasualtyLoadingAircraft(); if (!mode.CasualtyLoadingTransport(aircraft) || !Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin())) || !AircraftReady(aircraft)) return false;
if (!aircraft || !Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin())) || !AircraftReady(aircraft)) return false;
Faction faction = mode.PlayerFaction(); Faction faction = mode.PlayerFaction();
return faction && SCR_FactionManager.SGetFaction(caller) == faction && SCR_FactionManager.SGetFaction(casualty) == faction && LineOfSight(caller, casualty); return faction && SCR_FactionManager.SGetFaction(caller) == faction && SCR_FactionManager.SGetFaction(casualty) == faction && LineOfSight(caller, casualty);
} }
static bool TransportBenchPrefab(string prefab)
{
// Exact authored bench owners, including the two unarmed Huey side seats.
// Aircraft-root pilot seats, turret owners and patient slots stay excluded.
return prefab.EndsWith("Prefabs/Vehicles/Helicopters/Mi8MT/VehParts/Seats/VehPart_MI8_seats_cargo.et") || prefab.EndsWith("Prefabs/Vehicles/Helicopters/Mi8MT/VehParts/Seats/VehPart_MI8_CIV_seats_cargo.et") || prefab.EndsWith("Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_seats_cargo.et") || prefab.EndsWith("Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_seats_cargo_openDoor.et") || prefab.EndsWith("Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_seats_gunners.et") || prefab.EndsWith("Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_CIV_seats_cargo.et") || prefab.EndsWith("Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_CIV_seats_gunners.et");
}
static bool TransportBench(BaseCompartmentSlot seat, Vehicle aircraft) static bool TransportBench(BaseCompartmentSlot seat, Vehicle aircraft)
{ {
if (!seat || !aircraft || seat.GetType() != ECompartmentType.CARGO || SCR_PatientCompartmentSlot.Cast(seat)) return false; if (!seat || !aircraft || seat.GetType() != ECompartmentType.CARGO || SCR_PatientCompartmentSlot.Cast(seat)) return false;
IEntity owner = seat.GetOwner(); IEntity owner = seat.GetOwner();
if (!owner || owner == aircraft || !owner.GetPrefabData() || !owner.GetPrefabData().GetPrefabName().Contains("VehPart_MI8_seats")) return false; if (!owner || owner == aircraft || !owner.GetPrefabData() || !TransportBenchPrefab(owner.GetPrefabData().GetPrefabName())) return false;
while (owner) while (owner)
{ {
if (owner == aircraft) return true; if (owner == aircraft) return true;
@@ -203,7 +213,7 @@ class ERR_CasualtyLoadingService
{ {
if (!Replication.IsServer() || m_Stopped || !ERR_CasualtyLoadingRules.Nearby(m_Mode, caller, casualty)) return false; if (!Replication.IsServer() || m_Stopped || !ERR_CasualtyLoadingRules.Nearby(m_Mode, caller, casualty)) return false;
if (!CurrentLife(caller) || !CurrentLife(casualty) || !Settled(casualty, System.GetTickCount()) || PendingActor(caller, casualty)) return false; if (!CurrentLife(caller) || !CurrentLife(casualty) || !Settled(casualty, System.GetTickCount()) || PendingActor(caller, casualty)) return false;
return AvailableSeat(casualty, m_Mode.ExtractionVehicle()) != null; return AvailableSeat(casualty, m_Mode.CasualtyLoadingAircraftFor(caller, casualty)) != null;
} }
// Reasons never replace CanLoad/Begin authority or reserve a seat. // Reasons never replace CanLoad/Begin authority or reserve a seat.
string RejectionReason(IEntity caller, IEntity casualty) string RejectionReason(IEntity caller, IEntity casualty)
@@ -214,7 +224,7 @@ class ERR_CasualtyLoadingService
if (!CurrentLife(caller) || !CurrentLife(casualty)) return "Both prisoners must still be connected to this mission"; if (!CurrentLife(caller) || !CurrentLife(casualty)) return "Both prisoners must still be connected to this mission";
if (!Settled(casualty, System.GetTickCount())) return "Let the casualty rest on the ground for five seconds"; if (!Settled(casualty, System.GetTickCount())) return "Let the casualty rest on the ground for five seconds";
if (PendingActor(caller, casualty)) return "A casualty loading request is already in progress"; if (PendingActor(caller, casualty)) return "A casualty loading request is already in progress";
if (!AvailableSeat(casualty, m_Mode.ExtractionVehicle())) return "No accessible transport seat is available"; if (!AvailableSeat(casualty, m_Mode.CasualtyLoadingAircraftFor(caller, casualty))) return "No accessible transport seat is available";
return string.Empty; return string.Empty;
} }
protected void NotifyCaller(IEntity caller, string message) protected void NotifyCaller(IEntity caller, string message)
@@ -238,10 +248,11 @@ class ERR_CasualtyLoadingService
if (!CanLoad(caller, casualty) || m_NextNonce >= 2147483646) return false; if (!CanLoad(caller, casualty) || m_NextNonce >= 2147483646) return false;
ERR_PlayerLife callerLife = CurrentLife(caller); ERR_PlayerLife callerLife = CurrentLife(caller);
ERR_PlayerLife casualtyLife = CurrentLife(casualty); ERR_PlayerLife casualtyLife = CurrentLife(casualty);
Vehicle aircraft = m_Mode.CasualtyLoadingAircraftFor(caller, casualty);
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(casualtyLife.ConnectionId)); SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(casualtyLife.ConnectionId));
if (!controller || ERR_CasualtyLoadingRules.EntityId(m_Mode) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(caller) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(casualty) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(m_Mode.ExtractionVehicle()) == RplId.Invalid()) return false; if (!controller || ERR_CasualtyLoadingRules.EntityId(m_Mode) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(caller) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(casualty) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(aircraft) == RplId.Invalid()) return false;
ERR_CasualtyLoadTicket ticket = new ERR_CasualtyLoadTicket(); ERR_CasualtyLoadTicket ticket = new ERR_CasualtyLoadTicket();
ticket.Caller = caller; ticket.Casualty = casualty; ticket.Aircraft = m_Mode.ExtractionVehicle(); ticket.Caller = caller; ticket.Casualty = casualty; ticket.Aircraft = aircraft;
ticket.Seat = AvailableSeat(casualty, ticket.Aircraft); ticket.Seat = AvailableSeat(casualty, ticket.Aircraft);
if (!ticket.Seat || ERR_CasualtyLoadingRules.EntityId(ticket.Seat.GetOwner()) == RplId.Invalid()) return false; if (!ticket.Seat || ERR_CasualtyLoadingRules.EntityId(ticket.Seat.GetOwner()) == RplId.Invalid()) return false;
ticket.CallerAccount = callerLife.AccountId; ticket.CasualtyAccount = casualtyLife.AccountId; ticket.CallerAccount = callerLife.AccountId; ticket.CasualtyAccount = casualtyLife.AccountId;
@@ -269,7 +280,7 @@ class ERR_CasualtyLoadingService
ERR_CasualtyLoadTicket ticket = Ticket(controller, nonce); ERR_CasualtyLoadTicket ticket = Ticket(controller, nonce);
if (!ticket || ticket.Committed) return; if (!ticket || ticket.Committed) return;
int now = System.GetTickCount(); int now = System.GetTickCount();
if (!ValidTicket(ticket) || !ERR_CasualtyLoadingRules.Within(ticket.Started, now, 5000) || !Settled(ticket.Casualty, now) || !ERR_CasualtyLoadingRules.Nearby(m_Mode, ticket.Caller, ticket.Casualty) || ticket.Seat.GetOccupant() || !ticket.Seat.IsCompartmentAccessible() || ticket.Seat.IsGetInLockedFor(ticket.Casualty) || !ticket.Seat.IsReservedBy(ticket.Casualty)) { Resolve(ticket, false); return; } if (!ValidTicket(ticket) || !ERR_CasualtyLoadingRules.Within(ticket.Started, now, 5000) || !Settled(ticket.Casualty, now) || !ERR_CasualtyLoadingRules.NearbyAircraft(m_Mode, ticket.Caller, ticket.Casualty, ticket.Aircraft) || ticket.Seat.GetOccupant() || !ticket.Seat.IsCompartmentAccessible() || ticket.Seat.IsGetInLockedFor(ticket.Casualty) || !ticket.Seat.IsReservedBy(ticket.Casualty)) { Resolve(ticket, false); return; }
ticket.Committed = true; ticket.CommittedAt = now; ticket.Committed = true; ticket.CommittedAt = now;
// Final authority/physical validation is the commit boundary. No retries. // Final authority/physical validation is the commit boundary. No retries.
controller.ERR_CommitCasualtyLoad(ERR_CasualtyLoadingRules.EntityId(m_Mode), nonce, ERR_CasualtyLoadingRules.EntityId(ticket.Caller), ERR_CasualtyLoadingRules.EntityId(ticket.Casualty), ERR_CasualtyLoadingRules.EntityId(ticket.Aircraft), ERR_CasualtyLoadingRules.EntityId(ticket.Seat.GetOwner()), ticket.Seat.GetCompartmentSlotID()); controller.ERR_CommitCasualtyLoad(ERR_CasualtyLoadingRules.EntityId(m_Mode), nonce, ERR_CasualtyLoadingRules.EntityId(ticket.Caller), ERR_CasualtyLoadingRules.EntityId(ticket.Casualty), ERR_CasualtyLoadingRules.EntityId(ticket.Aircraft), ERR_CasualtyLoadingRules.EntityId(ticket.Seat.GetOwner()), ticket.Seat.GetCompartmentSlotID());
@@ -288,11 +299,11 @@ class ERR_CasualtyLoadingService
{ {
// Death cannot revoke an in-flight commit. Keep the vacant seat reserved // Death cannot revoke an in-flight commit. Keep the vacant seat reserved
// while the captured body exists; its owner may still see the prior state. // while the captured body exists; its owner may still see the prior state.
return m_Mode && m_Mode.Active() && m_Mode.Phase() == ERR_Phase.EXTRACTION && ticket.Aircraft == m_Mode.ExtractionVehicle() && ERR_Actors.Alive(ticket.Aircraft) && ticket.Casualty != null && ERR_CasualtyLoadingRules.TransportBench(ticket.Seat, ticket.Aircraft); return m_Mode && m_Mode.Active() && m_Mode.Phase() == ERR_Phase.EXTRACTION && m_Mode.CasualtyLoadingTransport(ticket.Aircraft) && ERR_Actors.Alive(ticket.Aircraft) && ticket.Casualty != null && ERR_CasualtyLoadingRules.TransportBench(ticket.Seat, ticket.Aircraft);
} }
protected bool ValidTicket(ERR_CasualtyLoadTicket ticket) protected bool ValidTicket(ERR_CasualtyLoadTicket ticket)
{ {
return m_Mode && m_Mode.Active() && m_Mode.Phase() == ERR_Phase.EXTRACTION && ticket.Aircraft == m_Mode.ExtractionVehicle() && ERR_Actors.Alive(ticket.Aircraft) && ERR_Actors.Alive(ticket.Casualty) && SameLife(ticket.Caller, ticket.CallerAccount, ticket.CallerConnection) && SameLife(ticket.Casualty, ticket.CasualtyAccount, ticket.CasualtyConnection) && ERR_CasualtyLoadingRules.TransportBench(ticket.Seat, ticket.Aircraft); return m_Mode && m_Mode.Active() && m_Mode.Phase() == ERR_Phase.EXTRACTION && m_Mode.CasualtyLoadingTransport(ticket.Aircraft) && ERR_Actors.Alive(ticket.Aircraft) && ERR_Actors.Alive(ticket.Casualty) && SameLife(ticket.Caller, ticket.CallerAccount, ticket.CallerConnection) && SameLife(ticket.Casualty, ticket.CasualtyAccount, ticket.CasualtyConnection) && ERR_CasualtyLoadingRules.TransportBench(ticket.Seat, ticket.Aircraft);
} }
protected void NotifyResolution(ERR_CasualtyLoadTicket ticket, bool success) protected void NotifyResolution(ERR_CasualtyLoadTicket ticket, bool success)
{ {
@@ -353,7 +364,7 @@ class ERR_CasualtyLoadingService
// leaves room for replication. Timeouts do not issue compensating teleports. // leaves room for replication. Timeouts do not issue compensating teleports.
if (!ERR_CasualtyLoadingRules.Within(ticket.CommittedAt, now, 8000)) Resolve(ticket, false); if (!ERR_CasualtyLoadingRules.Within(ticket.CommittedAt, now, 8000)) Resolve(ticket, false);
} }
else if (!ERR_CasualtyLoadingRules.Within(ticket.Started, now, 5000) || !ERR_CasualtyLoadingRules.Nearby(m_Mode, ticket.Caller, ticket.Casualty)) Resolve(ticket, false); else if (!ERR_CasualtyLoadingRules.Within(ticket.Started, now, 5000) || !ERR_CasualtyLoadingRules.NearbyAircraft(m_Mode, ticket.Caller, ticket.Casualty, ticket.Aircraft)) Resolve(ticket, false);
} }
} }
void RosterChanged(int connection) { if (GetGame()) Tick(GetGame().GetWorld().GetWorldTime()); } void RosterChanged(int connection) { if (GetGame()) Tick(GetGame().GetWorld().GetWorldTime()); }
@@ -49,6 +49,7 @@ class ERR_ChatClient
if (m_Command) m_Command.Remove(OnCommand); if (m_Command) m_Command.Remove(OnCommand);
m_Command = null; m_Command = null;
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu(); ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu();
if (menu) menu.Close(); // A retained former controller must not close the current owner's panel.
if (menu && GetGame() && m_Controller == GetGame().GetPlayerController()) menu.Close();
} }
} }
@@ -0,0 +1,44 @@
class ERR_CommunityMapRecord
{
int wins = -1;
int losses = -1;
int completed = -1;
int partialMissions = -1;
bool Valid()
{
return wins >= 0 && wins <= 1000000000 && losses >= 0 && losses <= 1000000000 && completed == wins + losses && completed > 0 && partialMissions >= 0 && partialMissions <= completed;
}
string Line(string label)
{
int rate = Math.Round(100.0 * wins / completed);
return string.Format("%1: %2 wins | %3 failures | ", label, wins, losses) + rate.ToString() + "% win rate";
}
}
class ERR_CommunityStatistics
{
int schemaVersion;
string terrain;
string serverId;
string missionId;
string eventId;
string outcome;
ref ERR_CommunityMapRecord server;
ref ERR_CommunityMapRecord allServers;
bool Matches(string expectedServer, string expectedMission, string expectedEvent, string expectedTerrain, string expectedOutcome)
{
if (schemaVersion != 1 || serverId != expectedServer || missionId != expectedMission || eventId != expectedEvent || terrain != expectedTerrain || outcome != expectedOutcome) return false;
if (outcome != "victory" && outcome != "defeat") return false;
if (!server || !allServers || !server.Valid() || !allServers.Valid()) return false;
if (server.wins > allServers.wins || server.losses > allServers.losses) return false;
if (outcome == "victory" && server.wins == 0) return false;
if (outcome == "defeat" && server.losses == 0) return false;
return true;
}
string Summary()
{
string text = "This map's record (including this round)\n";
text += server.Line("This server") + "\n" + allServers.Line("All reporting servers");
if (allServers.partialMissions > 0) text += "\nIncludes rounds with partial statistics.";
return text;
}
}
@@ -7,7 +7,7 @@ modded class ERR_AdminMenu
{ {
Open(); Open();
ERR_AdminMenu menu = GetOpenMenu(); ERR_AdminMenu menu = GetOpenMenu();
if (menu) menu.ShowPage(3); if (menu) menu.ShowPage(ERR_AdminPage.MODS);
} }
override void OnMenuOpen() override void OnMenuOpen()
{ {
@@ -23,6 +23,8 @@ modded class ERR_AdminMenu
SCR_ButtonTextComponent enable = SCR_ButtonTextComponent.GetButtonText("Enable", row); SCR_ButtonTextComponent enable = SCR_ButtonTextComponent.GetButtonText("Enable", row);
SCR_ButtonTextComponent disable = SCR_ButtonTextComponent.GetButtonText("Disable", row); SCR_ButtonTextComponent disable = SCR_ButtonTextComponent.GetButtonText("Disable", row);
if (!enable || !disable) { Close(); return; } if (!enable || !disable) { Close(); return; }
ERR_SelectionStyle.Action(enable, true); ERR_SelectionStyle.Action(disable, false);
ERR_SelectionStyle.Status(row, "SelectionState", -1);
enable.m_OnClicked.Insert(OnModButton); disable.m_OnClicked.Insert(OnModButton); enable.m_OnClicked.Insert(OnModButton); disable.m_OnClicked.Insert(OnModButton);
} }
UpdateActions(); UpdateActions();
@@ -49,7 +51,11 @@ modded class ERR_AdminMenu
SCR_ButtonTextComponent disable = SCR_ButtonTextComponent.GetButtonText("Disable", row); SCR_ButtonTextComponent disable = SCR_ButtonTextComponent.GetButtonText("Disable", row);
if (enable) enable.SetEnabled(ready && m_ModSaved.Get(pack) == 0); if (enable) enable.SetEnabled(ready && m_ModSaved.Get(pack) == 0);
if (disable) disable.SetEnabled(ready && m_ModSaved.Get(pack) == 1); if (disable) disable.SetEnabled(ready && m_ModSaved.Get(pack) == 1);
int selected = -1; if (m_ModSaved.Contains(pack)) selected = m_ModSaved.Get(pack);
ERR_SelectionStyle.Pair(row, "Enable", "Disable", selected);
TextWidget state = TextWidget.Cast(row.FindAnyWidget("PackState")); TextWidget state = TextWidget.Cast(row.FindAnyWidget("PackState"));
int badge = -1; if (ready) badge = m_ModActive.Get(pack);
ERR_SelectionStyle.Status(row, "SelectionState", badge);
if (!state) continue; if (!state) continue;
if (!ready) state.SetText("Waiting for server settings..."); if (!ready) state.SetText("Waiting for server settings...");
else state.SetText("Current round: " + ERR_ContentRegistry.State(m_ModActive.Get(pack) == 1) + " | Saved preference: " + ERR_ContentRegistry.State(m_ModSaved.Get(pack) == 1)); else state.SetText("Current round: " + ERR_ContentRegistry.State(m_ModActive.Get(pack) == 1) + " | Saved preference: " + ERR_ContentRegistry.State(m_ModSaved.Get(pack) == 1));
@@ -103,6 +109,24 @@ modded class PauseMenuUI
m_ERRModsButton = SCR_ButtonTextComponent.Cast(widget.FindHandler(SCR_ButtonTextComponent)); m_ERRModsButton = SCR_ButtonTextComponent.Cast(widget.FindHandler(SCR_ButtonTextComponent));
if (m_ERRModsButton) m_ERRModsButton.m_OnClicked.Insert(ERR_OpenMods); if (m_ERRModsButton) m_ERRModsButton.m_OnClicked.Insert(ERR_OpenMods);
} }
// Vanilla disables this button while limited, but does not enable it again
// when an admin mode arrives asynchronously. Refresh from native permissions.
protected void ERR_RefreshGameMasterButton()
{
if (!ERR_GameMode.Get() || !m_EditorUnlimitedOpenButton) return;
SCR_EditorManagerEntity editor = SCR_EditorManagerEntity.GetInstance();
m_EditorUnlimitedOpenButton.SetEnabled(editor && !editor.IsLimited() && editor.CanOpen());
}
protected override void OnEditorModeChanged(SCR_EditorModeEntity modeEntity)
{
super.OnEditorModeChanged(modeEntity);
ERR_RefreshGameMasterButton();
}
override void OnMenuUpdate(float tDelta)
{
super.OnMenuUpdate(tDelta);
ERR_RefreshGameMasterButton();
}
protected void ERR_OpenMods() protected void ERR_OpenMods()
{ {
// Unpause local simulation before requesting an authenticated server snapshot. // Unpause local simulation before requesting an authenticated server snapshot.
@@ -1,4 +1,4 @@
// Separate, config-driven preferences. Missing newly installed options use their // Content preferences in the shared profile. Newly installed options use their
// authored defaults. Failed writes preserve both previous state and backup. // authored defaults. Failed writes preserve both previous state and backup.
class ERR_ContentSettings class ERR_ContentSettings
{ {
@@ -20,7 +20,7 @@ class ERR_ContentSettings
if (!Replication.IsServer()) { m_ReadOnly = true; return; } if (!Replication.IsServer()) { m_ReadOnly = true; return; }
map<string, int> loaded; map<string, int> loaded;
if (Read(m_Path, loaded) || Read(m_Path + ".bak", loaded)) m_Values = loaded; if (Read(m_Path, loaded) || Read(m_Path + ".bak", loaded)) m_Values = loaded;
else if (FileIO.FileExists(m_Path) || FileIO.FileExists(m_Path + ".bak")) else if (ERR_ProfileConfig.Exists(m_Path) || ERR_ProfileConfig.Exists(m_Path + ".bak"))
{ {
m_ReadOnly = true; m_ReadOnly = true;
Print("Co10 Escape Reintroduced: invalid content preferences; authored defaults active, existing files preserved", LogLevel.ERROR); Print("Co10 Escape Reintroduced: invalid content preferences; authored defaults active, existing files preserved", LogLevel.ERROR);
@@ -30,7 +30,10 @@ class ERR_ContentSettings
protected bool Read(string path, out map<string, int> values) protected bool Read(string path, out map<string, int> values)
{ {
JsonLoadContext json = new JsonLoadContext(); JsonLoadContext json = new JsonLoadContext();
if (!json.LoadFromFile(path)) return false; return ERR_ProfileConfig.Load(path, json) && Parse(json, values);
}
static bool Parse(JsonLoadContext json, out map<string, int> values)
{
values = new map<string, int>(); values = new map<string, int>();
foreach (ERR_ContentOption option : ERR_ContentRegistry.Definition().Options) foreach (ERR_ContentOption option : ERR_ContentRegistry.Definition().Options)
{ {
@@ -65,6 +68,11 @@ class ERR_ContentSettings
if (option == id) saved = value; if (option == id) saved = value;
json.WriteValue(option, saved); json.WriteValue(option, saved);
} }
if (m_Path == PATH)
{
if (!ERR_ProfileConfig.Save(PATH, json)) return false;
m_Values.Set(id, value); return true;
}
map<string, int> checked; map<string, int> checked;
if (!json.SaveToFile(m_Path + ".tmp") || !Read(m_Path + ".tmp", checked) || !Matches(checked, id, value)) return false; if (!json.SaveToFile(m_Path + ".tmp") || !Read(m_Path + ".tmp", checked) || !Matches(checked, id, value)) return false;
map<string, int> previous; map<string, int> previous;
@@ -72,7 +80,7 @@ class ERR_ContentSettings
if (hadPrevious && !FileIO.CopyFile(m_Path, m_Path + ".bak")) return false; if (hadPrevious && !FileIO.CopyFile(m_Path, m_Path + ".bak")) return false;
if (!FileIO.CopyFile(m_Path + ".tmp", m_Path) || !Read(m_Path, checked) || !Matches(checked, id, value)) if (!FileIO.CopyFile(m_Path + ".tmp", m_Path) || !Read(m_Path, checked) || !Matches(checked, id, value))
{ {
if (FileIO.FileExists(m_Path + ".bak")) FileIO.CopyFile(m_Path + ".bak", m_Path); if (ERR_ProfileConfig.Exists(m_Path + ".bak")) FileIO.CopyFile(m_Path + ".bak", m_Path);
else if (!hadPrevious) FileIO.DeleteFile(m_Path); else if (!hadPrevious) FileIO.DeleteFile(m_Path);
return false; return false;
} }
@@ -0,0 +1,15 @@
// Owned static aftermath; no aircraft, crew, flight or inventory systems.
class ERR_CrashSiteResources
{
static ResourceName SitePrefab() { return "{174211DDE2F6422B}Prefabs/ERR/CrashSite.et"; }
static ResourceName WreckPrefab() { return "{486DDD0B811047EE}Prefabs/ERR/CrashWreck.et"; }
static bool Bounds(out vector minimum, out vector maximum)
{
// Exact owned static wrapper measured in WORKBENCH on 2026-10-03.
// Native bounds: (-1.43719, 0.512451, -8.79651) to (1.4325, 3.98193, 3.94385).
// Expand by 1mm then round outward to millimetres; terrain survey adds its own clearance.
minimum = "-1.439 0.511 -8.798";
maximum = "1.434 3.983 3.945";
return true;
}
}
@@ -1,10 +1,19 @@
// A first clue identifies a stable search square, not the exact objective. // Normal clues use stable search squares to prevent triangulating the site.
// Stable grid centers prevent repeated updates from triangulating the site. // The optional anonymous-site mode intentionally exposes positions, never types.
class ERR_DiscoveryMapRules class ERR_DiscoveryMapRules
{ {
// -1 hidden, 0 anonymous exact position, 1 approximate clue, 2 identified.
static int DisplayKnowledge(int knowledge, bool prepared, bool prison, bool showAll)
{
if (prison || !prepared) return -1;
if (knowledge >= 2) return 2;
if (showAll) return 0;
if (knowledge == 1) return 1;
return -1;
}
static int Coordinate(float coordinate, int knowledge) static int Coordinate(float coordinate, int knowledge)
{ {
if (knowledge >= 2) return Math.Round(coordinate); if (knowledge == 0 || knowledge >= 2) return Math.Round(coordinate);
return Math.Floor(coordinate / 500.0) * 500 + 250; return Math.Floor(coordinate / 500.0) * 500 + 250;
} }
} }
@@ -15,6 +24,7 @@ class ERR_DiscoveryMapRules
class ERR_DiscoveryMarker : SCR_MapMarkerBase class ERR_DiscoveryMarker : SCR_MapMarkerBase
{ {
protected string m_Icon = "supplies"; protected string m_Icon = "supplies";
protected ref ERR_ObjectiveMapPresentation m_ERRPresentation;
static string Icon(ERR_LocationKind kind, string id) static string Icon(ERR_LocationKind kind, string id)
{ {
if (kind == ERR_LocationKind.EXTRACTION) return "helicopters"; if (kind == ERR_LocationKind.EXTRACTION) return "helicopters";
@@ -22,6 +32,7 @@ class ERR_DiscoveryMarker : SCR_MapMarkerBase
if (kind == ERR_LocationKind.MORTAR) return "artilleryFireMission"; if (kind == ERR_LocationKind.MORTAR) return "artilleryFireMission";
if (kind == ERR_LocationKind.OUTPOST) return "building"; if (kind == ERR_LocationKind.OUTPOST) return "building";
if (kind == ERR_LocationKind.HOSPITAL || id.Contains("medical")) return "medical"; if (kind == ERR_LocationKind.HOSPITAL || id.Contains("medical")) return "medical";
if (kind == ERR_LocationKind.PRISON) return "prison";
return "supplies"; return "supplies";
} }
void SetLocationIcon(ERR_LocationKind kind, string id, int knowledge = 2) void SetLocationIcon(ERR_LocationKind kind, string id, int knowledge = 2)
@@ -29,27 +40,37 @@ class ERR_DiscoveryMarker : SCR_MapMarkerBase
m_Icon = "mark-question"; m_Icon = "mark-question";
if (knowledge >= 2) m_Icon = Icon(kind, id); if (knowledge >= 2) m_Icon = Icon(kind, id);
ApplyIcon(); ApplyIcon();
RefreshPresentation();
} }
protected void ApplyIcon() protected void ApplyIcon()
{ {
SCR_MapMarkerWidgetComponent widget = GetMarkerComponent(); SCR_MapMarkerWidgetComponent widget = GetMarkerComponent();
if (!GetRootWidget() || !widget) return; if (!GetRootWidget() || !widget) return;
if (m_Icon == "mark-question") widget.SetImage("{E0248EBF90751BA5}UI/Textures/ERR/SiteMarkers.imageset", m_Icon);
{ widget.SetGlowImage("{E0248EBF90751BA5}UI/Textures/ERR/SiteMarkers.imageset", m_Icon);
widget.SetImage("{E23427CAC80DA8B7}UI/Textures/Icons/icons_mapMarkersUI.imageset", m_Icon); // Preserve the atlas colours instead of tinting the violet disc dark grey.
widget.SetGlowImage("{67B3A6DC2D712B52}UI/Textures/Icons/icons_mapMarkersUI-glow.imageset", m_Icon); widget.SetColor(Color.FromSRGBA(255, 255, 255, 255));
} }
else // Production authored objectives use a separate map UI element. They never
{ // enter the player-annotation registry or change its content restrictions.
widget.SetImage("{3262679C50EF4F01}UI/Textures/Icons/icons_wrapperUI.imageset", m_Icon); void StartPresentation()
widget.SetGlowImage("{00FE3DBDFD15227B}UI/Textures/Icons/icons_wrapperUI-glow.imageset", m_Icon); {
} if (!m_ERRPresentation) m_ERRPresentation = new ERR_ObjectiveMapPresentation();
widget.SetColor(Color.FromSRGBA(35, 45, 50, 255)); RefreshPresentation();
m_ERRPresentation.Start();
}
void RefreshPresentation()
{
if (!m_ERRPresentation) return;
int point[2];
GetWorldPos(point);
m_ERRPresentation.SetData(Vector(point[0], 0, point[1]), GetCustomText(), m_Icon);
} }
void ERR_DiscoveryMarker() { SetCanBeRemovedByOwner(false); } void ERR_DiscoveryMarker() { SetCanBeRemovedByOwner(false); }
void SetDiscoveryPosition(vector position, int knowledge) void SetDiscoveryPosition(vector position, int knowledge)
{ {
SetWorldPos(ERR_DiscoveryMapRules.Coordinate(position[0], knowledge), ERR_DiscoveryMapRules.Coordinate(position[2], knowledge)); SetWorldPos(ERR_DiscoveryMapRules.Coordinate(position[0], knowledge), ERR_DiscoveryMapRules.Coordinate(position[2], knowledge));
RefreshPresentation();
} }
override void OnCreateMarker(bool skipProfanityFilter = false) override void OnCreateMarker(bool skipProfanityFilter = false)
{ {
@@ -61,6 +82,8 @@ class ERR_DiscoveryMarker : SCR_MapMarkerBase
} }
override void OnDelete() override void OnDelete()
{ {
if (m_ERRPresentation) m_ERRPresentation.Stop();
m_ERRPresentation = null;
SCR_MapEntity.GetOnMapClose().Remove(OnMapClosed); SCR_MapEntity.GetOnMapClose().Remove(OnMapClosed);
SCR_MapEntity.GetOnLayerChanged().Remove(OnMapLayerChanged); SCR_MapEntity.GetOnLayerChanged().Remove(OnMapLayerChanged);
super.OnDelete(); super.OnDelete();
@@ -70,6 +93,7 @@ class ERR_DiscoveryMarker : SCR_MapMarkerBase
override void SetCustomText(string text) override void SetCustomText(string text)
{ {
super.SetCustomText(text); super.SetCustomText(text);
RefreshPresentation();
// Native SetCustomText updates data only; an already-open map needs its // Native SetCustomText updates data only; an already-open map needs its
// existing widget updated too. Closed maps use the saved text on creation. // existing widget updated too. Closed maps use the saved text on creation.
SCR_MapMarkerWidgetComponent widget = GetMarkerComponent(); SCR_MapMarkerWidgetComponent widget = GetMarkerComponent();
@@ -81,6 +105,10 @@ class ERR_DiscoveryMarker : SCR_MapMarkerBase
// must also leave the disabled list or map movement can resurrect it later. // must also leave the disabled list or map movement can resurrect it later.
modded class SCR_MapMarkerManagerComponent modded class SCR_MapMarkerManagerComponent
{ {
void ERR_InsertDiscoveryMarker(ERR_DiscoveryMarker marker)
{
if (marker) marker.StartPresentation();
}
void ERR_RemoveDiscoveryMarker(ERR_DiscoveryMarker marker) void ERR_RemoveDiscoveryMarker(ERR_DiscoveryMarker marker)
{ {
if (!marker || marker.GetMarkerID() != -1) return; if (!marker || marker.GetMarkerID() != -1) return;
@@ -90,12 +118,12 @@ modded class SCR_MapMarkerManagerComponent
} }
} }
// Read replicated discovery locally. A server-wide marker list never leaks // Read replicated discovery and the administrator-selected visibility locally.
// locations which have not been recovered through gameplay. // Anonymous markers never expose a site label or type before discovery.
modded class ERR_LocationComponent modded class ERR_LocationComponent
{ {
protected ref ERR_DiscoveryMarker m_ERRMarker; protected ref ERR_DiscoveryMarker m_ERRMarker;
protected int m_ERRShown; protected int m_ERRShown = -1;
protected string m_ERRShownLabel; protected string m_ERRShownLabel;
protected override void EOnInit(IEntity owner) protected override void EOnInit(IEntity owner)
@@ -107,23 +135,28 @@ modded class ERR_LocationComponent
protected void ERR_UpdateMarker() protected void ERR_UpdateMarker()
{ {
if (!GetGame().GetPlayerController()) return; if (!GetGame().GetPlayerController()) return;
if (Kind() == ERR_LocationKind.PRISON || Knowledge() < 1) ERR_GameMode mode = ERR_GameMode.Get();
bool showAll = mode && mode.ShowAllSites();
int shown = ERR_DiscoveryMapRules.DisplayKnowledge(Knowledge(), Prepared(), Kind() == ERR_LocationKind.PRISON, showAll);
if (shown < 0)
{ {
SCR_MapMarkerManagerComponent oldManager = SCR_MapMarkerManagerComponent.GetInstance(); SCR_MapMarkerManagerComponent oldManager = SCR_MapMarkerManagerComponent.GetInstance();
if (m_ERRMarker && oldManager) oldManager.ERR_RemoveDiscoveryMarker(m_ERRMarker); if (m_ERRMarker && oldManager) oldManager.ERR_RemoveDiscoveryMarker(m_ERRMarker);
else if (m_ERRMarker) m_ERRMarker.OnDelete(); else if (m_ERRMarker) m_ERRMarker.OnDelete();
m_ERRMarker = null; m_ERRShown = 0; m_ERRShownLabel = ""; m_ERRMarker = null; m_ERRShown = -1; m_ERRShownLabel = "";
return; return;
} }
if (Knowledge() == m_ERRShown && Label() == m_ERRShownLabel) return; if (m_ERRMarker) m_ERRMarker.RefreshPresentation();
if (m_ERRMarker && shown == m_ERRShown && Label() == m_ERRShownLabel) return;
SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance(); SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
if (!manager || !manager.GetMarkerConfig()) return; if (!manager || !manager.GetMarkerConfig()) return;
string label = "Unknown location (500 m area)"; string label = "Unknown location (500 m area)";
if (Knowledge() == 2) if (shown == 0) label = "Unknown site";
if (shown == 2)
{ {
if (!Label().IsEmpty()) label = Label(); if (!Label().IsEmpty()) label = Label();
else if (Kind() == ERR_LocationKind.RADIO) label = "Radio station"; else if (Kind() == ERR_LocationKind.RADIO) label = "Radio station";
else if (Kind() == ERR_LocationKind.EXTRACTION) label = "Initial helicopter pickup site"; else if (Kind() == ERR_LocationKind.EXTRACTION) label = "Extraction pickup - throw smoke here";
else if (Kind() == ERR_LocationKind.DEPOT) label = "Supply cache"; else if (Kind() == ERR_LocationKind.DEPOT) label = "Supply cache";
else if (Kind() == ERR_LocationKind.MORTAR) label = "Mortar site"; else if (Kind() == ERR_LocationKind.MORTAR) label = "Mortar site";
else label = Id(); else label = Id();
@@ -132,20 +165,18 @@ modded class ERR_LocationComponent
{ {
m_ERRMarker = new ERR_DiscoveryMarker(); m_ERRMarker = new ERR_DiscoveryMarker();
m_ERRMarker.SetType(SCR_EMapMarkerType.PLACED_CUSTOM); m_ERRMarker.SetType(SCR_EMapMarkerType.PLACED_CUSTOM);
m_ERRMarker.SetDiscoveryPosition(Position(), Knowledge()); m_ERRMarker.SetDiscoveryPosition(Position(), shown);
m_ERRMarker.SetMarkerConfigID(0); m_ERRMarker.SetMarkerConfigID(0);
m_ERRMarker.SetLocationIcon(Kind(), Id(), Knowledge()); m_ERRMarker.SetLocationIcon(Kind(), Id(), shown);
m_ERRMarker.SetCustomText(label); m_ERRMarker.SetCustomText(label);
manager.InsertStaticMarker(m_ERRMarker, true); manager.ERR_InsertDiscoveryMarker(m_ERRMarker);
} }
m_ERRMarker.SetDiscoveryPosition(Position(), Knowledge()); m_ERRMarker.SetDiscoveryPosition(Position(), shown);
m_ERRMarker.SetLocationIcon(Kind(), Id(), Knowledge()); m_ERRMarker.SetLocationIcon(Kind(), Id(), shown);
m_ERRMarker.SetCustomText(label); m_ERRMarker.SetCustomText(label);
// Reactivate a previously culled rough marker if the exact point moves // The objective presentation performs its own normal viewport culling.
// into the open map's view. Native update performs normal visibility checks. m_ERRMarker.RefreshPresentation();
SCR_MapEntity mapEntity = SCR_MapEntity.GetMapInstance(); m_ERRShown = shown;
if (mapEntity && mapEntity.IsOpen()) manager.SetStaticMarkerDisabled(m_ERRMarker, false);
m_ERRShown = Knowledge();
m_ERRShownLabel = Label(); m_ERRShownLabel = Label();
} }
protected override void OnDelete(IEntity owner) protected override void OnDelete(IEntity owner)
@@ -1,10 +1,86 @@
// Base mission discovery: recovering physical documents works without companions. // Base mission discovery: recovering physical documents works without companions.
// The Intel addon optionally assigns extra documents to enemy inventories. // The Intel addon optionally assigns extra documents to enemy inventories.
class ERR_IntelDeleteCallback : ScriptedInventoryOperationCallback
{
bool Finished;
bool Succeeded;
protected void Finish(bool success) { if (!Finished) { Finished = true; Succeeded = success; } }
protected override void OnComplete() { Finish(true); }
protected override void OnFailed() { Finish(false); }
protected override void OnAllComplete() { Finish(true); }
protected override void OnAllFailed() { Finish(false); }
}
class ERR_SpentIntelCleanup
{
IEntity Folder;
ref ERR_IntelDeleteCallback Operation;
int Attempts;
float NextAttempt;
float Deadline;
bool TimeoutReported;
}
class ERR_DiscoveryService class ERR_DiscoveryService
{ {
protected ERR_GameMode m_Mode; protected ERR_GameMode m_Mode;
protected ref set<IEntity> m_Spent = new set<IEntity>(); protected ref set<IEntity> m_Spent = new set<IEntity>();
void ERR_DiscoveryService(ERR_GameMode mode) { m_Mode = mode; } protected ref array<ref ERR_SpentIntelCleanup> m_Cleanup = {};
protected int m_CleanupCursor;
protected float m_CleanupNow;
protected bool m_Stopped;
protected int m_RadioIntelRequired;
protected int m_RadioIntelIdentified;
protected bool m_RadioIntelFinalized;
protected bool m_RadioIntelUnlocked;
void ERR_DiscoveryService(ERR_GameMode mode)
{
m_Mode = mode;
// Freeze the saved setting for this round; menu edits affect the next one.
m_RadioIntelRequired = mode.Settings().Get("radioIntelSiteCount");
m_RadioIntelUnlocked = m_RadioIntelRequired == 0;
}
protected bool CountsForRadioIntel(ERR_LocationComponent location)
{
if (!location || !location.Prepared() || location.Kind() == ERR_LocationKind.RADIO || location.Kind() == ERR_LocationKind.PRISON || location.Kind() == ERR_LocationKind.EXTRACTION) return false;
ERR_SiteService sites = m_Mode.Sites();
ERR_SiteRuntime site;
if (sites) site = sites.Find(location.Id());
return !site || !site.Definition || !site.Definition.RevealOnEscape;
}
protected void RefreshRadioIntel()
{
if (m_Stopped || !m_Mode || !Replication.IsServer()) return;
array<ERR_LocationComponent> locations = {};
m_Mode.Locations(locations);
set<string> available = new set<string>();
set<string> identified = new set<string>();
foreach (ERR_LocationComponent location : locations)
{
if (!CountsForRadioIntel(location)) continue;
available.Insert(location.Id());
if (location.Knowledge() == 2) identified.Insert(location.Id());
}
m_RadioIntelIdentified = identified.Count();
// Background generation can still add eligible sites. Finalize only once
// the active world's preparation has finished, including sparse terrains.
if (!m_RadioIntelFinalized && m_Mode.Active() && !m_Mode.WorldPreparing())
{
m_RadioIntelRequired = Math.Min(m_RadioIntelRequired, available.Count());
m_RadioIntelFinalized = true;
}
if (m_RadioIntelIdentified >= m_RadioIntelRequired) m_RadioIntelUnlocked = true;
}
bool RadioIntelAllowed() { RefreshRadioIntel(); return m_RadioIntelUnlocked; }
string RadioIntelStatus()
{
RefreshRadioIntel();
string state = "locked";
if (m_RadioIntelUnlocked) state = "unlocked";
string progress = string.Format("Radio intel %1: %2 / %3 non-radio sites identified this round.", state, m_RadioIntelIdentified, m_RadioIntelRequired);
if (!m_RadioIntelFinalized) progress += " Site preparation is still in progress.";
return progress;
}
// Shared ledger for recovered and administratively retired documents. // Shared ledger for recovered and administratively retired documents.
bool IsSpent(IEntity folder) { return folder && m_Spent.Contains(folder); } bool IsSpent(IEntity folder) { return folder && m_Spent.Contains(folder); }
bool CanMarkSpent(IEntity folder) bool CanMarkSpent(IEntity folder)
@@ -37,14 +113,74 @@ class ERR_DiscoveryService
void Tick(float now) void Tick(float now)
{ {
if (!Replication.IsServer()) return; if (!Replication.IsServer() || m_Stopped || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) return;
array<IEntity> players = {}; array<IEntity> players = {};
ERR_Actors.Players(players); ERR_Actors.Players(players);
m_CleanupNow = now;
foreach (IEntity player : players) Recover(player); foreach (IEntity player : players) Recover(player);
// Pending callbacks are observed only; even a missing callback never
// permits a second operation against the same physical item.
int budget = Math.Min(8, m_Cleanup.Count());
while (budget-- > 0)
{
if (m_CleanupCursor >= m_Cleanup.Count()) m_CleanupCursor = 0;
if (ProcessCleanup(m_Cleanup[m_CleanupCursor], players, now)) m_Cleanup.Remove(m_CleanupCursor);
else m_CleanupCursor++;
}
}
protected void QueueCleanup(IEntity folder, float now)
{
if (!folder || folder.FindComponent(ERR_DocumentComponent)) return;
// One request per newly spent folder; admission shares the spent-ledger
// bound. Administrative retirement does not enter this queue.
ERR_SpentIntelCleanup request = new ERR_SpentIntelCleanup();
request.Folder = folder;
request.Deadline = now + 60000;
m_Cleanup.Insert(request);
}
protected bool ProcessCleanup(ERR_SpentIntelCleanup request, array<IEntity> players, float now)
{
if (!request.Folder) return true;
if (request.Operation)
{
if (!request.Operation.Finished)
{
if (now >= request.Deadline && !request.TimeoutReported)
{
request.TimeoutReported = true;
Print("Reintroduced [intel]: spent-folder deletion still pending; no duplicate request", LogLevel.WARNING);
}
return false;
}
if (request.Operation.Succeeded) return true;
request.Operation = null;
request.NextAttempt = now + 5000;
}
if (now >= request.Deadline || request.Attempts >= 3)
{
Print("Reintroduced [intel]: spent-folder cleanup expired; item retained without granting more intel", LogLevel.WARNING);
return true;
}
if (request.Folder.FindComponent(ERR_DocumentComponent)) return true;
if (now < request.NextAttempt) return false;
foreach (IEntity player : players)
{
if (!m_Mode.Players() || !m_Mode.Players().CurrentActor(player) || !ERR_Actors.Alive(player)) continue;
if (!m_Mode.PlayerFaction() || SCR_FactionManager.SGetFaction(player) != m_Mode.PlayerFaction()) continue;
if (!ERR_InfantryAmmoTransfer.StoredUnder(request.Folder, player)) continue;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(player.FindComponent(InventoryStorageManagerComponent));
if (!inventory || !inventory.Contains(request.Folder)) continue;
request.Attempts++;
request.NextAttempt = now + 5000;
request.Operation = new ERR_IntelDeleteCallback();
if (!inventory.TryDeleteItem(request.Folder, request.Operation)) request.Operation = null;
return false;
}
return false;
} }
protected void Recover(IEntity player) protected void Recover(IEntity player)
{ {
if (!Replication.IsServer() || !m_Mode || !m_Mode.Players() || !m_Mode.Players().CurrentActor(player) || !ERR_Actors.Alive(player)) return; if (!Replication.IsServer() || m_Stopped || !m_Mode || !m_Mode.Players() || !m_Mode.Players().CurrentActor(player) || !ERR_Actors.Alive(player)) return;
if (!m_Mode.PlayerFaction() || SCR_FactionManager.SGetFaction(player) != m_Mode.PlayerFaction()) return; if (!m_Mode.PlayerFaction() || SCR_FactionManager.SGetFaction(player) != m_Mode.PlayerFaction()) return;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(player.FindComponent(InventoryStorageManagerComponent)); InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(player.FindComponent(InventoryStorageManagerComponent));
if (!inventory) return; if (!inventory) return;
@@ -53,15 +189,29 @@ class ERR_DiscoveryService
foreach (IEntity folder : folders) foreach (IEntity folder : folders)
{ {
if (!folder || m_Spent.Contains(folder) || m_Spent.Count() >= 4096) continue; if (!folder || m_Spent.Contains(folder) || m_Spent.Count() >= 4096) continue;
bool mineRecovered = false;
if (m_Mode.MineIntel()) mineRecovered = m_Mode.MineIntel().Recover(player, folder);
array<ERR_LocationComponent> locations = {}; array<ERR_LocationComponent> locations = {};
m_Mode.Locations(locations); m_Mode.Locations(locations);
array<ERR_LocationComponent> eligible = {}; array<ERR_LocationComponent> eligible = {};
ERR_DocumentComponent document = ERR_DocumentComponent.Cast(folder.FindComponent(ERR_DocumentComponent)); ERR_DocumentComponent document = ERR_DocumentComponent.Cast(folder.FindComponent(ERR_DocumentComponent));
string sealedTarget = m_Mode.Evidence().Target(folder); string sealedTarget = m_Mode.Evidence().Target(folder);
bool radioAllowed = RadioIntelAllowed();
bool redirectRadio;
if (!radioAllowed && !document && !sealedTarget.IsEmpty())
{
foreach (ERR_LocationComponent target : locations)
if (target && target.Id() == sealedTarget && target.Kind() == ERR_LocationKind.RADIO) redirectRadio = true;
}
foreach (ERR_LocationComponent location : locations) foreach (ERR_LocationComponent location : locations)
{ {
if (!location || location.Knowledge() >= 2) continue; if (!location || location.Knowledge() >= 2) continue;
if (!sealedTarget.IsEmpty() && location.Id() != sealedTarget) continue; if (!radioAllowed && location.Kind() == ERR_LocationKind.RADIO)
{
if (document && location.Id() == document.TargetId()) m_Mode.ERR_RadioIntelWaiting();
continue;
}
if (!redirectRadio && !sealedTarget.IsEmpty() && location.Id() != sealedTarget) continue;
if (document) if (document)
{ {
if (location.Id() == document.TargetId()) eligible.Insert(location); if (location.Id() == document.TargetId()) eligible.Insert(location);
@@ -70,15 +220,24 @@ class ERR_DiscoveryService
} }
if (eligible.IsEmpty()) if (eligible.IsEmpty())
{ {
if (!document && MarkSpent(folder)) m_Mode.ERR_IntelExhausted(!sealedTarget.IsEmpty()); if (!document && radioAllowed && !m_Mode.WorldPreparing() && MarkSpent(folder))
{
QueueCleanup(folder, m_CleanupNow);
if (!mineRecovered) m_Mode.ERR_IntelExhausted(!sealedTarget.IsEmpty());
}
continue; continue;
} }
ERR_LocationComponent chosen = eligible.GetRandomElement(); ERR_LocationComponent chosen = eligible.GetRandomElement();
if (!chosen.Discover(Math.RandomInt(0, 100), m_Mode.Settings().Runtime("intelRevealChancePercent"))) continue; if (!chosen.Discover(Math.RandomInt(0, 100), m_Mode.Settings().Runtime("intelRevealChancePercent"))) continue;
if (redirectRadio) m_Mode.Evidence().Retarget(folder, chosen.Id());
m_Spent.Insert(folder); m_Spent.Insert(folder);
PrintFormat("Reintroduced [intel]: player %1 recovered folder; location=%2 knowledge=%3", GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(player), chosen.Id(), chosen.Knowledge()); PrintFormat("Reintroduced [intel]: player %1 recovered folder; location=%2 knowledge=%3", GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(player), chosen.Id(), chosen.Knowledge());
// Preserve issued evidence: a later corpse inspection must still
// report this site's missing document after the physical item is gone.
if (!document) QueueCleanup(folder, m_CleanupNow);
} }
} }
void Stop() { m_Spent.Clear(); } void Stop() { m_Stopped = true; m_Cleanup.Clear(); m_Spent.Clear(); }
} }
@@ -24,25 +24,82 @@ class ERR_EpinephrineEffect : SCR_DotDamageEffect
} }
override EDamageType GetDefaultDamageType() { return EDamageType.HEALING; } override EDamageType GetDefaultDamageType() { return EDamageType.HEALING; }
} }
// Separate stronger field injector. Standard epinephrine above keeps its
// resilience-over-time behavior; neither item restores a dead character.
class ERR_CombatEpinephrineRules
{
static float Heal(float current, bool leg)
{
float amount = 0.15;
if (leg) amount = 0.5;
return Math.Min(current + amount, 1.0);
}
static float Blood(float current, float threshold) { return Math.Min(1, current + threshold + 0.01); }
}
[BaseContainerProps()]
class ERR_ConsumableCombatEpinephrine : SCR_ConsumableMorphine
{
override bool CanApplyEffect(notnull IEntity target, notnull IEntity user, out SCR_EConsumableFailReason failReason)
{
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(target.FindComponent(SCR_CharacterDamageManagerComponent));
if (!damage || damage.GetState() == EDamageState.DESTROYED) { failReason = SCR_EConsumableFailReason.UNDAMAGED; return false; }
if (damage.IsBleeding()) return true;
HitZone blood = damage.GetBloodHitZone(), resilience = damage.GetResilienceHitZone();
if ((blood && blood.GetHealthScaled() < 1) || (resilience && resilience.GetHealthScaled() < 1)) return true;
array<HitZone> zones = {}; damage.GetPhysicalHitZones(zones);
foreach (HitZone zone : zones) if (zone && zone.GetHealthScaled() < 1) return true;
failReason = SCR_EConsumableFailReason.UNDAMAGED; return false;
}
}
class ERR_CombatEpinephrineEffect : SCR_InstantDamageEffect
{
override EDamageType GetDefaultDamageType() { return EDamageType.HEALING; }
override void OnEffectAdded(SCR_ExtendedDamageManagerComponent dmgManager)
{
super.OnEffectAdded(dmgManager);
if (!Replication.IsServer()) return;
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(dmgManager);
if (!damage || damage.GetState() == EDamageState.DESTROYED) return;
foreach (ECharacterHitZoneGroup group : {ECharacterHitZoneGroup.HEAD, ECharacterHitZoneGroup.UPPERTORSO, ECharacterHitZoneGroup.LOWERTORSO, ECharacterHitZoneGroup.LEFTARM, ECharacterHitZoneGroup.RIGHTARM, ECharacterHitZoneGroup.LEFTLEG, ECharacterHitZoneGroup.RIGHTLEG}) damage.RemoveGroupBleeding(group);
SCR_CharacterBloodHitZone blood = damage.GetBloodHitZone();
if (blood && blood.GetHealthScaled() <= blood.GetDamageStateThreshold(ECharacterBloodState.FAINTING))
blood.SetHealthScaled(ERR_CombatEpinephrineRules.Blood(blood.GetHealthScaled(), blood.GetDamageStateThreshold(ECharacterBloodState.UNCONSCIOUS)));
SCR_CharacterResilienceHitZone resilience = damage.GetResilienceHitZone();
if (resilience) resilience.SetHealthScaled(1);
array<HitZone> zones = {}; damage.GetPhysicalHitZones(zones);
foreach (HitZone zone : zones)
{
SCR_CharacterHitZone physical = SCR_CharacterHitZone.Cast(zone);
if (!physical || zone.GetHealthScaled() >= 1) continue;
int group = physical.GetHitZoneGroup();
zone.SetHealthScaled(ERR_CombatEpinephrineRules.Heal(zone.GetHealthScaled(), group == ECharacterHitZoneGroup.LEFTLEG || group == ECharacterHitZoneGroup.RIGHTLEG));
}
CharacterControllerComponent controller = CharacterControllerComponent.Cast(damage.GetOwner().FindComponent(CharacterControllerComponent));
if (controller && controller.IsUnconscious()) controller.SetUnconscious(false);
}
}
// The held injector uses the existing native injection interaction and animation. // The held injector uses the existing native injection interaction and animation.
// Only its displayed name changes; ordinary morphine keeps its native behavior. // Only its displayed name changes; ordinary morphine keeps its native behavior.
modded class SCR_MorphineUserAction modded class SCR_MorphineUserAction
{ {
protected bool m_ERRHoldingEpinephrine; protected bool m_ERRHoldingEpinephrine;
protected bool m_ERRHoldingCombatEpinephrine;
override bool CanBeShownScript(IEntity user) override bool CanBeShownScript(IEntity user)
{ {
m_ERRHoldingEpinephrine = false; m_ERRHoldingEpinephrine = false; m_ERRHoldingCombatEpinephrine = false;
bool shown = super.CanBeShownScript(user); bool shown = super.CanBeShownScript(user);
ChimeraCharacter character = ChimeraCharacter.Cast(user); ChimeraCharacter character = ChimeraCharacter.Cast(user);
if (shown && character) if (shown && character)
{ {
SCR_ConsumableItemComponent item = GetConsumableComponent(character); SCR_ConsumableItemComponent item = GetConsumableComponent(character);
m_ERRHoldingEpinephrine = item && ERR_ConsumableEpinephrine.Cast(item.GetConsumableEffect()); m_ERRHoldingEpinephrine = item && ERR_ConsumableEpinephrine.Cast(item.GetConsumableEffect());
m_ERRHoldingCombatEpinephrine = item && ERR_ConsumableCombatEpinephrine.Cast(item.GetConsumableEffect());
} }
return shown; return shown;
} }
override bool GetActionNameScript(out string outName) override bool GetActionNameScript(out string outName)
{ {
if (m_ERRHoldingCombatEpinephrine) { outName = "Administer combat epinephrine"; return true; }
if (m_ERRHoldingEpinephrine) { outName = "Administer epinephrine"; return true; } if (m_ERRHoldingEpinephrine) { outName = "Administer epinephrine"; return true; }
return super.GetActionNameScript(outName); return super.GetActionNameScript(outName);
} }
@@ -11,7 +11,7 @@ class ERR_EscapeGameOverScreenInfo : SCR_BaseGameOverScreenInfo
{ {
string title = "Round ended"; string title = "Round ended";
ERR_PersonalEscapeResult result = PersonalResult(playerId); ERR_PersonalEscapeResult result = PersonalResult(playerId);
if (result == ERR_PersonalEscapeResult.ESCAPED) title = "You escaped Everon"; if (result == ERR_PersonalEscapeResult.ESCAPED) title = "You escaped";
else if (result == ERR_PersonalEscapeResult.MISSED) title = "The helicopter escaped"; else if (result == ERR_PersonalEscapeResult.MISSED) title = "The helicopter escaped";
#ifdef WORKBENCH #ifdef WORKBENCH
PrintFormat("ERR RESULT UI: player=%1 title=%2", playerId, title); PrintFormat("ERR RESULT UI: player=%1 title=%2", playerId, title);
@@ -33,13 +33,22 @@ class ERR_EscapeGameOverScreenInfo : SCR_BaseGameOverScreenInfo
modded class SCR_GameOverScreenContentUIComponent modded class SCR_GameOverScreenContentUIComponent
{ {
protected bool m_ERREscapeContent; protected bool m_ERREscapeContent;
protected ERR_GameMode m_ERRStatisticsMode;
protected ERR_GameMode m_ERRFailureMode; protected ERR_GameMode m_ERRFailureMode;
override void InitContent(SCR_GameOverScreenUIContentData endScreenUIContent) override void InitContent(SCR_GameOverScreenUIContentData endScreenUIContent)
{ {
SCR_PlayerController.ERR_OnEscapeResultChanged().Remove(ERR_RefreshEscapeContent); SCR_PlayerController.ERR_OnEscapeResultChanged().Remove(ERR_RefreshEscapeContent);
if (m_ERRFailureMode) m_ERRFailureMode.OnEndReasonChanged().Remove(ERR_RefreshFailureContent); if (m_ERRFailureMode) m_ERRFailureMode.OnEndReasonChanged().Remove(ERR_RefreshFailureContent);
m_ERRFailureMode = null; m_ERRFailureMode = null;
if (m_ERRStatisticsMode) m_ERRStatisticsMode.ERR_OnStatisticsChanged().Remove(ERR_RefreshStatistics);
m_ERRStatisticsMode = null;
super.InitContent(endScreenUIContent); super.InitContent(endScreenUIContent);
if (ERR_EscapeGameOverScreenInfo.Cast(endScreenUIContent.m_GameOverInfo) || ERR_FailureGameOverScreenInfo.Cast(endScreenUIContent.m_GameOverInfo))
{
m_ERRStatisticsMode = ERR_GameMode.Get();
if (m_ERRStatisticsMode) m_ERRStatisticsMode.ERR_OnStatisticsChanged().Insert(ERR_RefreshStatistics);
ERR_RefreshStatistics();
}
if (ERR_FailureGameOverScreenInfo.Cast(endScreenUIContent.m_GameOverInfo)) if (ERR_FailureGameOverScreenInfo.Cast(endScreenUIContent.m_GameOverInfo))
{ {
m_ERRFailureMode = ERR_GameMode.Get(); m_ERRFailureMode = ERR_GameMode.Get();
@@ -71,10 +80,21 @@ modded class SCR_GameOverScreenContentUIComponent
TextWidget debrief = TextWidget.Cast(m_wRoot.FindAnyWidget(m_sDebriefingName)); TextWidget debrief = TextWidget.Cast(m_wRoot.FindAnyWidget(m_sDebriefingName));
if (debrief) debrief.SetText(info.GetDebriefing(-1, null)); if (debrief) debrief.SetText(info.GetDebriefing(-1, null));
} }
protected void ERR_RefreshStatistics()
{
if (!m_wRoot || !m_ERRStatisticsMode || m_ERRStatisticsMode != ERR_GameMode.Get()) return;
TextWidget text = TextWidget.Cast(m_wRoot.FindAnyWidget("RoundStatistics"));
if (!text) return;
string summary = m_ERRStatisticsMode.ERR_RoundSummary();
if (summary.IsEmpty()) summary = "Waiting for mission statistics...";
text.SetText(summary);
}
override void HandlerDeattached(Widget w) override void HandlerDeattached(Widget w)
{ {
SCR_PlayerController.ERR_OnEscapeResultChanged().Remove(ERR_RefreshEscapeContent); SCR_PlayerController.ERR_OnEscapeResultChanged().Remove(ERR_RefreshEscapeContent);
m_ERREscapeContent = false; m_ERREscapeContent = false;
if (m_ERRStatisticsMode) m_ERRStatisticsMode.ERR_OnStatisticsChanged().Remove(ERR_RefreshStatistics);
m_ERRStatisticsMode = null;
if (m_ERRFailureMode) m_ERRFailureMode.OnEndReasonChanged().Remove(ERR_RefreshFailureContent); if (m_ERRFailureMode) m_ERRFailureMode.OnEndReasonChanged().Remove(ERR_RefreshFailureContent);
m_ERRFailureMode = null; m_ERRFailureMode = null;
super.HandlerDeattached(w); super.HandlerDeattached(w);
@@ -0,0 +1,50 @@
// Finite objective announcements. Text always arrives; optional TTS shares the
// configured speech service. No provider key is replicated to players.
class ERR_ExtractionSpeechReceiver : ERR_SpeechReceiver
{
protected ERR_GameMode m_Mode;
protected int m_Stage;
void ERR_ExtractionSpeechReceiver(ERR_GameMode mode, int stage) { m_Mode = mode; m_Stage = stage; }
override void Complete(string url, string text)
{
if (GetGame() && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active() && m_Mode.PickupStage() == m_Stage)
m_Mode.DeliverExtractionRadio(text, url);
}
}
modded class ERR_GameMode
{
[RplProp()] protected int m_PickupStage;
int PickupStage() { return m_PickupStage; }
void PublishPickupStage(int stage)
{
if (!Replication.IsServer() || stage == m_PickupStage) return;
m_PickupStage = stage; Replication.BumpMe();
}
void DeliverExtractionRadio(string text, string url = "")
{
if (!Replication.IsServer() || !Active()) return;
array<int> players = {}; GetGame().GetPlayerManager().GetPlayers(players);
foreach (int id : players)
{
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
if (controller) controller.ERR_ExtractionRadio(text, url);
}
}
}
modded class SCR_PlayerController
{
void ERR_ExtractionRadio(string text, string url)
{
if (Replication.IsServer()) Rpc(ERR_ReceiveExtractionRadio, text, url);
}
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_ReceiveExtractionRadio(string text, string url)
{
if (this != GetGame().GetPlayerController() || !ERR_GameMode.Get()) return;
if (!ERR_GameMode.Get().ERR_RecordingsEnabled() && !ERR_BakedSpeech.Graph(url).IsEmpty()) url = "";
if (!url.IsEmpty()) { GetGame().ERR_PlaySpeechPreview(url, text, "Extraction radio"); return; }
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
if (hints) hints.ShowCustom(text, "Extraction radio", 20);
SCR_ChatComponent.RadioProtocolMessage(text);
}
}
@@ -1,16 +1,250 @@
enum ERR_ExtractionState { WAITING, ESCAPED, LOST } enum ERR_ExtractionState { WAITING, ESCAPED, LOST }
// Mission progression consumes the result; this service never spawns a vehicle. class ERR_RescueRules
{
static float BoardingSince(int alive, int aboard, float since, float now)
{
if (alive <= 0 || aboard != alive || now < 0) return -1;
if (since < 0 || now < since) return now;
return since;
}
static float AirborneSince(bool airborne, float since, float now)
{
if (!airborne || now < 0) return -1;
if (since < 0 || now < since) return now;
return since;
}
static bool FlightComplete(float since, float now)
{
return since >= 0 && now >= since && now - since >= 30000;
}
static bool CanDepart(float since, float now)
{
return since >= 0 && now >= since && now - since >= 8000;
}
}
// Round-owned smoke signal, friendly rescue dispatch, boarding and escape result.
class ERR_ExtractionService class ERR_ExtractionService
{ {
static const int REQUIRED_SEATS = 12; static const int REQUIRED_SEATS = 12;
protected ref ERR_PlayerRoster m_Roster; protected ref ERR_PlayerRoster m_Roster;
protected ERR_GameMode m_Mode;
protected vector m_Pickup;
protected bool m_PickupConfigured, m_Signal;
protected ref array<IEntity> m_PreviousSignals = {};
protected int m_Stage, m_DispatchAttempts;
protected float m_NextDispatch, m_AllAboardSince = -1;
protected ref array<ERR_RescueFlightComponent> m_Escorts = {};
protected ERR_RescueFlightComponent m_Transport;
protected ref array<ERR_RescueFlightComponent> m_Transports = {};
protected ref array<vector> m_TransportPickups = {};
protected string m_RescueFaction;
protected string m_RadioPending;
protected float m_RadioUntil;
bool CarryingSurvivorsToSafety()
{
if (m_Stopped || m_Stage != 5 || !FleetAlive()) return false;
array<ref ERR_PlayerLife> lives = {}; m_Roster.Participants(lives);
int aboard;
foreach (ERR_PlayerLife life : lives)
{
if (!ERR_Actors.Alive(life.Actor)) continue;
if (!IsTransport(Vehicle.Cast(CompartmentAccessComponent.GetVehicleIn(life.Actor)))) return false;
aboard++;
}
return aboard > 0;
}
bool PickupConfigured() { return m_PickupConfigured; }
vector PickupPosition() { return m_Pickup; }
bool ConfigurePickup(vector position)
{
if (!Replication.IsServer() || m_PickupConfigured || m_Begun || m_Stopped) return false;
m_Mode = ERR_GameMode.Get(); if (!m_Mode) return false;
if (!m_Mode.PlayerFaction()) return false;
m_RescueFaction = m_Mode.PlayerFaction().GetFactionKey();
if (!ERR_RescueLanding.Survey(position, ERR_RescueAssets.TransportCount(m_RescueFaction), m_TransportPickups))
{
PrintFormat("Reintroduced [extraction]: pickup rejected before preparation; faction=%1 required transports=%2 primary=%3; no safe secondary landing footprint", m_RescueFaction, ERR_RescueAssets.TransportCount(m_RescueFaction), position);
return false;
}
m_Pickup = position; m_PickupConfigured = true; return true;
}
protected void Stage(int value, string message)
{
m_Stage = value; m_Mode.PublishPickupStage(value);
PrintFormat("Reintroduced [extraction]: stage=%1 %2", value, message);
string bundled = ERR_BakedSpeech.Radio(message);
string audio = bundled;
if (!m_Mode.ERR_RecordingsEnabled()) audio = "";
m_Mode.DeliverExtractionRadio(message, audio);
if (!bundled.IsEmpty()) { m_RadioPending = ""; return; }
m_RadioPending = message; m_RadioUntil = GetGame().GetWorld().GetWorldTime() + 30000;
}
void PreparingRoute()
{
if (!Replication.IsServer() || !m_Mode || m_Stopped) return;
Stage(0, "Escapists, this is rescue command. We are preparing your escape route and assembling transports with armed escort helicopters. Hold the radio position until preparations are complete. Keep your smoke grenades ready and stand by for pickup coordinates.");
}
void OpenPickup()
{
if (!Replication.IsServer() || !m_Mode || m_Stage != 0 || m_Stopped) return;
GetGame().GetWorld().QueryEntitiesBySphere(m_Pickup, 25, PreviousSmoke, null, EQueryEntitiesFlags.ALL);
Stage(1, "Your escape route is ready. The exact pickup point is marked on your map. Gather your team there and throw a smoke grenade within 25 metres of the mark. We will send the transport and armed escorts when we see your smoke.");
}
protected void Radio(float now)
{
if (m_RadioPending.IsEmpty()) return;
if (now > m_RadioUntil) { m_RadioPending = ""; return; }
if (m_Mode.ERR_Speech() && m_Mode.ERR_Speech().Request(m_RadioPending, new ERR_ExtractionSpeechReceiver(m_Mode, m_Stage))) m_RadioPending = "";
}
protected bool PreviousSmoke(IEntity entity)
{
if (!entity || entity.GetParent() || m_PreviousSignals.Count() >= 32) return true;
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(entity.FindComponent(BaseWeaponComponent));
BaseTriggerComponent trigger = BaseTriggerComponent.Cast(entity.FindComponent(BaseTriggerComponent));
if (weapon && weapon.GetWeaponType() == EWeaponType.WT_SMOKEGRENADE && trigger && trigger.WasTriggered()) m_PreviousSignals.Insert(entity);
return true;
}
protected bool Smoke(IEntity entity)
{
if (!entity || entity.GetParent() || m_PreviousSignals.Contains(entity)) return true;
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(entity.FindComponent(BaseWeaponComponent));
BaseTriggerComponent trigger = BaseTriggerComponent.Cast(entity.FindComponent(BaseTriggerComponent));
if (!weapon || weapon.GetWeaponType() != EWeaponType.WT_SMOKEGRENADE || !trigger || !trigger.WasTriggered()) return true;
Instigator instigator = trigger.GetInstigator();
if (!instigator || !m_Roster.CurrentActor(instigator.GetInstigatorEntity())) return true;
// Authority-side grenade state, independent of a dedicated server's renderer.
// Canisters already triggered at this point before the reveal are ignored.
if (Math.AbsFloat(entity.GetOrigin()[1] - m_Pickup[1]) > 8) return true;
m_Signal = true; return false;
}
protected bool Approach(out vector position)
{
vector low, high; GetGame().GetWorldEntity().GetWorldBounds(low, high);
array<IEntity> players = {}; ERR_Actors.Players(players, true);
// Smaller terrains can use a shorter approach; reserve room for both escorts.
for (int band = 0; band < 3; band++)
for (int attempt = 0; attempt < 12; attempt++)
{
float distance = 1600 - band * 400;
float angle = attempt * Math.PI2 / 12;
position = m_Pickup + Vector(Math.Cos(angle) * distance, 0, Math.Sin(angle) * distance);
if (position[0] < low[0] + 400 || position[0] > high[0] - 400 || position[2] < low[2] + 400 || position[2] > high[2] - 400) continue;
bool near = false;
foreach (IEntity player : players) if (vector.DistanceXZ(player.GetOrigin(), position) < Math.Min(1000, distance - 300)) near = true;
if (near) continue;
position[1] = SCR_TerrainHelper.GetTerrainY(position, GetGame().GetWorld(), noUnderwater: true) + 200;
return true;
}
return false;
}
protected bool LandingPads()
{
int count = ERR_RescueAssets.TransportCount(m_RescueFaction);
// A vehicle or other obstruction may occupy the reserved extra footprint
// during the round. Reuse it only while clear, otherwise survey once again
// before allocating any aircraft. The smoke marker is not rechecked against
// the signalling players gathered there; native landing safety remains active.
if (m_TransportPickups.Count() == count)
if (count == 1 || ERR_RescueLanding.SecondaryClear(m_TransportPickups[1])) return true;
return ERR_RescueLanding.Survey(m_Pickup, count, m_TransportPickups);
}
protected ERR_RescueFlightComponent SpawnFlight(vector origin, int serial, bool escort)
{
ResourceName prefab = ERR_RescueAssets.Aircraft(m_RescueFaction, escort);
Vehicle aircraft = Vehicle.Cast(ERR_Actors.Spawn(prefab, origin));
if (!aircraft) return null;
m_Mode.Own(aircraft);
ERR_RescueFlightComponent flight = ERR_RescueFlightComponent.Cast(aircraft.FindComponent(ERR_RescueFlightComponent));
if (!flight) { ERR_Actors.DeleteSpawned(aircraft); return null; }
vector pickup = m_Pickup;
if (!escort) pickup = m_TransportPickups[serial];
flight.StartRescue(m_Mode, origin, pickup, serial, escort);
PrintFormat("Reintroduced [extraction]: selected faction=%1 escort=%2 serial=%3 prefab=%4 pickup=%5", m_RescueFaction, escort, serial, prefab, pickup);
return flight;
}
protected void Dispatch(float now)
{
if (now < m_NextDispatch) return;
m_NextDispatch = now + 10000;
if (++m_DispatchAttempts > 6) { Stage(6, "Rescue command cannot establish a safe approach. Extraction has failed."); Resolve(ERR_ExtractionState.LOST); return; }
vector origin; if (!LandingPads() || !Approach(origin)) return;
for (int transportIndex = 0; transportIndex < ERR_RescueAssets.TransportCount(m_RescueFaction); transportIndex++)
{
vector transportOrigin = origin + (m_TransportPickups[transportIndex] - m_Pickup) + Vector(0, transportIndex * 40, 0);
ERR_RescueFlightComponent transport = SpawnFlight(transportOrigin, transportIndex, false);
if (!transport)
{
foreach (ERR_RescueFlightComponent partial : m_Transports) if (partial) partial.Release(false);
Stage(6, "Rescue command could not assemble the required transport capacity. Extraction has failed."); Resolve(ERR_ExtractionState.LOST); return;
}
m_Transports.Insert(transport);
}
m_Transport = m_Transports[0];
if (!Assign(m_Transport.Aircraft())) { foreach (ERR_RescueFlightComponent invalid : m_Transports) invalid.Release(false); Resolve(ERR_ExtractionState.LOST); return; }
// Different approach lanes and altitudes keep rotor discs well separated.
for (int index = 0; index < 2; index++)
{
float side = -180; if (index == 1) side = 180;
vector escortOrigin = origin + Vector(side, 60 + index * 40, 180);
ERR_RescueFlightComponent escort = SpawnFlight(escortOrigin, index + m_Transports.Count(), true);
if (escort) m_Escorts.Insert(escort);
}
string inbound = "Smoke confirmed. Rescue transport and armed cover are inbound. Clear the marked landing point. Bring your wounded; we wait for all surviving prisoners to board.";
if (m_Transports.Count() > 1) inbound = "Smoke confirmed. Two rescue transports and armed cover are inbound. The helicopters will land separately near the marker. Use either transport and bring your wounded; both wait until everyone is aboard.";
Stage(3, inbound);
}
protected void RescueTick(float now)
{
Radio(now);
if (m_Stage == 1)
{
GetGame().GetWorld().QueryEntitiesBySphere(m_Pickup, 25, Smoke, null, EQueryEntitiesFlags.ALL);
if (m_Signal) Stage(2, "Signal received. Launching the rescue flight. Keep the landing point clear.");
}
if (m_Stage == 2) Dispatch(now);
if (!m_Transport) return;
foreach (ERR_RescueFlightComponent transport : m_Transports) if (transport) transport.Update(now);
foreach (ERR_RescueFlightComponent escort : m_Escorts) if (escort && !escort.Lost()) escort.Update(now);
if (!FleetAlive()) { Stage(6, "A rescue transport is down or unable to continue. Extraction has failed."); Resolve(ERR_ExtractionState.LOST); return; }
bool landed = FleetLanded();
if (m_Stage == 3 && landed) Stage(4, "Rescue transports are on the ground. Everyone aboard, including your wounded. Use either rescue transport if there are two. Escorts are covering the pickup. We leave when all surviving prisoners are inside.");
if (m_Stage != 4) return;
if (!landed) { m_AllAboardSince = -1; return; }
array<ref ERR_PlayerLife> lives = {}; m_Roster.Participants(lives);
int alive, aboard;
foreach (ERR_PlayerLife life : lives)
{
if (!ERR_Actors.Alive(life.Actor)) continue;
alive++;
if (IsTransport(Vehicle.Cast(CompartmentAccessComponent.GetVehicleIn(life.Actor)))) aboard++;
}
m_AllAboardSince = ERR_RescueRules.BoardingSince(alive, aboard, m_AllAboardSince, now);
if (!ERR_RescueRules.CanDepart(m_AllAboardSince, now)) return;
vector direction = m_Pickup - m_Center; direction[1] = 0;
if (direction.Length() < 1) direction = "1 0 0";
direction.Normalize();
float safeDistance = Math.Max(m_Radius + 400, vector.DistanceXZ(m_Pickup, m_Center) + 2000);
vector safety = m_Center + direction * safeDistance;
safety[1] = SCR_TerrainHelper.GetTerrainY(safety, GetGame().GetWorld(), noUnderwater: true);
vector lateral = Vector(-direction[2], 0, direction[0]);
foreach (int departureIndex, ERR_RescueFlightComponent outbound : m_Transports)
{
vector destination = safety + lateral * (departureIndex * 120);
destination[1] = SCR_TerrainHelper.GetTerrainY(destination, GetGame().GetWorld(), noUnderwater: true);
outbound.Depart(destination);
}
foreach (ERR_RescueFlightComponent cover : m_Escorts) if (cover && !cover.Lost()) cover.Depart(safety + "0 100 200");
Stage(5, "All survivors aboard. Lifting now. Stay inside the transport; escorts are taking you to safety.");
}
protected Vehicle m_Vehicle; protected Vehicle m_Vehicle;
protected bool m_Assigned; protected bool m_Assigned;
protected bool m_Begun; protected bool m_Begun;
protected bool m_Stopped; protected bool m_Stopped;
protected vector m_Center; protected vector m_Center;
protected float m_Radius; protected float m_Radius;
protected float m_AirborneSince = -1;
protected ERR_ExtractionState m_State; protected ERR_ExtractionState m_State;
protected ref set<UUID> m_Escapees = new set<UUID>(); protected ref set<UUID> m_Escapees = new set<UUID>();
protected int m_ParticipantCount; protected int m_ParticipantCount;
@@ -18,6 +252,34 @@ class ERR_ExtractionService
void ERR_ExtractionService(ERR_PlayerRoster roster) { m_Roster = roster; } void ERR_ExtractionService(ERR_PlayerRoster roster) { m_Roster = roster; }
Vehicle VehicleEntity() { return m_Vehicle; } Vehicle VehicleEntity() { return m_Vehicle; }
int Vehicles(out array<Vehicle> vehicles)
{
vehicles.Clear();
foreach (ERR_RescueFlightComponent transport : m_Transports)
if (transport && transport.Aircraft() && !vehicles.Contains(transport.Aircraft())) vehicles.Insert(transport.Aircraft());
if (m_Assigned && m_Vehicle && !vehicles.Contains(m_Vehicle)) vehicles.Insert(m_Vehicle);
return vehicles.Count();
}
bool IsTransport(Vehicle aircraft)
{
if (!aircraft || !m_Assigned) return false;
array<Vehicle> vehicles = {}; Vehicles(vehicles);
return vehicles.Contains(aircraft);
}
protected bool FleetAlive()
{
if (!m_Transport || m_Transports.IsEmpty()) return false;
foreach (ERR_RescueFlightComponent transport : m_Transports)
if (!transport || transport.Lost() || !ERR_Actors.Alive(transport.Aircraft())) return false;
return true;
}
protected bool FleetLanded()
{
bool landed = true;
foreach (ERR_RescueFlightComponent transport : m_Transports)
if (!transport || !transport.Landed()) landed = false;
return landed && !m_Transports.IsEmpty();
}
ERR_ExtractionState State() { return m_State; } ERR_ExtractionState State() { return m_State; }
ScriptInvoker OnResolved() { return m_OnResolved; } ScriptInvoker OnResolved() { return m_OnResolved; }
int EscapedCount() { return m_Escapees.Count(); } int EscapedCount() { return m_Escapees.Count(); }
@@ -34,18 +296,23 @@ class ERR_ExtractionService
int Seats(out array<BaseCompartmentSlot> seats) int Seats(out array<BaseCompartmentSlot> seats)
{ {
seats.Clear(); seats.Clear();
if (!m_Vehicle) return 0; array<Vehicle> vehicles = {}; Vehicles(vehicles);
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(m_Vehicle.FindComponent(BaseCompartmentManagerComponent)); foreach (Vehicle aircraft : vehicles) CollectSeats(aircraft, seats);
if (!manager) return 0; return seats.Count();
}
protected void CollectSeats(IEntity entity, array<BaseCompartmentSlot> seats)
{
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(entity.FindComponent(BaseCompartmentManagerComponent));
array<BaseCompartmentSlot> all = {}; array<BaseCompartmentSlot> all = {};
manager.GetCompartments(all); if (manager) manager.GetCompartments(all);
foreach (BaseCompartmentSlot slot : all) foreach (BaseCompartmentSlot slot : all)
{ {
if (!slot) continue; if (!slot) continue;
ECompartmentType type = SCR_CompartmentAccessComponent.GetCompartmentType(slot); ECompartmentType type = SCR_CompartmentAccessComponent.GetCompartmentType(slot);
if (type == ECompartmentType.PILOT || type == ECompartmentType.CARGO) seats.Insert(slot); if ((type == ECompartmentType.PILOT || type == ECompartmentType.CARGO) && !seats.Contains(slot)) seats.Insert(slot);
} }
return seats.Count(); IEntity child = entity.GetChildren();
while (child) { CollectSeats(child, seats); child = child.GetSibling(); }
} }
bool Ready() bool Ready()
{ {
@@ -58,7 +325,7 @@ class ERR_ExtractionService
} }
bool Begin(vector center, float radius) bool Begin(vector center, float radius)
{ {
if (!Replication.IsServer() || m_Begun || m_Stopped || !m_Roster || radius <= 0 || !Ready()) return false; if (!Replication.IsServer() || m_Begun || m_Stopped || !m_Roster || radius <= 0 || !m_PickupConfigured) return false;
m_Center = center; m_Center = center;
m_Radius = radius; m_Radius = radius;
m_Begun = true; m_Begun = true;
@@ -68,13 +335,21 @@ class ERR_ExtractionService
{ {
if (!Replication.IsServer() || !m_Begun || m_Stopped || m_State != ERR_ExtractionState.WAITING) return; if (!Replication.IsServer() || !m_Begun || m_Stopped || m_State != ERR_ExtractionState.WAITING) return;
if (phase < ERR_Phase.PRISON || ERR_MissionRules.Terminal(phase)) return; if (phase < ERR_Phase.PRISON || ERR_MissionRules.Terminal(phase)) return;
if (!ERR_Actors.Alive(m_Vehicle)) { Resolve(ERR_ExtractionState.LOST); return; } if (m_Mode) RescueTick(GetGame().GetWorld().GetWorldTime());
if (phase != ERR_Phase.EXTRACTION || vector.DistanceXZ(m_Vehicle.GetOrigin(), m_Center) <= m_Radius) return; if (m_State != ERR_ExtractionState.WAITING || !m_Assigned) return;
if (!FleetAlive()) { Resolve(ERR_ExtractionState.LOST); return; }
if (m_Stage != 5 || phase != ERR_Phase.EXTRACTION) return;
bool airborne = !m_Transports.IsEmpty();
foreach (ERR_RescueFlightComponent departing : m_Transports)
if (!departing || !departing.AirborneAfterPickup()) airborne = false;
float now = GetGame().GetWorld().GetWorldTime();
m_AirborneSince = ERR_RescueRules.AirborneSince(airborne, m_AirborneSince, now);
if (!ERR_RescueRules.FlightComplete(m_AirborneSince, now)) return;
array<ref ERR_PlayerLife> lives = {}; array<ref ERR_PlayerLife> lives = {};
m_Roster.Participants(lives); m_Roster.Participants(lives);
foreach (ERR_PlayerLife life : lives) foreach (ERR_PlayerLife life : lives)
{ {
if (!ERR_Actors.Alive(life.Actor) || CompartmentAccessComponent.GetVehicleIn(life.Actor) != m_Vehicle) continue; if (!ERR_Actors.Alive(life.Actor) || !IsTransport(Vehicle.Cast(CompartmentAccessComponent.GetVehicleIn(life.Actor)))) continue;
// Reserved disconnected characters and unconscious passengers remain alive. // Reserved disconnected characters and unconscious passengers remain alive.
// A recycled connection ID is never an identity for the escape snapshot. // A recycled connection ID is never an identity for the escape snapshot.
m_Escapees.Insert(life.AccountId); m_Escapees.Insert(life.AccountId);
@@ -97,5 +372,18 @@ class ERR_ExtractionService
foreach (ERR_PlayerLife life : lives) foreach (ERR_PlayerLife life : lives)
if (life.Connected && m_Roster.Get(life.ConnectionId) == life && m_Escapees.Contains(life.AccountId)) winners.Insert(life.ConnectionId); if (life.Connected && m_Roster.Get(life.ConnectionId) == life && m_Escapees.Contains(life.AccountId)) winners.Insert(life.ConnectionId);
} }
void Stop() { m_Stopped = true; } void Stop()
{
// Capture the final aircraft state before terminal cleanup releases autopilots
// and removes the AI groups. Keep the same occupied airframes until reload.
if (m_State == ERR_ExtractionState.ESCAPED)
{
foreach (ERR_RescueFlightComponent safeTransport : m_Transports) if (safeTransport) safeTransport.HoldForResults();
foreach (ERR_RescueFlightComponent safeEscort : m_Escorts) if (safeEscort && !safeEscort.Lost()) safeEscort.HoldForResults();
}
m_Stopped = true; m_RadioPending = ""; m_PreviousSignals.Clear();
foreach (ERR_RescueFlightComponent transport : m_Transports) if (transport) transport.Release(false);
foreach (ERR_RescueFlightComponent escort : m_Escorts) if (escort) escort.Release(false);
m_Escorts.Clear(); m_Transports.Clear(); m_Transport = null;
}
} }
@@ -30,6 +30,9 @@ modded class ERR_AdminMenu
bool ready = m_Ready && m_FactionSaved.Count() == 3 && m_FactionActive.Count() == 3; bool ready = m_Ready && m_FactionSaved.Count() == 3 && m_FactionActive.Count() == 3;
foreach (SCR_ButtonTextComponent button : m_FactionButtons) foreach (SCR_ButtonTextComponent button : m_FactionButtons)
if (button) button.SetEnabled(ready); if (button) button.SetEnabled(ready);
bool allRandom = ready;
foreach (string savedRole : ERR_FactionChoices.Roles()) if (m_FactionSaved.Get(savedRole) != "random") allRandom = false;
ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText("FactionsRandomAll", GetRootWidget()), allRandom);
foreach (string role : ERR_FactionChoices.Roles()) foreach (string role : ERR_FactionChoices.Roles())
{ {
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("FactionSelect_" + role, GetRootWidget()); SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("FactionSelect_" + role, GetRootWidget());
@@ -96,13 +96,16 @@ class ERR_FactionSelection
if (!Replication.IsServer()) { m_ReadOnly = true; return; } if (!Replication.IsServer()) { m_ReadOnly = true; return; }
map<string, string> loaded; map<string, string> loaded;
if (Read(m_Path, loaded) || Read(m_Path + ".bak", loaded)) m_Saved = loaded; if (Read(m_Path, loaded) || Read(m_Path + ".bak", loaded)) m_Saved = loaded;
else if (FileIO.FileExists(m_Path) || FileIO.FileExists(m_Path + ".bak")) else if (ERR_ProfileConfig.Exists(m_Path) || ERR_ProfileConfig.Exists(m_Path + ".bak"))
{ m_ReadOnly = true; Print("Co10 Escape Reintroduced: invalid faction selections; defaults active and files preserved", LogLevel.ERROR); } { m_ReadOnly = true; Print("Co10 Escape Reintroduced: invalid faction selections; defaults active and files preserved", LogLevel.ERROR); }
} }
protected bool Read(string path, out map<string, string> values) protected bool Read(string path, out map<string, string> values)
{ {
JsonLoadContext json = new JsonLoadContext(); JsonLoadContext json = new JsonLoadContext();
if (!json.LoadFromFile(path)) return false; return ERR_ProfileConfig.Load(path, json) && Parse(json, values);
}
static bool Parse(JsonLoadContext json, out map<string, string> values)
{
values = new map<string, string>(); values = new map<string, string>();
foreach (string role : ERR_FactionChoices.Roles()) foreach (string role : ERR_FactionChoices.Roles())
{ {
@@ -146,6 +149,11 @@ class ERR_FactionSelection
if (!ERR_FactionChoices.Distinct(values)) return false; if (!ERR_FactionChoices.Distinct(values)) return false;
JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false); JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false);
foreach (string role : roles) json.WriteValue(role, values.Get(role)); foreach (string role : roles) json.WriteValue(role, values.Get(role));
if (m_Path == PATH)
{
if (!ERR_ProfileConfig.Save(PATH, json)) return false;
m_Saved = values; return true;
}
map<string, string> checked; map<string, string> checked;
if (!json.SaveToFile(m_Path + ".tmp") || !Read(m_Path + ".tmp", checked) || !Same(values, checked)) return false; if (!json.SaveToFile(m_Path + ".tmp") || !Read(m_Path + ".tmp", checked) || !Same(values, checked)) return false;
map<string, string> previous; map<string, string> previous;
@@ -26,6 +26,33 @@ class ERR_GameMode : SCR_BaseGameMode
m_SelectedPlayerFaction = player; m_SelectedEnemyFaction = enemy; Replication.BumpMe(); m_SelectedPlayerFaction = player; m_SelectedEnemyFaction = enemy; Replication.BumpMe();
} }
protected ref ERR_Settings m_Settings; protected ref ERR_Settings m_Settings;
[RplProp()] protected bool m_ShowAllSites;
bool ShowAllSites() { return m_ShowAllSites; }
void PublishSiteVisibility()
{
if (!Replication.IsServer() || !m_Settings || m_Stopped || m_NativeEndHandled) return;
bool enabled = m_Settings.Get("showAllSites") == 1;
if (m_ShowAllSites == enabled) return;
m_ShowAllSites = enabled;
Replication.BumpMe();
}
[RplProp()] protected int m_StartMode;
protected bool m_StartModeFrozen;
protected bool m_StartPrepared;
int StartMode() { return m_StartMode; }
protected bool FreezeStartMode()
{
if (!Replication.IsServer() || m_Stopped || m_NativeEndHandled || m_Phase != ERR_Phase.PREPARING || !m_Settings) return false;
if (m_StartModeFrozen) return true;
int selected = m_Settings.Get("startMode");
if (!ERR_StartRules.Valid(selected)) return false;
m_StartMode = selected; m_StartModeFrozen = true;
Replication.BumpMe();
return true;
}
[RplProp(onRplName: "ApplyLocalViewDistance")]
protected int m_MissionViewDistance;
protected ref ERR_ViewDistance m_LocalViewDistance = new ERR_ViewDistance();
protected ref ERR_ContentPolicy m_Content; protected ref ERR_ContentPolicy m_Content;
protected ref ERR_ContentSettings m_ContentSettings; protected ref ERR_ContentSettings m_ContentSettings;
protected ref ERR_TimeService m_Time = new ERR_TimeService(); protected ref ERR_TimeService m_Time = new ERR_TimeService();
@@ -35,6 +62,10 @@ class ERR_GameMode : SCR_BaseGameMode
protected ref ERR_PlayerRoster m_Roster = new ERR_PlayerRoster(); protected ref ERR_PlayerRoster m_Roster = new ERR_PlayerRoster();
protected ref ERR_AdminService m_Admin = new ERR_AdminService(); protected ref ERR_AdminService m_Admin = new ERR_AdminService();
protected ref ERR_CommandService m_Command; protected ref ERR_CommandService m_Command;
protected ref ERR_Performance m_Performance;
ERR_Performance Performance() { return m_Performance; }
protected ref ERR_CivilianTraffic m_CivilianTraffic;
protected ref ERR_GarageVehicles m_GarageVehicles;
protected ref ERR_ForceLedger m_Forces = new ERR_ForceLedger(); protected ref ERR_ForceLedger m_Forces = new ERR_ForceLedger();
protected ref ERR_ForceSimulation m_ForceSimulation; protected ref ERR_ForceSimulation m_ForceSimulation;
protected ref array<AIAgent> m_ForceAgents = {}; protected ref array<AIAgent> m_ForceAgents = {};
@@ -53,6 +84,9 @@ class ERR_GameMode : SCR_BaseGameMode
[RplProp()] [RplProp()]
protected string m_RecoveryStatus; protected string m_RecoveryStatus;
protected bool m_StartedPlayers; protected bool m_StartedPlayers;
protected bool m_HadConnectedPlayers;
protected bool m_EmptyRestartRequested;
protected int m_EmptySince;
protected ref map<int, PlayerController> m_ExhaustedInitialAdmissions = new map<int, PlayerController>(); protected ref map<int, PlayerController> m_ExhaustedInitialAdmissions = new map<int, PlayerController>();
protected bool m_Stopped; protected bool m_Stopped;
protected bool m_NativeEndHandled; protected bool m_NativeEndHandled;
@@ -73,6 +107,27 @@ class ERR_GameMode : SCR_BaseGameMode
ERR_PlayerRoster Players() { return m_Roster; } ERR_PlayerRoster Players() { return m_Roster; }
ERR_AdminService Admin() { return m_Admin; } ERR_AdminService Admin() { return m_Admin; }
ERR_Settings Settings() { return m_Settings; } ERR_Settings Settings() { return m_Settings; }
// Publish the server-bounded value; replicated state also reaches late joiners.
void PublishViewDistance()
{
if (!Replication.IsServer() || !m_Settings || m_Stopped || m_NativeEndHandled) return;
m_MissionViewDistance = ERR_ViewDistance.ServerDistance(m_Settings.Get("viewDistanceMetres"), GetGame().GetViewDistanceServerLimit());
Replication.BumpMe();
ApplyLocalViewDistance();
}
protected void ApplyLocalViewDistance()
{
if (!GetGame() || System.IsConsoleApp() || m_Stopped || m_NativeEndHandled) return;
// Inventory and video menus temporarily own the native camera distance.
if (GetGame().GetMenuManager() && GetGame().GetMenuManager().GetTopMenu()) return;
m_LocalViewDistance.Apply(m_MissionViewDistance);
}
protected void StopLocalViewDistance()
{
if (!GetGame()) return;
GetGame().GetCallqueue().Remove(ApplyLocalViewDistance);
m_LocalViewDistance.Restore();
}
ERR_ContentPolicy Content() { return m_Content; } ERR_ContentPolicy Content() { return m_Content; }
ERR_ContentSettings ContentSettings() { return m_ContentSettings; } ERR_ContentSettings ContentSettings() { return m_ContentSettings; }
bool IsMissionEnemy(Faction faction) bool IsMissionEnemy(Faction faction)
@@ -88,6 +143,8 @@ class ERR_GameMode : SCR_BaseGameMode
string RecoveryStatus() { return m_RecoveryStatus; } string RecoveryStatus() { return m_RecoveryStatus; }
ERR_ForceLedger Forces() { return m_Forces; } ERR_ForceLedger Forces() { return m_Forces; }
ERR_CommandService Command() { return m_Command; } ERR_CommandService Command() { return m_Command; }
ERR_CivilianTraffic CivilianTraffic() { return m_CivilianTraffic; }
ERR_GarageVehicles GarageVehicles() { return m_GarageVehicles; }
ERR_Objectives Objectives() { return m_Objectives; } ERR_Objectives Objectives() { return m_Objectives; }
ERR_ExtractionService Extraction() { return m_Extraction; } ERR_ExtractionService Extraction() { return m_Extraction; }
string EndReason() { return m_EndReason; } string EndReason() { return m_EndReason; }
@@ -130,14 +187,18 @@ class ERR_GameMode : SCR_BaseGameMode
ERR_FactionCatalog Factions() { return m_Factions; } ERR_FactionCatalog Factions() { return m_Factions; }
Faction PlayerFaction() Faction PlayerFaction()
{ {
if (!m_Factions) return null; if (!m_Factions || !GetGame()) return null;
if (!m_SelectedPlayerFaction.IsEmpty()) return GetGame().GetFactionManager().GetFactionByKey(m_SelectedPlayerFaction); FactionManager manager = GetGame().GetFactionManager();
if (!manager) return null;
if (!m_SelectedPlayerFaction.IsEmpty()) return manager.GetFactionByKey(m_SelectedPlayerFaction);
return m_Factions.PlayerFaction(); return m_Factions.PlayerFaction();
} }
Faction EnemyFaction() Faction EnemyFaction()
{ {
if (!m_Factions) return null; if (!m_Factions || !GetGame()) return null;
if (!m_SelectedEnemyFaction.IsEmpty()) return GetGame().GetFactionManager().GetFactionByKey(m_SelectedEnemyFaction); FactionManager manager = GetGame().GetFactionManager();
if (!manager) return null;
if (!m_SelectedEnemyFaction.IsEmpty()) return manager.GetFactionByKey(m_SelectedEnemyFaction);
return m_Factions.EnemyFaction(); return m_Factions.EnemyFaction();
} }
string PreparationError() { return m_PreparationError; } string PreparationError() { return m_PreparationError; }
@@ -201,7 +262,7 @@ class ERR_GameMode : SCR_BaseGameMode
} }
void RegisterLocation(ERR_LocationComponent location) void RegisterLocation(ERR_LocationComponent location)
{ {
if (!location || location.Id().IsEmpty() || m_Locations.Contains(location)) return; if (!location || !location.Prepared() || location.Id().IsEmpty() || m_Locations.Contains(location)) return;
foreach (ERR_LocationComponent existing : m_Locations) foreach (ERR_LocationComponent existing : m_Locations)
{ {
if (existing.Id() == location.Id()) if (existing.Id() == location.Id())
@@ -212,9 +273,29 @@ class ERR_GameMode : SCR_BaseGameMode
} }
m_Locations.Insert(location); m_Locations.Insert(location);
} }
bool WorldPreparing()
{
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(FindComponent(ERR_ScenarioComponent));
return scenario && scenario.BackgroundPending();
}
void WorldPreparationFailed(string reason)
{
if (!Replication.IsServer() || !Active()) return;
m_EndReason = "World generation failed: " + reason;
Print(m_EndReason, LogLevel.ERROR);
Transition(ERR_Phase.DEFEAT);
}
bool HasLocation(ERR_LocationComponent location) { return location && m_Locations.Contains(location); } bool HasLocation(ERR_LocationComponent location) { return location && m_Locations.Contains(location); }
void UnregisterLocation(ERR_LocationComponent location) { m_Locations.RemoveItem(location); } void UnregisterLocation(ERR_LocationComponent location) { m_Locations.RemoveItem(location); }
void Locations(out array<ERR_LocationComponent> locations) { locations.Copy(m_Locations); } void Locations(out array<ERR_LocationComponent> locations) { locations.Copy(m_Locations); }
// Read only, once per telemetry window. Does not query/stream the terrain.
int OwnedRootCount()
{
int count;
foreach (IEntity entity : m_OwnedEntities) if (entity) count++;
return count;
}
void Own(IEntity entity) { if (entity && !m_OwnedEntities.Contains(entity)) m_OwnedEntities.Insert(entity); } void Own(IEntity entity) { if (entity && !m_OwnedEntities.Contains(entity)) m_OwnedEntities.Insert(entity); }
bool SetExtractionVehicle(Vehicle vehicle) bool SetExtractionVehicle(Vehicle vehicle)
{ {
@@ -285,10 +366,11 @@ class ERR_GameMode : SCR_BaseGameMode
} }
bool Transition(ERR_Phase phase) bool Transition(ERR_Phase phase)
{ {
if (!Replication.IsServer() || !m_PreparationError.IsEmpty() || !ERR_MissionRules.CanTransition(m_Phase, phase)) return false; if (!Replication.IsServer() || !m_PreparationError.IsEmpty() || !ERR_MissionRules.CanTransition(m_Phase, phase, m_StartModeFrozen && m_StartPrepared && m_StartMode == 1)) return false;
if (m_Phase == ERR_Phase.PREPARING && !m_StartPrepared) return false;
ERR_Phase previous = m_Phase; ERR_Phase previous = m_Phase;
m_Phase = phase; m_Phase = phase;
if (previous == ERR_Phase.PRISON && phase == ERR_Phase.RADIO_SEARCH) if ((previous == ERR_Phase.PRISON || previous == ERR_Phase.PREPARING) && phase == ERR_Phase.RADIO_SEARCH)
{ {
// Dormant patrols must not catch up the time spent waiting in prison. // Dormant patrols must not catch up the time spent waiting in prison.
for (int i = 0; i < m_Forces.Count(); i++) for (int i = 0; i < m_Forces.Count(); i++)
@@ -330,13 +412,14 @@ class ERR_GameMode : SCR_BaseGameMode
{ {
if (!Replication.IsServer() || !m_RadioDefence || location != m_RadioDefence.Location()) return; if (!Replication.IsServer() || !m_RadioDefence || location != m_RadioDefence.Location()) return;
if (!Transition(ERR_Phase.RADIO_DEFENCE)) m_RadioDefence.Stop(); if (!Transition(ERR_Phase.RADIO_DEFENCE)) m_RadioDefence.Stop();
else m_Extraction.PreparingRoute();
} }
protected void RadioCompleted(ERR_LocationComponent location) protected void RadioCompleted(ERR_LocationComponent location)
{ {
if (!Replication.IsServer() || m_Phase != ERR_Phase.RADIO_DEFENCE || !m_RadioDefence || m_RadioDefence.State() != ERR_RadioDefenceState.COMPLETED || location != m_RadioDefence.Location()) return; if (!Replication.IsServer() || m_Phase != ERR_Phase.RADIO_DEFENCE || !m_RadioDefence || m_RadioDefence.State() != ERR_RadioDefenceState.COMPLETED || location != m_RadioDefence.Location()) return;
foreach (ERR_LocationComponent candidate : m_Locations) foreach (ERR_LocationComponent candidate : m_Locations)
if (candidate.Kind() == ERR_LocationKind.EXTRACTION) candidate.Discover(0, 100); if (candidate.Kind() == ERR_LocationKind.EXTRACTION) candidate.Discover(0, 100);
Transition(ERR_Phase.EXTRACTION); if (Transition(ERR_Phase.EXTRACTION)) m_Extraction.OpenPickup();
} }
void PlayerSpawned(int id, IEntity actor) void PlayerSpawned(int id, IEntity actor)
@@ -381,19 +464,61 @@ class ERR_GameMode : SCR_BaseGameMode
protected string SettingsPath() { return ERR_Settings.PATH; } protected string SettingsPath() { return ERR_Settings.PATH; }
// A connection can arrive and leave between server ticks, even before audit
// or spawning. Defer the population check until native disconnect cleanup ends.
void ObservePlayerDisconnect()
{
if (!Replication.IsServer() || m_Stopped || m_NativeEndHandled) return;
m_HadConnectedPlayers = true;
m_EmptySince = System.GetTickCount();
}
protected bool CheckEmptyServerRestart()
{
if (!Replication.IsServer() || m_Stopped || m_NativeEndHandled || !m_Settings) return false;
if (m_EmptyRestartRequested) return true;
PlayerManager manager = GetGame().GetPlayerManager();
if (!manager) return false;
// Count all connections, including admission and spectators, not living bodies.
if (manager.GetPlayerCount() > 0)
{
m_HadConnectedPlayers = true;
m_EmptySince = System.GetTickCount();
return false;
}
// Real elapsed milliseconds: mission clock settings cannot accelerate this.
bool afterDisconnect = m_HadConnectedPlayers && m_Settings.Get("restartOnEmpty") == 1;
int hours = m_Settings.Get("emptyRestartHours");
if (!afterDisconnect && (hours == 0 || System.GetTickCount() - m_EmptySince < hours * 3600000)) return false;
m_EmptyRestartRequested = true;
m_EndReason = string.Format("The server has been empty for %1 hours. Restarting the mission.", hours);
if (afterDisconnect) m_EndReason = "All players disconnected. Restarting the mission.";
PrintFormat("Reintroduced [round lifecycle]: %1", m_EndReason);
// Native termination also handles PREPARING, before Transition is available.
// OnGameModeEnd stops our services; vanilla owns the scenario reload.
EndGameMode(SCR_GameModeEndData.Create(EGameOverTypes.LOOSE));
return true;
}
protected override void EOnInit(IEntity owner) protected override void EOnInit(IEntity owner)
{ {
super.EOnInit(owner); super.EOnInit(owner);
if (SCR_Global.IsEditMode()) return; if (SCR_Global.IsEditMode()) return;
m_Factions = ERR_FactionCatalog.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_FactionDefinition)); m_Factions = ERR_FactionCatalog.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_FactionDefinition));
if (!System.IsConsoleApp()) GetGame().GetCallqueue().CallLater(BindLocalObserver, 250, true); if (!System.IsConsoleApp()) GetGame().GetCallqueue().CallLater(BindLocalObserver, 250, true);
if (!System.IsConsoleApp()) GetGame().GetCallqueue().CallLater(ApplyLocalViewDistance, 1000, true);
if (!Replication.IsServer()) return; if (!Replication.IsServer()) return;
m_EmptySince = System.GetTickCount();
m_Settings = new ERR_Settings(SettingsPath()); m_Settings = new ERR_Settings(SettingsPath());
PublishSiteVisibility();
if (!System.IsCLIParam("errNoPerf")) m_Performance = new ERR_Performance();
PublishViewDistance();
m_ContentSettings = new ERR_ContentSettings(); m_ContentSettings = new ERR_ContentSettings();
m_Content = new ERR_ContentPolicy(m_ContentSettings); m_Content = new ERR_ContentPolicy(m_ContentSettings);
m_FactionSelection = new ERR_FactionSelection(); m_FactionSelection = new ERR_FactionSelection();
m_Admin.Register(new ERR_ContentAdminProvider(m_Content)); m_Admin.Register(new ERR_ContentAdminProvider(m_Content));
m_Admin.Register(new ERR_FactionAdminProvider()); m_Admin.Register(new ERR_FactionAdminProvider());
m_Admin.Register(new ERR_TerrainAdminProvider());
OnStop().Insert(m_Environment.Stop); OnStop().Insert(m_Environment.Stop);
m_Recovery = new ERR_RecoveryService(this); m_Recovery = new ERR_RecoveryService(this);
m_Recovery.OnChanged().Insert(RecoveryChanged); m_Recovery.OnChanged().Insert(RecoveryChanged);
@@ -421,6 +546,11 @@ class ERR_GameMode : SCR_BaseGameMode
OnStop().Insert(m_Objectives.Stop); OnStop().Insert(m_Objectives.Stop);
m_ForceSimulation = new ERR_ForceSimulation(m_Forces); m_ForceSimulation = new ERR_ForceSimulation(m_Forces);
m_Command = CreateCommandService(); m_Command = CreateCommandService();
m_CivilianTraffic = new ERR_CivilianTraffic(this);
OnStop().Insert(m_CivilianTraffic.Stop);
m_GarageVehicles = new ERR_GarageVehicles(this);
OnStop().Insert(m_GarageVehicles.Stop);
m_GarageVehicles.Start();
GetGame().GetCallqueue().CallLater(ServerTick, 1000, true); GetGame().GetCallqueue().CallLater(ServerTick, 1000, true);
GetGame().GetCallqueue().CallLater(StartScenario, 1500, false); GetGame().GetCallqueue().CallLater(StartScenario, 1500, false);
} }
@@ -435,7 +565,7 @@ class ERR_GameMode : SCR_BaseGameMode
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController()); SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!controller) return; if (!controller) return;
controller.ERR_StartObserverMonitor(); controller.ERR_StartObserverMonitor();
GetGame().GetCallqueue().Remove(BindLocalObserver); // Keep watching for a replacement local controller; Start is idempotent.
} }
protected void StartScenario() protected void StartScenario()
@@ -444,6 +574,7 @@ class ERR_GameMode : SCR_BaseGameMode
if (!m_FactionSelection || !m_FactionSelection.Prepare(this)) { FailPreparation("Three enabled faction profiles are required for the mission roles."); return; } if (!m_FactionSelection || !m_FactionSelection.Prepare(this)) { FailPreparation("Three enabled faction profiles are required for the mission roles."); return; }
if (!m_Factions || !m_Factions.WorldReady()) { FailPreparation("The configured player and enemy factions or required assets are missing, invalid or not hostile."); return; } if (!m_Factions || !m_Factions.WorldReady()) { FailPreparation("The configured player and enemy factions or required assets are missing, invalid or not hostile."); return; }
if (!m_Content || !m_Content.PrepareFactions(this)) { FailPreparation("The content registry or enabled faction profiles are invalid. Check faction keys, guard groups and the mission faction manager."); return; } if (!m_Content || !m_Content.PrepareFactions(this)) { FailPreparation("The content registry or enabled faction profiles are invalid. Check faction keys, guard groups and the mission faction manager."); return; }
if (!FreezeStartMode()) { FailPreparation("The selected mission start is invalid."); return; }
// Scenario content is created through a separate authoring component. // Scenario content is created through a separate authoring component.
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(FindComponent(ERR_ScenarioComponent)); ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(FindComponent(ERR_ScenarioComponent));
ERR_PrepareResult prepared = ERR_PrepareResult.FAILED; ERR_PrepareResult prepared = ERR_PrepareResult.FAILED;
@@ -462,12 +593,14 @@ class ERR_GameMode : SCR_BaseGameMode
} }
if (!m_Extraction || !m_Extraction.Begin(m_EscapeCenter, m_EscapeRadius)) if (!m_Extraction || !m_Extraction.Begin(m_EscapeCenter, m_EscapeRadius))
{ {
FailPreparation("The extraction vehicle must be a helicopter with a pilot position and at least twelve pilot/passenger seats, with a valid escape boundary."); FailPreparation("The extraction pickup must be a prepared landing zone with a valid escape boundary.");
return; return;
} }
if (!m_Environment.Start(this)) { FailPreparation("The selected date or vanilla weather profile could not be initialized."); return; } if (!m_Environment.Start(this)) { FailPreparation("The selected date or vanilla weather profile could not be initialized."); return; }
if (!m_Time.Start(this)) { FailPreparation("The world clock could not be initialized."); return; } if (!m_Time.Start(this)) { FailPreparation("The world clock could not be initialized."); return; }
Transition(ERR_Phase.PRISON); m_StartPrepared = true;
if (m_StartMode == 1) Transition(ERR_Phase.RADIO_SEARCH);
else Transition(ERR_Phase.PRISON);
} }
protected void ExtractionResolved(ERR_ExtractionState result) protected void ExtractionResolved(ERR_ExtractionState result)
@@ -486,6 +619,16 @@ class ERR_GameMode : SCR_BaseGameMode
} }
protected void ServerTick() protected void ServerTick()
{
if (CheckEmptyServerRestart()) return;
if (!Replication.IsServer() || !Active()) return;
int started;
if (m_Performance) started = m_Performance.BeginTick();
ServerTickMeasured();
if (m_Performance) m_Performance.EndTick(started);
}
protected void ServerTickMeasured()
{ {
if (!Replication.IsServer() || !Active()) return; if (!Replication.IsServer() || !Active()) return;
float now = GetWorld().GetWorldTime(); float now = GetWorld().GetWorldTime();
@@ -502,27 +645,40 @@ class ERR_GameMode : SCR_BaseGameMode
Transition(ERR_Phase.DEFEAT); Transition(ERR_Phase.DEFEAT);
return; return;
} }
int perfStamp;
if (m_Performance) perfStamp = System.GetTickCount();
if (m_Command) m_Command.Maintain(now); if (m_Command) m_Command.Maintain(now);
if (m_Performance) perfStamp = m_Performance.Mark(ERR_PerfMetric.MAINTENANCE, perfStamp);
if (m_CivilianTraffic) m_CivilianTraffic.Tick(now);
if (m_Performance) perfStamp = m_Performance.Mark(ERR_PerfMetric.CIVILIANS, perfStamp);
// Maintenance can finish or stop the round; do not resume tactical work. // Maintenance can finish or stop the round; do not resume tactical work.
if (!Active()) return; if (!Active()) return;
if (m_Phase >= ERR_Phase.RADIO_SEARCH) TickForces(now); if (m_Phase >= ERR_Phase.RADIO_SEARCH)
if (GetGame().GetPlayerManager().GetPlayerCount() == 0) {
{ TickForces(now);
m_NoSurvivorsSince = -1; if (m_Performance) m_Performance.Mark(ERR_PerfMetric.FORCES, perfStamp);
return; }
}
if ((m_StartedPlayers || InitialAdmissionsExhausted()) && !m_Roster.HasSurvivor()) if ((m_StartedPlayers || InitialAdmissionsExhausted()) && !m_Roster.HasSurvivor())
{ {
if (m_NoSurvivorsSince < 0) m_NoSurvivorsSince = now; if (m_NoSurvivorsSince < 0)
{
m_NoSurvivorsSince = now;
PrintFormat("Reintroduced [round lifecycle]: no survivors; starting 30s grace; previouslyPlayed=%1 connected=%2", m_StartedPlayers, GetGame().GetPlayerManager().GetPlayerCount());
}
if (now - m_NoSurvivorsSince >= 30000) if (now - m_NoSurvivorsSince >= 30000)
{ {
m_EndReason = "No surviving prisoners remain."; m_EndReason = "No surviving prisoners remain.";
if (!m_StartedPlayers) m_EndReason = "No prisoners could be admitted to this round."; if (!m_StartedPlayers) m_EndReason = "No prisoners could be admitted to this round.";
PrintFormat("Reintroduced [round lifecycle]: ending exhausted round; connected=%1; %2", GetGame().GetPlayerManager().GetPlayerCount(), m_EndReason);
Transition(ERR_Phase.DEFEAT); Transition(ERR_Phase.DEFEAT);
return; return;
} }
} }
else m_NoSurvivorsSince = -1; else m_NoSurvivorsSince = -1;
// Empty-server restart is checked by ServerTick before tactical work.
// Keep tactical/observer work idle when nobody is connected.
if (GetGame().GetPlayerManager().GetPlayerCount() == 0) return;
if (m_Performance) perfStamp = System.GetTickCount();
array<int> connected = {}; array<int> connected = {};
GetGame().GetPlayerManager().GetPlayers(connected); GetGame().GetPlayerManager().GetPlayers(connected);
foreach (int id : connected) foreach (int id : connected)
@@ -530,8 +686,14 @@ class ERR_GameMode : SCR_BaseGameMode
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id)); SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
if (controller) controller.ERR_UpdateObserverServer(); if (controller) controller.ERR_UpdateObserverServer();
} }
if (m_Command && m_Phase >= ERR_Phase.RADIO_SEARCH) m_Command.Tick(now); if (m_Performance) perfStamp = m_Performance.Mark(ERR_PerfMetric.OBSERVERS, perfStamp);
if (m_Command && m_Phase >= ERR_Phase.RADIO_SEARCH)
{
m_Command.Tick(now);
if (m_Performance) perfStamp = m_Performance.Mark(ERR_PerfMetric.HQ_TICK, perfStamp);
}
m_OnTick.Invoke(now); m_OnTick.Invoke(now);
if (m_Performance) m_Performance.Mark(ERR_PerfMetric.LISTENERS, perfStamp);
} }
// Vanilla editor/session termination enters POSTGAME directly, without our // Vanilla editor/session termination enters POSTGAME directly, without our
@@ -540,6 +702,7 @@ class ERR_GameMode : SCR_BaseGameMode
{ {
if (m_NativeEndHandled) return; if (m_NativeEndHandled) return;
m_NativeEndHandled = true; m_NativeEndHandled = true;
StopLocalViewDistance();
GetGame().GetCallqueue().Remove(BindLocalObserver); GetGame().GetCallqueue().Remove(BindLocalObserver);
if (Replication.IsServer()) if (Replication.IsServer())
{ {
@@ -564,11 +727,13 @@ class ERR_GameMode : SCR_BaseGameMode
{ {
if (m_Stopped) return; if (m_Stopped) return;
m_Stopped = true; m_Stopped = true;
StopLocalViewDistance();
GetGame().GetCallqueue().Remove(StartScenario); GetGame().GetCallqueue().Remove(StartScenario);
GetGame().GetCallqueue().Remove(ServerTick); GetGame().GetCallqueue().Remove(ServerTick);
GetGame().GetCallqueue().Remove(BindLocalObserver); GetGame().GetCallqueue().Remove(BindLocalObserver);
if (m_Command) m_Command.Stop(); if (m_Command) m_Command.Stop();
m_OnStop.Invoke(); m_OnStop.Invoke();
if (m_Performance) m_Performance.Stop(m_Phase);
m_Forces.Clear(); m_Forces.Clear();
m_ForceAgents.Clear(); m_ForceAgents.Clear();
if (Replication.IsServer()) if (Replication.IsServer())
@@ -1,8 +1,11 @@
// Clear-ground sampling adapted from the owner's improvised-prison extension (see migration provenance). // Clear-ground sampling adapted from the owner's improvised-prison extension (see migration provenance).
// Searches create no entities and finish before any site starts spawning. // Searches create no entities; startup placements can precede the rest of the island.
class ERR_CampTerrain class ERR_CampTerrain
{ {
protected ref array<vector> m_Airfields = {}; protected ref array<vector> m_Airfields = {};
protected string m_Rejection;
string Rejection() { return m_Rejection; }
protected bool Reject(string reason) { m_Rejection = reason; return false; }
protected bool Airport(IEntity entity) protected bool Airport(IEntity entity)
{ {
if (!entity || !entity.GetPrefabData()) return false; if (!entity || !entity.GetPrefabData()) return false;
@@ -12,6 +15,7 @@ class ERR_CampTerrain
protected bool AddAirport(IEntity entity) { m_Airfields.Insert(entity.GetOrigin()); return true; } protected bool AddAirport(IEntity entity) { m_Airfields.Insert(entity.GetOrigin()); return true; }
void Collect(vector low, vector high) void Collect(vector low, vector high)
{ {
ERR_AirfieldPlacement.AuthoredAirports(m_Airfields);
GetGame().GetWorld().QueryEntitiesByAABB(Vector(low[0], -1000, low[2]), Vector(high[0], 5000, high[2]), AddAirport, Airport, EQueryEntitiesFlags.ALL); GetGame().GetWorld().QueryEntitiesByAABB(Vector(low[0], -1000, low[2]), Vector(high[0], 5000, high[2]), AddAirport, Airport, EQueryEntitiesFlags.ALL);
} }
static vector Rotate(vector offset, float heading) static vector Rotate(vector offset, float heading)
@@ -24,23 +28,21 @@ class ERR_CampTerrain
vector rotation[3]; Math3D.AnglesToMatrix(Vector(heading, 0, 0), rotation); vector rotation[3]; Math3D.AnglesToMatrix(Vector(heading, 0, 0), rotation);
return Vector(vector.Dot(offset, rotation[0]), offset[1], vector.Dot(offset, rotation[2])); return Vector(vector.Dot(offset, rotation[0]), offset[1], vector.Dot(offset, rotation[2]));
} }
bool Clear(vector center, float heading, float halfSize, float maximumRange, out float lowest) bool Clear(vector center, float heading, float halfSize, float maximumRange, out float lowest, float clearanceHeight = 4)
{ {
m_Rejection = "";
lowest = float.MAX; float highest = -float.MAX; lowest = float.MAX; float highest = -float.MAX;
for (int x = -1; x <= 1; x++)
for (int z = -1; z <= 1; z++)
{
vector corner = center + Rotate(Vector(x * halfSize, 0, z * halfSize), heading);
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), corner)) return false;
}
// A 2m grid covers the walls, door approach, furniture and all interior slots. // A 2m grid covers the walls, door approach, furniture and all interior slots.
for (float dx = -halfSize; dx <= halfSize; dx += 2) for (float dx = -halfSize; dx <= halfSize; dx += 2)
for (float dz = -halfSize; dz <= halfSize; dz += 2) for (float dz = -halfSize; dz <= halfSize; dz += 2)
{ {
vector sample = center + Rotate(Vector(dx, 0, dz), heading); vector sample = center + Rotate(Vector(dx, 0, dz), heading);
float height = GetGame().GetWorld().GetSurfaceY(sample[0], sample[2]); float height = GetGame().GetWorld().GetSurfaceY(sample[0], sample[2]);
if (!ERR_ForceRules.Finite(height)) return Reject("invalid terrain height");
sample[1] = height;
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), sample)) return Reject("water");
lowest = Math.Min(lowest, height); highest = Math.Max(highest, height); lowest = Math.Min(lowest, height); highest = Math.Max(highest, height);
if (lowest < 1 || highest - lowest > maximumRange) return false; if (highest - lowest > maximumRange) return Reject("uneven ground");
} }
for (float cx = -halfSize + 1; cx < halfSize; cx += 2) for (float cx = -halfSize + 1; cx < halfSize; cx += 2)
for (float cz = -halfSize + 1; cz < halfSize; cz += 2) for (float cz = -halfSize + 1; cz < halfSize; cz += 2)
@@ -48,30 +50,46 @@ class ERR_CampTerrain
vector cell = center + Rotate(Vector(cx, 0, cz), heading); vector cell = center + Rotate(Vector(cx, 0, cz), heading);
float ground = GetGame().GetWorld().GetSurfaceY(cell[0], cell[2]); float ground = GetGame().GetWorld().GetSurfaceY(cell[0], cell[2]);
TraceBox trace = new TraceBox(); TraceBox trace = new TraceBox();
trace.Start = Vector(cell[0], ground + 0.12, cell[2]); trace.End = trace.Start + "0 4 0"; trace.Start = Vector(cell[0], ground + 0.12, cell[2]); trace.End = trace.Start + Vector(0, clearanceHeight, 0);
// Bounding the rotated cell conservatively also protects the exit corridor. // Bounding the rotated cell conservatively also protects the exit corridor.
trace.Mins = "-1.42 0 -1.42"; trace.Maxs = "1.42 0.1 1.42"; trace.Mins = "-1.42 0 -1.42"; trace.Maxs = "1.42 0.1 1.42";
trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD; trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
if (GetGame().GetWorld().TraceMove(trace, null) < 1) return false; if (GetGame().GetWorld().TraceMove(trace, null) < 1) return Reject("obstructed footprint");
} }
return true; return true;
} }
bool Site(ERR_SiteDefinition site, vector position, float heading, out float baseHeight) bool Site(ERR_SiteDefinition site, vector position, float heading, out float baseHeight, float airportBuffer = 1500)
{ {
m_Rejection = "";
bool prison = site.Kind == ERR_LocationKind.PRISON; bool prison = site.Kind == ERR_LocationKind.PRISON;
if (prison) if (prison)
{ {
foreach (vector airport : m_Airfields) if (vector.DistanceXZ(position, airport) < 1500) return false; foreach (vector airport : m_Airfields) if (vector.DistanceXZ(position, airport) < airportBuffer) return Reject("airport separation");
}
bool extraction = site.Kind == ERR_LocationKind.EXTRACTION;
// Camps must stay off roads; a helicopter pad may use clear paved ground.
// Its physical terrain, obstacle and fleet checks still decide acceptance.
if (!extraction)
{
SCR_AIWorld aiWorld = SCR_AIWorld.Cast(GetGame().GetAIWorld());
if (!aiWorld || !aiWorld.GetRoadNetworkManager()) return Reject("road network unavailable");
BaseRoad road; float distance;
aiWorld.GetRoadNetworkManager().GetClosestRoad(position, road, distance, skipNavlinks: true);
if (road && distance < 12) return Reject("road proximity");
}
float maximumRange = 0.6;
if (extraction) maximumRange = 0.8;
if (!Clear(position, heading, 10, maximumRange, baseHeight)) return false;
if (ERR_StartSetup.IsCrash(site))
{
vector offset, center; float halfSize, height;
if (!ERR_StartSetup.Geometry(offset, center, halfSize, height)) return Reject("crash footprint unavailable");
float wreckFloor;
if (!Clear(position + Rotate(center, heading), heading, halfSize, 0.6, wreckFloor, height)) return Reject("wreck area: " + m_Rejection);
} }
SCR_AIWorld aiWorld = SCR_AIWorld.Cast(GetGame().GetAIWorld());
if (!aiWorld || !aiWorld.GetRoadNetworkManager()) return false;
BaseRoad road; float distance;
aiWorld.GetRoadNetworkManager().GetClosestRoad(position, road, distance, skipNavlinks: true);
if (road && distance < 12) return false;
if (!Clear(position, heading, 10, 0.6, baseHeight)) return false;
float guardHeight; float guardHeight;
if (site.Guarded && !Clear(position + Rotate(site.GuardOffset, heading), heading, 6, 0.6, guardHeight)) return false; if (site.Guarded && !Clear(position + Rotate(site.GuardOffset, heading), heading, 6, 0.6, guardHeight)) return Reject("guard area: " + m_Rejection);
if (site.ResponseCount > 0 && !Clear(position + Rotate(site.ResponseOffset, heading), heading, 6, 0.6, guardHeight)) return false; if (site.ResponseCount > 0 && !Clear(position + Rotate(site.ResponseOffset, heading), heading, 6, 0.6, guardHeight)) return Reject("patrol area: " + m_Rejection);
return true; return true;
} }
} }
@@ -82,16 +100,67 @@ class ERR_GeneratedSites
protected ref array<ERR_SiteDefinition> m_Pending = {}; protected ref array<ERR_SiteDefinition> m_Pending = {};
protected vector m_Low, m_High; protected vector m_Low, m_High;
protected int m_Index, m_Attempts, m_Began; protected int m_Index, m_Attempts, m_Began;
protected int m_NextDiagnostic;
protected ref map<string, int> m_Rejections = new map<string, int>();
protected void RecordRejection(string reason)
{
if (reason.IsEmpty()) reason = "unspecified terrain clearance";
int count;
m_Rejections.Find(reason, count);
m_Rejections.Set(reason, count + 1);
}
protected string RejectionSummary()
{
string summary;
foreach (string reason, int count : m_Rejections)
{
if (!summary.IsEmpty()) summary += "; ";
summary += reason + "=" + count.ToString();
}
if (summary.IsEmpty()) return "none recorded";
return summary;
}
protected void ReportSearch(ERR_SiteDefinition site, string stage)
{
PrintFormat("Reintroduced [placement search]: stage=%1 site=%2 (%3) attempts=%4 elapsedMs=%5 failed/deferred checks: %6", stage, site.Id, site.Label, m_Attempts, System.GetTickCount() - m_Began, RejectionSummary());
}
protected void ReportProgress(ERR_SiteDefinition site)
{
int now = System.GetTickCount();
if (now - m_Began < 30000 || now < m_NextDiagnostic) return;
ReportSearch(site, "still searching");
m_NextDiagnostic = now + 30000;
}
protected void ClearDiagnostics() { m_Rejections.Clear(); m_NextDiagnostic = 0; }
protected bool m_Started; protected bool m_Started;
protected ERR_PrepareResult m_Result = ERR_PrepareResult.PENDING; protected ERR_PrepareResult m_Result = ERR_PrepareResult.PENDING;
protected string m_Error; protected string m_Error;
string Error() { return m_Error; } string Error() { return m_Error; }
protected bool m_StartupOnly;
static bool StartupSite(ERR_SiteDefinition site)
{
return site && (site.Kind == ERR_LocationKind.PRISON || site.Kind == ERR_LocationKind.EXTRACTION);
}
void StartupOnly(bool value)
{
if (m_StartupOnly && !value) m_Began = System.GetTickCount();
m_StartupOnly = value;
}
bool StartupReady()
{
if (!m_Started || !m_Definition || m_Result == ERR_PrepareResult.FAILED) return false;
foreach (ERR_SiteDefinition site : m_Definition.Sites)
if (StartupSite(site) && (site.RandomClear || m_Definition.TerrainGenerated) && !site.PlacementResolved) return false;
return true;
}
bool Begin(ERR_ScenarioDefinition definition) bool Begin(ERR_ScenarioDefinition definition)
{ {
if (m_Started || !Replication.IsServer() || !definition || !definition.Valid() || !GetGame().GetWorldEntity()) return false; if (m_Started || !Replication.IsServer() || !definition || !definition.Valid() || !GetGame().GetWorldEntity()) return false;
m_Started = true; m_Definition = definition; m_Started = true; m_Definition = definition;
// Prison resolves first, irrespective of authored array order. // Prison resolves first, irrespective of authored array order.
foreach (ERR_SiteDefinition prison : definition.Sites) if (prison.RandomClear && prison.Kind == ERR_LocationKind.PRISON) m_Pending.Insert(prison); foreach (ERR_SiteDefinition prison : definition.Sites) if (prison.RandomClear && prison.Kind == ERR_LocationKind.PRISON) m_Pending.Insert(prison);
foreach (ERR_SiteDefinition extraction : definition.Sites) if (extraction.RandomClear && extraction.Kind == ERR_LocationKind.EXTRACTION) m_Pending.Insert(extraction);
foreach (ERR_SiteDefinition radio : definition.Sites) if (radio.RandomClear && radio.Kind == ERR_LocationKind.RADIO) m_Pending.Insert(radio); foreach (ERR_SiteDefinition radio : definition.Sites) if (radio.RandomClear && radio.Kind == ERR_LocationKind.RADIO) m_Pending.Insert(radio);
foreach (ERR_SiteDefinition mortar : definition.Sites) if (mortar.RandomClear && mortar.Kind == ERR_LocationKind.MORTAR) m_Pending.Insert(mortar); foreach (ERR_SiteDefinition mortar : definition.Sites) if (mortar.RandomClear && mortar.Kind == ERR_LocationKind.MORTAR) m_Pending.Insert(mortar);
foreach (ERR_SiteDefinition optional : definition.Sites) foreach (ERR_SiteDefinition optional : definition.Sites)
@@ -117,6 +186,7 @@ class ERR_GeneratedSites
if (candidate.Kind == ERR_LocationKind.RADIO && other.Kind == ERR_LocationKind.PRISON) minimum = 1000; if (candidate.Kind == ERR_LocationKind.RADIO && other.Kind == ERR_LocationKind.PRISON) minimum = 1000;
if (vector.DistanceXZ(position, other.Position) < minimum) return false; if (vector.DistanceXZ(position, other.Position) < minimum) return false;
if (candidate.Kind == ERR_LocationKind.PRISON && other.Kind == ERR_LocationKind.EXTRACTION && vector.DistanceXZ(position, other.Position) + 100 >= ERR_GameMode.Get().EscapeRadius()) return false; if (candidate.Kind == ERR_LocationKind.PRISON && other.Kind == ERR_LocationKind.EXTRACTION && vector.DistanceXZ(position, other.Position) + 100 >= ERR_GameMode.Get().EscapeRadius()) return false;
if (candidate.Kind == ERR_LocationKind.EXTRACTION && other.Kind == ERR_LocationKind.PRISON && vector.DistanceXZ(position, other.Position) + 100 >= ERR_GameMode.Get().EscapeRadius()) return false;
} }
return true; return true;
} }
@@ -138,6 +208,16 @@ class ERR_GeneratedSites
PrintFormat("Co10 Escape Reintroduced [site skipped]: %1 (%2): no safe placement after %3 attempts and authored fallback rejected; omitted for this round; mission preparation continues", retained.Label, retained.Id, m_Attempts); PrintFormat("Co10 Escape Reintroduced [site skipped]: %1 (%2): no safe placement after %3 attempts and authored fallback rejected; omitted for this round; mission preparation continues", retained.Label, retained.Id, m_Attempts);
return true; return true;
} }
protected bool RescueFits(ERR_SiteDefinition site, vector position, float heading)
{
if (site.Kind != ERR_LocationKind.EXTRACTION) return true;
vector primary = ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(site.AircraftOffset, heading));
// Validate the primary pad before committing a random candidate, so an
// obstructed clearing is retried instead of failing later during site creation.
if (site.RequireAirfield) { if (!ERR_AirfieldPlacement.Clear(primary)) return false; }
else if (!ERR_AirfieldPlacement.ClearGround(primary)) return false;
return ERR_RescueLanding.Fits(primary);
}
ERR_PrepareResult Tick() ERR_PrepareResult Tick()
{ {
if (m_Result != ERR_PrepareResult.PENDING) return m_Result; if (m_Result != ERR_PrepareResult.PENDING) return m_Result;
@@ -145,7 +225,9 @@ class ERR_GeneratedSites
int tick = System.GetTickCount(); int tick = System.GetTickCount();
while (m_Index < m_Pending.Count()) while (m_Index < m_Pending.Count())
{ {
if (m_StartupOnly && StartupReady()) return ERR_PrepareResult.PENDING;
ERR_SiteDefinition site = m_Pending[m_Index]; ERR_SiteDefinition site = m_Pending[m_Index];
ReportProgress(site);
bool fallback = m_Attempts >= 12000 || System.GetTickCount() - m_Began >= 30000; bool fallback = m_Attempts >= 12000 || System.GetTickCount() - m_Began >= 30000;
vector position = site.Position; float heading = site.Heading; vector position = site.Position; float heading = site.Heading;
if (!fallback) if (!fallback)
@@ -154,22 +236,30 @@ class ERR_GeneratedSites
heading = Math.RandomFloat(0, 360); m_Attempts++; heading = Math.RandomFloat(0, 360); m_Attempts++;
} }
float floor; float floor;
if (Separated(site, position, fallback) && m_Terrain.Site(site, position, heading, floor)) bool fits;
if (!Separated(site, position, fallback)) RecordRejection("site spacing / escape boundary");
else if (!m_Terrain.Site(site, position, heading, floor)) RecordRejection(m_Terrain.Rejection());
else if (!RescueFits(site, position, heading)) RecordRejection("aircraft landing clearance / fleet space");
else fits = true;
if (fits)
{ {
position[1] = floor + 0.02; position[1] = floor + 0.02;
site.Position = position; site.Heading = heading; site.PlacementResolved = true; site.Position = position; site.Heading = heading; site.PlacementResolved = true;
PrintFormat("Co10 Escape Reintroduced [generated site]: id=%1 position=%2 heading=%3 attempts=%4 fallback=%5", site.Id, position, heading, m_Attempts, fallback); PrintFormat("Co10 Escape Reintroduced [generated site]: id=%1 position=%2 heading=%3 attempts=%4 fallback=%5", site.Id, position, heading, m_Attempts, fallback);
ReportSearch(site, "placed"); ClearDiagnostics();
m_Index++; m_Attempts = 0; m_Began = System.GetTickCount(); m_Index++; m_Attempts = 0; m_Began = System.GetTickCount();
} }
else if (fallback) else if (fallback)
{ {
ReportSearch(site, "exhausted; authored fallback rejected");
if (SkipOptional(site)) if (SkipOptional(site))
{ {
ClearDiagnostics();
m_Index++; m_Attempts = 0; m_Began = System.GetTickCount(); m_Index++; m_Attempts = 0; m_Began = System.GetTickCount();
} }
else else
{ {
m_Error = "No safe terrain or valid authored fallback for required site " + site.Id; m_Error = "No safe terrain or valid authored fallback for required site " + site.Id + ". Checks: " + RejectionSummary();
m_Result = ERR_PrepareResult.FAILED; return m_Result; m_Result = ERR_PrepareResult.FAILED; return m_Result;
} }
} }

Some files were not shown because too many files have changed in this diff Show More