Compare commits
40
Commits
0a7102cd51
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
828bb0a1a7 | ||
|
|
4f02ad64b4 | ||
|
|
6ba3036de6 | ||
|
|
b8dfa9184e | ||
|
|
0e1e7790f9 | ||
|
|
2c2158702d | ||
|
|
d21adb90d5 | ||
|
|
6738eda748 | ||
|
|
2dcbe3107d | ||
|
|
3ec882dc4e | ||
|
|
08e5cbf5ea | ||
|
|
681d5e5eae | ||
|
|
213c31dfb2 | ||
|
|
5453159867 | ||
|
|
c6ee764c6c | ||
|
|
970dd338f5 | ||
|
|
95d21b06ff | ||
|
|
9233320fd1 | ||
|
|
6397fe89b6 | ||
|
|
3768ce7ed8 | ||
|
|
253dde90cd | ||
|
|
ac285b5c97 | ||
|
|
f41d8ba2f9 | ||
|
|
87d85ff02e | ||
|
|
ae2aedd131 | ||
|
|
cc322572b4 | ||
|
|
372fca7a68 | ||
|
|
82b52ae1fc | ||
|
|
d5464f7438 | ||
|
|
0e71ef1fa7 | ||
|
|
38fd8f7dd2 | ||
|
|
8f2e1f210d | ||
|
|
7a7ac59edf | ||
|
|
96f471ef4a | ||
|
|
f15c915e54 | ||
|
|
a66aca35bf | ||
|
|
ae9aaafaae | ||
|
|
009b2fb591 | ||
|
|
07c4d70de5 | ||
|
|
a2106cfa26 |
+31
@@ -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
|
||||||
@@ -1,23 +1,32 @@
|
|||||||
# Push changelogs
|
# Push changelogs
|
||||||
|
|
||||||
The user authorized Discord changelogs after new changes are successfully pushed.
|
Drone is the sole Discord changelog sender. Its first delivery was verified in
|
||||||
After each successful push to `origin/main` from this checkout:
|
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.
|
||||||
|
|
||||||
1. Prepare a concise, player-facing changelog covering the commits in that push.
|
For player-facing changes, maintain the newest entry in `CHANGELOG.md` before
|
||||||
Include only the changelog: describe fixes/features accurately, without server,
|
pushing to `origin/main`. Keep it concise and describe only fixes/features, without
|
||||||
deployment, or Git push status. Do not add operational disclaimers or footers.
|
server, deployment, Git push status, operational disclaimers or footers. Do not
|
||||||
Phrase gameplay fixes without mentioning the server (for example, "Fixed radio
|
backfill old changes unless requested. Internal workflow-only changes need no
|
||||||
station generation after an empty-session wait").
|
announcement.
|
||||||
2. Save the message to a temporary file and run
|
|
||||||
`python3 tools/discord_changelog.py --commit <pushed-sha> --message-file <path>`.
|
|
||||||
3. Report a delivery failure honestly. Do not blindly retry a pending delivery;
|
|
||||||
check Discord first to avoid duplicates.
|
|
||||||
|
|
||||||
The webhook credential is in `.local/discord-webhook.url` (ignored, owner-only).
|
After a confirmed successful Workbench Workshop upload, run
|
||||||
Never print it, commit it, or put it into changelogs or command-line arguments.
|
`python3 tools/workshop_changelog.py --published` before committing the changelog.
|
||||||
The helper verifies the remote commit, disables mentions, and records delivery in
|
This reads the local publishing manifest and records the version in the changelog
|
||||||
`.local/discord-changelog-state.json`. Do not backfill old commits unless requested.
|
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.
|
||||||
|
|
||||||
This is an assistant workflow, not a server-side Git hook. Notifications for
|
The `.drone.yml` pipeline posts the latest changed entry using Drone's
|
||||||
pushes from other checkouts require a Discord push webhook in Gitea repository
|
`discord_webhook_url` repository secret. See `docs/discord-changelog-ci.md`.
|
||||||
settings; that server-side automation has not been configured.
|
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
@@ -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.
|
||||||
@@ -39,12 +39,26 @@ 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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -70,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
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 {
|
ActionContext ERR_FieldMapContext {
|
||||||
Priority 60
|
Priority 60
|
||||||
Flags 0xa 0
|
Flags 0xa 0
|
||||||
|
|||||||
@@ -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 { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,6 @@ ERR_GameMode : "{0F307326459A1395}Prefabs/MP/Modes/GameMode_Base.et" {
|
|||||||
m_SpawnLogic ERR_SpawnLogic "{79AD722B2EFD595F}" {}
|
m_SpawnLogic ERR_SpawnLogic "{79AD722B2EFD595F}" {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_fAutoReloadTime 10
|
m_fAutoReloadTime 120
|
||||||
m_bUseSpawnPreload 1
|
m_bUseSpawnPreload 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
GenericEntity {
|
GenericEntity {
|
||||||
|
ID "E83CA269A0D647B1"
|
||||||
components {
|
components {
|
||||||
MeshObject { Object "{76D738EEB0CA431F}Assets/Items/ERR_Atlas/ERR_AtlasLeaf.xob" InheritVisibility 1 }
|
MeshObject { Object "{76D738EEB0CA431F}Assets/Items/ERR_Atlas/ERR_AtlasLeaf.xob" InheritVisibility 1 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,91 +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 {"aiDoctrine", "aiShadow", "automaticEquipmentChancePercent", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "emptyRestartHours", "startMode", "radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "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 == "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 == "motorbikeCount") prefix = "mission motorbikes";
|
|
||||||
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 == "emptyRestartHours") prefix = "mission emptyhours";
|
|
||||||
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 == "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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
||||||
{
|
{
|
||||||
@@ -131,16 +46,9 @@ 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 = {"PlaneOn", "PlaneOff", "Close", "OpenSettings", "OpenWorld", "OpenFactions", "OpenHQ", "OpenJournal", "PauseJournal", "JournalDetails", "LatestJournal", "KeepWeaponOn", "KeepWeaponOff", "SelfEpiOn", "SelfEpiOff", "IntelSummary", "RerollIntel", "OpenMods", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems", "OpenTTS"};
|
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);
|
||||||
names.Insert("Default_viewDistanceMetres");
|
foreach (string pageButton : Navigation()) names.Insert(pageButton);
|
||||||
names.Insert("OpenForces");
|
|
||||||
names.Insert("RestartEmptyOn"); names.Insert("RestartEmptyOff");
|
|
||||||
names.Insert("OpenGuards"); names.Insert("GuardsBack");
|
|
||||||
names.Insert("OpenSites"); names.Insert("SitesBack");
|
|
||||||
names.Insert("OpenPlane"); names.Insert("PlaneBack");
|
|
||||||
names.Insert("OpenAI"); names.Insert("AIBack");
|
|
||||||
names.Insert("EquipmentOverview"); names.Insert("OpenAllowedItems"); names.Insert("OpenItemPresets"); names.Insert("OpenWeights"); names.Insert("OpenRecommendations"); names.Insert("RecommendationsBack"); names.Insert("RecommendedPlane"); names.Insert("RecommendedWeapons");
|
|
||||||
for (int density = 1; density <= 4; density++) names.Insert("GuardEvery_" + density.ToString());
|
for (int density = 1; density <= 4; density++) names.Insert("GuardEvery_" + density.ToString());
|
||||||
for (int policy = 0; policy <= 2; policy++) names.Insert("GuardHQ_" + policy.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())
|
||||||
@@ -164,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();
|
||||||
}
|
}
|
||||||
@@ -191,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)
|
||||||
@@ -227,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)
|
||||||
@@ -235,32 +143,24 @@ 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", "TerrainsPage", "ItemsPage", "TTSPage", "ForcesPage", "GuardsPage", "SitesPage", "PlanePage", "RecommendationsPage", "WeightsPage", "AllowedItemsPage", "ItemPresetsPage", "AIPage"}; }
|
static array<string> Pages() { return ERR_AdminMenuPages.Pages(); }
|
||||||
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"}; }
|
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);
|
|
||||||
bool selected = i == page || ((page == 12 || page == 13) && i == 11) || ((page == 16 || page == 17 || page == 18) && i == 9) || ((page == 14 || page == 19) && i == 1);
|
|
||||||
GetRootWidget().FindAnyWidget("Selected_" + navigation[i]).SetVisible(selected);
|
|
||||||
ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText(navigation[i], GetRootWidget()), selected);
|
|
||||||
}
|
|
||||||
GetRootWidget().FindAnyWidget("EquipmentNavigation").SetVisible(page == 9 || page == 16 || page == 17 || page == 18);
|
|
||||||
GetRootWidget().FindAnyWidget("Selected_EquipmentOverview").SetVisible(page == 9);
|
|
||||||
ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText("EquipmentOverview", GetRootWidget()), page == 9);
|
|
||||||
if (page == 2) m_LogPoll = 1;
|
|
||||||
}
|
}
|
||||||
protected int m_KeepWeaponState = -1;
|
protected int m_KeepWeaponState = -1;
|
||||||
protected int m_SelfEpiState = -1;
|
protected int m_SelfEpiState = -1;
|
||||||
|
protected int m_SitesState = -1;
|
||||||
protected int m_RestartEmptyState = -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(), "SelfEpiOn", "SelfEpiOff", m_SelfEpiState);
|
||||||
ERR_SelectionStyle.Pair(GetRootWidget(), "KeepWeaponOn", "KeepWeaponOff", m_KeepWeaponState);
|
ERR_SelectionStyle.Pair(GetRootWidget(), "KeepWeaponOn", "KeepWeaponOff", m_KeepWeaponState);
|
||||||
ERR_SelectionStyle.Pair(GetRootWidget(), "RestartEmptyOn", "RestartEmptyOff", m_RestartEmptyState);
|
ERR_SelectionStyle.Pair(GetRootWidget(), "RestartEmptyOn", "RestartEmptyOff", m_RestartEmptyState);
|
||||||
@@ -278,8 +178,12 @@ class ERR_AdminMenu : ChimeraMenuBase
|
|||||||
else if (name.IndexOf("GuardHQ_") == 0) button.SetEnabled(m_Ready && m_HQ && m_AvailableFields.Contains("objectGuardHQMode"));
|
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 == "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 == "RestartEmptyOn") button.SetEnabled(m_Ready && m_RestartEmptyState == 0);
|
||||||
else if (name == "RestartEmptyOff") button.SetEnabled(m_Ready && m_RestartEmptyState == 1);
|
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 == "SelfEpiOn") button.SetEnabled(m_Ready && m_SelfEpiState == 0);
|
||||||
else if (name == "SelfEpiOff") button.SetEnabled(m_Ready && m_SelfEpiState == 1);
|
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 == "KeepWeaponOn") button.SetEnabled(m_Ready && m_KeepWeaponState == 0);
|
||||||
@@ -290,7 +194,7 @@ class ERR_AdminMenu : ChimeraMenuBase
|
|||||||
{
|
{
|
||||||
if (!authorized) { Close(); return; }
|
if (!authorized) { Close(); return; }
|
||||||
if (!values) return;
|
if (!values) return;
|
||||||
m_HQ = false; m_Intel = false; m_AvailableFields.Clear(); m_KeepWeaponState = -1; m_SelfEpiState = -1; m_RestartEmptyState = -1;
|
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; }
|
||||||
@@ -298,6 +202,8 @@ 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 == "recommendedBikeRuntime") { SetText("RecommendedBikeRuntime", entry.Substring(divider + 1, entry.Length() - divider - 1)); continue; }
|
||||||
if (key.IndexOf("recommended_") == 0)
|
if (key.IndexOf("recommended_") == 0)
|
||||||
{
|
{
|
||||||
@@ -309,8 +215,8 @@ class ERR_AdminMenu : ChimeraMenuBase
|
|||||||
Widget state = GetRootWidget().FindAnyWidget("recommended_" + addonKey);
|
Widget state = GetRootWidget().FindAnyWidget("recommended_" + addonKey);
|
||||||
if (state)
|
if (state)
|
||||||
{
|
{
|
||||||
if (entry.Substring(divider + 1, entry.Length() - divider - 1) == "1") state.SetColor(Color.FromRGBA(105, 190, 140, 255));
|
if (entry.Substring(divider + 1, entry.Length() - divider - 1) == "1") state.SetColor(Color.FromInt(ERR_MenuTheme.READY_TEXT));
|
||||||
else state.SetColor(Color.FromRGBA(220, 115, 115, 255));
|
else state.SetColor(Color.FromInt(ERR_MenuTheme.UNAVAILABLE_TEXT));
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -326,6 +232,15 @@ class ERR_AdminMenu : ChimeraMenuBase
|
|||||||
ERR_SelectionStyle.Pair(GetRootWidget(), "PlaneOn", "PlaneOff", planeState);
|
ERR_SelectionStyle.Pair(GetRootWidget(), "PlaneOn", "PlaneOff", planeState);
|
||||||
m_AvailableFields.Insert(key); continue;
|
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")
|
if (key == "restartOnEmpty")
|
||||||
{
|
{
|
||||||
m_RestartEmptyState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
|
m_RestartEmptyState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
|
||||||
@@ -334,6 +249,11 @@ class ERR_AdminMenu : ChimeraMenuBase
|
|||||||
SetText("Saved_restartOnEmpty", "Saved: " + restartState);
|
SetText("Saved_restartOnEmpty", "Saved: " + restartState);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (key == "showAllSites")
|
||||||
|
{
|
||||||
|
m_SitesState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (key == "selfEpinephrine")
|
if (key == "selfEpinephrine")
|
||||||
{
|
{
|
||||||
m_SelfEpiState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
|
m_SelfEpiState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
|
||||||
@@ -432,13 +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; }
|
||||||
if (name == "RecommendationsBack") { ShowPage(3); return; }
|
int section = ERR_AdminMenuPages.MainButtons().Find(name);
|
||||||
if (name == "RecommendedPlane") { ShowPage(14); return; }
|
if (section >= 0) { ShowPage(ERR_AdminMenuPages.MainPage(section)); return; }
|
||||||
if (name == "EquipmentOverview") { ShowPage(9); return; }
|
if (name == "RecommendedDrones") { ShowPage(ERR_AdminPage.DRONES); return; }
|
||||||
if (name == "RecommendedWeapons") { ShowPage(9); return; }
|
if (name == "RecommendedPlane") { ShowPage(ERR_AdminPage.PLANE); return; }
|
||||||
if (name == "PlaneBack") { ShowPage(1); return; }
|
if (name == "RecommendedWeapons") { ShowPage(ERR_AdminPage.ITEMS); return; }
|
||||||
if (name == "AIBack") { ShowPage(1); return; }
|
|
||||||
if (name == "GuardsBack" || name == "SitesBack") { ShowPage(11); 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")
|
||||||
@@ -487,6 +405,12 @@ class ERR_AdminMenu : ChimeraMenuBase
|
|||||||
m_ClickDelay = 0.3; SetText("Feedback", "Saving reconnaissance plane selection for next round...");
|
m_ClickDelay = 0.3; SetText("Feedback", "Saving reconnaissance plane selection for next round...");
|
||||||
m_Controller.ERR_AdminSubmit("mission plane set " + enabled, true); return;
|
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 (name == "RestartEmptyOn" || name == "RestartEmptyOff")
|
||||||
{
|
{
|
||||||
if (m_RestartEmptyState < 0) return;
|
if (m_RestartEmptyState < 0) return;
|
||||||
@@ -494,6 +418,13 @@ class ERR_AdminMenu : ChimeraMenuBase
|
|||||||
m_ClickDelay = 0.3; SetText("Feedback", "Saving restart after last disconnect...");
|
m_ClickDelay = 0.3; SetText("Feedback", "Saving restart after last disconnect...");
|
||||||
m_Controller.ERR_AdminSubmit("mission restartempty set " + restartEnabled, true); return;
|
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 (name == "SelfEpiOn" || name == "SelfEpiOff")
|
||||||
{
|
{
|
||||||
if (m_SelfEpiState < 0) return;
|
if (m_SelfEpiState < 0) return;
|
||||||
|
|||||||
@@ -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,12 +4,16 @@ 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 == "doctrine") return "aiDoctrine";
|
||||||
if (section == "mission" && option == "aishadow") return "aiShadow";
|
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 == "motorbikes") return "motorbikeCount";
|
||||||
if (section == "mission" && option == "planealtitude") return "planeAltitudeMetres";
|
if (section == "mission" && option == "planealtitude") return "planeAltitudeMetres";
|
||||||
if (section == "mission" && option == "planereplacement") return "planeReplacementSeconds";
|
if (section == "mission" && option == "planereplacement") return "planeReplacementSeconds";
|
||||||
if (section == "mission" && option == "planesector") return "planeSectorSeconds";
|
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 == "plane") return "planeReconEnabled";
|
||||||
if (section == "mission" && option == "camps") return "fieldCampCount";
|
if (section == "mission" && option == "camps") return "fieldCampCount";
|
||||||
if (section == "mission" && option == "hospitals") return "fieldHospitalCount";
|
if (section == "mission" && option == "hospitals") return "fieldHospitalCount";
|
||||||
@@ -20,6 +24,12 @@ class ERR_CommandArguments
|
|||||||
if (section == "mission" && option == "objectguards") return "objectGuardCount";
|
if (section == "mission" && option == "objectguards") return "objectGuardCount";
|
||||||
if (section == "mission" && option == "hunterdistance") return "hunterSpawnDistanceMetres";
|
if (section == "mission" && option == "hunterdistance") return "hunterSpawnDistanceMetres";
|
||||||
if (section == "mission" && option == "roadblocks") return "roadblockCount";
|
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 == "traffic") return "civilianTrafficCount";
|
||||||
if (section == "mission" && option == "start") return "startMode";
|
if (section == "mission" && option == "start") return "startMode";
|
||||||
if (section == "mission" && option == "selfepi") return "selfEpinephrine";
|
if (section == "mission" && option == "selfepi") return "selfEpinephrine";
|
||||||
@@ -27,6 +37,8 @@ class ERR_CommandArguments
|
|||||||
if (section == "mission" && option == "restartempty") return "restartOnEmpty";
|
if (section == "mission" && option == "restartempty") return "restartOnEmpty";
|
||||||
if (section == "mission" && option == "emptyhours") return "emptyRestartHours";
|
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 "";
|
||||||
}
|
}
|
||||||
@@ -34,8 +46,8 @@ class ERR_CommandArguments
|
|||||||
{
|
{
|
||||||
if (key == "aiDoctrine" && text == "current") { value = 0; return true; }
|
if (key == "aiDoctrine" && text == "current") { value = 0; return true; }
|
||||||
if (key == "aiDoctrine" && text == "coordinated") { value = 1; return true; }
|
if (key == "aiDoctrine" && text == "coordinated") { value = 1; return true; }
|
||||||
if ((key == "aiShadow" || key == "restartOnEmpty") && text == "off") { value = 0; return true; }
|
if ((key == "aiShadow" || key == "restartOnEmpty" || key == "showAllSites") && text == "off") { value = 0; return true; }
|
||||||
if ((key == "aiShadow" || key == "restartOnEmpty") && text == "on") { value = 1; 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 == "prison") { value = 0; return true; }
|
||||||
if (key == "startMode" && text == "crash") { value = 1; 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; }
|
||||||
@@ -170,7 +182,10 @@ class ERR_AdminService
|
|||||||
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 == "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 == "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 == "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 == "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 == "planeAltitudeMetres") return "150-500; default 250. Applies next round.";
|
||||||
if (key == "planeReplacementSeconds") return "30-1800; default 60. 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 == "planeSectorSeconds") return "30-600; default 120. Applies next round.";
|
||||||
@@ -181,22 +196,32 @@ class ERR_AdminService
|
|||||||
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 == "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 == "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 == "hunterSpawnDistanceMetres") return "Opening UAZ and troop-truck patrol placement. Applies next round.";
|
||||||
if (key == "roadblockCount") return "Set 0..30 roadblocks (default 4; 0 disables). Applies next round. Each has four guards; unsuitable sites are skipped.";
|
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 == "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 == "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 == "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 == "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 "";
|
return "";
|
||||||
}
|
}
|
||||||
protected string RoadblockTiming(string key)
|
protected string RoadblockTiming(string key)
|
||||||
{
|
{
|
||||||
if (key == "aiDoctrine" || key == "aiShadow") return " for next round";
|
if (key == "radioIntelSiteCount" || key == "garageVehicleCount" || key == "garageChancePercent" || key == "garageRadiusMetres" || key == "garageSpawnIntervalSeconds") 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") 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)
|
||||||
@@ -238,6 +263,7 @@ class ERR_AdminService
|
|||||||
}
|
}
|
||||||
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;
|
||||||
@@ -327,6 +353,7 @@ class ERR_AdminService
|
|||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,11 +68,38 @@ class ERR_AdminSession
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
array<string> replies = {};
|
array<string> replies = {};
|
||||||
if (!ItemCatalogCommand(arguments, replies) && !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.
|
// Per-session paging: another administrator cannot change this menu's selection.
|
||||||
protected bool ItemCatalogCommand(string arguments, array<string> replies)
|
protected bool ItemCatalogCommand(string arguments, array<string> replies)
|
||||||
{
|
{
|
||||||
@@ -132,9 +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);
|
ERR_RecommendedAddons.Snapshot(m_Mode, values);
|
||||||
GetGame().ERR_ServerItems().Snapshot(m_Mode, values, m_ItemAddon, m_ItemPage);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class ERR_AirfieldPlacement
|
|||||||
}
|
}
|
||||||
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();
|
||||||
array<vector> authored = {};
|
array<vector> authored = {};
|
||||||
AuthoredAirports(authored);
|
AuthoredAirports(authored);
|
||||||
@@ -49,12 +49,12 @@ class ERR_AirfieldPlacement
|
|||||||
// Same aircraft footprint/vertical sweep, without requiring airport scenery.
|
// Same aircraft footprint/vertical sweep, without requiring airport scenery.
|
||||||
static bool ClearGround(vector position)
|
static bool ClearGround(vector position)
|
||||||
{
|
{
|
||||||
if (!Replication.IsServer() || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), position)) return false;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,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)
|
||||||
@@ -126,20 +135,24 @@ 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 (m_ERRMarker) m_ERRMarker.RefreshPresentation();
|
if (m_ERRMarker) m_ERRMarker.RefreshPresentation();
|
||||||
if (Knowledge() == m_ERRShown && Label() == m_ERRShownLabel) return;
|
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";
|
||||||
@@ -152,18 +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.ERR_InsertDiscoveryMarker(m_ERRMarker);
|
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);
|
||||||
// The objective presentation performs its own normal viewport culling.
|
// The objective presentation performs its own normal viewport culling.
|
||||||
m_ERRMarker.RefreshPresentation();
|
m_ERRMarker.RefreshPresentation();
|
||||||
m_ERRShown = Knowledge();
|
m_ERRShown = shown;
|
||||||
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;
|
||||||
@@ -60,10 +196,22 @@ class ERR_DiscoveryService
|
|||||||
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);
|
||||||
@@ -72,15 +220,24 @@ class ERR_DiscoveryService
|
|||||||
}
|
}
|
||||||
if (eligible.IsEmpty())
|
if (eligible.IsEmpty())
|
||||||
{
|
{
|
||||||
if (!document && !m_Mode.WorldPreparing() && MarkSpent(folder) && !mineRecovered) 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(); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ modded class SCR_PlayerController
|
|||||||
protected void ERR_ReceiveExtractionRadio(string text, string url)
|
protected void ERR_ReceiveExtractionRadio(string text, string url)
|
||||||
{
|
{
|
||||||
if (this != GetGame().GetPlayerController() || !ERR_GameMode.Get()) return;
|
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; }
|
if (!url.IsEmpty()) { GetGame().ERR_PlaySpeechPreview(url, text, "Extraction radio"); return; }
|
||||||
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
|
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
|
||||||
if (hints) hints.ShowCustom(text, "Extraction radio", 20);
|
if (hints) hints.ShowCustom(text, "Extraction radio", 20);
|
||||||
|
|||||||
@@ -8,6 +8,16 @@ class ERR_RescueRules
|
|||||||
if (since < 0 || now < since) return now;
|
if (since < 0 || now < since) return now;
|
||||||
return since;
|
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)
|
static bool CanDepart(float since, float now)
|
||||||
{
|
{
|
||||||
return since >= 0 && now >= since && now - since >= 8000;
|
return since >= 0 && now >= since && now - since >= 8000;
|
||||||
@@ -64,7 +74,11 @@ class ERR_ExtractionService
|
|||||||
{
|
{
|
||||||
m_Stage = value; m_Mode.PublishPickupStage(value);
|
m_Stage = value; m_Mode.PublishPickupStage(value);
|
||||||
PrintFormat("Reintroduced [extraction]: stage=%1 %2", value, message);
|
PrintFormat("Reintroduced [extraction]: stage=%1 %2", value, message);
|
||||||
m_Mode.DeliverExtractionRadio(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;
|
m_RadioPending = message; m_RadioUntil = GetGame().GetWorld().GetWorldTime() + 30000;
|
||||||
}
|
}
|
||||||
void PreparingRoute()
|
void PreparingRoute()
|
||||||
@@ -230,6 +244,7 @@ class ERR_ExtractionService
|
|||||||
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;
|
||||||
@@ -324,8 +339,12 @@ class ERR_ExtractionService
|
|||||||
if (m_State != ERR_ExtractionState.WAITING || !m_Assigned) return;
|
if (m_State != ERR_ExtractionState.WAITING || !m_Assigned) return;
|
||||||
if (!FleetAlive()) { Resolve(ERR_ExtractionState.LOST); return; }
|
if (!FleetAlive()) { Resolve(ERR_ExtractionState.LOST); return; }
|
||||||
if (m_Stage != 5 || phase != ERR_Phase.EXTRACTION) return;
|
if (m_Stage != 5 || phase != ERR_Phase.EXTRACTION) return;
|
||||||
|
bool airborne = !m_Transports.IsEmpty();
|
||||||
foreach (ERR_RescueFlightComponent departing : m_Transports)
|
foreach (ERR_RescueFlightComponent departing : m_Transports)
|
||||||
if (vector.DistanceXZ(departing.Aircraft().GetOrigin(), m_Pickup) < 1000 || vector.DistanceXZ(departing.Aircraft().GetOrigin(), m_Center) <= m_Radius) return;
|
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)
|
||||||
@@ -355,6 +374,13 @@ class ERR_ExtractionService
|
|||||||
}
|
}
|
||||||
void Stop()
|
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();
|
m_Stopped = true; m_RadioPending = ""; m_PreviousSignals.Clear();
|
||||||
foreach (ERR_RescueFlightComponent transport : m_Transports) if (transport) transport.Release(false);
|
foreach (ERR_RescueFlightComponent transport : m_Transports) if (transport) transport.Release(false);
|
||||||
foreach (ERR_RescueFlightComponent escort : m_Escorts) if (escort) escort.Release(false);
|
foreach (ERR_RescueFlightComponent escort : m_Escorts) if (escort) escort.Release(false);
|
||||||
|
|||||||
@@ -26,6 +26,16 @@ 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;
|
[RplProp()] protected int m_StartMode;
|
||||||
protected bool m_StartModeFrozen;
|
protected bool m_StartModeFrozen;
|
||||||
protected bool m_StartPrepared;
|
protected bool m_StartPrepared;
|
||||||
@@ -55,6 +65,7 @@ class ERR_GameMode : SCR_BaseGameMode
|
|||||||
protected ref ERR_Performance m_Performance;
|
protected ref ERR_Performance m_Performance;
|
||||||
ERR_Performance Performance() { return m_Performance; }
|
ERR_Performance Performance() { return m_Performance; }
|
||||||
protected ref ERR_CivilianTraffic m_CivilianTraffic;
|
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 = {};
|
||||||
@@ -133,6 +144,7 @@ class ERR_GameMode : SCR_BaseGameMode
|
|||||||
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_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; }
|
||||||
@@ -498,6 +510,7 @@ class ERR_GameMode : SCR_BaseGameMode
|
|||||||
if (!Replication.IsServer()) return;
|
if (!Replication.IsServer()) return;
|
||||||
m_EmptySince = System.GetTickCount();
|
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();
|
if (!System.IsCLIParam("errNoPerf")) m_Performance = new ERR_Performance();
|
||||||
PublishViewDistance();
|
PublishViewDistance();
|
||||||
m_ContentSettings = new ERR_ContentSettings();
|
m_ContentSettings = new ERR_ContentSettings();
|
||||||
@@ -535,6 +548,9 @@ class ERR_GameMode : SCR_BaseGameMode
|
|||||||
m_Command = CreateCommandService();
|
m_Command = CreateCommandService();
|
||||||
m_CivilianTraffic = new ERR_CivilianTraffic(this);
|
m_CivilianTraffic = new ERR_CivilianTraffic(this);
|
||||||
OnStop().Insert(m_CivilianTraffic.Stop);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
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;
|
||||||
@@ -27,21 +30,19 @@ class ERR_CampTerrain
|
|||||||
}
|
}
|
||||||
bool Clear(vector center, float heading, float halfSize, float maximumRange, out float lowest, float clearanceHeight = 4)
|
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)
|
||||||
@@ -53,33 +54,42 @@ class ERR_CampTerrain
|
|||||||
// 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");
|
||||||
}
|
}
|
||||||
SCR_AIWorld aiWorld = SCR_AIWorld.Cast(GetGame().GetAIWorld());
|
bool extraction = site.Kind == ERR_LocationKind.EXTRACTION;
|
||||||
if (!aiWorld || !aiWorld.GetRoadNetworkManager()) return false;
|
// Camps must stay off roads; a helicopter pad may use clear paved ground.
|
||||||
BaseRoad road; float distance;
|
// Its physical terrain, obstacle and fleet checks still decide acceptance.
|
||||||
aiWorld.GetRoadNetworkManager().GetClosestRoad(position, road, distance, skipNavlinks: true);
|
if (!extraction)
|
||||||
if (road && distance < 12) return false;
|
{
|
||||||
if (!Clear(position, heading, 10, 0.6, baseHeight)) return false;
|
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))
|
if (ERR_StartSetup.IsCrash(site))
|
||||||
{
|
{
|
||||||
vector offset, center; float halfSize, height;
|
vector offset, center; float halfSize, height;
|
||||||
if (!ERR_StartSetup.Geometry(offset, center, halfSize, height)) return false;
|
if (!ERR_StartSetup.Geometry(offset, center, halfSize, height)) return Reject("crash footprint unavailable");
|
||||||
float wreckFloor;
|
float wreckFloor;
|
||||||
if (!Clear(position + Rotate(center, heading), heading, halfSize, 0.6, wreckFloor, height)) return false;
|
if (!Clear(position + Rotate(center, heading), heading, halfSize, 0.6, wreckFloor, height)) return Reject("wreck area: " + m_Rejection);
|
||||||
}
|
}
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,6 +100,38 @@ 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;
|
||||||
@@ -144,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;
|
||||||
}
|
}
|
||||||
@@ -169,6 +212,10 @@ class ERR_GeneratedSites
|
|||||||
{
|
{
|
||||||
if (site.Kind != ERR_LocationKind.EXTRACTION) return true;
|
if (site.Kind != ERR_LocationKind.EXTRACTION) return true;
|
||||||
vector primary = ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(site.AircraftOffset, heading));
|
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);
|
return ERR_RescueLanding.Fits(primary);
|
||||||
}
|
}
|
||||||
ERR_PrepareResult Tick()
|
ERR_PrepareResult Tick()
|
||||||
@@ -180,6 +227,7 @@ class ERR_GeneratedSites
|
|||||||
{
|
{
|
||||||
if (m_StartupOnly && StartupReady()) return ERR_PrepareResult.PENDING;
|
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)
|
||||||
@@ -188,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) && RescueFits(site, position, heading))
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,10 +50,11 @@ class ERR_LocationComponent : ScriptComponent
|
|||||||
bool Discover(int roll, int chance)
|
bool Discover(int roll, int chance)
|
||||||
{
|
{
|
||||||
if (!Replication.IsServer() || !m_Prepared || m_Knowledge >= 2) return false;
|
if (!Replication.IsServer() || !m_Prepared || m_Knowledge >= 2) return false;
|
||||||
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
|
if (mode && mode.ShowAllSites()) chance = 100;
|
||||||
int previous = m_Knowledge;
|
int previous = m_Knowledge;
|
||||||
m_Knowledge = ERR_MissionRules.Discovery(m_Knowledge, roll, chance);
|
m_Knowledge = ERR_MissionRules.Discovery(m_Knowledge, roll, chance);
|
||||||
Replication.BumpMe();
|
Replication.BumpMe();
|
||||||
ERR_GameMode mode = ERR_GameMode.Get();
|
|
||||||
if (mode) mode.LocationDiscovered(this, previous, m_Knowledge);
|
if (mode) mode.LocationDiscovered(this, previous, m_Knowledge);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Generated by tools/build_admin_menu.py; edit ui/admin_menu/theme.json.
|
||||||
|
class ERR_MenuTheme
|
||||||
|
{
|
||||||
|
static const int NEUTRAL = 0xFF454B52;
|
||||||
|
static const int NEUTRAL_HOVER = 0xFF606870;
|
||||||
|
static const int POSITIVE = 0xFF246B3C;
|
||||||
|
static const int POSITIVE_HOVER = 0xFF31894F;
|
||||||
|
static const int NEGATIVE = 0xFF8F2929;
|
||||||
|
static const int NEGATIVE_HOVER = 0xFFB43B3B;
|
||||||
|
static const int MIXED = 0xFF805B17;
|
||||||
|
static const int READY_TEXT = 0xFF69BE8C;
|
||||||
|
static const int UNAVAILABLE_TEXT = 0xFFDC7373;
|
||||||
|
}
|
||||||
@@ -19,16 +19,33 @@ modded class ERR_AdminMenu
|
|||||||
{
|
{
|
||||||
GetRootWidget().Update();
|
GetRootWidget().Update();
|
||||||
if (m_Page != expected) return false;
|
if (m_Page != expected) return false;
|
||||||
|
int activeGroup = ERR_AdminMenuPages.Group(expected);
|
||||||
|
Widget content = GetRootWidget().FindAnyWidget(Pages()[expected]);
|
||||||
|
if (!content) return false;
|
||||||
|
float contentX, contentY, contentWidth, contentHeight;
|
||||||
|
content.GetScreenPos(contentX, contentY); content.GetScreenSize(contentWidth, contentHeight);
|
||||||
|
array<string> mainButtons = ERR_AdminMenuPages.MainButtons();
|
||||||
|
array<string> sidebars = ERR_AdminMenuPages.Sidebars();
|
||||||
|
for (int group = 0; group < mainButtons.Count(); group++)
|
||||||
|
{
|
||||||
|
Widget mainTab = GetRootWidget().FindAnyWidget(mainButtons[group]);
|
||||||
|
Widget marker = GetRootWidget().FindAnyWidget("Selected_" + mainButtons[group]);
|
||||||
|
Widget sidebar = GetRootWidget().FindAnyWidget(sidebars[group]);
|
||||||
|
if (!mainTab || !marker || !sidebar || !mainTab.IsVisible() || marker.IsVisible() != (group == activeGroup) || sidebar.IsVisible() != (group == activeGroup)) return false;
|
||||||
|
float x, y, width, height;
|
||||||
|
mainTab.GetScreenPos(x, y); mainTab.GetScreenSize(width, height);
|
||||||
|
if (width <= 1 || height <= 1 || y + height > contentY) return false;
|
||||||
|
}
|
||||||
for (int i = 0; i < Pages().Count(); i++)
|
for (int i = 0; i < Pages().Count(); i++)
|
||||||
{
|
{
|
||||||
Widget panel = GetRootWidget().FindAnyWidget(Pages()[i]);
|
Widget panel = GetRootWidget().FindAnyWidget(Pages()[i]);
|
||||||
Widget tab = GetRootWidget().FindAnyWidget(Navigation()[i]);
|
Widget tab = GetRootWidget().FindAnyWidget(Navigation()[i]);
|
||||||
Widget selected = GetRootWidget().FindAnyWidget("Selected_" + Navigation()[i]);
|
Widget selected = GetRootWidget().FindAnyWidget("Selected_" + Navigation()[i]);
|
||||||
if (!panel || !tab || !selected || panel.IsVisible() != (i == expected) || !tab.IsVisible() || selected.IsVisible() != (i == expected)) return false;
|
if (!panel || !tab || !selected || panel.IsVisible() != (i == expected) || selected.IsVisible() != (i == expected)) return false;
|
||||||
float x, y, width, height, px, py;
|
if (ERR_AdminMenuPages.Group(i) != activeGroup) continue;
|
||||||
tab.GetScreenPos(x, y); tab.GetScreenSize(width, height); panel.GetScreenPos(px, py);
|
float x, y, width, height;
|
||||||
if (width <= 1 || height <= 1 || (i == expected && y + height > py))
|
tab.GetScreenPos(x, y); tab.GetScreenSize(width, height);
|
||||||
{ PrintFormat("ERR ADMIN NAV BOUNDS: page=%1 tab=%2 bounds=%3,%4,%5,%6 panelY=%7", expected, i, x, y, width, height, py); return false; }
|
if (!tab.IsVisible() || width <= 1 || height <= 1 || x + width > contentX || y < contentY || y + height > contentY + contentHeight) return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -163,6 +180,36 @@ modded class ERR_AdminMenu
|
|||||||
}
|
}
|
||||||
ERR_ProbePage(0); return true;
|
ERR_ProbePage(0); return true;
|
||||||
}
|
}
|
||||||
|
bool ERR_ProbeReportOverflow(string textName, string scrollName)
|
||||||
|
{
|
||||||
|
TextWidget text = TextWidget.Cast(GetRootWidget().FindAnyWidget(textName));
|
||||||
|
ScrollLayoutWidget scroll = ScrollLayoutWidget.Cast(GetRootWidget().FindAnyWidget(scrollName));
|
||||||
|
if (!text || !scroll) return false;
|
||||||
|
string saved = text.GetText();
|
||||||
|
string report;
|
||||||
|
for (int i = 0; i < 80; i++) report += "Report line " + i.ToString() + ": a long mission report must remain readable.\n";
|
||||||
|
report += "FINAL REPORT LINE";
|
||||||
|
text.SetText(report); GetRootWidget().Update();
|
||||||
|
float width, height, textWidth, textHeight, viewWidth, viewHeight;
|
||||||
|
text.GetScreenSize(width, height); text.GetTextSize(textWidth, textHeight);
|
||||||
|
scroll.GetScreenSize(viewWidth, viewHeight);
|
||||||
|
bool fits = GetGame().GetWorkspace().DPIUnscale(height) >= textHeight - 2 && height > viewHeight;
|
||||||
|
scroll.SetSliderPos(0, 1); GetRootWidget().Update();
|
||||||
|
float x, y, viewX, viewY, sliderX, sliderY;
|
||||||
|
text.GetScreenPos(x, y); scroll.GetScreenPos(viewX, viewY); scroll.GetSliderPos(sliderX, sliderY);
|
||||||
|
bool reachable = sliderY >= 0.98 && y + height <= viewY + viewHeight + 2 && y + height > viewY;
|
||||||
|
PrintFormat("ERR REPORT OVERFLOW: widget=%1 height=%2 textHeight=%3 viewport=%4 slider=%5 fits=%6 reachable=%7", textName, height, textHeight, viewHeight, sliderY, fits, reachable);
|
||||||
|
text.SetText(saved); scroll.SetSliderPos(0, 0); GetRootWidget().Update();
|
||||||
|
return fits && reachable;
|
||||||
|
}
|
||||||
|
bool ERR_ProbeHunterSummary()
|
||||||
|
{
|
||||||
|
TextWidget text = TextWidget.Cast(GetRootWidget().FindAnyWidget("Status"));
|
||||||
|
if (!text) return false;
|
||||||
|
string report = text.GetText();
|
||||||
|
array<string> parts = {}; report.Split("Sarka waves:", parts, false);
|
||||||
|
return report.Contains("HQ hunting patrols:") && parts.Count() == 2 && report.Contains("One car per");
|
||||||
|
}
|
||||||
bool ERR_ProbeJournalVisible()
|
bool ERR_ProbeJournalVisible()
|
||||||
{
|
{
|
||||||
TextWidget text = TextWidget.Cast(GetRootWidget().FindAnyWidget("JournalText"));
|
TextWidget text = TextWidget.Cast(GetRootWidget().FindAnyWidget("JournalText"));
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ modded class ERR_GameMode
|
|||||||
if (!ERR_MortarIntelReady()) return;
|
if (!ERR_MortarIntelReady()) return;
|
||||||
if (!ERR_AirfieldCheck(!Sites().Find("everon-supply-2"), "guarded car-and-crate site absent")) return;
|
if (!ERR_AirfieldCheck(!Sites().Find("everon-supply-2"), "guarded car-and-crate site absent")) return;
|
||||||
ERR_SiteRuntime extraction = Sites().Find("everon-extraction-1");
|
ERR_SiteRuntime extraction = Sites().Find("everon-extraction-1");
|
||||||
if (!ERR_AirfieldCheck(extraction.Definition.RequireAirfield && vector.DistanceXZ(ExtractionVehicle().GetOrigin(), "4800.953 0 11487.412") < 3 && ERR_CasualtyLoadingRules.AircraftReady(ExtractionVehicle()), "production aircraft settled intact on validated Everon airfield pad")) return;
|
if (!ERR_AirfieldCheck(!extraction.Definition.RequireAirfield && extraction.Definition.PlacementResolved && vector.DistanceXZ(ExtractionVehicle().GetOrigin(), extraction.Definition.Position) < 3 && ERR_CasualtyLoadingRules.AircraftReady(ExtractionVehicle()), "production aircraft settled intact on validated generated landing zone")) return;
|
||||||
if (!ERR_AirfieldCheck(!ERR_AirfieldPlacement.Clear(ERR_Actors.Ground("7012 0 7500")), "old off-airfield helicopter position rejected")) return;
|
if (!ERR_AirfieldCheck(!ERR_AirfieldPlacement.Clear(ERR_Actors.Ground("7012 0 7500")), "old off-airfield helicopter position rejected")) return;
|
||||||
if (!ERR_AirfieldCheck(!ERR_AirfieldPlacement.Clear(ExtractionVehicle().GetOrigin()), "occupied airfield pad rejected")) return;
|
if (!ERR_AirfieldCheck(!ERR_AirfieldPlacement.Clear(ExtractionVehicle().GetOrigin()), "occupied airfield pad rejected")) return;
|
||||||
if (!ERR_AirfieldCheck(Sites().Find("everon-medical-1").Definition.RevealOnEscape && Sites().Find("everon-supply-1").Definition.RevealOnEscape, "two reachable intel sites retained")) return;
|
if (!ERR_AirfieldCheck(Sites().Find("everon-medical-1").Definition.RevealOnEscape && Sites().Find("everon-supply-1").Definition.RevealOnEscape, "two reachable intel sites retained")) return;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ modded class ERR_GameMode
|
|||||||
string enabled;
|
string enabled;
|
||||||
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
|
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
|
||||||
string nativeEndCase;
|
string nativeEndCase;
|
||||||
array<string> independentFixtures = {"coordinated-ai", "optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "self-epinephrine", "objective-map", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "native-end-active", "native-end-preparing", "generated-sites", "roadblocks", "mortar-station", "opening-tools", "airfield-markers", "air-logistics", "air-assault", "air-recon"};
|
array<string> independentFixtures = {"coordinated-ai", "optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "self-epinephrine", "starter-backpack", "speech-download", "bundled-speech", "objective-map", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "sarka-hunters", "native-end-active", "native-end-preparing", "generated-sites", "roadblocks", "mortar-station", "opening-tools", "airfield-markers", "air-logistics", "air-assault", "air-recon"};
|
||||||
if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && independentFixtures.Contains(nativeEndCase)) return;
|
if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && independentFixtures.Contains(nativeEndCase)) return;
|
||||||
ERR_NativeObjectiveProbe.BeginRound();
|
ERR_NativeObjectiveProbe.BeginRound();
|
||||||
ERR_NativeProgressionIdentityProbe.BeginRound();
|
ERR_NativeProgressionIdentityProbe.BeginRound();
|
||||||
@@ -775,6 +775,10 @@ modded class ERR_GameMode
|
|||||||
ERR_SiteRuntime medical = Sites().Find("everon-medical-1");
|
ERR_SiteRuntime medical = Sites().Find("everon-medical-1");
|
||||||
if (!ERR_BaseCheck(medical.Location.Knowledge() == 2 && medical.Location.Label() == "Medical stash" && m_ERRMedicalDiscoveries == 1, "conscious nearby player visually discovers named medical stash once")) return;
|
if (!ERR_BaseCheck(medical.Location.Knowledge() == 2 && medical.Location.Label() == "Medical stash" && m_ERRMedicalDiscoveries == 1, "conscious nearby player visually discovers named medical stash once")) return;
|
||||||
if (!ERR_BaseCheck(!Sites().CanObserve(m_ERRBasePlayer, Sites().Find("everon-radio-1")), "distant medical-site discovery does not reveal the radio")) return;
|
if (!ERR_BaseCheck(!Sites().CanObserve(m_ERRBasePlayer, Sites().Find("everon-radio-1")), "distant medical-site discovery does not reveal the radio")) return;
|
||||||
|
// This sparse historical fixture has two eligible sites. Arrange the
|
||||||
|
// remaining identification before testing document ownership and recovery.
|
||||||
|
Sites().Find("everon-outpost-1").Location.Discover(0, 100);
|
||||||
|
if (!ERR_BaseCheck(Discovery().RadioIntelAllowed(), "identified optional sites unlock the historical radio documents")) return;
|
||||||
m_ERRBaseStep = 1;
|
m_ERRBaseStep = 1;
|
||||||
}
|
}
|
||||||
else if (m_ERRBaseStep == 1 || m_ERRBaseStep == 2)
|
else if (m_ERRBaseStep == 1 || m_ERRBaseStep == 2)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class ERR_NativeObjectiveMapProbe
|
|||||||
protected ref ERR_ObjectiveMapPresentation m_Presentation;
|
protected ref ERR_ObjectiveMapPresentation m_Presentation;
|
||||||
protected SCR_MapMenuUI m_Menu;
|
protected SCR_MapMenuUI m_Menu;
|
||||||
protected Widget m_SharedRoot, m_ReusedRoot, m_ExpectedRoot, m_PreviousWidget;
|
protected Widget m_SharedRoot, m_ReusedRoot, m_ExpectedRoot, m_PreviousWidget;
|
||||||
|
protected ref ERRM_AtlasPresentation m_Atlas;
|
||||||
protected ref MapConfiguration m_Config;
|
protected ref MapConfiguration m_Config;
|
||||||
protected MapConfiguration m_Ready;
|
protected MapConfiguration m_Ready;
|
||||||
protected vector m_Point;
|
protected vector m_Point;
|
||||||
@@ -94,7 +95,7 @@ class ERR_NativeObjectiveMapProbe
|
|||||||
m_CloseObserved = false;
|
m_CloseObserved = false;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
protected Widget CreateSharedRoot()
|
protected Widget CreateSharedRoot(int page = ERRM_PaperPages.OVERVIEW)
|
||||||
{
|
{
|
||||||
Widget root = GetGame().GetWorkspace().CreateWidgets("{255E1265F702B4AE}UI/layouts/ERRMap/SharedMapRenderTarget.layout");
|
Widget root = GetGame().GetWorkspace().CreateWidgets("{255E1265F702B4AE}UI/layouts/ERRMap/SharedMapRenderTarget.layout");
|
||||||
if (!root) return null;
|
if (!root) return null;
|
||||||
@@ -104,6 +105,12 @@ class ERR_NativeObjectiveMapProbe
|
|||||||
// is bound, so these observations cannot qualify the physical held texture.
|
// is bound, so these observations cannot qualify the physical held texture.
|
||||||
target.SetEnabled(false);
|
target.SetEnabled(false);
|
||||||
root.SetVisible(true);
|
root.SetVisible(true);
|
||||||
|
// Exercise the production atlas gutter, index grid and objective visibility.
|
||||||
|
m_Atlas = new ERRM_AtlasPresentation(root);
|
||||||
|
float zoom; int columns, rows;
|
||||||
|
if (!ERRM_PaperPages.Layout(m_Map.Size(), 0.5, zoom, columns, rows)) return null;
|
||||||
|
m_Atlas.Prepare(page, m_Map.Size(), zoom, columns, rows);
|
||||||
|
m_Atlas.Draw(page, zoom, columns, rows, m_Map.Size());
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
protected bool OpenShared(Widget root)
|
protected bool OpenShared(Widget root)
|
||||||
@@ -122,7 +129,9 @@ class ERR_NativeObjectiveMapProbe
|
|||||||
protected void Center(float zoom)
|
protected void Center(float zoom)
|
||||||
{
|
{
|
||||||
int position[2]; m_Marker.GetWorldPos(position);
|
int position[2]; m_Marker.GetWorldPos(position);
|
||||||
m_Map.ZoomPanSmooth(zoom, position[0] + 100.0 / zoom, position[1], 0.1);
|
float width, height; m_Map.GetMapWidget().GetScreenSize(width, height);
|
||||||
|
float offset = Math.Min(100, width * 0.25);
|
||||||
|
m_Map.ZoomPanSmooth(zoom, position[0] + offset / zoom, position[1], 0.1);
|
||||||
}
|
}
|
||||||
protected bool Inspect(string label, bool rough)
|
protected bool Inspect(string label, bool rough)
|
||||||
{
|
{
|
||||||
@@ -133,6 +142,13 @@ class ERR_NativeObjectiveMapProbe
|
|||||||
Widget nativeIcons = m_ExpectedRoot.FindAnyWidget("UIIconsContainer");
|
Widget nativeIcons = m_ExpectedRoot.FindAnyWidget("UIIconsContainer");
|
||||||
if (!container && nativeIcons) container = nativeIcons.GetParent();
|
if (!container && nativeIcons) container = nativeIcons.GetParent();
|
||||||
if (!Check(root && element && container && container != m_Map.GetMapWidget() && root.GetParent() == container && m_Presentation.ERR_ProbeContainer() == container && m_Presentation.ERR_ProbeCanvas() == m_Map.GetMapWidget(), "actual authored layout handler belongs to the exact supported objective container and native canvas")) return false;
|
if (!Check(root && element && container && container != m_Map.GetMapWidget() && root.GetParent() == container && m_Presentation.ERR_ProbeContainer() == container && m_Presentation.ERR_ProbeCanvas() == m_Map.GetMapWidget(), "actual authored layout handler belongs to the exact supported objective container and native canvas")) return false;
|
||||||
|
if (m_ExpectedRoot.FindAnyWidget("ERR_ObjectiveOverlay"))
|
||||||
|
{
|
||||||
|
vector mapPos = FrameSlot.GetPos(m_Map.GetMapWidget()), overlayPos = FrameSlot.GetPos(container);
|
||||||
|
float mapW, mapH, overlayW, overlayH;
|
||||||
|
m_Map.GetMapWidget().GetScreenSize(mapW, mapH); container.GetScreenSize(overlayW, overlayH);
|
||||||
|
if (!Check(container.IsVisible() && container.GetZOrder() > 10 && Math.AbsFloat(mapPos[0] - overlayPos[0]) < 0.1 && Math.AbsFloat(mapPos[1] - overlayPos[1]) < 0.1 && Math.AbsFloat(mapW - overlayW) < 1 && Math.AbsFloat(mapH - overlayH) < 1, "atlas index/regional overlay is visible above print and matches the map rectangle")) return false;
|
||||||
|
}
|
||||||
TextWidget text = TextWidget.Cast(root.FindAnyWidget("ObjectiveText"));
|
TextWidget text = TextWidget.Cast(root.FindAnyWidget("ObjectiveText"));
|
||||||
ImageWidget image = ImageWidget.Cast(root.FindAnyWidget("Image"));
|
ImageWidget image = ImageWidget.Cast(root.FindAnyWidget("Image"));
|
||||||
if (!Check(text && image && text.GetText() == label && root.IsVisible(), "actual visible widget contains the current objective label and icon widget")) return false;
|
if (!Check(text && image && text.GetText() == label && root.IsVisible(), "actual visible widget contains the current objective label and icon widget")) return false;
|
||||||
@@ -285,7 +301,7 @@ class ERR_NativeObjectiveMapProbe
|
|||||||
MapConfiguration previousConfig = m_Config;
|
MapConfiguration previousConfig = m_Config;
|
||||||
m_Map.CloseMap();
|
m_Map.CloseMap();
|
||||||
if (!Check(!m_Presentation.ERR_ProbeRoot(), "closing shared root clears only owned objective widget")) return -1;
|
if (!Check(!m_Presentation.ERR_ProbeRoot(), "closing shared root clears only owned objective widget")) return -1;
|
||||||
m_ReusedRoot = CreateSharedRoot();
|
m_ReusedRoot = CreateSharedRoot(ERRM_PaperPages.REGION);
|
||||||
if (!OpenShared(m_ReusedRoot)) return -1;
|
if (!OpenShared(m_ReusedRoot)) return -1;
|
||||||
if (!Check(m_Config == previousConfig && m_ReusedRoot != m_SharedRoot, "native plain config is reused with a distinct root")) return -1;
|
if (!Check(m_Config == previousConfig && m_ReusedRoot != m_SharedRoot, "native plain config is reused with a distinct root")) return -1;
|
||||||
m_Stage = 11; m_Wait = now + 1000; return 0;
|
m_Stage = 11; m_Wait = now + 1000; return 0;
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
modded class ERR_CampTerrain
|
modded class ERR_CampTerrain
|
||||||
{
|
{
|
||||||
static bool s_ERROptionalSiteRefused;
|
static bool s_ERROptionalSiteRefused;
|
||||||
override bool Site(ERR_SiteDefinition site, vector position, float heading, out float baseHeight)
|
override bool Site(ERR_SiteDefinition site, vector position, float heading, out float baseHeight, float airportBuffer = 1500)
|
||||||
{
|
{
|
||||||
string outcome;
|
string outcome;
|
||||||
if ((site.Id == "everon-camp-5" || site.Id == "everon-outpost-1") && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "optional-site-skip")
|
if ((site.Id == "everon-camp-5" || site.Id == "everon-outpost-1") && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "optional-site-skip")
|
||||||
{
|
{
|
||||||
s_ERROptionalSiteRefused = true; return false;
|
s_ERROptionalSiteRefused = true; return false;
|
||||||
}
|
}
|
||||||
return super.Site(site, position, heading, baseHeight);
|
return super.Site(site, position, heading, baseHeight, airportBuffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
modded class ERR_GameMode
|
modded class ERR_GameMode
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ modded class ERR_GameMode
|
|||||||
float now = GetWorld().GetWorldTime();
|
float now = GetWorld().GetWorldTime();
|
||||||
if (Phase() == ERR_Phase.VICTORY)
|
if (Phase() == ERR_Phase.VICTORY)
|
||||||
{
|
{
|
||||||
ERR_RescueCheck(Extraction().EscapedCount() == 1, "native flight carried the original prisoner across the escape boundary");
|
ERR_RescueCheck(Extraction().EscapedCount() == 1, "native flight carried the original prisoner through thirty seconds of airborne evacuation");
|
||||||
Print("ERR RESCUE COMPLETE"); GetGame().GetCallqueue().Remove(ERR_RescueProbeTick); return;
|
Print("ERR RESCUE COMPLETE"); GetGame().GetCallqueue().Remove(ERR_RescueProbeTick); return;
|
||||||
}
|
}
|
||||||
if (!PreparationError().IsEmpty() || Phase() == ERR_Phase.DEFEAT) { ERR_RescueCheck(false, "mission failed: " + PreparationError()); return; }
|
if (!PreparationError().IsEmpty() || Phase() == ERR_Phase.DEFEAT) { ERR_RescueCheck(false, "mission failed: " + PreparationError()); return; }
|
||||||
|
|||||||
@@ -1,4 +1,23 @@
|
|||||||
#ifdef WORKBENCH
|
#ifdef WORKBENCH
|
||||||
|
class ERR_RoadblockProbeRules
|
||||||
|
{
|
||||||
|
static int SmallTarget()
|
||||||
|
{
|
||||||
|
string value;
|
||||||
|
if (System.GetCLIParam("errRoadblockProbeCount", value)) return Math.Clamp(value.ToInt(), 0, 30);
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
|
static int LargeTarget()
|
||||||
|
{
|
||||||
|
string value;
|
||||||
|
if (System.GetCLIParam("errLargeRoadblockProbeCount", value)) return Math.Clamp(value.ToInt(), 0, 30);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
static int Target() { return SmallTarget() + LargeTarget(); }
|
||||||
|
static int Guards() { return SmallTarget() * 4 + LargeTarget() * 8; }
|
||||||
|
static int Props() { return SmallTarget() * 8 + LargeTarget() * 14; }
|
||||||
|
|
||||||
|
}
|
||||||
modded class ERR_HQ
|
modded class ERR_HQ
|
||||||
{
|
{
|
||||||
bool ERR_ProbeRoadblockGroup(SCR_AIGroup group)
|
bool ERR_ProbeRoadblockGroup(SCR_AIGroup group)
|
||||||
@@ -14,7 +33,11 @@ modded class ERR_Roadblocks
|
|||||||
{
|
{
|
||||||
string outcome;
|
string outcome;
|
||||||
if (!m_Begun && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "roadblocks")
|
if (!m_Begun && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "roadblocks")
|
||||||
m_Mode.Settings().Set("roadblockCount", 30);
|
{
|
||||||
|
m_Mode.Settings().Set("roadblockCount", ERR_RoadblockProbeRules.SmallTarget());
|
||||||
|
m_Mode.Settings().Set("largeRoadblockCount", ERR_RoadblockProbeRules.LargeTarget());
|
||||||
|
m_Mode.Settings().Set("mainRoadWidthMetres", 6);
|
||||||
|
}
|
||||||
return super.Prepare(definition);
|
return super.Prepare(definition);
|
||||||
}
|
}
|
||||||
protected int m_ERRLayoutPosts;
|
protected int m_ERRLayoutPosts;
|
||||||
@@ -27,11 +50,11 @@ modded class ERR_Roadblocks
|
|||||||
// Inspect the actual spawned models before the transaction clears its refs.
|
// Inspect the actual spawned models before the transaction clears its refs.
|
||||||
if (m_BuildHold)
|
if (m_BuildHold)
|
||||||
{
|
{
|
||||||
bool valid = m_BuildCovers.Count() == 10 && m_BuildSpawned.Count() == 15;
|
bool valid = ShareProps();
|
||||||
for (int i = 0; i < m_BuildCovers.Count() && valid; i++)
|
for (int i = 0; i < m_BuildCovers.Count() && valid; i++)
|
||||||
{
|
{
|
||||||
ERR_RoadblockProp prop = m_BuildCovers[i];
|
ERR_RoadblockProp prop = m_BuildCovers[i];
|
||||||
IEntity entity = m_BuildSpawned[i + 4];
|
IEntity entity = m_BuildSpawned[i + m_BuildPositions.Count()];
|
||||||
vector low, high; entity.GetBounds(low, high);
|
vector low, high; entity.GetBounds(low, high);
|
||||||
if (m_ERRLayoutPosts == 0) PrintFormat("ERR ROADBLOCK PROP NATIVE: prefab=%1 low=%2 high=%3", prop.Prefab, low, high);
|
if (m_ERRLayoutPosts == 0) PrintFormat("ERR ROADBLOCK PROP NATIVE: prefab=%1 low=%2 high=%3", prop.Prefab, low, high);
|
||||||
if (Math.Max(Math.AbsFloat(low[0]), Math.AbsFloat(high[0])) > prop.HalfWidth
|
if (Math.Max(Math.AbsFloat(low[0]), Math.AbsFloat(high[0])) > prop.HalfWidth
|
||||||
@@ -89,32 +112,33 @@ modded class ERR_Roadblocks
|
|||||||
bool ERR_ProbeBackgroundPending() { return m_Background; }
|
bool ERR_ProbeBackgroundPending() { return m_Background; }
|
||||||
bool ERR_ProbeCompleteRoadblocks()
|
bool ERR_ProbeCompleteRoadblocks()
|
||||||
{
|
{
|
||||||
if (m_ERRLayoutFailed || m_ERRLayoutPosts < 30 || m_Target != 30 || m_Positions.Count() != 30 || m_Background || !m_Ready) return false;
|
if (m_ERRLayoutFailed || m_ERRLayoutPosts < ERR_RoadblockProbeRules.Target() || m_Target != ERR_RoadblockProbeRules.Target() || m_Positions.Count() != m_Target || m_Background || !m_Ready) return false;
|
||||||
foreach (SCR_AIGroup group : m_Groups)
|
foreach (SCR_AIGroup group : m_Groups)
|
||||||
{
|
{
|
||||||
ERR_ForceRecord record = m_Mode.Forces().FindGroup(group);
|
ERR_ForceRecord record = m_Mode.Forces().FindGroup(group);
|
||||||
if (!group || !record || record.Kind != ERR_ForceKind.CHECKPOINT || record.InitialStrength != 4 || m_Mode.ForceLiving(group) != 4) return false;
|
if (!group || !record || record.Kind != ERR_ForceKind.CHECKPOINT || (record.InitialStrength != 4 && record.InitialStrength != 8) || m_Mode.ForceLiving(group) != record.InitialStrength) return false;
|
||||||
if (group.GetFaction() != m_Mode.EnemyFaction() || !ERR_HQ.Get().ERR_ProbeRoadblockGroup(group)) return false;
|
if (group.GetFaction() != m_Mode.EnemyFaction() || !ERR_HQ.Get().ERR_ProbeRoadblockGroup(group)) return false;
|
||||||
}
|
}
|
||||||
|
if (m_SmallPlaced != ERR_RoadblockProbeRules.SmallTarget() || m_LargePlaced != ERR_RoadblockProbeRules.LargeTarget()) return false;
|
||||||
int count = m_Positions.Count();
|
int count = m_Positions.Count();
|
||||||
BackgroundStep();
|
BackgroundStep();
|
||||||
PrintFormat("ERR ROADBLOCK BACKGROUND NATIVE: posts=%1 guards=%2 steps=%3 workMs=%4 maxStepMs=%5 attempts=%6", count, count * 4, m_BackgroundSteps, m_BackgroundWork, m_MaxBackgroundStep, m_Attempts);
|
PrintFormat("ERR ROADBLOCK BACKGROUND NATIVE: posts=%1 guards=%2 steps=%3 workMs=%4 maxStepMs=%5 attempts=%6", count, ERR_RoadblockProbeRules.Guards(), m_BackgroundSteps, m_BackgroundWork, m_MaxBackgroundStep, m_Attempts);
|
||||||
return m_Positions.Count() == count && m_BackgroundSteps > 0;
|
return m_Positions.Count() == count && (m_BackgroundSteps > 0 || ERR_RoadblockProbeRules.Target() <= 4);
|
||||||
}
|
}
|
||||||
bool ERR_ProbePreparedRoadblocks()
|
bool ERR_ProbePreparedRoadblocks()
|
||||||
{
|
{
|
||||||
if (!m_Ready || m_Target != 4 || m_Groups.IsEmpty() || m_Groups.Count() > m_Target || m_Groups.Count() != m_Positions.Count()) return false;
|
if (!m_Ready || m_Target != m_SmallTarget + m_LargeTarget || m_Groups.IsEmpty() || m_Groups.Count() > m_Target || m_Groups.Count() != m_Positions.Count()) return false;
|
||||||
int count = m_Groups.Count();
|
int count = m_Groups.Count();
|
||||||
foreach (SCR_AIGroup group : m_Groups)
|
foreach (SCR_AIGroup group : m_Groups)
|
||||||
{
|
{
|
||||||
ERR_ForceRecord record = m_Mode.Forces().FindGroup(group);
|
ERR_ForceRecord record = m_Mode.Forces().FindGroup(group);
|
||||||
if (!record || record.Kind != ERR_ForceKind.CHECKPOINT || record.InitialStrength != 4 || m_Mode.ForceLiving(group) != 4
|
if (!record || record.Kind != ERR_ForceKind.CHECKPOINT || (record.InitialStrength != 4 && record.InitialStrength != 8) || m_Mode.ForceLiving(group) != record.InitialStrength
|
||||||
|| group.GetFaction() != m_Mode.EnemyFaction() || !ERR_HQ.Get().ERR_ProbeRoadblockGroup(group)) return false;
|
|| group.GetFaction() != m_Mode.EnemyFaction() || !ERR_HQ.Get().ERR_ProbeRoadblockGroup(group)) return false;
|
||||||
}
|
}
|
||||||
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf"));
|
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf"));
|
||||||
// Repeated preparation cannot replenish or create another squad.
|
// Repeated preparation cannot replenish or create another squad.
|
||||||
if (Prepare(definition) != ERR_PrepareResult.READY || m_Groups.Count() != count) return false;
|
if (Prepare(definition) != ERR_PrepareResult.READY || m_Groups.Count() != count) return false;
|
||||||
PrintFormat("ERR ROADBLOCK NATIVE: posts=%1 soldiers=%2 ledger=checkpoint HQ=available repeatedPrepare=unchanged", count, count * 4);
|
PrintFormat("ERR ROADBLOCK NATIVE: posts=%1 soldiers=%2 ledger=checkpoint HQ=available repeatedPrepare=unchanged", count, m_SmallPlaced * 4 + m_LargePlaced * 8);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,13 +171,13 @@ modded class ERR_GameMode
|
|||||||
IEntity player; if (controller) player = controller.GetControlledEntity();
|
IEntity player; if (controller) player = controller.GetControlledEntity();
|
||||||
if (!player || !Players().CurrentActor(player)) return;
|
if (!player || !Players().CurrentActor(player)) return;
|
||||||
DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||||
if (!posts.ERR_ProbeBackgroundPending() || posts.ERR_ProbePostCount() >= 30 || !UnlockPrison(player)) { ERR_RoadblockProbeFail("did not enter background search with prison unlocked"); return; }
|
if ((ERR_RoadblockProbeRules.Target() > 4 && (!posts.ERR_ProbeBackgroundPending() || posts.ERR_ProbePostCount() >= ERR_RoadblockProbeRules.Target())) || !UnlockPrison(player)) { ERR_RoadblockProbeFail("did not enter background search with prison unlocked"); return; }
|
||||||
m_ERRRoadblockInitial = posts.ERR_ProbePostCount();
|
m_ERRRoadblockInitial = posts.ERR_ProbePostCount();
|
||||||
PrintFormat("ERR SMOKE PASS: roadblocks prison unlocked with %1/30 posts; background still pending", m_ERRRoadblockInitial);
|
PrintFormat("ERR SMOKE PASS: roadblocks prison unlocked with %1/%2 posts; placement observation started", m_ERRRoadblockInitial, ERR_RoadblockProbeRules.Target());
|
||||||
}
|
}
|
||||||
if (posts.ERR_ProbeBackgroundPending()) return;
|
if (posts.ERR_ProbeBackgroundPending()) return;
|
||||||
if (!posts.ERR_ProbeCompleteRoadblocks() || posts.ERR_ProbePostCount() <= m_ERRRoadblockInitial) { ERR_RoadblockProbeFail("30 physical four-guard posts, ledger and HQ admission"); return; }
|
if (!posts.ERR_ProbeCompleteRoadblocks() || (ERR_RoadblockProbeRules.Target() > 4 && posts.ERR_ProbePostCount() <= m_ERRRoadblockInitial)) { ERR_RoadblockProbeFail("physical small/large posts, independent quotas, replicated props, ledger and HQ admission"); return; }
|
||||||
Print("ERR SMOKE PASS: roadblocks 30 physical posts / 120 guards / 300 checkpoint props; verified model footprints and centre lane; background completed after prison unlock, no extra posts on repeat tick");
|
PrintFormat("ERR SMOKE PASS: roadblocks %1 physical posts / %2 guards / %3 replicated checkpoint props with distance streaming disabled; verified model footprints and centre lane; placement complete after prison unlock, no extra posts on repeat tick", posts.ERR_ProbePostCount(), ERR_RoadblockProbeRules.Guards(), ERR_RoadblockProbeRules.Props());
|
||||||
ERR_StopRoadblockProbe();
|
ERR_StopRoadblockProbe();
|
||||||
Transition(ERR_Phase.DEFEAT);
|
Transition(ERR_Phase.DEFEAT);
|
||||||
Print("ERR SMOKE COMPLETE: roadblocks background completion on Everon; scripted local fixture");
|
Print("ERR SMOKE COMPLETE: roadblocks background completion on Everon; scripted local fixture");
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
#ifdef WORKBENCH
|
||||||
|
modded class ERR_AdminMenu
|
||||||
|
{
|
||||||
|
bool ERR_SarkaProbeReady() { return m_Ready && m_AvailableFields.Contains("sarkaAngerPerCar"); }
|
||||||
|
bool ERR_SarkaProbeSet(int points)
|
||||||
|
{
|
||||||
|
ShowPage(Navigation().Find("OpenSarka"));
|
||||||
|
SCR_EditBoxComponent edit = SCR_EditBoxComponent.GetEditBoxComponent("sarkaAngerPerCar", GetRootWidget());
|
||||||
|
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText("Apply_sarkaAngerPerCar", GetRootWidget());
|
||||||
|
if (!edit || !button || !button.IsEnabled()) return false;
|
||||||
|
edit.SetValue(points.ToString()); OnButton(button); return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modded class ERR_HunterService
|
||||||
|
{
|
||||||
|
void ERR_ProbeSarkaDue(float now) { m_SarkaWaves.NextWave = now; }
|
||||||
|
bool ERR_ProbeSarkaDisabled() { return m_SarkaWaves.AngerPerCar == 0 && m_SarkaWaves.Pending == 0; }
|
||||||
|
void ERR_ProbeSarkas(out array<ERR_HunterVehicle> cars)
|
||||||
|
{
|
||||||
|
cars.Clear();
|
||||||
|
foreach (ERR_HunterVehicle unit : m_Hunters) if (unit.Sarka) cars.Insert(unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modded class ERR_HQ
|
||||||
|
{
|
||||||
|
void ERR_ProbeSarkaBodies(float now)
|
||||||
|
{
|
||||||
|
for (int slot = 0; slot < 24; slot++)
|
||||||
|
{
|
||||||
|
ERR_HQReport report = new ERR_HQReport();
|
||||||
|
report.TravelMode = "body"; report.MessageId = 200000 + slot;
|
||||||
|
report.CasualtySourceId = "fixture-sarka-bodies"; report.CasualtyStrength = 24; report.CasualtySlot = slot;
|
||||||
|
report.ObservedAt = now; report.Position = "5000 50 5000";
|
||||||
|
Accept(report, now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modded class ERR_GameMode
|
||||||
|
{
|
||||||
|
protected int m_ERRSarkaStage;
|
||||||
|
protected int m_ERRSarkaMenuPage;
|
||||||
|
protected int m_ERRSarkaDeadline;
|
||||||
|
protected bool m_ERRSarkaOrder;
|
||||||
|
protected override void EOnInit(IEntity owner)
|
||||||
|
{
|
||||||
|
super.EOnInit(owner);
|
||||||
|
string outcome;
|
||||||
|
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "sarka-hunters") return;
|
||||||
|
m_ERRSarkaDeadline = System.GetTickCount() + 420000;
|
||||||
|
GetGame().GetCallqueue().CallLater(ERR_SarkaProbeTick, 1000, true);
|
||||||
|
}
|
||||||
|
protected bool ERR_SarkaProbeCheck(bool ok, string detail)
|
||||||
|
{
|
||||||
|
if (ok) { Print("ERR SMOKE PASS: sarka " + detail); return true; }
|
||||||
|
Print("ERR SMOKE FAIL: sarka " + detail, LogLevel.ERROR);
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_SarkaProbeTick); return false;
|
||||||
|
}
|
||||||
|
protected void ERR_SarkaProbeTick()
|
||||||
|
{
|
||||||
|
if (System.GetTickCount() >= m_ERRSarkaDeadline || Phase() == ERR_Phase.DEFEAT) { ERR_SarkaProbeCheck(false, "native spawn/admission/driving deadline"); return; }
|
||||||
|
if (Phase() < ERR_Phase.PRISON || !GetGame().GetPlayerController()) return;
|
||||||
|
IEntity player = GetGame().GetPlayerController().GetControlledEntity(); if (!player) return;
|
||||||
|
DamageManagerComponent damage = DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent));
|
||||||
|
if (damage) damage.EnableDamageHandling(false);
|
||||||
|
ERR_HQ hq = ERR_HQ.Get(); if (!hq) return;
|
||||||
|
float now = GetWorld().GetWorldTime();
|
||||||
|
if (m_ERRSarkaStage == 0)
|
||||||
|
{
|
||||||
|
if (!ERR_SarkaProbeCheck(Settings().Get("sarkaAngerPerCar") == 3, "default three anger points per car")) return;
|
||||||
|
ERR_AdminMenu.Open(); m_ERRSarkaStage = 9; return;
|
||||||
|
}
|
||||||
|
if (m_ERRSarkaStage == 9)
|
||||||
|
{
|
||||||
|
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu();
|
||||||
|
if (!menu || !menu.ERR_SarkaProbeReady()) return;
|
||||||
|
menu.ERR_ProbePage(m_ERRSarkaMenuPage); m_ERRSarkaStage = 90; return;
|
||||||
|
}
|
||||||
|
if (m_ERRSarkaStage == 90)
|
||||||
|
{
|
||||||
|
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu();
|
||||||
|
if (!ERR_SarkaProbeCheck(menu && menu.ERR_ProbeNavigation(m_ERRSarkaMenuPage), "top main tabs and left sidebar reach page " + m_ERRSarkaMenuPage.ToString())) return;
|
||||||
|
if (m_ERRSarkaMenuPage == ERR_AdminPage.STATUS)
|
||||||
|
{
|
||||||
|
if (!ERR_SarkaProbeCheck(menu.ERR_ProbeHunterSummary(), "patrol summary and exactly one Sarka wave summary")) return;
|
||||||
|
if (!ERR_SarkaProbeCheck(menu.ERR_ProbeReportOverflow("Status", "StatusScroll"), "long status grows and last line scrolls into view")) return;
|
||||||
|
}
|
||||||
|
if (m_ERRSarkaMenuPage == ERR_AdminPage.JOURNAL)
|
||||||
|
if (!ERR_SarkaProbeCheck(menu.ERR_ProbeReportOverflow("JournalText", "JournalScroll"), "long journal grows and last line scrolls into view")) return;
|
||||||
|
System.MakeScreenshot("$logs:err-menu-sidebar-" + m_ERRSarkaMenuPage.ToString());
|
||||||
|
m_ERRSarkaStage = 91; return;
|
||||||
|
}
|
||||||
|
if (m_ERRSarkaStage == 91)
|
||||||
|
{
|
||||||
|
m_ERRSarkaMenuPage++;
|
||||||
|
if (m_ERRSarkaMenuPage < ERR_AdminMenu.Pages().Count()) m_ERRSarkaStage = 9;
|
||||||
|
else m_ERRSarkaStage = 10;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m_ERRSarkaStage == 10)
|
||||||
|
{
|
||||||
|
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu();
|
||||||
|
if (!menu || !menu.ERR_SarkaProbeReady()) return;
|
||||||
|
if (!ERR_SarkaProbeCheck(menu.ERR_SarkaProbeSet(2), "real menu submits two anger points per car")) return;
|
||||||
|
m_ERRSarkaStage = 11; return;
|
||||||
|
}
|
||||||
|
if (m_ERRSarkaStage == 11)
|
||||||
|
{
|
||||||
|
if (Settings().Get("sarkaAngerPerCar") != 2) return;
|
||||||
|
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu(); if (!menu || !menu.ERR_SarkaProbeReady()) return;
|
||||||
|
System.MakeScreenshot("$logs:err-sarka-settings"); menu.Close();
|
||||||
|
ERR_Settings reloaded = new ERR_Settings();
|
||||||
|
if (!ERR_SarkaProbeCheck(reloaded.Get("sarkaAngerPerCar") == 2, "menu owner RPC persists anger threshold")) return;
|
||||||
|
if (!ERR_SarkaProbeCheck(UnlockPrison(player) || Phase() >= ERR_Phase.RADIO_SEARCH, "prison released")) return;
|
||||||
|
hq.ERR_ProbeSarkaBodies(now);
|
||||||
|
if (!ERR_SarkaProbeCheck(hq.Watch().Aggression() == 8, "24 delivered bodies produce eight anger points")) return;
|
||||||
|
// Accelerate only the opt-in wave deadline; production placement/admission run unchanged.
|
||||||
|
hq.Hunters().ERR_ProbeSarkaDue(now); m_ERRSarkaStage = 1;
|
||||||
|
}
|
||||||
|
array<ERR_HunterVehicle> cars = {}; hq.Hunters().ERR_ProbeSarkas(cars);
|
||||||
|
if (cars.Count() < 4) return;
|
||||||
|
if (m_ERRSarkaStage == 20)
|
||||||
|
{
|
||||||
|
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu();
|
||||||
|
if (!menu || !menu.ERR_SarkaProbeReady()) return;
|
||||||
|
if (!ERR_SarkaProbeCheck(menu.ERR_SarkaProbeSet(0), "real menu submits disable")) return;
|
||||||
|
m_ERRSarkaStage = 21; return;
|
||||||
|
}
|
||||||
|
if (m_ERRSarkaStage == 21)
|
||||||
|
{
|
||||||
|
if (Settings().Get("sarkaAngerPerCar") != 0 || !hq.Hunters().ERR_ProbeSarkaDisabled()) return;
|
||||||
|
foreach (ERR_HunterVehicle retained : cars)
|
||||||
|
if (!ERR_SarkaProbeCheck(retained.Entity && retained.Members.Count() == 4, "disabling retains deployed cars and teams")) return;
|
||||||
|
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu(); if (menu) menu.Close();
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_SarkaProbeTick);
|
||||||
|
Print("ERR SMOKE COMPLETE: sarka menu save, anger threshold, four SF cars, radio, driving and disable"); return;
|
||||||
|
}
|
||||||
|
if (m_ERRSarkaStage == 1)
|
||||||
|
{
|
||||||
|
if (!ERR_SarkaProbeCheck(cars.Count() == 4, "eight anger at two points per car produces four physical cars")) return;
|
||||||
|
array<vector> positions = {player.GetOrigin()};
|
||||||
|
foreach (ERR_HunterVehicle car : cars)
|
||||||
|
{
|
||||||
|
if (!car.Registered) return;
|
||||||
|
if (!ERR_SarkaProbeCheck(car.Entity.GetPrefabData().GetPrefabName() == ERR_SarkaRules.Prefab() && car.CrewValid(this) && car.Members.Count() == 4 && car.Seats.Count() == 4, "native S105 with four original seated crew")) return;
|
||||||
|
if (!ERR_SarkaProbeCheck(ERR_SarkaRules.InBand(car.PatrolHome, positions), "actual road spawn 1-1.25 km from player")) return;
|
||||||
|
if (!ERR_SarkaProbeCheck(hq.Hunters().VehicleRadioCapability(car.Driver) == 2 && car.Group.GetFaction() == EnemyFaction(), "physical vehicle manpack and selected hostile faction")) return;
|
||||||
|
int medics;
|
||||||
|
foreach (IEntity member : car.Members)
|
||||||
|
{
|
||||||
|
ResourceName profile = member.GetPrefabData().GetPrefabName();
|
||||||
|
if (profile == ERR_EnemyMedicalCatalog.Crew(this, true)) medics++;
|
||||||
|
else if (!ERR_SarkaProbeCheck(profile == ERR_EnemyMedicalCatalog.Crew(this, false), "selected special-forces profile")) return;
|
||||||
|
SCR_AICombatComponent combat = SCR_AICombatComponent.Cast(member.FindComponent(SCR_AICombatComponent));
|
||||||
|
if (!combat || combat.GetAISkill() != EAISkill.CYLON || !combat.ERR_EnhancedEnemy()) return;
|
||||||
|
}
|
||||||
|
if (!ERR_SarkaProbeCheck(medics == 1, "one medic within the four-person team")) return;
|
||||||
|
}
|
||||||
|
m_ERRSarkaStage = 2;
|
||||||
|
}
|
||||||
|
if (cars[0].Waypoint) m_ERRSarkaOrder = true;
|
||||||
|
if (!m_ERRSarkaOrder || vector.DistanceXZ(cars[0].PatrolHome, cars[0].Entity.GetOrigin()) < 30) return;
|
||||||
|
if (!ERR_SarkaProbeCheck(hq.Hunters().Deployed() <= hq.Watch().HunterTarget(), "cars do not spend ordinary hunter allowance")) return;
|
||||||
|
ERR_SarkaProbeCheck(true, "native patrol order and over 30 metres driven; all crew enhanced");
|
||||||
|
ERR_AdminMenu.Open(); m_ERRSarkaStage = 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#ifdef WORKBENCH
|
||||||
|
// Explicit local HTTP fixture. It exercises the production REST downloader,
|
||||||
|
// incremental preparation and native audio handle without Google or game RPCs.
|
||||||
|
class ERR_SpeechDownloadProbeClient : ERR_WelcomeVoiceClient
|
||||||
|
{
|
||||||
|
bool Played;
|
||||||
|
bool PlaybackValid;
|
||||||
|
void ERR_SpeechDownloadProbeClient(ERR_GameMode mode, int serial, ERR_WelcomeConnection connection, bool requirePermit = false, string title = "Speech transport test") {}
|
||||||
|
override protected void Play()
|
||||||
|
{
|
||||||
|
PlaybackValid = FileIO.CopyFile(m_Path, "$profile:speech-downloaded.wav");
|
||||||
|
m_Handle = AudioSystem.PlaySound(m_Path);
|
||||||
|
PlaybackValid = PlaybackValid && m_Handle != AudioHandle.Invalid;
|
||||||
|
Played = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modded class ERR_GameMode
|
||||||
|
{
|
||||||
|
protected ref ERR_SpeechDownloadProbeClient m_ERRSpeechDownloadProbe;
|
||||||
|
protected int m_ERRSpeechDownloadDeadline;
|
||||||
|
protected string m_ERRSpeechProbeURL;
|
||||||
|
protected override void EOnInit(IEntity owner)
|
||||||
|
{
|
||||||
|
super.EOnInit(owner);
|
||||||
|
string outcome;
|
||||||
|
if (SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "speech-download") return;
|
||||||
|
// Only explicit loopback fixtures: no paid synthesis or production endpoint.
|
||||||
|
if (!System.GetCLIParam("errSpeechProbeURL", m_ERRSpeechProbeURL) || m_ERRSpeechProbeURL.IndexOf("http://127.0.0.1:") != 0)
|
||||||
|
{ Print("ERR SMOKE FAIL: speech download needs an explicit loopback URL", LogLevel.ERROR); return; }
|
||||||
|
m_ERRSpeechDownloadDeadline = System.GetTickCount() + 180000;
|
||||||
|
OnStop().Insert(ERR_StopSpeechDownloadProbe);
|
||||||
|
GetGame().GetCallqueue().CallLater(ERR_TickSpeechDownloadProbe, 100, true);
|
||||||
|
}
|
||||||
|
protected void ERR_TickSpeechDownloadProbe()
|
||||||
|
{
|
||||||
|
if (System.GetTickCount() >= m_ERRSpeechDownloadDeadline || !PreparationError().IsEmpty())
|
||||||
|
{
|
||||||
|
Print("ERR SMOKE FAIL: speech download preparation/deadline", LogLevel.ERROR);
|
||||||
|
ERR_StopSpeechDownloadProbe(); return;
|
||||||
|
}
|
||||||
|
if (!Active()) return;
|
||||||
|
if (!m_ERRSpeechDownloadProbe) m_ERRSpeechDownloadProbe = new ERR_SpeechDownloadProbeClient(this, 910, new ERR_WelcomeConnection(), false);
|
||||||
|
m_ERRSpeechDownloadProbe.Tick(0.1, true, m_ERRSpeechProbeURL, "Local transport test");
|
||||||
|
if (!m_ERRSpeechDownloadProbe.Played) return;
|
||||||
|
if (m_ERRSpeechDownloadProbe.PlaybackValid)
|
||||||
|
Print("ERR SMOKE COMPLETE: native REST multipart speech download, incremental preparation and valid playback handle; compare speech-downloaded.wav to source for byte integrity; not an audible multiplayer test");
|
||||||
|
else Print("ERR SMOKE FAIL: speech download native output/playback", LogLevel.ERROR);
|
||||||
|
ERR_StopSpeechDownloadProbe();
|
||||||
|
}
|
||||||
|
protected void ERR_StopSpeechDownloadProbe()
|
||||||
|
{
|
||||||
|
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_TickSpeechDownloadProbe);
|
||||||
|
if (m_ERRSpeechDownloadProbe) m_ERRSpeechDownloadProbe.Stop();
|
||||||
|
OnStop().Remove(ERR_StopSpeechDownloadProbe);
|
||||||
|
}
|
||||||
|
void ~ERR_GameMode() { ERR_StopSpeechDownloadProbe(); }
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -35,7 +35,7 @@ class ERR_ObjectiveText
|
|||||||
ERR_Phase phase = mode.Phase();
|
ERR_Phase phase = mode.Phase();
|
||||||
if (phase == ERR_Phase.PREPARING)
|
if (phase == ERR_Phase.PREPARING)
|
||||||
{
|
{
|
||||||
if (!mode.PreparationError().IsEmpty()) return "The mission could not start. An administrator can use /esc status for the cause. Spawning is disabled until the configuration is corrected and the scenario restarted.";
|
if (!mode.PreparationError().IsEmpty()) return "Reason: " + mode.PreparationError() + "\n\nAn administrator must correct the problem and restart the scenario before players can spawn.";
|
||||||
return "Preparing mission sites and equipment. You will spawn when the mission is ready.";
|
return "Preparing mission sites and equipment. You will spawn when the mission is ready.";
|
||||||
}
|
}
|
||||||
if (phase == ERR_Phase.PRISON)
|
if (phase == ERR_Phase.PRISON)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class ERR_ObserverGuidance
|
|||||||
}
|
}
|
||||||
if (!survivorAvailable)
|
if (!survivorAvailable)
|
||||||
return message + " No teammate or body is currently available to follow. Use camera look controls to orbit the last known position and zoom controls to change distance.";
|
return message + " No teammate or body is currently available to follow. Use camera look controls to orbit the last known position and zoom controls to change distance.";
|
||||||
return message + " Press Left / Right arrow to change teammate or body (or /esc spectate next / /esc spectate previous). Press Enter to switch between third-person and the living teammate's camera. Player view waits in third-person if no live camera stream is available. Use camera look controls to orbit and zoom controls to change distance in third-person.";
|
return message + " Press Left / Right arrow to change teammate or body, including your own (or /esc spectate next / /esc spectate previous). Press Enter to switch between third-person and the living teammate's camera. Player view waits in third-person if no live camera stream is available. Use camera look controls to orbit and zoom controls to change distance in third-person.";
|
||||||
}
|
}
|
||||||
static bool Refresh(bool shown, int previousReason, int reason, bool previousAvailable, bool survivorAvailable)
|
static bool Refresh(bool shown, int previousReason, int reason, bool previousAvailable, bool survivorAvailable)
|
||||||
{
|
{
|
||||||
@@ -225,7 +225,7 @@ class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
|
|||||||
{
|
{
|
||||||
target.GetWorldTransform(transform);
|
target.GetWorldTransform(transform);
|
||||||
aim = transform[3] + "0 1.5 0";
|
aim = transform[3] + "0 1.5 0";
|
||||||
if (!ERR_Actors.Alive(target)) aim = transform[3] + "0 0.4 0";
|
if (!ERR_Actors.CanFight(target)) aim = transform[3] + "0 0.4 0";
|
||||||
}
|
}
|
||||||
if (target != m_Target || vehicle != m_Vehicle)
|
if (target != m_Target || vehicle != m_Vehicle)
|
||||||
{
|
{
|
||||||
@@ -242,7 +242,7 @@ class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
|
|||||||
m_Target = null; m_Vehicle = null;
|
m_Target = null; m_Vehicle = null;
|
||||||
m_Distance = Math.Clamp(m_Distance, 2, 12);
|
m_Distance = Math.Clamp(m_Distance, 2, 12);
|
||||||
}
|
}
|
||||||
if ((!m_PlayerView || !target || !ERR_Actors.Alive(target)) && param.isManualInputEnabled)
|
if ((!m_PlayerView || !target || !ERR_Actors.CanFight(target)) && param.isManualInputEnabled)
|
||||||
{
|
{
|
||||||
m_Yaw += GetInputManager().GetActionValue("ManualCameraRotateYaw") * 90 * param.timeSlice;
|
m_Yaw += GetInputManager().GetActionValue("ManualCameraRotateYaw") * 90 * param.timeSlice;
|
||||||
m_Pitch = Math.Clamp(m_Pitch - GetInputManager().GetActionValue("ManualCameraRotatePitch") * 90 * param.timeSlice, 5, 75);
|
m_Pitch = Math.Clamp(m_Pitch - GetInputManager().GetActionValue("ManualCameraRotatePitch") * 90 * param.timeSlice, 5, 75);
|
||||||
@@ -284,6 +284,8 @@ modded class SCR_PlayerController
|
|||||||
protected ERR_ObserverReason m_ERRShownObserverReason;
|
protected ERR_ObserverReason m_ERRShownObserverReason;
|
||||||
protected bool m_ERRShownObserverAvailable;
|
protected bool m_ERRShownObserverAvailable;
|
||||||
protected string m_ERRShownRecoveryStatus;
|
protected string m_ERRShownRecoveryStatus;
|
||||||
|
protected string m_ERRPendingRecoveryStatus;
|
||||||
|
protected int m_ERRRecoveryStatusSince;
|
||||||
protected float m_ERRLastCycle = -1000;
|
protected float m_ERRLastCycle = -1000;
|
||||||
protected IEntity m_ERRRecoveryBody;
|
protected IEntity m_ERRRecoveryBody;
|
||||||
protected float m_ERRRecoveryDeadline = -1;
|
protected float m_ERRRecoveryDeadline = -1;
|
||||||
@@ -364,10 +366,13 @@ modded class SCR_PlayerController
|
|||||||
if (survivors[i] == body || (casualty && !ERR_Actors.CanFight(survivors[i])) || !friendly || !mode.Players() || !mode.Players().CurrentActor(survivors[i]) || SCR_FactionManager.SGetFaction(survivors[i]) != friendly) survivors.RemoveOrdered(i);
|
if (survivors[i] == body || (casualty && !ERR_Actors.CanFight(survivors[i])) || !friendly || !mode.Players() || !mode.Players().CurrentActor(survivors[i]) || SCR_FactionManager.SGetFaction(survivors[i]) != friendly) survivors.RemoveOrdered(i);
|
||||||
// Dead teammates remain selectable alongside survivors, including our own body.
|
// Dead teammates remain selectable alongside survivors, including our own body.
|
||||||
ERR_ObserverTargets.Bodies(mode, survivors);
|
ERR_ObserverTargets.Bodies(mode, survivors);
|
||||||
|
// Lethal damage normally leaves a treatable casualty, not a native corpse.
|
||||||
|
// Keep that player's own canonical body in the cycle without possessing it.
|
||||||
|
if (casualty && !survivors.Contains(body)) survivors.Insert(body);
|
||||||
int index = survivors.Find(m_ERRServerObserved);
|
int index = survivors.Find(m_ERRServerObserved);
|
||||||
if (index < 0)
|
if (index < 0)
|
||||||
{
|
{
|
||||||
if (life && life.Actor) index = survivors.Find(life.Actor);
|
if (!casualty && life && life.Actor) index = survivors.Find(life.Actor);
|
||||||
if (index < 0) index = 0;
|
if (index < 0) index = 0;
|
||||||
}
|
}
|
||||||
else index += direction;
|
else index += direction;
|
||||||
@@ -468,13 +473,24 @@ modded class SCR_PlayerController
|
|||||||
string recovery = mode.RecoveryStatus();
|
string recovery = mode.RecoveryStatus();
|
||||||
string selfEpiHint = ERR_SelfEpinephrineHint();
|
string selfEpiHint = ERR_SelfEpinephrineHint();
|
||||||
if (!selfEpiHint.IsEmpty()) recovery += "\n" + selfEpiHint;
|
if (!selfEpiHint.IsEmpty()) recovery += "\n" + selfEpiHint;
|
||||||
if (ERR_ObserverGuidance.Refresh(m_ERRObserverReady, m_ERRShownObserverReason, m_ERRObserverReason, m_ERRShownObserverAvailable, survivorAvailable) || m_ERRShownRecoveryStatus != recovery)
|
bool contextChanged = ERR_ObserverGuidance.Refresh(m_ERRObserverReady, m_ERRShownObserverReason, m_ERRObserverReason, m_ERRShownObserverAvailable, survivorAvailable);
|
||||||
|
int now = System.GetTickCount();
|
||||||
|
// Medical samples can alternate between improving and stalled. Keep the
|
||||||
|
// accepted hint readable until the new status has remained stable for 3s.
|
||||||
|
// This affects presentation only, never the authoritative defeat timer.
|
||||||
|
if (m_ERRPendingRecoveryStatus != recovery || now < m_ERRRecoveryStatusSince)
|
||||||
|
{
|
||||||
|
m_ERRPendingRecoveryStatus = recovery;
|
||||||
|
m_ERRRecoveryStatusSince = now;
|
||||||
|
}
|
||||||
|
if (!contextChanged && m_ERRShownRecoveryStatus != recovery && now - m_ERRRecoveryStatusSince < 3000) return;
|
||||||
|
if (contextChanged || m_ERRShownRecoveryStatus != recovery)
|
||||||
{
|
{
|
||||||
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
|
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
|
||||||
if (!hints) return; // retry on the existing monitor; do not lose the first hint
|
if (!hints) return; // retry on the existing monitor; do not lose the first hint
|
||||||
string message = ERR_ObserverGuidance.Message(m_ERRObserverReason, survivorAvailable);
|
string message = ERR_ObserverGuidance.Message(m_ERRObserverReason, survivorAvailable);
|
||||||
if (!recovery.IsEmpty()) message += "\n" + recovery;
|
if (!recovery.IsEmpty()) message += "\n" + recovery;
|
||||||
if (!hints.ShowCustom(message, "Spectating", 12)) return;
|
if (!hints.ShowCustom(message, "Spectating", 12, m_ERRObserverReady)) return;
|
||||||
m_ERRObserverReady = true;
|
m_ERRObserverReady = true;
|
||||||
m_ERRShownObserverReason = m_ERRObserverReason;
|
m_ERRShownObserverReason = m_ERRObserverReason;
|
||||||
m_ERRShownObserverAvailable = survivorAvailable;
|
m_ERRShownObserverAvailable = survivorAvailable;
|
||||||
@@ -510,6 +526,8 @@ modded class SCR_PlayerController
|
|||||||
{
|
{
|
||||||
m_ERRObserverReady = false;
|
m_ERRObserverReady = false;
|
||||||
m_ERRShownRecoveryStatus = "";
|
m_ERRShownRecoveryStatus = "";
|
||||||
|
m_ERRPendingRecoveryStatus = "";
|
||||||
|
m_ERRRecoveryStatusSince = 0;
|
||||||
if (!m_ERRObserverCamera) return;
|
if (!m_ERRObserverCamera) return;
|
||||||
if (GetGame().GetPlayerController() == this) m_ERRObserverCamera.TrySwitchToControlledEntityCamera();
|
if (GetGame().GetPlayerController() == this) m_ERRObserverCamera.TrySwitchToControlledEntityCamera();
|
||||||
m_ERRObserverCamera.Terminate();
|
m_ERRObserverCamera.Terminate();
|
||||||
|
|||||||
@@ -3,30 +3,44 @@
|
|||||||
class ERR_ObserverViewRules
|
class ERR_ObserverViewRules
|
||||||
{
|
{
|
||||||
// One unreliable packet contains both cameras, so loss/reordering cannot
|
// One unreliable packet contains both cameras, so loss/reordering cannot
|
||||||
// pair a new eye pose with an old aiming pose. Four vectors for ordinary/2D,
|
// pair a new eye pose with an old aiming pose. Five vectors for ordinary/2D,
|
||||||
// nine for PiP: scope pose/lens, eye pose/lens, native lens geometry.
|
// ten for PiP: the existing pose/lens data followed by the sampled body origin.
|
||||||
|
// Older offset-only packets are rejected rather than decoded as world poses.
|
||||||
static bool Packet(array<vector> packet)
|
static bool Packet(array<vector> packet)
|
||||||
{
|
{
|
||||||
if (!packet || (packet.Count() != 4 && packet.Count() != 9)) return false;
|
if (!packet || (packet.Count() != 5 && packet.Count() != 10)) return false;
|
||||||
|
vector origin = packet[packet.Count() - 1];
|
||||||
|
if (!Origin(origin) || !Origin(WorldPosition(origin, packet[0]))) return false;
|
||||||
if (!Sample(packet[0], packet[1], packet[2], packet[3][0], packet[3][1])) return false;
|
if (!Sample(packet[0], packet[1], packet[2], packet[3][0], packet[3][1])) return false;
|
||||||
if (packet.Count() == 4) return packet[3][2] == 0;
|
if (packet.Count() == 5) return packet[3][2] == 0;
|
||||||
if (!(packet[3][2] >= 0.01 && packet[3][2] <= 150)) return false;
|
if (!(packet[3][2] >= 0.01 && packet[3][2] <= 150)) return false;
|
||||||
|
if (!Origin(WorldPosition(origin, packet[4]))) return false;
|
||||||
if (!Sample(packet[4], packet[5], packet[6], packet[7][0], packet[7][1])) return false;
|
if (!Sample(packet[4], packet[5], packet[6], packet[7][0], packet[7][1])) return false;
|
||||||
return packet[7][2] >= 0 && packet[7][2] <= 100 && Math.AbsFloat(packet[8][0]) <= 90 && Math.AbsFloat(packet[8][1]) <= 90 && packet[8][2] >= 0.001 && packet[8][2] <= 100;
|
return packet[7][2] >= 0 && packet[7][2] <= 100 && Math.AbsFloat(packet[8][0]) <= 90 && Math.AbsFloat(packet[8][1]) <= 90 && packet[8][2] >= 0.001 && packet[8][2] <= 100;
|
||||||
}
|
}
|
||||||
// The body and camera arrive through independently timed replication streams.
|
// Sample axes, world-axis eye offset and body origin on the owner together.
|
||||||
// Keep the sampled world axes and world-axis eye offset together. Applying
|
// Neither the proxy body rotation nor its position is used during playback.
|
||||||
// the proxy body's rotation to delayed body-local aim adds its yaw error to
|
|
||||||
// both aim and eye position, even when the owner's camera is steady.
|
|
||||||
static void Encode(vector body[4], vector view[4], out vector pose[4])
|
static void Encode(vector body[4], vector view[4], out vector pose[4])
|
||||||
{
|
{
|
||||||
for (int axis = 0; axis < 3; axis++) pose[axis] = view[axis];
|
for (int axis = 0; axis < 3; axis++) pose[axis] = view[axis];
|
||||||
pose[3] = view[3] - body[3];
|
pose[3] = view[3] - body[3];
|
||||||
}
|
}
|
||||||
static void Decode(vector body[4], vector pose[4], out vector view[4])
|
// Reconstruct before interpolation, once per received sample. Both eye and
|
||||||
|
// scope use the exact same captured origin, never the current proxy position.
|
||||||
|
static vector WorldPosition(vector origin, vector offset) { return origin + offset; }
|
||||||
|
static bool Origin(vector position)
|
||||||
{
|
{
|
||||||
for (int axis = 0; axis < 3; axis++) view[axis] = pose[axis];
|
// Finite coordinate bounds cover large terrains while rejecting NaN/infinity.
|
||||||
view[3] = body[3] + pose[3];
|
return Math.AbsFloat(position[0]) <= 10000000 && Math.AbsFloat(position[1]) <= 10000000 && Math.AbsFloat(position[2]) <= 10000000;
|
||||||
|
}
|
||||||
|
static bool NearBody(vector origin, vector authoritative)
|
||||||
|
{
|
||||||
|
if (!Origin(origin) || !Origin(authoritative)) return false;
|
||||||
|
return (origin - authoritative).LengthSq() <= 10000;
|
||||||
|
}
|
||||||
|
static bool WorldSample(vector position, vector forward, vector up, float fov, float nearPlane)
|
||||||
|
{
|
||||||
|
return Origin(position) && Sample(vector.Zero, forward, up, fov, nearPlane);
|
||||||
}
|
}
|
||||||
static bool Sample(vector position, vector forward, vector up, float fov, float nearPlane)
|
static bool Sample(vector position, vector forward, vector up, float fov, float nearPlane)
|
||||||
{
|
{
|
||||||
@@ -42,6 +56,7 @@ class ERR_ObserverViewRules
|
|||||||
class ERR_ObserverViewSample
|
class ERR_ObserverViewSample
|
||||||
{
|
{
|
||||||
int Stamp;
|
int Stamp;
|
||||||
|
// Complete sampled world pose, not an offset from the replicated proxy.
|
||||||
vector Position, Forward, Up;
|
vector Position, Forward, Up;
|
||||||
float Fov, Near;
|
float Fov, Near;
|
||||||
float PipFov, Vignette;
|
float PipFov, Vignette;
|
||||||
@@ -74,7 +89,7 @@ class ERR_ObserverViewBuffer
|
|||||||
void Push(RplId target, int stamp, float now, vector position, vector forward, vector up, float fov, float nearPlane, RplId scopeWeapon, vector scopeData, float pipFov = 0, vector geometry = "0 0 0", float vignette = 0)
|
void Push(RplId target, int stamp, float now, vector position, vector forward, vector up, float fov, float nearPlane, RplId scopeWeapon, vector scopeData, float pipFov = 0, vector geometry = "0 0 0", float vignette = 0)
|
||||||
{
|
{
|
||||||
if (!ERR_ObserverScope.Valid(scopeWeapon, scopeData)) return;
|
if (!ERR_ObserverScope.Valid(scopeWeapon, scopeData)) return;
|
||||||
if (!ERR_ObserverViewRules.Sample(position, forward, up, fov, nearPlane)) return;
|
if (!ERR_ObserverViewRules.WorldSample(position, forward, up, fov, nearPlane)) return;
|
||||||
if (target == m_Target && stamp <= m_Stamp) return;
|
if (target == m_Target && stamp <= m_Stamp) return;
|
||||||
if (target != m_Target || scopeWeapon != m_ScopeWeapon || scopeData[0] != m_ScopeData[0] || !ERR_ObserverViewRules.Fresh(now, m_Received))
|
if (target != m_Target || scopeWeapon != m_ScopeWeapon || scopeData[0] != m_ScopeData[0] || !ERR_ObserverViewRules.Fresh(now, m_Received))
|
||||||
{
|
{
|
||||||
@@ -144,13 +159,11 @@ modded class SCR_PlayerController
|
|||||||
bool ERR_ReadPlayerEye(IEntity target, float seconds, out vector transform[4], out float fov, out float nearPlane, out float pipFov, out vector geometry, out float vignette)
|
bool ERR_ReadPlayerEye(IEntity target, float seconds, out vector transform[4], out float fov, out float nearPlane, out float pipFov, out vector geometry, out float vignette)
|
||||||
{
|
{
|
||||||
if (!m_ERRHasPIP || this != GetGame().GetPlayerController() || !target || !ERR_Actors.CanFight(target)) return false;
|
if (!m_ERRHasPIP || this != GetGame().GetPlayerController() || !target || !ERR_Actors.CanFight(target)) return false;
|
||||||
vector pose[4], world[4];
|
|
||||||
float now = GetGame().GetWorld().GetWorldTime();
|
float now = GetGame().GetWorld().GetWorldTime();
|
||||||
// Use the exact aiming playhead even after a streamed optic temporarily
|
// Use the exact aiming playhead even after a streamed optic temporarily
|
||||||
// fails resolution and this eye buffer is not read for several frames.
|
// fails resolution and this eye buffer is not read for several frames.
|
||||||
m_ERREyeBuffer.Align(m_ERRViewBuffer.Playback(), now);
|
m_ERREyeBuffer.Align(m_ERRViewBuffer.Playback(), now);
|
||||||
if (!m_ERREyeBuffer.Read(m_ERRObservedId, now, seconds, pose, fov, nearPlane)) return false;
|
if (!m_ERREyeBuffer.Read(m_ERRObservedId, now, seconds, transform, fov, nearPlane)) return false;
|
||||||
target.GetWorldTransform(world); ERR_ObserverViewRules.Decode(world, pose, transform);
|
|
||||||
m_ERRViewBuffer.PIP(pipFov, geometry, vignette);
|
m_ERRViewBuffer.PIP(pipFov, geometry, vignette);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -160,13 +173,10 @@ modded class SCR_PlayerController
|
|||||||
float now = GetGame().GetWorld().GetWorldTime();
|
float now = GetGame().GetWorld().GetWorldTime();
|
||||||
if (now - m_ERRViewRequestAt >= 500)
|
if (now - m_ERRViewRequestAt >= 500)
|
||||||
{ m_ERRViewRequestAt = now; Rpc(ERR_RequestPlayerView); }
|
{ m_ERRViewRequestAt = now; Rpc(ERR_RequestPlayerView); }
|
||||||
vector pose[4], world[4];
|
if (!m_ERRViewBuffer.Read(m_ERRObservedId, now, seconds, transform, fov, nearPlane)) return false;
|
||||||
if (!m_ERRViewBuffer.Read(m_ERRObservedId, now, seconds, pose, fov, nearPlane)) return false;
|
|
||||||
RplId scopeWeapon; vector scopeData;
|
RplId scopeWeapon; vector scopeData;
|
||||||
m_ERRViewBuffer.Scope(scopeWeapon, scopeData);
|
m_ERRViewBuffer.Scope(scopeWeapon, scopeData);
|
||||||
if (scopeWeapon != RplId.Invalid() && !ERR_ObserverScope.Resolve(target, scopeWeapon, scopeData)) return false;
|
if (scopeWeapon != RplId.Invalid() && !ERR_ObserverScope.Resolve(target, scopeWeapon, scopeData)) return false;
|
||||||
target.GetWorldTransform(world);
|
|
||||||
ERR_ObserverViewRules.Decode(world, pose, transform);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
|
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
|
||||||
@@ -229,12 +239,14 @@ modded class SCR_PlayerController
|
|||||||
packet[3] = Vector(fov, nearPlane, pip.GetPIPCamera().GetVerticalFOV());
|
packet[3] = Vector(fov, nearPlane, pip.GetPIPCamera().GetVerticalFOV());
|
||||||
packet.Insert(eye[3]); packet.Insert(eye[2]); packet.Insert(eye[1]);
|
packet.Insert(eye[3]); packet.Insert(eye[2]); packet.Insert(eye[1]);
|
||||||
packet.Insert(Vector(eyeFov, nearPlane, pip.ERR_ObserverPIPVignette(actor, camera))); packet.Insert(pip.ERR_ObserverPIPGeometry());
|
packet.Insert(Vector(eyeFov, nearPlane, pip.ERR_ObserverPIPVignette(actor, camera))); packet.Insert(pip.ERR_ObserverPIPGeometry());
|
||||||
|
packet.Insert(world[3]);
|
||||||
if (!ERR_ObserverViewRules.Packet(packet))
|
if (!ERR_ObserverViewRules.Packet(packet))
|
||||||
{
|
{
|
||||||
// An unusual addon lens must not suppress an otherwise valid 2D view.
|
// An unusual addon lens must not suppress an otherwise valid 2D view.
|
||||||
packet.Resize(4); packet[3] = Vector(fov, nearPlane, 0);
|
packet.Resize(4); packet[3] = Vector(fov, nearPlane, 0); packet.Insert(world[3]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (packet.Count() == 4) packet.Insert(world[3]);
|
||||||
if (!ERR_ObserverViewRules.Packet(packet)) return;
|
if (!ERR_ObserverViewRules.Packet(packet)) return;
|
||||||
Rpc(ERR_SubmitPlayerView, ++m_ERRViewSequence, packet, scopeWeapon, scopeData);
|
Rpc(ERR_SubmitPlayerView, ++m_ERRViewSequence, packet, scopeWeapon, scopeData);
|
||||||
}
|
}
|
||||||
@@ -242,12 +254,13 @@ modded class SCR_PlayerController
|
|||||||
protected void ERR_SubmitPlayerView(int sequence, array<vector> packet, RplId scopeWeapon, vector scopeData)
|
protected void ERR_SubmitPlayerView(int sequence, array<vector> packet, RplId scopeWeapon, vector scopeData)
|
||||||
{
|
{
|
||||||
if (!Replication.IsServer() || !ERR_ObserverScope.Valid(scopeWeapon, scopeData) || !ERR_ObserverViewRules.Packet(packet)) return;
|
if (!Replication.IsServer() || !ERR_ObserverScope.Valid(scopeWeapon, scopeData) || !ERR_ObserverViewRules.Packet(packet)) return;
|
||||||
if (packet.Count() == 9 && scopeWeapon == RplId.Invalid()) return;
|
if (packet.Count() == 10 && scopeWeapon == RplId.Invalid()) return;
|
||||||
ERR_GameMode mode = ERR_GameMode.Get();
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
IEntity actor = GetControlledEntity();
|
IEntity actor = GetControlledEntity();
|
||||||
if (!mode || !mode.Active() || !ERR_Actors.CanFight(actor)) return;
|
if (!mode || !mode.Active() || !ERR_Actors.CanFight(actor)) return;
|
||||||
ERR_PlayerLife life = mode.Players().CurrentActor(actor);
|
ERR_PlayerLife life = mode.Players().CurrentActor(actor);
|
||||||
if (!life || GetGame().GetPlayerManager().GetPlayerController(life.ConnectionId) != this) return;
|
if (!life || GetGame().GetPlayerManager().GetPlayerController(life.ConnectionId) != this) return;
|
||||||
|
if (!ERR_ObserverViewRules.NearBody(packet[packet.Count() - 1], actor.GetOrigin())) return;
|
||||||
float now = GetGame().GetWorld().GetWorldTime();
|
float now = GetGame().GetWorld().GetWorldTime();
|
||||||
if (actor != m_ERRViewSourceBody) { m_ERRViewSourceBody = actor; m_ERRViewInputSequence = 0; }
|
if (actor != m_ERRViewSourceBody) { m_ERRViewSourceBody = actor; m_ERRViewInputSequence = 0; }
|
||||||
if (sequence <= m_ERRViewInputSequence || now - m_ERRViewLastPacket < 35) return;
|
if (sequence <= m_ERRViewInputSequence || now - m_ERRViewLastPacket < 35) return;
|
||||||
@@ -275,19 +288,20 @@ modded class SCR_PlayerController
|
|||||||
ERR_GameMode mode = ERR_GameMode.Get();
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
if (this != GetGame().GetPlayerController() || target == RplId.Invalid() || target != m_ERRObservedId || !mode || !ERR_ObserverRules.View(m_ERRObserverReason, mode.Active(), ERR_Actors.Alive(GetControlledEntity()), ERR_Actors.CanFight(GetControlledEntity()))) return;
|
if (this != GetGame().GetPlayerController() || target == RplId.Invalid() || target != m_ERRObservedId || !mode || !ERR_ObserverRules.View(m_ERRObserverReason, mode.Active(), ERR_Actors.Alive(GetControlledEntity()), ERR_Actors.CanFight(GetControlledEntity()))) return;
|
||||||
if (!ERR_ObserverViewRules.Packet(packet) || !ERR_ObserverScope.Valid(scopeWeapon, scopeData)) return;
|
if (!ERR_ObserverViewRules.Packet(packet) || !ERR_ObserverScope.Valid(scopeWeapon, scopeData)) return;
|
||||||
if (packet.Count() == 9 && scopeWeapon == RplId.Invalid()) return;
|
if (packet.Count() == 10 && scopeWeapon == RplId.Invalid()) return;
|
||||||
// Reject before updating either stream or PiP metadata.
|
// Reject before updating either stream or PiP metadata.
|
||||||
if (!m_ERRViewBuffer.Accepts(target, stamp)) return;
|
if (!m_ERRViewBuffer.Accepts(target, stamp)) return;
|
||||||
float now = GetGame().GetWorld().GetWorldTime();
|
float now = GetGame().GetWorld().GetWorldTime();
|
||||||
bool pip = packet.Count() == 9;
|
bool pip = packet.Count() == 10;
|
||||||
if (pip != m_ERRHasPIP) { m_ERRViewBuffer.Clear(); m_ERREyeBuffer.Clear(); }
|
if (pip != m_ERRHasPIP) { m_ERRViewBuffer.Clear(); m_ERREyeBuffer.Clear(); }
|
||||||
vector geometry; float vignette;
|
vector geometry; float vignette;
|
||||||
if (pip) { geometry = packet[8]; vignette = packet[7][2]; }
|
if (pip) { geometry = packet[8]; vignette = packet[7][2]; }
|
||||||
m_ERRViewBuffer.Push(target, stamp, now, packet[0], packet[1], packet[2], packet[3][0], packet[3][1], scopeWeapon, scopeData, packet[3][2], geometry, vignette);
|
vector origin = packet[packet.Count() - 1];
|
||||||
|
m_ERRViewBuffer.Push(target, stamp, now, ERR_ObserverViewRules.WorldPosition(origin, packet[0]), packet[1], packet[2], packet[3][0], packet[3][1], scopeWeapon, scopeData, packet[3][2], geometry, vignette);
|
||||||
m_ERRHasPIP = pip;
|
m_ERRHasPIP = pip;
|
||||||
if (pip)
|
if (pip)
|
||||||
{
|
{
|
||||||
m_ERREyeBuffer.Push(target, stamp, now, packet[4], packet[5], packet[6], packet[7][0], packet[7][1], scopeWeapon, scopeData);
|
m_ERREyeBuffer.Push(target, stamp, now, ERR_ObserverViewRules.WorldPosition(origin, packet[4]), packet[5], packet[6], packet[7][0], packet[7][1], scopeWeapon, scopeData);
|
||||||
}
|
}
|
||||||
else m_ERREyeBuffer.Clear();
|
else m_ERREyeBuffer.Clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
// Front-seat passenger assistance is an extra held input, not a parking-brake
|
||||||
|
// toggle. The driver's persistent brake, gears and service brake stay native.
|
||||||
|
class ERR_PassengerHandbrake
|
||||||
|
{
|
||||||
|
static bool Beside(vector delta)
|
||||||
|
{
|
||||||
|
float lateral = Math.AbsFloat(delta[0]);
|
||||||
|
return lateral >= 0.35 && lateral <= 1.7 && Math.AbsFloat(delta[1]) <= 0.5 && Math.AbsFloat(delta[2]) <= 0.65;
|
||||||
|
}
|
||||||
|
static bool FrontSeat(IEntity vehicle, BaseCompartmentSlot slot)
|
||||||
|
{
|
||||||
|
if (!vehicle || !slot || slot.GetType() != ECompartmentType.CARGO) return false;
|
||||||
|
CarControllerComponent car = CarControllerComponent.Cast(vehicle.FindComponent(CarControllerComponent));
|
||||||
|
if (!car) return false;
|
||||||
|
BaseCompartmentSlot pilot = car.GetPilotCompartmentSlot();
|
||||||
|
if (!pilot || pilot == slot || !slot.GetOwner() || slot.GetOwner().GetRootParent() != vehicle.GetRootParent()) return false;
|
||||||
|
PointInfo passengerPoint = slot.GetPassengerPointInfo(), pilotPoint = pilot.GetPassengerPointInfo();
|
||||||
|
if (!passengerPoint || !pilotPoint) return false;
|
||||||
|
vector passenger = vehicle.CoordToLocal(passengerPoint.GetWorldTransformAxis(3));
|
||||||
|
vector driver = vehicle.CoordToLocal(pilotPoint.GetWorldTransformAxis(3));
|
||||||
|
return Beside(passenger - driver);
|
||||||
|
}
|
||||||
|
static IEntity VehicleFor(IEntity actor)
|
||||||
|
{
|
||||||
|
if (!ERR_Actors.CanFight(actor)) return null;
|
||||||
|
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(actor.FindComponent(CompartmentAccessComponent));
|
||||||
|
if (!access) return null;
|
||||||
|
BaseCompartmentSlot slot = access.GetCompartment();
|
||||||
|
IEntity vehicle = CompartmentAccessComponent.GetVehicleIn(actor);
|
||||||
|
if (!slot || slot.GetOccupant() != actor || !FrontSeat(vehicle, slot)) return null;
|
||||||
|
return vehicle;
|
||||||
|
}
|
||||||
|
static bool Requested(IEntity driver)
|
||||||
|
{
|
||||||
|
IEntity vehicle = CompartmentAccessComponent.GetVehicleIn(driver);
|
||||||
|
if (!vehicle) return false;
|
||||||
|
CarControllerComponent car = CarControllerComponent.Cast(vehicle.FindComponent(CarControllerComponent));
|
||||||
|
if (!car || !car.GetPilotCompartmentSlot() || car.GetPilotCompartmentSlot().GetOccupant() != driver) return false;
|
||||||
|
// Native held-handbrake input releases the parking latch. A parked car
|
||||||
|
// already satisfies the passenger's demand, so leave that latch untouched.
|
||||||
|
if (car.GetPersistentHandBrake()) return false;
|
||||||
|
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(vehicle.FindComponent(BaseCompartmentManagerComponent));
|
||||||
|
if (!manager) return false;
|
||||||
|
array<BaseCompartmentSlot> slots = {};
|
||||||
|
manager.GetCompartments(slots);
|
||||||
|
foreach (BaseCompartmentSlot slot : slots)
|
||||||
|
{
|
||||||
|
IEntity actor = slot.GetOccupant();
|
||||||
|
if (!actor || actor == driver) continue;
|
||||||
|
SCR_CharacterControllerComponent character = SCR_CharacterControllerComponent.Cast(actor.FindComponent(SCR_CharacterControllerComponent));
|
||||||
|
if (character && character.ERR_PassengerBrakeHeld() && VehicleFor(actor) == vehicle) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modded class SCR_PlayerController
|
||||||
|
{
|
||||||
|
protected SCR_CharacterControllerComponent m_ERRBrakingPassenger;
|
||||||
|
// No caller-supplied body, seat or vehicle: authority derives all three.
|
||||||
|
void ERR_SendPassengerBrake(bool held)
|
||||||
|
{
|
||||||
|
if (this != GetGame().GetPlayerController()) return;
|
||||||
|
if (Replication.IsServer()) ERR_RequestPassengerBrake(held);
|
||||||
|
else Rpc(ERR_RequestPassengerBrake, held);
|
||||||
|
}
|
||||||
|
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
|
||||||
|
protected void ERR_RequestPassengerBrake(bool held)
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer()) return;
|
||||||
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
|
IEntity actor = GetControlledEntity();
|
||||||
|
SCR_CharacterControllerComponent character;
|
||||||
|
if (actor) character = SCR_CharacterControllerComponent.Cast(actor.FindComponent(SCR_CharacterControllerComponent));
|
||||||
|
if (m_ERRBrakingPassenger && (m_ERRBrakingPassenger != character || !held)) m_ERRBrakingPassenger.ERR_SetPassengerBrake(false);
|
||||||
|
m_ERRBrakingPassenger = null;
|
||||||
|
if (!character) return;
|
||||||
|
bool allowed = held && mode && mode.Active() && mode.Players() && mode.Players().CurrentActor(actor) && ERR_PassengerHandbrake.VehicleFor(actor);
|
||||||
|
character.ERR_SetPassengerBrake(allowed);
|
||||||
|
if (allowed) m_ERRBrakingPassenger = character;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modded class SCR_CharacterControllerComponent
|
||||||
|
{
|
||||||
|
[RplProp()] protected bool m_ERRPassengerBrake;
|
||||||
|
protected int m_ERRPassengerBrakeHeartbeat;
|
||||||
|
protected bool m_ERRPassengerBrakeSent;
|
||||||
|
protected int m_ERRPassengerBrakeNextSend;
|
||||||
|
protected IEntity m_ERRPassengerBrakeHintVehicle;
|
||||||
|
|
||||||
|
bool ERR_PassengerBrakeHeld() { return m_ERRPassengerBrake; }
|
||||||
|
void ERR_SetPassengerBrake(bool held)
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer()) return;
|
||||||
|
m_ERRPassengerBrakeHeartbeat = System.GetTickCount();
|
||||||
|
if (held == m_ERRPassengerBrake) return;
|
||||||
|
m_ERRPassengerBrake = held;
|
||||||
|
Replication.BumpMe();
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_ExpirePassengerBrake);
|
||||||
|
if (held) GetGame().GetCallqueue().CallLater(ERR_ExpirePassengerBrake, 250, true);
|
||||||
|
}
|
||||||
|
protected void ERR_ExpirePassengerBrake()
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer() || !m_ERRPassengerBrake) return;
|
||||||
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
|
int age = System.GetTickCount() - m_ERRPassengerBrakeHeartbeat;
|
||||||
|
if (!mode || !mode.Active() || age < 0 || age > 1000 || !ERR_PassengerHandbrake.VehicleFor(GetOwner())) ERR_SetPassengerBrake(false);
|
||||||
|
}
|
||||||
|
void ~SCR_CharacterControllerComponent()
|
||||||
|
{
|
||||||
|
if (GetGame() && GetGame().GetCallqueue()) GetGame().GetCallqueue().Remove(ERR_ExpirePassengerBrake);
|
||||||
|
}
|
||||||
|
override void OnPrepareControls(IEntity owner, ActionManager am, float dt, bool player)
|
||||||
|
{
|
||||||
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
|
// Feed the actor's native action before it prepares driving controls. The
|
||||||
|
// device/AI refreshes this value each tick; only add a held demand, never
|
||||||
|
// write a release over the driver's own handbrake or parking-brake input.
|
||||||
|
if (am && mode && mode.Active() && !am.GetActionTriggered("CarHandBrakePersistent") && ERR_PassengerHandbrake.Requested(owner)) am.SetActionValue("CarHandBrake", 1);
|
||||||
|
super.OnPrepareControls(owner, am, dt, player);
|
||||||
|
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||||
|
if (!controller || controller.GetControlledEntity() != owner) return;
|
||||||
|
IEntity vehicle;
|
||||||
|
if (mode && mode.Active()) vehicle = ERR_PassengerHandbrake.VehicleFor(owner);
|
||||||
|
bool held;
|
||||||
|
InputManager input = GetGame().GetInputManager();
|
||||||
|
if (vehicle && input && GetGame().GetMenuManager() && !GetGame().GetMenuManager().GetTopMenu() && !ERR_ObserverChatInput.Capture())
|
||||||
|
{
|
||||||
|
input.ActivateContext("ERR_PassengerBrakeContext", 1);
|
||||||
|
held = input.GetActionValue("ERR_PassengerBrake") > 0.5;
|
||||||
|
if (m_ERRPassengerBrakeHintVehicle != vehicle)
|
||||||
|
{
|
||||||
|
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
|
||||||
|
if (hints) hints.ShowCustom("Hold Space to apply the handbrake. Release Space to let go.", "Passenger handbrake", 5);
|
||||||
|
m_ERRPassengerBrakeHintVehicle = vehicle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!vehicle) m_ERRPassengerBrakeHintVehicle = null;
|
||||||
|
int now = System.GetTickCount();
|
||||||
|
if (held != m_ERRPassengerBrakeSent || (held && now >= m_ERRPassengerBrakeNextSend))
|
||||||
|
{
|
||||||
|
controller.ERR_SendPassengerBrake(held);
|
||||||
|
m_ERRPassengerBrakeSent = held;
|
||||||
|
m_ERRPassengerBrakeNextSend = now + 250;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,7 +40,9 @@ modded class SCR_PlayerController
|
|||||||
}
|
}
|
||||||
ERR_GameMode mode = ERR_GameMode.Get();
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
if (!mode) return;
|
if (!mode) return;
|
||||||
if (mode.Phase() != ERR_Phase.PREPARING && (ERR_ObserverState() != ERR_ObserverReason.NONE || !ERR_Actors.Alive(GetControlledEntity()))) return;
|
// Local unconsciousness can arrive before the replicated observer reason.
|
||||||
|
// Do not let objective hints compete with casualty/spectator guidance then.
|
||||||
|
if (mode.Phase() != ERR_Phase.PREPARING && (ERR_ObserverState() != ERR_ObserverReason.NONE || !ERR_Actors.CanFight(GetControlledEntity()))) return;
|
||||||
string message = ERR_ObjectiveText.Description(mode, false);
|
string message = ERR_ObjectiveText.Description(mode, false);
|
||||||
if (message.IsEmpty()) return;
|
if (message.IsEmpty()) return;
|
||||||
if (m_ERRGuidePhase == mode.Phase() && m_ERRGuideMessage == message) return;
|
if (m_ERRGuidePhase == mode.Phase() && m_ERRGuideMessage == message) return;
|
||||||
|
|||||||
@@ -43,6 +43,22 @@ modded class SCR_ReconnectComponent
|
|||||||
PrintFormat("Reintroduced [reconnect]: player %1 original body reserved; timeoutSeconds=%2", playerId, seconds);
|
PrintFormat("Reintroduced [reconnect]: player %1 original body reserved; timeoutSeconds=%2", playerId, seconds);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Only an actual disconnected account may receive a replacement. Retire an
|
||||||
|
// orphaned living body through native cleanup before admitting a fresh one.
|
||||||
|
protected void PrepareReplacement(int playerId, ERR_GameMode mode, ERR_PlayerLife life)
|
||||||
|
{
|
||||||
|
if (!life.ReconnectPending || !ERR_PlayerLifeRules.CanEnter(false, mode.Phase())) return;
|
||||||
|
if (GetGame().GetPlayerManager().GetPlayerControlledEntity(playerId)) return;
|
||||||
|
if (life.Actor && GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(life.Actor) > 0) return;
|
||||||
|
if (ERR_Actors.Alive(life.Actor))
|
||||||
|
{
|
||||||
|
SCR_ReconnectData orphan = new SCR_ReconnectData();
|
||||||
|
orphan.m_ReservedEntity = life.Actor;
|
||||||
|
HandleDataExpiery(orphan);
|
||||||
|
}
|
||||||
|
if (mode.Players().AllowReconnectReplacement(playerId, mode.Phase()))
|
||||||
|
PrintFormat("Reintroduced [reconnect]: player %1 has no recoverable reservation; fresh admission allowed", playerId);
|
||||||
|
}
|
||||||
override bool HandlePlayerReconnect(int playerId)
|
override bool HandlePlayerReconnect(int playerId)
|
||||||
{
|
{
|
||||||
ERR_GameMode mode = ERR_GameMode.Get();
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
@@ -54,11 +70,21 @@ modded class SCR_ReconnectComponent
|
|||||||
UUID identity = SCR_PlayerIdentityUtils.GetPlayerIdentityId(playerId);
|
UUID identity = SCR_PlayerIdentityUtils.GetPlayerIdentityId(playerId);
|
||||||
if (!life || !life.Connected || !life.UsedLife || identity.IsNull() || life.AccountId != identity) return false;
|
if (!life || !life.Connected || !life.UsedLife || identity.IsNull() || life.AccountId != identity) return false;
|
||||||
SCR_ReconnectData data;
|
SCR_ReconnectData data;
|
||||||
if (!m_mReconnectData.Find(identity, data)) return false;
|
if (!m_mReconnectData.Find(identity, data))
|
||||||
|
{
|
||||||
|
PrepareReplacement(playerId, mode, life);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
IEntity actor = data.m_ReservedEntity;
|
IEntity actor = data.m_ReservedEntity;
|
||||||
int owner;
|
int owner;
|
||||||
if (actor) owner = GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(actor);
|
if (actor) owner = GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(actor);
|
||||||
if (!actor || actor != life.Actor || (owner > 0 && owner != playerId))
|
if (!actor)
|
||||||
|
{
|
||||||
|
m_mReconnectData.Remove(identity); UpdateExpieryCheck();
|
||||||
|
PrepareReplacement(playerId, mode, life);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (actor != life.Actor || (owner > 0 && owner != playerId))
|
||||||
{
|
{
|
||||||
m_mReconnectData.Remove(identity); UpdateExpieryCheck();
|
m_mReconnectData.Remove(identity); UpdateExpieryCheck();
|
||||||
PrintFormat("Reintroduced [reconnect]: player %1 rejected mismatched body reservation", playerId);
|
PrintFormat("Reintroduced [reconnect]: player %1 rejected mismatched body reservation", playerId);
|
||||||
@@ -69,7 +95,7 @@ modded class SCR_ReconnectComponent
|
|||||||
if (IsDataExpired(data, GetGame().GetWorld().GetTimestamp()) || !IsDataRelevant(data))
|
if (IsDataExpired(data, GetGame().GetWorld().GetTimestamp()) || !IsDataRelevant(data))
|
||||||
{
|
{
|
||||||
m_mReconnectData.Remove(identity); HandleDataExpiery(data); UpdateExpieryCheck();
|
m_mReconnectData.Remove(identity); HandleDataExpiery(data); UpdateExpieryCheck();
|
||||||
PrintFormat("Reintroduced [reconnect]: player %1 reservation expired or body lost; no replacement life", playerId);
|
PrepareReplacement(playerId, mode, life);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
bool restored = super.HandlePlayerReconnect(playerId);
|
bool restored = super.HandlePlayerReconnect(playerId);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class ERR_PlayerLife
|
|||||||
int ConnectionId;
|
int ConnectionId;
|
||||||
bool Connected;
|
bool Connected;
|
||||||
bool UsedLife;
|
bool UsedLife;
|
||||||
|
bool ReconnectPending;
|
||||||
IEntity Actor;
|
IEntity Actor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +67,18 @@ class ERR_PlayerRoster
|
|||||||
ERR_PlayerLife life = Get(playerId);
|
ERR_PlayerLife life = Get(playerId);
|
||||||
return life && life.Connected && ERR_PlayerLifeRules.CanEnter(life.UsedLife, phase);
|
return life && life.Connected && ERR_PlayerLifeRules.CanEnter(life.UsedLife, phase);
|
||||||
}
|
}
|
||||||
|
bool AllowReconnectReplacement(int playerId, int phase)
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer()) return false;
|
||||||
|
ERR_PlayerLife life = Get(playerId);
|
||||||
|
if (!life || !life.Connected || !life.UsedLife || !life.ReconnectPending || !ERR_PlayerLifeRules.CanEnter(false, phase)) return false;
|
||||||
|
if (ERR_Actors.Alive(life.Actor)) return false;
|
||||||
|
life.Actor = null;
|
||||||
|
life.UsedLife = false;
|
||||||
|
life.ReconnectPending = false;
|
||||||
|
m_OnChanged.Invoke(playerId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
void Spawned(int playerId, IEntity actor)
|
void Spawned(int playerId, IEntity actor)
|
||||||
{
|
{
|
||||||
if (!Replication.IsServer() || !actor) return;
|
if (!Replication.IsServer() || !actor) return;
|
||||||
@@ -73,13 +86,18 @@ class ERR_PlayerRoster
|
|||||||
if (!life) return;
|
if (!life) return;
|
||||||
life.Actor = actor;
|
life.Actor = actor;
|
||||||
life.UsedLife = true;
|
life.UsedLife = true;
|
||||||
|
life.ReconnectPending = false;
|
||||||
m_OnChanged.Invoke(playerId);
|
m_OnChanged.Invoke(playerId);
|
||||||
}
|
}
|
||||||
void Disconnect(int playerId)
|
void Disconnect(int playerId)
|
||||||
{
|
{
|
||||||
if (!Replication.IsServer()) return;
|
if (!Replication.IsServer()) return;
|
||||||
ERR_PlayerLife life = Get(playerId);
|
ERR_PlayerLife life = Get(playerId);
|
||||||
if (life) life.Connected = false;
|
if (life)
|
||||||
|
{
|
||||||
|
life.Connected = false;
|
||||||
|
life.ReconnectPending = life.UsedLife;
|
||||||
|
}
|
||||||
m_Connections.Remove(playerId);
|
m_Connections.Remove(playerId);
|
||||||
m_OnChanged.Invoke(playerId);
|
m_OnChanged.Invoke(playerId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
modded enum ChimeraMenuPreset
|
||||||
|
{
|
||||||
|
ERR_PreparationFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
// The native loading placeholder is not a menu and has no focus/input lifetime.
|
||||||
|
// Use a real local menu on failure; never grant admission or change server state.
|
||||||
|
class ERR_PreparationFailureMenu : ChimeraMenuBase
|
||||||
|
{
|
||||||
|
protected ERR_GameMode m_Mode;
|
||||||
|
protected SCR_ButtonTextComponent m_ReturnButton, m_PauseButton;
|
||||||
|
protected TextWidget m_Description;
|
||||||
|
protected string m_LastError;
|
||||||
|
protected bool m_Bound, m_Leaving;
|
||||||
|
|
||||||
|
static bool Failed(ERR_GameMode mode)
|
||||||
|
{
|
||||||
|
return mode && mode == ERR_GameMode.Get() && mode.Phase() == ERR_Phase.PREPARING && !mode.PreparationError().IsEmpty();
|
||||||
|
}
|
||||||
|
static bool Show(ERR_GameMode mode)
|
||||||
|
{
|
||||||
|
if (System.IsConsoleApp() || !Failed(mode) || !GetGame() || !GetGame().GetMenuManager()) return false;
|
||||||
|
MenuManager manager = GetGame().GetMenuManager();
|
||||||
|
// Leave existing menus and exit confirmation dialogs in control of focus.
|
||||||
|
if (manager.IsAnyMenuOpen() || manager.IsAnyDialogOpen()) return true;
|
||||||
|
if (manager.OpenMenu(ChimeraMenuPreset.ERR_PreparationFailure)) return true;
|
||||||
|
// A missing custom layout must not trap the player on the loading overlay.
|
||||||
|
GetGame().OpenPauseMenu(false, true);
|
||||||
|
return manager.FindMenuByPreset(ChimeraMenuPreset.PauseMenu) != null;
|
||||||
|
}
|
||||||
|
override void OnMenuOpen()
|
||||||
|
{
|
||||||
|
super.OnMenuOpen();
|
||||||
|
m_Mode = ERR_GameMode.Get();
|
||||||
|
if (!Failed(m_Mode)) { Close(); return; }
|
||||||
|
m_Description = TextWidget.Cast(GetRootWidget().FindAnyWidget("FailureDescription"));
|
||||||
|
m_ReturnButton = SCR_ButtonTextComponent.GetButtonText("ReturnToMainMenu", GetRootWidget());
|
||||||
|
m_PauseButton = SCR_ButtonTextComponent.GetButtonText("OpenPauseMenu", GetRootWidget());
|
||||||
|
if (m_ReturnButton) m_ReturnButton.m_OnClicked.Insert(OnReturn);
|
||||||
|
if (m_PauseButton) m_PauseButton.m_OnClicked.Insert(OnPause);
|
||||||
|
GetGame().GetInputManager().AddActionListener(UIConstants.MENU_ACTION_BACK, EActionTrigger.PRESSED, OnBack);
|
||||||
|
m_Bound = true;
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
protected void Refresh()
|
||||||
|
{
|
||||||
|
string detail = ERR_ObjectiveText.Description(m_Mode, false);
|
||||||
|
if (m_Description && detail != m_LastError) { m_Description.SetText(detail); m_LastError = detail; }
|
||||||
|
}
|
||||||
|
override void OnMenuUpdate(float tDelta)
|
||||||
|
{
|
||||||
|
super.OnMenuUpdate(tDelta);
|
||||||
|
if (!Failed(m_Mode)) { Close(); return; }
|
||||||
|
if (!m_Leaving) Refresh();
|
||||||
|
}
|
||||||
|
protected void OpenPause()
|
||||||
|
{
|
||||||
|
if (!IsFocused() || m_Leaving || !Failed(m_Mode)) return;
|
||||||
|
GetGame().OpenPauseMenu(false, true);
|
||||||
|
}
|
||||||
|
protected void OnBack() { OpenPause(); }
|
||||||
|
protected void OnPause(SCR_ButtonBaseComponent button) { OpenPause(); }
|
||||||
|
protected void OnReturn(SCR_ButtonBaseComponent button)
|
||||||
|
{
|
||||||
|
if (!IsFocused() || m_Leaving || !Failed(m_Mode)) return;
|
||||||
|
m_Leaving = true;
|
||||||
|
if (m_ReturnButton) m_ReturnButton.SetEnabled(false);
|
||||||
|
if (m_PauseButton) m_PauseButton.SetEnabled(false);
|
||||||
|
ChimeraWorld world = GetGame().GetWorld();
|
||||||
|
if (world) world.PauseGameTime(false);
|
||||||
|
// Same local exit transition as vanilla's pause-menu exit. No mission RPC.
|
||||||
|
GameStateTransitions.RequestGameplayEndTransition();
|
||||||
|
}
|
||||||
|
override void OnMenuClose()
|
||||||
|
{
|
||||||
|
if (m_Bound && GetGame() && GetGame().GetInputManager())
|
||||||
|
GetGame().GetInputManager().RemoveActionListener(UIConstants.MENU_ACTION_BACK, EActionTrigger.PRESSED, OnBack);
|
||||||
|
m_Bound = false;
|
||||||
|
if (m_ReturnButton) m_ReturnButton.m_OnClicked.Remove(OnReturn);
|
||||||
|
if (m_PauseButton) m_PauseButton.m_OnClicked.Remove(OnPause);
|
||||||
|
m_ReturnButton = null; m_PauseButton = null; m_Description = null; m_Mode = null;
|
||||||
|
super.OnMenuClose();
|
||||||
|
}
|
||||||
|
#ifdef WORKBENCH
|
||||||
|
static bool Visible(ERR_GameMode mode)
|
||||||
|
{
|
||||||
|
if (!Failed(mode)) return false;
|
||||||
|
ERR_PreparationFailureMenu menu = ERR_PreparationFailureMenu.Cast(GetGame().GetMenuManager().FindMenuByPreset(ChimeraMenuPreset.ERR_PreparationFailure));
|
||||||
|
return menu && menu.m_Mode == mode && menu.GetRootWidget().IsVisible() && menu.m_Description && menu.m_ReturnButton && menu.m_PauseButton && menu.m_Description.GetText() == ERR_ObjectiveText.Description(mode, false);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -109,6 +109,10 @@ modded class SCR_RespawnSystemComponent
|
|||||||
if (m_ERRPreparationSpinner) m_ERRPreparationSpinner.SetVisible(m_ERRPreparationSpinnerWasVisible);
|
if (m_ERRPreparationSpinner) m_ERRPreparationSpinner.SetVisible(m_ERRPreparationSpinnerWasVisible);
|
||||||
m_ERRPreparationSpinnerHidden = false;
|
m_ERRPreparationSpinnerHidden = false;
|
||||||
}
|
}
|
||||||
|
// The placeholder's high-Z black layer would obscure even the pause menu.
|
||||||
|
// Hide it only after an interactive menu is available; native updates restore
|
||||||
|
// it if preparation recovers. The failed round still admits no players.
|
||||||
|
if (failed && ERR_PreparationFailureMenu.Show(mode)) m_wLoadingPlaceholder.SetVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
override void DestroyLoadingPlaceholder()
|
override void DestroyLoadingPlaceholder()
|
||||||
@@ -130,6 +134,7 @@ modded class SCR_RespawnSystemComponent
|
|||||||
// Inspect the actual production-created widgets, without creating or changing them.
|
// Inspect the actual production-created widgets, without creating or changing them.
|
||||||
bool ERR_ProbePreparationVisible(ERR_GameMode mode, bool failed)
|
bool ERR_ProbePreparationVisible(ERR_GameMode mode, bool failed)
|
||||||
{
|
{
|
||||||
|
if (failed && ERR_PreparationFailureMenu.Visible(mode)) return true;
|
||||||
if (!mode || mode != ERR_GameMode.Get() || m_pGameMode != mode || !m_wLoadingPlaceholder || !m_wLoadingPlaceholder.IsVisible()) return false;
|
if (!mode || mode != ERR_GameMode.Get() || m_pGameMode != mode || !m_wLoadingPlaceholder || !m_wLoadingPlaceholder.IsVisible()) return false;
|
||||||
if (!m_ERRPreparationRoot || m_ERRPreparationParent != m_wLoadingPlaceholder || m_ERRPreparationRoot.GetParent() != m_wLoadingPlaceholder || !m_ERRPreparationRoot.IsVisible()) return false;
|
if (!m_ERRPreparationRoot || m_ERRPreparationParent != m_wLoadingPlaceholder || m_ERRPreparationRoot.GetParent() != m_wLoadingPlaceholder || !m_ERRPreparationRoot.IsVisible()) return false;
|
||||||
if (!ERR_PreparationWidgetVisible(m_ERRPreparationTitle) || !ERR_PreparationWidgetVisible(m_ERRPreparationDescription) || !ERR_PreparationWidgetVisible(m_ERRPreparationRoot)) return false;
|
if (!ERR_PreparationWidgetVisible(m_ERRPreparationTitle) || !ERR_PreparationWidgetVisible(m_ERRPreparationDescription) || !ERR_PreparationWidgetVisible(m_ERRPreparationRoot)) return false;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class ERR_ProfileConfig
|
|||||||
static const string VOICE = "$profile:EscapistsWelcomeVoice.json";
|
static const string VOICE = "$profile:EscapistsWelcomeVoice.json";
|
||||||
static array<string> LegacyPaths()
|
static array<string> LegacyPaths()
|
||||||
{
|
{
|
||||||
return {ERR_Settings.PATH, ERR_ContentSettings.PATH, ERR_FactionSelection.PATH, ERR_TerrainPreference.PATH, ERR_ItemPreferences.PATH, VOICE};
|
return {ERR_Settings.PATH, ERR_ContentSettings.PATH, ERR_FactionSelection.PATH, ERR_TerrainPreference.PATH, ERR_ItemPreferences.PATH, ERR_StartBackpack.PATH, VOICE};
|
||||||
}
|
}
|
||||||
static string Section(string path)
|
static string Section(string path)
|
||||||
{
|
{
|
||||||
@@ -15,6 +15,7 @@ class ERR_ProfileConfig
|
|||||||
if (path == ERR_FactionSelection.PATH) return "factions";
|
if (path == ERR_FactionSelection.PATH) return "factions";
|
||||||
if (path == ERR_TerrainPreference.PATH) return "terrain";
|
if (path == ERR_TerrainPreference.PATH) return "terrain";
|
||||||
if (path == ERR_ItemPreferences.PATH) return "items";
|
if (path == ERR_ItemPreferences.PATH) return "items";
|
||||||
|
if (path == ERR_StartBackpack.PATH) return "startBackpack";
|
||||||
if (path == VOICE) return "voice";
|
if (path == VOICE) return "voice";
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -86,6 +87,7 @@ class ERR_ProfileConfig
|
|||||||
if (section == "factions") return ERR_FactionSelection.Parse(json, roles);
|
if (section == "factions") return ERR_FactionSelection.Parse(json, roles);
|
||||||
if (section == "terrain") return ERR_TerrainPreference.Parse(json, scenario);
|
if (section == "terrain") return ERR_TerrainPreference.Parse(json, scenario);
|
||||||
if (section == "items") return ERR_ItemPreferences.Parse(json, records);
|
if (section == "items") return ERR_ItemPreferences.Parse(json, records);
|
||||||
|
if (section == "startBackpack") return ERR_StartBackpack.Parse(json, records);
|
||||||
string url;
|
string url;
|
||||||
return section == "voice" && json.ReadValue("welcomeUrl", url) && (url.IsEmpty() || ERR_WelcomeVoiceRules.URL(url));
|
return section == "voice" && json.ReadValue("welcomeUrl", url) && (url.IsEmpty() || ERR_WelcomeVoiceRules.URL(url));
|
||||||
}
|
}
|
||||||
@@ -95,6 +97,7 @@ class ERR_ProfileConfig
|
|||||||
if (section == "factions") return "{\"escapists\":\"us\",\"enemy\":\"ussr\",\"independent\":\"fia\"}";
|
if (section == "factions") return "{\"escapists\":\"us\",\"enemy\":\"ussr\",\"independent\":\"fia\"}";
|
||||||
if (section == "terrain") return "{\"scenario\":\"\"}";
|
if (section == "terrain") return "{\"scenario\":\"\"}";
|
||||||
if (section == "items") return "{\"version\":1,\"records\":[\"active\"]}";
|
if (section == "items") return "{\"version\":1,\"records\":[\"active\"]}";
|
||||||
|
if (section == "startBackpack") return "{\"version\":1,\"records\":[\"default\"]}";
|
||||||
if (section == "voice") return "{\"welcomeUrl\":\"\"}";
|
if (section == "voice") return "{\"welcomeUrl\":\"\"}";
|
||||||
JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false);
|
JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false);
|
||||||
foreach (string key : ERR_Settings.Keys())
|
foreach (string key : ERR_Settings.Keys())
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
// Immutable snapshots only: never tick native effects or alter a patient's zones.
|
||||||
|
// Seconds: >=0 = predicted wake, -1 = beyond horizon, -2 = unsupported/uncertain.
|
||||||
|
class ERR_RecoveryFlow
|
||||||
|
{
|
||||||
|
bool Passive;
|
||||||
|
float BloodRate;
|
||||||
|
float ResilienceRate;
|
||||||
|
float Delay;
|
||||||
|
float End = 301;
|
||||||
|
}
|
||||||
|
class ERR_RecoveryForecast
|
||||||
|
{
|
||||||
|
static bool KeepsRound(float forecast, bool selfRescue, bool existingRecovery)
|
||||||
|
{
|
||||||
|
return selfRescue || forecast >= 0 || (forecast == -2 && existingRecovery);
|
||||||
|
}
|
||||||
|
static float Horizon() { return 300; }
|
||||||
|
static bool Awake(float blood, float resilience, float bloodThreshold, float resilienceThreshold)
|
||||||
|
{
|
||||||
|
return blood > bloodThreshold && resilience > resilienceThreshold;
|
||||||
|
}
|
||||||
|
static float Overlap(float start, float end, float delay, float expires)
|
||||||
|
{
|
||||||
|
return Math.Max(0, Math.Min(end, expires) - Math.Max(start, delay));
|
||||||
|
}
|
||||||
|
static float Predict(float blood, float resilience, float bloodThreshold, float resilienceThreshold, array<ref ERR_RecoveryFlow> flows)
|
||||||
|
{
|
||||||
|
if (Awake(blood, resilience, bloodThreshold, resilienceThreshold)) return 0;
|
||||||
|
for (int second = 0; second < 300; second++)
|
||||||
|
{
|
||||||
|
// All flow starts/ends partition this second, including finite medicines.
|
||||||
|
float time = second;
|
||||||
|
while (time < second + 1)
|
||||||
|
{
|
||||||
|
float next = second + 1;
|
||||||
|
foreach (ERR_RecoveryFlow flow : flows)
|
||||||
|
{
|
||||||
|
if (flow.Delay > time && flow.Delay < next) next = flow.Delay;
|
||||||
|
if (flow.End > time && flow.End < next) next = flow.End;
|
||||||
|
}
|
||||||
|
float db, dr;
|
||||||
|
foreach (ERR_RecoveryFlow rate : flows)
|
||||||
|
{
|
||||||
|
float duration = Overlap(time, next, rate.Delay, rate.End);
|
||||||
|
db += rate.BloodRate * duration; dr += rate.ResilienceRate * duration;
|
||||||
|
}
|
||||||
|
blood = Math.Clamp(blood + db, 0, 1);
|
||||||
|
resilience = Math.Clamp(resilience + dr, 0, 1);
|
||||||
|
if (Awake(blood, resilience, bloodThreshold, resilienceThreshold)) return next;
|
||||||
|
time = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
static float Estimate(SCR_CharacterDamageManagerComponent damage)
|
||||||
|
{
|
||||||
|
if (!damage || !damage.ERR_IsRevivablePlayer()) return -2;
|
||||||
|
return damage.ERR_ForecastRecovery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modded class SCR_PassiveHitZoneRegenDamageEffect
|
||||||
|
{
|
||||||
|
float ERR_RemainingRegenDelay() { return Math.Max(0, m_fLocalRegenDelayTime - m_fDelayTimer); }
|
||||||
|
}
|
||||||
|
modded class SCR_CharacterDamageManagerComponent
|
||||||
|
{
|
||||||
|
float ERR_ForecastRecovery()
|
||||||
|
{
|
||||||
|
SCR_CharacterBloodHitZone blood = GetBloodHitZone();
|
||||||
|
SCR_CharacterResilienceHitZone resilience = GetResilienceHitZone();
|
||||||
|
if (!blood || !resilience || blood.GetMaxHealth() <= 0 || resilience.GetMaxHealth() <= 0) return -2;
|
||||||
|
if (m_aBeingHealedGroup && !m_aBeingHealedGroup.IsEmpty()) return -2;
|
||||||
|
array<ref ERR_RecoveryFlow> flows = {};
|
||||||
|
array<ref SCR_PersistentDamageEffect> effects = GetAllPersistentEffectsOfType(SCR_PersistentDamageEffect, true);
|
||||||
|
bool bloodChanges, passiveResilience, unknown, pausedNatural;
|
||||||
|
foreach (SCR_PersistentDamageEffect effect : effects)
|
||||||
|
{
|
||||||
|
DotDamageEffect dot = DotDamageEffect.Cast(effect);
|
||||||
|
if (!dot) { unknown = true; continue; }
|
||||||
|
HitZone zone = dot.GetAffectedHitZone();
|
||||||
|
typename kind = effect.Type();
|
||||||
|
bool bleeding = kind == SCR_BleedingDamageEffect;
|
||||||
|
bool passive = kind == SCR_PassiveHitZoneRegenDamageEffect;
|
||||||
|
bool saline = kind == SCR_SalineDamageEffect;
|
||||||
|
bool epi = kind == ERR_EpinephrineEffect;
|
||||||
|
// Physical wound regen and native morphine do not restore consciousness.
|
||||||
|
if (kind == SCR_PhysicalHitZonesRegenDamageEffect || kind == SCR_MorphineDamageEffect) continue;
|
||||||
|
if (passive && !effect.IsActive()) { pausedNatural = true; continue; }
|
||||||
|
if (!effect.IsActive()) { unknown = true; continue; }
|
||||||
|
if (!bleeding && !passive && !saline && !epi) { unknown = true; continue; }
|
||||||
|
if (!bleeding && zone != blood && zone != resilience) continue;
|
||||||
|
ERR_RecoveryFlow flow = new ERR_RecoveryFlow();
|
||||||
|
flow.Passive = passive;
|
||||||
|
float maximum = dot.GetMaxDuration();
|
||||||
|
if (maximum > 0) flow.End = Math.Max(0, maximum - dot.GetCurrentDuration());
|
||||||
|
HitZone target = zone;
|
||||||
|
if (bleeding) target = blood;
|
||||||
|
float rate = -dot.GetDPS() * target.GetBaseDamageMultiplier() * target.GetDamageMultiplier(dot.GetDamageType());
|
||||||
|
if (passive)
|
||||||
|
{
|
||||||
|
flow.Delay = SCR_PassiveHitZoneRegenDamageEffect.Cast(effect).ERR_RemainingRegenDelay();
|
||||||
|
// A terminated blood regen can still be visible until native cleanup.
|
||||||
|
if (IsBleeding()) rate = 0;
|
||||||
|
if (zone == resilience) passiveResilience = true;
|
||||||
|
}
|
||||||
|
if (bleeding)
|
||||||
|
{
|
||||||
|
SCR_CharacterHitZone wound = SCR_CharacterHitZone.Cast(zone);
|
||||||
|
if (!wound) { unknown = true; continue; }
|
||||||
|
if (GetGroupTourniquetted(wound.GetHitZoneGroup())) rate *= GetTourniquetStrengthMultiplier();
|
||||||
|
flow.BloodRate = rate / blood.GetMaxHealth();
|
||||||
|
// Expiring bleeding can create new delayed native regen: do not declare
|
||||||
|
// hopelessness based on a snapshot that cannot yet contain that effect.
|
||||||
|
if (flow.End <= 300) unknown = true;
|
||||||
|
}
|
||||||
|
else if (zone == blood) flow.BloodRate = rate / blood.GetMaxHealth();
|
||||||
|
else flow.ResilienceRate = rate / resilience.GetMaxHealth();
|
||||||
|
if (flow.BloodRate != 0) bloodChanges = true;
|
||||||
|
flows.Insert(flow);
|
||||||
|
}
|
||||||
|
// Native treatment suppresses passive resilience regeneration while the
|
||||||
|
// medicine acts. Do not add both DPS values as simultaneous recovery.
|
||||||
|
bool treatmentOverlap;
|
||||||
|
foreach (ERR_RecoveryFlow regen : flows)
|
||||||
|
{
|
||||||
|
if (!regen.Passive || regen.ResilienceRate <= 0) continue;
|
||||||
|
foreach (ERR_RecoveryFlow medicine : flows)
|
||||||
|
{
|
||||||
|
if (medicine.Passive || medicine.ResilienceRate <= 0) continue;
|
||||||
|
regen.Delay = Math.Max(regen.Delay, medicine.End);
|
||||||
|
treatmentOverlap = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float bt = blood.GetDamageStateThreshold(ECharacterBloodState.UNCONSCIOUS);
|
||||||
|
float rt = resilience.GetDamageStateThreshold(ECharacterResilienceState.WEAKENED);
|
||||||
|
float predicted = ERR_RecoveryForecast.Predict(blood.GetHealthScaled(), resilience.GetHealthScaled(), bt, rt, flows);
|
||||||
|
if (unknown || (pausedNatural && predicted < 0)) return -2;
|
||||||
|
// If the estimate needs post-treatment passive recovery, the native delay
|
||||||
|
// reset may move that start later. Retain safeguards instead of inventing it.
|
||||||
|
if (treatmentOverlap)
|
||||||
|
foreach (ERR_RecoveryFlow treatment : flows)
|
||||||
|
if (!treatment.Passive && treatment.ResilienceRate > 0 && (predicted < 0 || predicted > treatment.End)) return -2;
|
||||||
|
// Native resilience rates depend on changing blood-state bands. A snapshot's
|
||||||
|
// DPS cannot establish that future rate. Use an optimistic blood-only bound
|
||||||
|
// to prove impossibility; otherwise defer rather than kill a viable casualty.
|
||||||
|
if (bloodChanges && passiveResilience)
|
||||||
|
{
|
||||||
|
float optimistic = ERR_RecoveryForecast.Predict(blood.GetHealthScaled(), 1, bt, rt, flows);
|
||||||
|
if (optimistic < 0) return -1;
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
return predicted;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ class ERR_RecoveryService
|
|||||||
protected float m_NextMedicalLog;
|
protected float m_NextMedicalLog;
|
||||||
protected string m_MedicalDetails;
|
protected string m_MedicalDetails;
|
||||||
protected bool m_DefeatLogged;
|
protected bool m_DefeatLogged;
|
||||||
|
protected float m_NextForecast;
|
||||||
protected ref ScriptInvoker m_OnChanged = new ScriptInvoker();
|
protected ref ScriptInvoker m_OnChanged = new ScriptInvoker();
|
||||||
void ERR_RecoveryService(ERR_GameMode mode) { m_Mode = mode; }
|
void ERR_RecoveryService(ERR_GameMode mode) { m_Mode = mode; }
|
||||||
ScriptInvoker OnChanged() { return m_OnChanged; }
|
ScriptInvoker OnChanged() { return m_OnChanged; }
|
||||||
@@ -55,7 +56,7 @@ class ERR_RecoveryService
|
|||||||
if (due && !m_DefeatLogged)
|
if (due && !m_DefeatLogged)
|
||||||
{
|
{
|
||||||
m_DefeatLogged = true;
|
m_DefeatLogged = true;
|
||||||
ERR_HQ.Event("MEDICAL DEFEAT", "30 seconds without a viable rescuer or recovery; " + m_MedicalDetails);
|
ERR_HQ.Event("MEDICAL DEFEAT", "30 seconds without a viable rescuer, self-injection or recovery within five minutes; " + m_MedicalDetails);
|
||||||
}
|
}
|
||||||
return due;
|
return due;
|
||||||
}
|
}
|
||||||
@@ -70,9 +71,11 @@ class ERR_RecoveryService
|
|||||||
{
|
{
|
||||||
if (!Replication.IsServer() || m_Stopped || !m_Mode || !m_Mode.Active()) return;
|
if (!Replication.IsServer() || m_Stopped || !m_Mode || !m_Mode.Active()) return;
|
||||||
float now = GetGame().GetWorld().GetWorldTime();
|
float now = GetGame().GetWorld().GetWorldTime();
|
||||||
|
if (now >= m_NextForecast - 1000 && now < m_NextForecast) return;
|
||||||
|
m_NextForecast = now + 1000;
|
||||||
bool logMedical = now >= m_NextMedicalLog;
|
bool logMedical = now >= m_NextMedicalLog;
|
||||||
string medicalDetails;
|
string medicalDetails;
|
||||||
int conscious, incapacitated, reserved, reservedRescuers, recovering;
|
int conscious, incapacitated, reserved, reservedRescuers, recovering, selfRescuers, uncertain;
|
||||||
map<IEntity, ref ERR_RecoverySample> nextSamples = new map<IEntity, ref ERR_RecoverySample>();
|
map<IEntity, ref ERR_RecoverySample> nextSamples = new map<IEntity, ref ERR_RecoverySample>();
|
||||||
array<ref ERR_PlayerLife> lives = {};
|
array<ref ERR_PlayerLife> lives = {};
|
||||||
m_Mode.Players().Participants(lives);
|
m_Mode.Players().Participants(lives);
|
||||||
@@ -99,7 +102,12 @@ class ERR_RecoveryService
|
|||||||
bool improving = previous && (sample.Blood > previous.Blood + 0.001 || sample.Resilience > previous.Resilience + 0.001);
|
bool improving = previous && (sample.Blood > previous.Blood + 0.001 || sample.Resilience > previous.Resilience + 0.001);
|
||||||
bool treatment = medical.ERR_TreatmentInProgress();
|
bool treatment = medical.ERR_TreatmentInProgress();
|
||||||
bool natural = medical.ERR_CanRecoverNaturally();
|
bool natural = medical.ERR_CanRecoverNaturally();
|
||||||
if (improving || treatment || natural) recovering++;
|
float forecast = ERR_RecoveryForecast.Estimate(medical);
|
||||||
|
bool selfRescue = life.Connected && controlled == life.Actor && m_Mode.Settings().Get("selfEpinephrine") == 1 && ERR_SelfEpinephrineInventory.FindUsable(life.Actor);
|
||||||
|
if (selfRescue) selfRescuers++;
|
||||||
|
if (forecast == -2) uncertain++;
|
||||||
|
if (ERR_RecoveryForecast.KeepsRound(forecast, selfRescue, improving || treatment || natural)) recovering++;
|
||||||
|
medicalDetails += string.Format("forecastSeconds=%1 selfInjection=%2; ", forecast, selfRescue);
|
||||||
medicalDetails += string.Format("player=%1 connected=%2 blood=%3 resilience=%4 bleeding=%5 improving=%6 treatment=%7 naturalRecovery=%8; ", life.ConnectionId, life.Connected, sample.Blood, sample.Resilience, medical.IsBleeding(), improving, treatment, natural);
|
medicalDetails += string.Format("player=%1 connected=%2 blood=%3 resilience=%4 bleeding=%5 improving=%6 treatment=%7 naturalRecovery=%8; ", life.ConnectionId, life.Connected, sample.Blood, sample.Resilience, medical.IsBleeding(), improving, treatment, natural);
|
||||||
nextSamples.Insert(life.Actor, sample);
|
nextSamples.Insert(life.Actor, sample);
|
||||||
}
|
}
|
||||||
@@ -122,9 +130,11 @@ class ERR_RecoveryService
|
|||||||
{
|
{
|
||||||
message = "No conscious prisoner is currently available to help.";
|
message = "No conscious prisoner is currently available to help.";
|
||||||
if (evacuating) message += " The rescue crew is carrying all living prisoners to safety; evacuation continues.";
|
if (evacuating) message += " The rescue crew is carrying all living prisoners to safety; evacuation continues.";
|
||||||
else if (recovering > 0) message += " Treatment or natural recovery is possible; the round continues.";
|
else if (selfRescuers > 0) message += " Self epinephrine is available. Press Space to use your carried injector; the round continues.";
|
||||||
|
else if (recovering > 0 && uncertain > 0) message += " Recovery timing is uncertain; existing treatment and recovery safeguards remain active.";
|
||||||
|
else if (recovering > 0) message += " A prisoner is expected to wake within five minutes under current medical conditions.";
|
||||||
else if (reservedRescuers > 0) message += " A conscious prisoner's reconnection may provide help.";
|
else if (reservedRescuers > 0) message += " A conscious prisoner's reconnection may provide help.";
|
||||||
else message += " Everyone is unconscious with no treatment or recovery progressing. The mission fails after 30 uninterrupted seconds in this state.";
|
else message += " Nobody is expected to wake within five minutes and no self-injection is available. The mission fails after 30 uninterrupted seconds in this state.";
|
||||||
}
|
}
|
||||||
Publish(message);
|
Publish(message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
// Round-local statistics are always available, independently of telemetry.
|
||||||
|
// Only authoritative participant bodies own records; transient connection IDs
|
||||||
|
// and native winner-ID lists never decide kill credit or escape results.
|
||||||
|
class ERR_RoundPlayerStats
|
||||||
|
{
|
||||||
|
UUID Account;
|
||||||
|
string Name;
|
||||||
|
IEntity Actor;
|
||||||
|
bool Connected;
|
||||||
|
bool Down;
|
||||||
|
int Kills;
|
||||||
|
int FriendlyKills;
|
||||||
|
int Deaths;
|
||||||
|
int Downed;
|
||||||
|
int Recoveries;
|
||||||
|
}
|
||||||
|
class ERR_RoundStatistics
|
||||||
|
{
|
||||||
|
protected ERR_GameMode m_Mode;
|
||||||
|
protected ref array<ref ERR_RoundPlayerStats> m_Players = {};
|
||||||
|
protected bool m_Finished;
|
||||||
|
protected bool m_Truncated;
|
||||||
|
protected int m_StartedAt = -1;
|
||||||
|
void ERR_RoundStatistics(ERR_GameMode mode) { m_Mode = mode; }
|
||||||
|
ERR_RoundPlayerStats Find(UUID account)
|
||||||
|
{
|
||||||
|
foreach (ERR_RoundPlayerStats row : m_Players) if (row.Account == account) return row;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
protected ERR_RoundPlayerStats Body(IEntity actor)
|
||||||
|
{
|
||||||
|
if (!actor) return null;
|
||||||
|
foreach (ERR_RoundPlayerStats row : m_Players) if (row.Actor == actor) return row;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
static string DisplayName(string name)
|
||||||
|
{
|
||||||
|
name.Replace("\n", " "); name.Replace("\r", " "); name.Replace("\t", " ");
|
||||||
|
if (name.Length() > 48) name = name.Substring(0, 48);
|
||||||
|
if (name.IsEmpty()) return "Player";
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
void RosterChanged(int playerId) { Sync(); }
|
||||||
|
void Sync()
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer() || m_Finished || !m_Mode || !m_Mode.Players()) return;
|
||||||
|
array<ref ERR_PlayerLife> lives = {}; m_Mode.Players().Participants(lives);
|
||||||
|
foreach (ERR_PlayerLife life : lives)
|
||||||
|
{
|
||||||
|
ERR_RoundPlayerStats row = Find(life.AccountId);
|
||||||
|
if (!row)
|
||||||
|
{
|
||||||
|
if (m_Players.Count() >= 128) { m_Truncated = true; continue; }
|
||||||
|
row = new ERR_RoundPlayerStats(); row.Account = life.AccountId; m_Players.Insert(row);
|
||||||
|
}
|
||||||
|
row.Connected = life.Connected;
|
||||||
|
if (life.Connected) row.Name = DisplayName(GetGame().GetPlayerManager().GetPlayerName(life.ConnectionId));
|
||||||
|
if (row.Actor != life.Actor) { row.Actor = life.Actor; row.Down = false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A consciousness edge is a down/recovery, not a death. Repeated callbacks
|
||||||
|
// cannot count the same incapacitation twice; corpse deletion is not a kill.
|
||||||
|
static void Consciousness(ERR_RoundPlayerStats row, bool conscious)
|
||||||
|
{
|
||||||
|
if (!row) return;
|
||||||
|
if (!conscious && !row.Down) row.Downed++;
|
||||||
|
if (conscious && row.Down) row.Recoveries++;
|
||||||
|
row.Down = !conscious;
|
||||||
|
}
|
||||||
|
void ConsciousnessChanged(IEntity actor, bool conscious)
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer() || m_Finished || !m_Mode || !m_Mode.Active() || !ERR_Actors.Alive(actor)) return;
|
||||||
|
Sync(); Consciousness(Body(actor), conscious);
|
||||||
|
}
|
||||||
|
static void CountDeath(ERR_RoundPlayerStats victim, ERR_RoundPlayerStats killer, bool friendly, bool credit)
|
||||||
|
{
|
||||||
|
if (victim) { victim.Deaths++; victim.Down = false; }
|
||||||
|
if (!credit || !killer || victim == killer) return;
|
||||||
|
if (friendly) killer.FriendlyKills++;
|
||||||
|
else killer.Kills++;
|
||||||
|
}
|
||||||
|
void Killed(IEntity victim, IEntity killerEntity, Instigator instigator)
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer() || m_Finished || !m_Mode || !m_Mode.Active() || !ChimeraCharacter.Cast(victim)) return;
|
||||||
|
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(victim.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||||
|
if (!damage || !damage.IsDestroyed()) return;
|
||||||
|
Sync();
|
||||||
|
ERR_RoundPlayerStats killed = Body(victim);
|
||||||
|
// Native damage normally provides the original instigator body, including
|
||||||
|
// delayed explosives. Never recover credit from a possibly recycled player ID.
|
||||||
|
IEntity source = killerEntity;
|
||||||
|
if (!ChimeraCharacter.Cast(source) && instigator) source = instigator.GetInstigatorEntity();
|
||||||
|
ERR_RoundPlayerStats killer = Body(source);
|
||||||
|
if (!killed && !killer) return;
|
||||||
|
if (!damage.ERR_ClaimRoundDeath()) return;
|
||||||
|
bool credit = instigator && instigator.GetInstigatorType() != InstigatorType.INSTIGATOR_GM;
|
||||||
|
bool friendly = killed != null;
|
||||||
|
Faction faction = SCR_FactionManager.SGetFaction(victim);
|
||||||
|
if (faction && faction == m_Mode.PlayerFaction()) friendly = true;
|
||||||
|
CountDeath(killed, killer, friendly, credit);
|
||||||
|
}
|
||||||
|
void PhaseChanged(ERR_Phase previous, ERR_Phase current)
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer() || m_Finished) return;
|
||||||
|
if (m_StartedAt < 0 && current >= ERR_Phase.PRISON && current <= ERR_Phase.EXTRACTION) m_StartedAt = System.GetTickCount();
|
||||||
|
if (ERR_MissionRules.Terminal(current)) Finish();
|
||||||
|
}
|
||||||
|
protected string Outcome(ERR_RoundPlayerStats row)
|
||||||
|
{
|
||||||
|
if (m_Mode.Extraction() && m_Mode.Extraction().DidEscape(row.Account)) return "Escaped";
|
||||||
|
if (row.Deaths > 0 && !ERR_Actors.Alive(row.Actor)) return "Dead";
|
||||||
|
if (!row.Connected) return "Disconnected - did not escape";
|
||||||
|
if (row.Down) return "Down - did not escape";
|
||||||
|
return "Did not escape";
|
||||||
|
}
|
||||||
|
void Finish()
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer() || m_Finished || !m_Mode) return;
|
||||||
|
Sync(); m_Finished = true;
|
||||||
|
int seconds;
|
||||||
|
if (m_StartedAt >= 0) seconds = Math.Max(0, (System.GetTickCount() - m_StartedAt) / 1000);
|
||||||
|
int kills, friendly, deaths, downed, recovered, escaped;
|
||||||
|
string rows;
|
||||||
|
foreach (ERR_RoundPlayerStats row : m_Players)
|
||||||
|
{
|
||||||
|
kills += row.Kills; friendly += row.FriendlyKills; deaths += row.Deaths; downed += row.Downed; recovered += row.Recoveries;
|
||||||
|
if (m_Mode.Extraction() && m_Mode.Extraction().DidEscape(row.Account)) escaped++;
|
||||||
|
if (!rows.IsEmpty()) rows += "\n\n";
|
||||||
|
rows += DisplayName(row.Name) + " - " + Outcome(row) + "\n";
|
||||||
|
rows += string.Format("Kills: %1 Friendly kills: %2 Deaths: %3 Downed: %4 Recoveries: %5", row.Kills, row.FriendlyKills, row.Deaths, row.Downed, row.Recoveries);
|
||||||
|
}
|
||||||
|
string summary = string.Format("Duration: %1m %2s Participants: %3 Escaped: %4\n", seconds / 60, Math.Mod(seconds, 60), m_Players.Count(), escaped);
|
||||||
|
summary += string.Format("Total kills: %1 Friendly kills: %2 Deaths: %3 Downed: %4 Recoveries: %5\n\n", kills, friendly, deaths, downed, recovered);
|
||||||
|
summary += "Downed counts unconscious episodes; recoveries include self-recovery. Deaths count fatal losses.\n\n";
|
||||||
|
if (rows.IsEmpty()) rows = "No players participated in this round.";
|
||||||
|
summary += rows;
|
||||||
|
if (m_Truncated) summary += "\n\nOnly the first 128 participants are shown; totals cover those players.";
|
||||||
|
m_Mode.ERR_PublishRoundStatistics(summary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modded class SCR_CharacterDamageManagerComponent
|
||||||
|
{
|
||||||
|
protected bool m_ERRRoundDeathCounted;
|
||||||
|
bool ERR_ClaimRoundDeath()
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer() || m_ERRRoundDeathCounted) return false;
|
||||||
|
m_ERRRoundDeathCounted = true; return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modded class SCR_CharacterControllerComponent
|
||||||
|
{
|
||||||
|
override void OnConsciousnessChanged(bool conscious)
|
||||||
|
{
|
||||||
|
super.OnConsciousnessChanged(conscious);
|
||||||
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
|
if (mode && mode.ERR_Statistics()) mode.ERR_Statistics().ConsciousnessChanged(GetOwner(), conscious);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
modded class ERR_GameMode
|
||||||
|
{
|
||||||
|
protected ref ERR_RoundStatistics m_ERRStatistics;
|
||||||
|
[RplProp(onRplName: "ERR_OnRoundStatisticsReplicated")]
|
||||||
|
protected string m_ERRRoundSummary;
|
||||||
|
[RplProp(onRplName: "ERR_OnRoundStatisticsReplicated")]
|
||||||
|
protected string m_ERRCommunitySummary = "Community map records unavailable.";
|
||||||
|
protected ref ScriptInvoker m_ERRStatisticsChanged = new ScriptInvoker();
|
||||||
|
ERR_RoundStatistics ERR_Statistics() { return m_ERRStatistics; }
|
||||||
|
string ERR_RoundSummary()
|
||||||
|
{
|
||||||
|
if (m_ERRRoundSummary.IsEmpty()) return "";
|
||||||
|
return m_ERRCommunitySummary + "\n\n" + m_ERRRoundSummary;
|
||||||
|
}
|
||||||
|
void ERR_PublishCommunityStatistics(string summary)
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer() || summary.Length() > 2048 || m_ERRCommunitySummary == summary) return;
|
||||||
|
m_ERRCommunitySummary = summary;
|
||||||
|
Replication.BumpMe(); ERR_OnRoundStatisticsReplicated();
|
||||||
|
}
|
||||||
|
ScriptInvoker ERR_OnStatisticsChanged() { return m_ERRStatisticsChanged; }
|
||||||
|
protected void ERR_OnRoundStatisticsReplicated() { m_ERRStatisticsChanged.Invoke(); }
|
||||||
|
void ERR_PublishRoundStatistics(string summary)
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer() || !m_ERRRoundSummary.IsEmpty()) return;
|
||||||
|
m_ERRRoundSummary = summary;
|
||||||
|
Replication.BumpMe(); ERR_OnRoundStatisticsReplicated();
|
||||||
|
}
|
||||||
|
protected override void EOnInit(IEntity owner)
|
||||||
|
{
|
||||||
|
super.EOnInit(owner);
|
||||||
|
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !Players()) return;
|
||||||
|
m_ERRStatistics = new ERR_RoundStatistics(this);
|
||||||
|
OnPhaseChanged().Insert(m_ERRStatistics.PhaseChanged);
|
||||||
|
Players().OnChanged().Insert(m_ERRStatistics.RosterChanged);
|
||||||
|
}
|
||||||
|
protected override void OnControllableDestroyed(IEntity entity, IEntity killerEntity, notnull Instigator instigator)
|
||||||
|
{
|
||||||
|
if (m_ERRStatistics) m_ERRStatistics.Killed(entity, killerEntity, instigator);
|
||||||
|
super.OnControllableDestroyed(entity, killerEntity, instigator);
|
||||||
|
}
|
||||||
|
protected override void OnPlayerKilled(int playerId, IEntity playerEntity, IEntity killerEntity, notnull Instigator killer)
|
||||||
|
{
|
||||||
|
if (m_ERRStatistics) m_ERRStatistics.Killed(playerEntity, killerEntity, killer);
|
||||||
|
super.OnPlayerKilled(playerId, playerEntity, killerEntity, killer);
|
||||||
|
}
|
||||||
|
protected override void OnGameModeEnd(SCR_GameModeEndData endData)
|
||||||
|
{
|
||||||
|
// Freeze before native shutdown can detach bodies or remove roster ownership.
|
||||||
|
if (m_ERRStatistics) m_ERRStatistics.Finish();
|
||||||
|
super.OnGameModeEnd(endData);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,7 +64,7 @@ class ERR_ScenarioComponent : ScriptComponent
|
|||||||
#ifdef WORKBENCH
|
#ifdef WORKBENCH
|
||||||
string historicalFixture;
|
string historicalFixture;
|
||||||
if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "normal-escape-route" && historicalFixture != "crash-start" && historicalFixture != "mounted-ammo" && historicalFixture != "alarm-hearing" && historicalFixture != "optional-site-skip" && historicalFixture != "infantry-combat" && historicalFixture != "site-garrisons" && historicalFixture != "generated-sites" && historicalFixture != "roadblocks" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
|
if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "normal-escape-route" && historicalFixture != "crash-start" && historicalFixture != "mounted-ammo" && historicalFixture != "alarm-hearing" && historicalFixture != "optional-site-skip" && historicalFixture != "infantry-combat" && historicalFixture != "site-garrisons" && historicalFixture != "generated-sites" && historicalFixture != "roadblocks" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
|
||||||
if (historicalFixture != "self-epinephrine") m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
|
if (historicalFixture != "sarka-hunters" && historicalFixture != "self-epinephrine" && historicalFixture != "unconscious-camera" && historicalFixture != "starter-backpack" && historicalFixture != "objective-map" && historicalFixture != "speech-download" && historicalFixture != "bundled-speech") m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
|
||||||
#endif
|
#endif
|
||||||
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
|
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
|
||||||
if (!m_RuntimeDefinition || !m_RuntimeDefinition.Valid()) { m_Failed = true; return ERR_PrepareResult.FAILED; }
|
if (!m_RuntimeDefinition || !m_RuntimeDefinition.Valid()) { m_Failed = true; return ERR_PrepareResult.FAILED; }
|
||||||
@@ -72,7 +72,7 @@ class ERR_ScenarioComponent : ScriptComponent
|
|||||||
// Existing deterministic probes intentionally use authored coordinates.
|
// Existing deterministic probes intentionally use authored coordinates.
|
||||||
string fixture;
|
string fixture;
|
||||||
if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "normal-escape-route" && fixture != "crash-start" && fixture != "mounted-ammo" && fixture != "alarm-hearing" && fixture != "optional-site-skip" && fixture != "infantry-combat" && fixture != "site-garrisons" && fixture != "generated-sites" && fixture != "roadblocks" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
|
if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "normal-escape-route" && fixture != "crash-start" && fixture != "mounted-ammo" && fixture != "alarm-hearing" && fixture != "optional-site-skip" && fixture != "infantry-combat" && fixture != "site-garrisons" && fixture != "generated-sites" && fixture != "roadblocks" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
|
||||||
if (fixture != "self-epinephrine") foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
|
if (fixture != "sarka-hunters" && fixture != "self-epinephrine" && fixture != "unconscious-camera" && fixture != "starter-backpack" && fixture != "objective-map" && fixture != "speech-download" && fixture != "bundled-speech") foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
|
||||||
if (!ERR_ConfigurePatrolHQFixture(fixture)) { m_Failed = true; return ERR_PrepareResult.FAILED; }
|
if (!ERR_ConfigurePatrolHQFixture(fixture)) { m_Failed = true; return ERR_PrepareResult.FAILED; }
|
||||||
#endif
|
#endif
|
||||||
if (m_Progressive && !m_RuntimeDefinition.ConfigurePopulation(m_Definition, mode.Settings().Get("fieldCampCount"), mode.Settings().Get("fieldHospitalCount")))
|
if (m_Progressive && !m_RuntimeDefinition.ConfigurePopulation(m_Definition, mode.Settings().Get("fieldCampCount"), mode.Settings().Get("fieldHospitalCount")))
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ class ERR_SelectionStyle
|
|||||||
static void Action(SCR_ButtonTextComponent button, bool enable)
|
static void Action(SCR_ButtonTextComponent button, bool enable)
|
||||||
{
|
{
|
||||||
if (!button) return;
|
if (!button) return;
|
||||||
Color normal = Color.FromInt(0xFF8F2929), hover = Color.FromInt(0xFFB43B3B);
|
Color normal = Color.FromInt(ERR_MenuTheme.NEGATIVE), hover = Color.FromInt(ERR_MenuTheme.NEGATIVE_HOVER);
|
||||||
if (enable) { normal = Color.FromInt(0xFF246B3C); hover = Color.FromInt(0xFF31894F); }
|
if (enable) { normal = Color.FromInt(ERR_MenuTheme.POSITIVE); hover = Color.FromInt(ERR_MenuTheme.POSITIVE_HOVER); }
|
||||||
button.SetBackgroundColors(normal, hover, normal, hover, hover);
|
button.SetBackgroundColors(normal, hover, normal, hover, hover);
|
||||||
button.ColorizeBackground(false);
|
button.ColorizeBackground(false);
|
||||||
}
|
}
|
||||||
@@ -14,11 +14,11 @@ class ERR_SelectionStyle
|
|||||||
static void Choice(SCR_ButtonTextComponent button, bool selected, bool positive = true)
|
static void Choice(SCR_ButtonTextComponent button, bool selected, bool positive = true)
|
||||||
{
|
{
|
||||||
if (!button) return;
|
if (!button) return;
|
||||||
Color normal = Color.FromInt(0xFF454B52), hover = Color.FromInt(0xFF606870);
|
Color normal = Color.FromInt(ERR_MenuTheme.NEUTRAL), hover = Color.FromInt(ERR_MenuTheme.NEUTRAL_HOVER);
|
||||||
if (selected)
|
if (selected)
|
||||||
{
|
{
|
||||||
normal = Color.FromInt(0xFF246B3C); hover = Color.FromInt(0xFF31894F);
|
normal = Color.FromInt(ERR_MenuTheme.POSITIVE); hover = Color.FromInt(ERR_MenuTheme.POSITIVE_HOVER);
|
||||||
if (!positive) { normal = Color.FromInt(0xFF8F2929); hover = Color.FromInt(0xFFB43B3B); }
|
if (!positive) { normal = Color.FromInt(ERR_MenuTheme.NEGATIVE); hover = Color.FromInt(ERR_MenuTheme.NEGATIVE_HOVER); }
|
||||||
}
|
}
|
||||||
button.SetDisabledOpacity(1);
|
button.SetDisabledOpacity(1);
|
||||||
button.SetBackgroundColors(normal, hover, normal, hover, hover);
|
button.SetBackgroundColors(normal, hover, normal, hover, hover);
|
||||||
@@ -34,10 +34,10 @@ class ERR_SelectionStyle
|
|||||||
{
|
{
|
||||||
SCR_ButtonTextComponent badge = SCR_ButtonTextComponent.GetButtonText(name, parent);
|
SCR_ButtonTextComponent badge = SCR_ButtonTextComponent.GetButtonText(name, parent);
|
||||||
if (!badge) return;
|
if (!badge) return;
|
||||||
string label = "Waiting..."; Color color = Color.FromInt(0xFF454B52);
|
string label = "Waiting..."; Color color = Color.FromInt(ERR_MenuTheme.NEUTRAL);
|
||||||
if (state == 0) { label = "Disabled"; color = Color.FromInt(0xFF8F2929); }
|
if (state == 0) { label = "Disabled"; color = Color.FromInt(ERR_MenuTheme.NEGATIVE); }
|
||||||
if (state == 1) { label = "Enabled"; color = Color.FromInt(0xFF246B3C); }
|
if (state == 1) { label = "Enabled"; color = Color.FromInt(ERR_MenuTheme.POSITIVE); }
|
||||||
if (state == 2) { label = "Mixed"; color = Color.FromInt(0xFF805B17); }
|
if (state == 2) { label = "Mixed"; color = Color.FromInt(ERR_MenuTheme.MIXED); }
|
||||||
badge.SetText(label); badge.SetDisabledOpacity(1); badge.SetEnabled(false, false);
|
badge.SetText(label); badge.SetDisabledOpacity(1); badge.SetEnabled(false, false);
|
||||||
badge.SetBackgroundColors(color, color, color, color, color); badge.ColorizeBackground(false);
|
badge.SetBackgroundColors(color, color, color, color, color); badge.ColorizeBackground(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,10 +51,34 @@ modded class SCR_PlayerController
|
|||||||
if (mode && mode.Players()) life = mode.Players().Get(GetPlayerId());
|
if (mode && mode.Players()) life = mode.Players().Get(GetPlayerId());
|
||||||
if (!mode || !ERR_SelfEpinephrineRules.Allowed(mode.Active(), mode.Settings().Get("selfEpinephrine") == 1,
|
if (!mode || !ERR_SelfEpinephrineRules.Allowed(mode.Active(), mode.Settings().Get("selfEpinephrine") == 1,
|
||||||
life && life.Actor == actor && mode.Players().CurrentActor(actor) == life, ERR_Actors.Alive(actor), controller && controller.IsUnconscious())) return "";
|
life && life.Actor == actor && mode.Players().CurrentActor(actor) == life, ERR_Actors.Alive(actor), controller && controller.IsUnconscious())) return "";
|
||||||
|
IEntity injector = ERR_SelfEpinephrineInventory.FindUsable(actor);
|
||||||
|
if (!injector) return "No usable epinephrine injector in your inventory, or treatment is already active.";
|
||||||
|
SCR_ConsumableItemComponent item = SCR_ConsumableItemComponent.Cast(injector.FindComponent(SCR_ConsumableItemComponent));
|
||||||
|
SCR_ConsumableMorphine effect = SCR_ConsumableMorphine.Cast(item.GetConsumableEffect());
|
||||||
|
item.ApplyItemEffect(actor, actor, effect.GetAnimationParameters(injector, actor), injector, effect.GetDeleteOnUse());
|
||||||
|
return "Epinephrine administered. Its normal medical effect is active.";
|
||||||
|
}
|
||||||
|
|
||||||
|
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
|
||||||
|
protected void ERR_SelfEpinephrineResponse(string message)
|
||||||
|
{
|
||||||
|
if (this != GetGame().GetPlayerController()) return;
|
||||||
|
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
|
||||||
|
if (hints) hints.ShowCustom(message, "Self epinephrine", 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared read-only eligibility: defeat prediction and the actual Space action
|
||||||
|
// must agree, including native duplicate-dose rejection and item preference.
|
||||||
|
class ERR_SelfEpinephrineInventory
|
||||||
|
{
|
||||||
|
static IEntity FindUsable(IEntity actor)
|
||||||
|
{
|
||||||
|
if (!ERR_Actors.Alive(actor) || ERR_Actors.CanFight(actor)) return null;
|
||||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
|
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
|
||||||
array<IEntity> items = {};
|
array<IEntity> items = {};
|
||||||
if (inventory) inventory.GetItems(items);
|
if (inventory) inventory.GetItems(items);
|
||||||
if (items.Count() > 512) return "Unable to inspect your medical supplies.";
|
if (items.Count() > 512) return null;
|
||||||
// Prefer the standard injector; preserve each item's ordinary medical effect.
|
// Prefer the standard injector; preserve each item's ordinary medical effect.
|
||||||
for (int kind = 0; kind < 2; kind++)
|
for (int kind = 0; kind < 2; kind++)
|
||||||
foreach (IEntity injector : items)
|
foreach (IEntity injector : items)
|
||||||
@@ -68,18 +92,8 @@ modded class SCR_PlayerController
|
|||||||
if (kind == 1 && !ERR_ConsumableCombatEpinephrine.Cast(effect)) continue;
|
if (kind == 1 && !ERR_ConsumableCombatEpinephrine.Cast(effect)) continue;
|
||||||
SCR_EConsumableFailReason reason;
|
SCR_EConsumableFailReason reason;
|
||||||
if (!effect.CanApplyEffect(actor, actor, reason)) continue;
|
if (!effect.CanApplyEffect(actor, actor, reason)) continue;
|
||||||
// An unconscious player cannot perform the standing injection animation.
|
return injector;
|
||||||
// Use the native effect/consumption path on this exact carried item once.
|
|
||||||
item.ApplyItemEffect(actor, actor, effect.GetAnimationParameters(injector, actor), injector, effect.GetDeleteOnUse());
|
|
||||||
return "Epinephrine administered. Its normal medical effect is active.";
|
|
||||||
}
|
}
|
||||||
return "No usable epinephrine injector in your inventory, or treatment is already active.";
|
return null;
|
||||||
}
|
|
||||||
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
|
|
||||||
protected void ERR_SelfEpinephrineResponse(string message)
|
|
||||||
{
|
|
||||||
if (this != GetGame().GetPlayerController()) return;
|
|
||||||
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
|
|
||||||
if (hints) hints.ShowCustom(message, "Self epinephrine", 6);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class ERR_ServerItemCatalog
|
|||||||
foreach (ERR_ServerAddonItems addon : m_Addons) if (addon.Id == id) return addon;
|
foreach (ERR_ServerAddonItems addon : m_Addons) if (addon.Id == id) return addon;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
ERR_ServerItem Item(string key) { return m_Seen.Get(key); }
|
||||||
bool Contains(string addonId, string itemId)
|
bool Contains(string addonId, string itemId)
|
||||||
{
|
{
|
||||||
ERR_ServerAddonItems addon = Find(addonId);
|
ERR_ServerAddonItems addon = Find(addonId);
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ modded class ERR_AdminMenu
|
|||||||
if (button.GetRootWidget() != row.FindAnyWidget("Enable")) continue;
|
if (button.GetRootWidget() != row.FindAnyWidget("Enable")) continue;
|
||||||
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("ItemsMod", GetRootWidget());
|
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("ItemsMod", GetRootWidget());
|
||||||
if (combo) combo.SetCurrentItem(m_ServerAddonKeys.Find(addon), false, false, false);
|
if (combo) combo.SetCurrentItem(m_ServerAddonKeys.Find(addon), false, false, false);
|
||||||
ShowPage(17); SubmitItems("items list " + addon + " 0"); return;
|
ShowPage(ERR_AdminPage.ALLOWED_ITEMS); SubmitItems("items list " + addon + " 0"); return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected void SubmitItems(string command)
|
protected void SubmitItems(string command)
|
||||||
|
|||||||
@@ -9,13 +9,17 @@ class ERR_Settings
|
|||||||
|
|
||||||
static array<string> Keys()
|
static array<string> Keys()
|
||||||
{
|
{
|
||||||
return {"aiDoctrine", "aiShadow", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "planeReconEnabled", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "startMode", "keepWeaponOnUnconscious", "selfEpinephrine", "restartOnEmpty", "emptyRestartHours", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "antiAirChancePercent", "antiPersonnelMineChancePercent", "automaticEquipmentChancePercent", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
|
return {"telemetryEnabled", "aiDoctrine", "aiShadow", "sarkaAngerPerCar", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "planeReconEnabled", "droneReconCount", "droneCooldownSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "largeRoadblockCount", "mainRoadWidthMetres", "civilianTrafficCount", "garageVehicleCount", "garageChancePercent", "garageRadiusMetres", "garageSpawnIntervalSeconds", "startMode", "keepWeaponOnUnconscious", "selfEpinephrine", "restartOnEmpty", "emptyRestartHours", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioIntelSiteCount", "showAllSites", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "antiAirChancePercent", "antiPersonnelMineChancePercent", "automaticEquipmentChancePercent", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool Valid(string key, int value)
|
static bool Valid(string key, int value)
|
||||||
{
|
{
|
||||||
if (key == "aiDoctrine" || key == "aiShadow") return value == 0 || value == 1;
|
if (key == "telemetryEnabled" || key == "aiDoctrine" || key == "aiShadow") return value == 0 || value == 1;
|
||||||
|
if (key == "radioIntelSiteCount") return value >= 0 && value <= 20;
|
||||||
|
if (key == "sarkaAngerPerCar") return value >= 0 && value <= 30;
|
||||||
if (key == "motorbikeCount") return value >= 0 && value <= 6;
|
if (key == "motorbikeCount") return value >= 0 && value <= 6;
|
||||||
|
if (key == "droneReconCount") return value >= 0 && value <= 3;
|
||||||
|
if (key == "droneCooldownSeconds") return value >= 60 && value <= 1800;
|
||||||
if (key == "planeAltitudeMetres") return value >= 150 && value <= 500;
|
if (key == "planeAltitudeMetres") return value >= 150 && value <= 500;
|
||||||
if (key == "planeReplacementSeconds") return value >= 30 && value <= 1800;
|
if (key == "planeReplacementSeconds") return value >= 30 && value <= 1800;
|
||||||
if (key == "planeSectorSeconds") return value >= 30 && value <= 600;
|
if (key == "planeSectorSeconds") return value >= 30 && value <= 600;
|
||||||
@@ -29,8 +33,15 @@ class ERR_Settings
|
|||||||
if (key == "objectGuardCount") return value >= 0 && value <= 16;
|
if (key == "objectGuardCount") return value >= 0 && value <= 16;
|
||||||
if (key == "hunterSpawnDistanceMetres") return value >= 800 && value <= 3000;
|
if (key == "hunterSpawnDistanceMetres") return value >= 800 && value <= 3000;
|
||||||
if (key == "roadblockCount") return value >= 0 && value <= 30;
|
if (key == "roadblockCount") return value >= 0 && value <= 30;
|
||||||
|
if (key == "largeRoadblockCount") return value >= 0 && value <= 30;
|
||||||
|
if (key == "mainRoadWidthMetres") return value >= 5 && value <= 12;
|
||||||
|
if (key == "garageChancePercent") return value >= 0 && value <= 100;
|
||||||
|
if (key == "garageRadiusMetres") return value >= 250 && value <= 1500;
|
||||||
|
if (key == "garageSpawnIntervalSeconds") return value >= 5 && value <= 60;
|
||||||
|
if (key == "garageVehicleCount") return value >= 0 && value <= 24;
|
||||||
if (key == "civilianTrafficCount") return value >= 0 && value <= 6;
|
if (key == "civilianTrafficCount") return value >= 0 && value <= 6;
|
||||||
if (key == "startMode") return value == 0 || value == 1;
|
if (key == "startMode") return value == 0 || value == 1;
|
||||||
|
if (key == "showAllSites") return value == 0 || value == 1;
|
||||||
if (key == "selfEpinephrine" || key == "keepWeaponOnUnconscious" || key == "restartOnEmpty") return value == 0 || value == 1;
|
if (key == "selfEpinephrine" || key == "keepWeaponOnUnconscious" || key == "restartOnEmpty") return value == 0 || value == 1;
|
||||||
if (key == "emptyRestartHours") return value >= 0 && value <= 168;
|
if (key == "emptyRestartHours") return value >= 0 && value <= 168;
|
||||||
if (key == "heliTimerMinutes") return value >= 1 && value <= 1440;
|
if (key == "heliTimerMinutes") return value >= 1 && value <= 1440;
|
||||||
@@ -117,7 +128,11 @@ class ERR_Settings
|
|||||||
static int AddedDefault(string key)
|
static int AddedDefault(string key)
|
||||||
{
|
{
|
||||||
if (key == "aiDoctrine" || key == "aiShadow") return 0;
|
if (key == "aiDoctrine" || key == "aiShadow") return 0;
|
||||||
|
if (key == "radioIntelSiteCount") return 5;
|
||||||
|
if (key == "sarkaAngerPerCar") return 3;
|
||||||
if (key == "motorbikeCount") return 2;
|
if (key == "motorbikeCount") return 2;
|
||||||
|
if (key == "droneReconCount") return 0;
|
||||||
|
if (key == "droneCooldownSeconds") return 300;
|
||||||
if (key == "planeAltitudeMetres") return 250;
|
if (key == "planeAltitudeMetres") return 250;
|
||||||
if (key == "planeReplacementSeconds") return 60;
|
if (key == "planeReplacementSeconds") return 60;
|
||||||
if (key == "planeSectorSeconds") return 120;
|
if (key == "planeSectorSeconds") return 120;
|
||||||
@@ -131,9 +146,16 @@ class ERR_Settings
|
|||||||
if (key == "objectGuardCount") return 8;
|
if (key == "objectGuardCount") return 8;
|
||||||
if (key == "hunterSpawnDistanceMetres") return 1000;
|
if (key == "hunterSpawnDistanceMetres") return 1000;
|
||||||
if (key == "roadblockCount") return 4;
|
if (key == "roadblockCount") return 4;
|
||||||
|
if (key == "largeRoadblockCount") return 4;
|
||||||
|
if (key == "mainRoadWidthMetres") return 6;
|
||||||
if (key == "civilianTrafficCount") return 3;
|
if (key == "civilianTrafficCount") return 3;
|
||||||
|
if (key == "garageChancePercent") return 70;
|
||||||
|
if (key == "garageRadiusMetres") return 1000;
|
||||||
|
if (key == "garageSpawnIntervalSeconds") return 5;
|
||||||
|
if (key == "garageVehicleCount") return 16;
|
||||||
|
if (key == "showAllSites") return 0;
|
||||||
if (key == "startMode" || key == "selfEpinephrine") return 0;
|
if (key == "startMode" || key == "selfEpinephrine") return 0;
|
||||||
if (key == "keepWeaponOnUnconscious" || key == "restartOnEmpty") return 1;
|
if (key == "telemetryEnabled" || key == "keepWeaponOnUnconscious" || key == "restartOnEmpty") return 1;
|
||||||
if (key == "emptyRestartHours") return 5;
|
if (key == "emptyRestartHours") return 5;
|
||||||
if (key == "heliTimerMinutes") return 15;
|
if (key == "heliTimerMinutes") return 15;
|
||||||
if (key == "convoyCount") return 3;
|
if (key == "convoyCount") return 3;
|
||||||
@@ -150,9 +172,17 @@ class ERR_Settings
|
|||||||
void ERR_Settings(string path = PATH)
|
void ERR_Settings(string path = PATH)
|
||||||
{
|
{
|
||||||
m_Path = path;
|
m_Path = path;
|
||||||
|
m_Values.Set("telemetryEnabled", AddedDefault("telemetryEnabled"));
|
||||||
m_Values.Set("aiDoctrine", AddedDefault("aiDoctrine"));
|
m_Values.Set("aiDoctrine", AddedDefault("aiDoctrine"));
|
||||||
m_Values.Set("aiShadow", AddedDefault("aiShadow"));
|
m_Values.Set("aiShadow", AddedDefault("aiShadow"));
|
||||||
|
m_Values.Set("sarkaAngerPerCar", AddedDefault("sarkaAngerPerCar"));
|
||||||
|
m_Values.Set("garageChancePercent", AddedDefault("garageChancePercent"));
|
||||||
|
m_Values.Set("garageRadiusMetres", AddedDefault("garageRadiusMetres"));
|
||||||
|
m_Values.Set("garageSpawnIntervalSeconds", AddedDefault("garageSpawnIntervalSeconds"));
|
||||||
|
m_Values.Set("garageVehicleCount", AddedDefault("garageVehicleCount"));
|
||||||
m_Values.Set("motorbikeCount", AddedDefault("motorbikeCount"));
|
m_Values.Set("motorbikeCount", AddedDefault("motorbikeCount"));
|
||||||
|
m_Values.Set("droneReconCount", AddedDefault("droneReconCount"));
|
||||||
|
m_Values.Set("droneCooldownSeconds", AddedDefault("droneCooldownSeconds"));
|
||||||
m_Values.Set("planeAltitudeMetres", AddedDefault("planeAltitudeMetres"));
|
m_Values.Set("planeAltitudeMetres", AddedDefault("planeAltitudeMetres"));
|
||||||
m_Values.Set("planeReplacementSeconds", AddedDefault("planeReplacementSeconds"));
|
m_Values.Set("planeReplacementSeconds", AddedDefault("planeReplacementSeconds"));
|
||||||
m_Values.Set("planeSectorSeconds", AddedDefault("planeSectorSeconds"));
|
m_Values.Set("planeSectorSeconds", AddedDefault("planeSectorSeconds"));
|
||||||
@@ -166,6 +196,8 @@ class ERR_Settings
|
|||||||
m_Values.Set("objectGuardHQMode", AddedDefault("objectGuardHQMode"));
|
m_Values.Set("objectGuardHQMode", AddedDefault("objectGuardHQMode"));
|
||||||
m_Values.Set("hunterSpawnDistanceMetres", AddedDefault("hunterSpawnDistanceMetres"));
|
m_Values.Set("hunterSpawnDistanceMetres", AddedDefault("hunterSpawnDistanceMetres"));
|
||||||
m_Values.Set("roadblockCount", AddedDefault("roadblockCount"));
|
m_Values.Set("roadblockCount", AddedDefault("roadblockCount"));
|
||||||
|
m_Values.Set("largeRoadblockCount", AddedDefault("largeRoadblockCount"));
|
||||||
|
m_Values.Set("mainRoadWidthMetres", AddedDefault("mainRoadWidthMetres"));
|
||||||
m_Values.Set("civilianTrafficCount", AddedDefault("civilianTrafficCount"));
|
m_Values.Set("civilianTrafficCount", AddedDefault("civilianTrafficCount"));
|
||||||
m_Values.Set("startMode", AddedDefault("startMode"));
|
m_Values.Set("startMode", AddedDefault("startMode"));
|
||||||
m_Values.Set("keepWeaponOnUnconscious", AddedDefault("keepWeaponOnUnconscious"));
|
m_Values.Set("keepWeaponOnUnconscious", AddedDefault("keepWeaponOnUnconscious"));
|
||||||
@@ -177,6 +209,8 @@ class ERR_Settings
|
|||||||
m_Values.Set("responseRadiusMetres", 2000);
|
m_Values.Set("responseRadiusMetres", 2000);
|
||||||
m_Values.Set("aiIntelChancePercent", 60);
|
m_Values.Set("aiIntelChancePercent", 60);
|
||||||
m_Values.Set("intelRevealChancePercent", 15);
|
m_Values.Set("intelRevealChancePercent", 15);
|
||||||
|
m_Values.Set("radioIntelSiteCount", 5);
|
||||||
|
m_Values.Set("showAllSites", AddedDefault("showAllSites"));
|
||||||
m_Values.Set("radioDefenceSeconds", 180);
|
m_Values.Set("radioDefenceSeconds", 180);
|
||||||
m_Values.Set("startTimeMinutes", AddedDefault("startTimeMinutes"));
|
m_Values.Set("startTimeMinutes", AddedDefault("startTimeMinutes"));
|
||||||
m_Values.Set("timeMultiplier", AddedDefault("timeMultiplier"));
|
m_Values.Set("timeMultiplier", AddedDefault("timeMultiplier"));
|
||||||
@@ -211,6 +245,19 @@ class ERR_Settings
|
|||||||
values = new map<string, int>();
|
values = new map<string, int>();
|
||||||
foreach (string key : Keys())
|
foreach (string key : Keys())
|
||||||
{
|
{
|
||||||
|
// The former multiplier cannot express a lower frequency. Keep disabled
|
||||||
|
// saves disabled; migrate enabled saves to the new three-points-per-car default.
|
||||||
|
// An explicit new setting always takes precedence over the legacy key.
|
||||||
|
if (key == "sarkaAngerPerCar" && !json.DoesKeyExist(key) && json.DoesKeyExist("sarkaCarsPerAnger"))
|
||||||
|
{
|
||||||
|
float oldCars;
|
||||||
|
if (!json.ReadValue("sarkaCarsPerAnger", oldCars)) return false;
|
||||||
|
int oldMultiplier = Math.Round(oldCars);
|
||||||
|
if (oldCars != oldMultiplier || oldMultiplier < 0 || oldMultiplier > 5) return false;
|
||||||
|
int points = AddedDefault(key);
|
||||||
|
if (oldMultiplier == 0) points = 0;
|
||||||
|
values.Set(key, points); continue;
|
||||||
|
}
|
||||||
// Migrate the former one-in-N setting to the nearest integer percent.
|
// Migrate the former one-in-N setting to the nearest integer percent.
|
||||||
if (key == "antiAirChancePercent" && !json.DoesKeyExist(key) && json.DoesKeyExist("stingerChanceOneIn"))
|
if (key == "antiAirChancePercent" && !json.DoesKeyExist(key) && json.DoesKeyExist("stingerChanceOneIn"))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
[BaseContainerProps()]
|
[BaseContainerProps()]
|
||||||
class ERR_SiteStock
|
class ERR_SiteStock
|
||||||
{
|
{
|
||||||
|
// Runtime starting-loadout choice; authored objective stock remains in storage.
|
||||||
|
bool OnGround;
|
||||||
[Attribute("", UIWidgets.ResourceNamePicker, "Physical inventory item", "et")]
|
[Attribute("", UIWidgets.ResourceNamePicker, "Physical inventory item", "et")]
|
||||||
ResourceName Prefab;
|
ResourceName Prefab;
|
||||||
[Attribute("1", UIWidgets.EditBox, "Finite initial count")]
|
[Attribute("1", UIWidgets.EditBox, "Finite initial count")]
|
||||||
@@ -59,7 +61,7 @@ class ERR_SiteDefinition
|
|||||||
ResourceName Prefab;
|
ResourceName Prefab;
|
||||||
[Attribute("0 0 0", UIWidgets.Coords)]
|
[Attribute("0 0 0", UIWidgets.Coords)]
|
||||||
vector Position;
|
vector Position;
|
||||||
[Attribute("0", UIWidgets.CheckBox, "Choose dry clear terrain once per round (prison, radio, mortar, hospital, depot or camp)")]
|
[Attribute("0", UIWidgets.CheckBox, "Choose dry clear terrain once per round (prison, extraction, radio, mortar, hospital, depot or camp)")]
|
||||||
bool RandomClear;
|
bool RandomClear;
|
||||||
[Attribute("0", UIWidgets.EditBox, "Composition heading in degrees")]
|
[Attribute("0", UIWidgets.EditBox, "Composition heading in degrees")]
|
||||||
float Heading;
|
float Heading;
|
||||||
@@ -183,7 +185,7 @@ class ERR_ScenarioDefinition
|
|||||||
if (!site || site.Id.IsEmpty() || ids.Contains(site.Id) || site.Prefab.IsEmpty()) return false;
|
if (!site || site.Id.IsEmpty() || ids.Contains(site.Id) || site.Prefab.IsEmpty()) return false;
|
||||||
if (site.RequireAirfield && site.Kind != ERR_LocationKind.EXTRACTION) return false;
|
if (site.RequireAirfield && site.Kind != ERR_LocationKind.EXTRACTION) return false;
|
||||||
if (site.Kind == ERR_LocationKind.RADIO && !site.Guarded && site.Guards.IsEmpty()) return false;
|
if (site.Kind == ERR_LocationKind.RADIO && !site.Guarded && site.Guards.IsEmpty()) return false;
|
||||||
if (site.RandomClear && site.Kind != ERR_LocationKind.PRISON && site.Kind != ERR_LocationKind.RADIO && site.Kind != ERR_LocationKind.MORTAR && site.Kind != ERR_LocationKind.HOSPITAL && site.Kind != ERR_LocationKind.OUTPOST && site.Kind != ERR_LocationKind.DEPOT) return false;
|
if (site.RandomClear && site.Kind != ERR_LocationKind.EXTRACTION && site.Kind != ERR_LocationKind.PRISON && site.Kind != ERR_LocationKind.RADIO && site.Kind != ERR_LocationKind.MORTAR && site.Kind != ERR_LocationKind.HOSPITAL && site.Kind != ERR_LocationKind.OUTPOST && site.Kind != ERR_LocationKind.DEPOT) return false;
|
||||||
if (site.Label.IsEmpty() || site.Label.Length() > 64) return false;
|
if (site.Label.IsEmpty() || site.Label.Length() > 64) return false;
|
||||||
if (!ERR_SiteDiscoveryRules.ValidRadius(site.Kind, site.DiscoverRadius)) return false;
|
if (!ERR_SiteDiscoveryRules.ValidRadius(site.Kind, site.DiscoverRadius)) return false;
|
||||||
bool optionalSite = site.Kind == ERR_LocationKind.DEPOT || site.Kind == ERR_LocationKind.OUTPOST || site.Kind == ERR_LocationKind.HOSPITAL;
|
bool optionalSite = site.Kind == ERR_LocationKind.DEPOT || site.Kind == ERR_LocationKind.OUTPOST || site.Kind == ERR_LocationKind.HOSPITAL;
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ class ERR_StockPlacement
|
|||||||
IEntity Item;
|
IEntity Item;
|
||||||
InventoryStorageManagerComponent Inventory;
|
InventoryStorageManagerComponent Inventory;
|
||||||
bool Accepted;
|
bool Accepted;
|
||||||
|
bool Optional;
|
||||||
|
bool Skipped;
|
||||||
|
bool OnGround;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ERR_SiteRuntime
|
class ERR_SiteRuntime
|
||||||
@@ -387,7 +390,7 @@ class ERR_SiteService
|
|||||||
if (only && spec != only) continue;
|
if (only && spec != only) continue;
|
||||||
if (startupOnly && !ERR_GeneratedSites.StartupSite(spec)) continue;
|
if (startupOnly && !ERR_GeneratedSites.StartupSite(spec)) continue;
|
||||||
vector position = ERR_Actors.Ground(spec.Position);
|
vector position = ERR_Actors.Ground(spec.Position);
|
||||||
if (position[1] < 1) return Fail("site below sea level: " + spec.Id);
|
if (!ERR_Actors.DryGround(position)) return Fail("site ground is submerged or invalid: " + spec.Id);
|
||||||
vector rootPosition = position;
|
vector rootPosition = position;
|
||||||
if (spec.PlacementResolved && spec.Kind == ERR_LocationKind.PRISON) rootPosition = spec.Position;
|
if (spec.PlacementResolved && spec.Kind == ERR_LocationKind.PRISON) rootPosition = spec.Position;
|
||||||
rootPosition[1] = rootPosition[1] + spec.RootHeight;
|
rootPosition[1] = rootPosition[1] + spec.RootHeight;
|
||||||
@@ -395,7 +398,7 @@ class ERR_SiteService
|
|||||||
{
|
{
|
||||||
rootPosition = ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(spec.AircraftOffset, spec.Heading));
|
rootPosition = ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(spec.AircraftOffset, spec.Heading));
|
||||||
if (spec.RequireAirfield && !ERR_AirfieldPlacement.Clear(rootPosition)) return Fail("extraction airfield pad is missing or obstructed; no off-airfield fallback");
|
if (spec.RequireAirfield && !ERR_AirfieldPlacement.Clear(rootPosition)) return Fail("extraction airfield pad is missing or obstructed; no off-airfield fallback");
|
||||||
if (definition.TerrainGenerated && !spec.RequireAirfield && !ERR_AirfieldPlacement.ClearGround(rootPosition)) return Fail("generated extraction landing zone is obstructed");
|
if ((definition.TerrainGenerated || spec.RandomClear) && !spec.RequireAirfield && !ERR_AirfieldPlacement.ClearGround(rootPosition)) return Fail("generated extraction landing zone is obstructed");
|
||||||
}
|
}
|
||||||
// Spawn the replicated marker at the final pickup coordinate from the start.
|
// Spawn the replicated marker at the final pickup coordinate from the start.
|
||||||
IEntity root = ERR_Actors.Spawn(spec.Prefab, rootPosition, Vector(spec.Heading, 0, 0));
|
IEntity root = ERR_Actors.Spawn(spec.Prefab, rootPosition, Vector(spec.Heading, 0, 0));
|
||||||
@@ -428,14 +431,23 @@ class ERR_SiteService
|
|||||||
BaseInventoryStorageComponent storage = BaseInventoryStorageComponent.Cast(site.Container.FindComponent(BaseInventoryStorageComponent));
|
BaseInventoryStorageComponent storage = BaseInventoryStorageComponent.Cast(site.Container.FindComponent(BaseInventoryStorageComponent));
|
||||||
if (!storage) return Fail("container storage: " + spec.Id);
|
if (!storage) return Fail("container storage: " + spec.Id);
|
||||||
inventory.AddStorage(storage);
|
inventory.AddStorage(storage);
|
||||||
if (spec.Stock)
|
array<ref ERR_SiteStock> stockPlan = spec.Stock;
|
||||||
|
bool customStart = spec.Kind == ERR_LocationKind.PRISON && GetGame().ERR_StartingSupplies().Custom();
|
||||||
|
if (customStart)
|
||||||
{
|
{
|
||||||
foreach (ERR_SiteStock stock : spec.Stock)
|
stockPlan = {};
|
||||||
|
// Keep the route's shared map; replace default weapons/ammo with exact selections.
|
||||||
|
ERR_SiteStock mapStock = new ERR_SiteStock(); mapStock.Prefab = ERR_Supplies.Map(); mapStock.Count = 1; stockPlan.Insert(mapStock);
|
||||||
|
GetGame().ERR_StartingSupplies().Stock(stockPlan);
|
||||||
|
}
|
||||||
|
if (stockPlan)
|
||||||
|
{
|
||||||
|
foreach (ERR_SiteStock stock : stockPlan)
|
||||||
{
|
{
|
||||||
ResourceName resource = stock.Prefab;
|
ResourceName resource = stock.Prefab;
|
||||||
if (resource == ERR_Supplies.Map()) resource = fieldMap;
|
if (resource == ERR_Supplies.Map()) resource = fieldMap;
|
||||||
int quantity = stock.Count;
|
int quantity = stock.Count;
|
||||||
if (spec.Kind == ERR_LocationKind.PRISON)
|
if (spec.Kind == ERR_LocationKind.PRISON && !customStart)
|
||||||
{
|
{
|
||||||
if (stock.Prefab == ERR_Supplies.Pistol()) quantity = startWeapons;
|
if (stock.Prefab == ERR_Supplies.Pistol()) quantity = startWeapons;
|
||||||
else if (stock.Prefab == ERR_Supplies.Magazine()) quantity = startMagazines;
|
else if (stock.Prefab == ERR_Supplies.Magazine()) quantity = startMagazines;
|
||||||
@@ -444,6 +456,8 @@ class ERR_SiteService
|
|||||||
{
|
{
|
||||||
// Created exactly once; insertion retries always use this same entity.
|
// Created exactly once; insertion retries always use this same entity.
|
||||||
IEntity item = Spawn(resource, site.Container.GetOrigin());
|
IEntity item = Spawn(resource, site.Container.GetOrigin());
|
||||||
|
if (!item && customStart && stock.Prefab != ERR_Supplies.Map())
|
||||||
|
{ Print("Reintroduced [starting backpack]: unavailable item skipped: " + resource, LogLevel.WARNING); continue; }
|
||||||
if (!item) return Fail("stock resource: " + spec.Id);
|
if (!item) return Fail("stock resource: " + spec.Id);
|
||||||
if (!stock.TargetId.IsEmpty())
|
if (!stock.TargetId.IsEmpty())
|
||||||
{
|
{
|
||||||
@@ -459,6 +473,8 @@ class ERR_SiteService
|
|||||||
else if (stock.Prefab == "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et"
|
else if (stock.Prefab == "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et"
|
||||||
&& site.DepotMines.Count() < 2) site.DepotMines.Insert(item);
|
&& site.DepotMines.Count() < 2) site.DepotMines.Insert(item);
|
||||||
}
|
}
|
||||||
|
placement.Optional = customStart && stock.Prefab != ERR_Supplies.Map();
|
||||||
|
placement.OnGround = customStart && stock.OnGround;
|
||||||
placement.Item = item;
|
placement.Item = item;
|
||||||
placement.Inventory = inventory;
|
placement.Inventory = inventory;
|
||||||
m_Placements.Insert(placement);
|
m_Placements.Insert(placement);
|
||||||
@@ -614,6 +630,75 @@ class ERR_SiteService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional starting items stay usable when native inventory rejects their
|
||||||
|
// dimensions/volume/type. Keep the exact entity; do not spawn copies or delete it.
|
||||||
|
protected void LeaveStartingItem(ERR_StockPlacement placement)
|
||||||
|
{
|
||||||
|
InventoryItemComponent item = InventoryItemComponent.Cast(placement.Item.FindComponent(InventoryItemComponent));
|
||||||
|
// An accepted asynchronous move or an administrator may already own the item.
|
||||||
|
// Never pull it out of another storage while resolving starter overflow.
|
||||||
|
if (!item || !item.GetParentSlot())
|
||||||
|
{
|
||||||
|
vector origin = placement.Inventory.GetOwner().GetOrigin();
|
||||||
|
array<vector> offsets = {"1.2 0 0", "0 0 1.2", "-1.2 0 0", "0 0 -1.2", "0.9 0 0.9", "-0.9 0 0.9", "-0.9 0 -0.9", "0.9 0 -0.9"};
|
||||||
|
foreach (vector offset : offsets)
|
||||||
|
{
|
||||||
|
vector point = ERR_Actors.Ground(origin + offset);
|
||||||
|
if (!ERR_Actors.DryGround(point) || Math.AbsFloat(point[1] - origin[1]) > 0.5 || !BackpackClearance(point)) continue;
|
||||||
|
// Do not place supplies across a cell wall or inside the crash wreckage.
|
||||||
|
TraceParam access = new TraceParam(); access.Start = origin + "0 0.6 0"; access.End = point + "0 0.6 0";
|
||||||
|
access.Flags = TraceFlags.ENTS | TraceFlags.WORLD; access.Exclude = placement.Inventory.GetOwner();
|
||||||
|
if (GetGame().GetWorld().TraceMove(access, null) < 1) continue;
|
||||||
|
placement.Item.SetOrigin(point + "0 0.2 0");
|
||||||
|
Physics physics = placement.Item.GetPhysics();
|
||||||
|
if (physics && physics.IsDynamic()) physics.SetActive(ActiveState.ACTIVE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
string resource = "unknown item";
|
||||||
|
if (placement.Item.GetPrefabData()) resource = placement.Item.GetPrefabData().GetPrefabName();
|
||||||
|
if (!placement.OnGround) Print("Reintroduced [starting backpack]: inventory rejected item; retained as starting-area loot: " + resource, LogLevel.WARNING);
|
||||||
|
placement.Skipped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ERR_PrepareResult PrepareStock()
|
||||||
|
{
|
||||||
|
int pending, accepted;
|
||||||
|
string firstRejected;
|
||||||
|
foreach (ERR_StockPlacement placement : m_Placements)
|
||||||
|
{
|
||||||
|
if (placement.Skipped) continue;
|
||||||
|
if (!placement.Item || !placement.Inventory)
|
||||||
|
{
|
||||||
|
if (placement.Optional) { placement.Skipped = true; Print("Reintroduced [starting backpack]: optional item disappeared before insertion", LogLevel.WARNING); continue; }
|
||||||
|
Fail("stock disappeared during initialization"); return ERR_PrepareResult.FAILED;
|
||||||
|
}
|
||||||
|
if (placement.OnGround) { LeaveStartingItem(placement); continue; }
|
||||||
|
array<IEntity> items = {};
|
||||||
|
placement.Inventory.GetItems(items);
|
||||||
|
if (items.Contains(placement.Item)) continue;
|
||||||
|
bool expired = GetGame().GetWorld().GetWorldTime() > m_Deadline;
|
||||||
|
if (placement.Optional && (expired || (!placement.Accepted && !placement.Inventory.CanInsertItem(placement.Item))))
|
||||||
|
{
|
||||||
|
LeaveStartingItem(placement); continue;
|
||||||
|
}
|
||||||
|
pending++;
|
||||||
|
if (firstRejected.IsEmpty() && placement.Item.GetPrefabData()) firstRejected = placement.Item.GetPrefabData().GetPrefabName();
|
||||||
|
if (placement.Accepted) accepted++;
|
||||||
|
if (!placement.Accepted) placement.Accepted = placement.Inventory.TryInsertItem(placement.Item);
|
||||||
|
}
|
||||||
|
if (pending)
|
||||||
|
{
|
||||||
|
if (GetGame().GetWorld().GetWorldTime() > m_Deadline)
|
||||||
|
{
|
||||||
|
Fail(string.Format("physical stock insertion timed out: pending=%1 accepted=%2 sample=%3", pending, accepted, firstRejected));
|
||||||
|
return ERR_PrepareResult.FAILED;
|
||||||
|
}
|
||||||
|
return ERR_PrepareResult.PENDING;
|
||||||
|
}
|
||||||
|
return ERR_PrepareResult.READY;
|
||||||
|
}
|
||||||
|
|
||||||
ERR_PrepareResult Prepare()
|
ERR_PrepareResult Prepare()
|
||||||
{
|
{
|
||||||
if (!Replication.IsServer() || !m_Begun || m_Stopped) return ERR_PrepareResult.FAILED;
|
if (!Replication.IsServer() || !m_Begun || m_Stopped) return ERR_PrepareResult.FAILED;
|
||||||
@@ -622,7 +707,7 @@ class ERR_SiteService
|
|||||||
ERR_PrepareResult aircraftPlacement = ERR_PrepareResult.READY;
|
ERR_PrepareResult aircraftPlacement = ERR_PrepareResult.READY;
|
||||||
if (m_ExtractionPlacement) aircraftPlacement = m_ExtractionPlacement.Poll(GetGame().GetWorld().GetWorldTime());
|
if (m_ExtractionPlacement) aircraftPlacement = m_ExtractionPlacement.Poll(GetGame().GetWorld().GetWorldTime());
|
||||||
if (aircraftPlacement == ERR_PrepareResult.FAILED) { Fail(m_ExtractionPlacement.FailureReason()); return m_State; }
|
if (aircraftPlacement == ERR_PrepareResult.FAILED) { Fail(m_ExtractionPlacement.FailureReason()); return m_State; }
|
||||||
int pending, accepted, pendingGuards;
|
int pendingGuards;
|
||||||
foreach (ERR_SiteRuntime site : m_Sites)
|
foreach (ERR_SiteRuntime site : m_Sites)
|
||||||
{
|
{
|
||||||
if (site.Definition.Transport && !site.Definition.Transport.Optional && !TransportReady(site))
|
if (site.Definition.Transport && !site.Definition.Transport.Optional && !TransportReady(site))
|
||||||
@@ -640,23 +725,7 @@ class ERR_SiteService
|
|||||||
if (mobile == ERR_PrepareResult.PENDING) pendingGuards++;
|
if (mobile == ERR_PrepareResult.PENDING) pendingGuards++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
string firstRejected;
|
if (PrepareStock() != ERR_PrepareResult.READY) return m_State;
|
||||||
foreach (ERR_StockPlacement placement : m_Placements)
|
|
||||||
{
|
|
||||||
if (!placement.Item || !placement.Inventory) { Fail("stock disappeared during initialization"); return m_State; }
|
|
||||||
array<IEntity> items = {};
|
|
||||||
placement.Inventory.GetItems(items);
|
|
||||||
if (items.Contains(placement.Item)) continue;
|
|
||||||
pending++;
|
|
||||||
if (firstRejected.IsEmpty() && placement.Item.GetPrefabData()) firstRejected = placement.Item.GetPrefabData().GetPrefabName();
|
|
||||||
if (placement.Accepted) accepted++;
|
|
||||||
if (!placement.Accepted) placement.Accepted = placement.Inventory.TryInsertItem(placement.Item);
|
|
||||||
}
|
|
||||||
if (pending)
|
|
||||||
{
|
|
||||||
if (GetGame().GetWorld().GetWorldTime() > m_Deadline) Fail(string.Format("physical stock insertion timed out: pending=%1 accepted=%2 sample=%3", pending, accepted, firstRejected));
|
|
||||||
return m_State;
|
|
||||||
}
|
|
||||||
if (pendingGuards > 0 || aircraftPlacement == ERR_PrepareResult.PENDING) return m_State;
|
if (pendingGuards > 0 || aircraftPlacement == ERR_PrepareResult.PENDING) return m_State;
|
||||||
// All required scenery, stock and physical guards now exist. Survey optional
|
// All required scenery, stock and physical guards now exist. Survey optional
|
||||||
// parking once against that real population, then bound only vehicle settling.
|
// parking once against that real population, then bound only vehicle settling.
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ class ERR_SpawnPlacement
|
|||||||
foreach (int id, vector reserved : reservations)
|
foreach (int id, vector reserved : reservations)
|
||||||
if (id != ownReservation && vector.DistanceXZ(position, reserved) < 1.2 && Math.AbsFloat(position[1] - reserved[1]) < 2) return false;
|
if (id != ownReservation && vector.DistanceXZ(position, reserved) < 1.2 && Math.AbsFloat(position[1] - reserved[1]) < 2) return false;
|
||||||
vector ground = ERR_Actors.Ground(position);
|
vector ground = ERR_Actors.Ground(position);
|
||||||
if (ground[1] < 1 || Math.AbsFloat(position[1] - ground[1]) > 0.3) return false;
|
if (!ERR_Actors.DryGround(ground) || Math.AbsFloat(position[1] - ground[1]) > 0.3) return false;
|
||||||
// Feet need supported, reasonably level terrain across the character width.
|
// Feet need supported, reasonably level terrain across the character width.
|
||||||
for (int x = -1; x <= 1; x++)
|
for (int x = -1; x <= 1; x++)
|
||||||
for (int z = -1; z <= 1; z++)
|
for (int z = -1; z <= 1; z++)
|
||||||
{
|
{
|
||||||
vector sample = ERR_Actors.Ground(position + Vector(x * 0.45, 0, z * 0.45));
|
vector sample = ERR_Actors.Ground(position + Vector(x * 0.45, 0, z * 0.45));
|
||||||
if (Math.AbsFloat(sample[1] - ground[1]) > 0.3) return false;
|
if (!ERR_Actors.DryGround(sample) || Math.AbsFloat(sample[1] - ground[1]) > 0.3) return false;
|
||||||
}
|
}
|
||||||
m_Exclude = exclude;
|
m_Exclude = exclude;
|
||||||
m_Candidate = ground; m_Blocked = false;
|
m_Candidate = ground; m_Blocked = false;
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ modded class ERR_AdminSession
|
|||||||
if (!service) { SpeechReply("Speech service is unavailable in this mission."); return; }
|
if (!service) { SpeechReply("Speech service is unavailable in this mission."); return; }
|
||||||
if (action == 0) service.TestAndEnable(this, endpoint, secret);
|
if (action == 0) service.TestAndEnable(this, endpoint, secret);
|
||||||
else if (action == 1) service.Disable(this);
|
else if (action == 1) service.Disable(this);
|
||||||
|
else if (action == 2) service.SetRecordings(this, true);
|
||||||
|
else if (action == 3) service.SetRecordings(this, false);
|
||||||
}
|
}
|
||||||
void SpeechReply(string text)
|
void SpeechReply(string text)
|
||||||
{
|
{
|
||||||
@@ -53,10 +55,11 @@ modded class ERR_AdminMenu
|
|||||||
protected bool m_SpeechBusy;
|
protected bool m_SpeechBusy;
|
||||||
protected bool m_SpeechEnabled;
|
protected bool m_SpeechEnabled;
|
||||||
protected int m_SpeechSelection = -1;
|
protected int m_SpeechSelection = -1;
|
||||||
|
protected int m_RecordingsSelection = -1;
|
||||||
override void OnMenuOpen()
|
override void OnMenuOpen()
|
||||||
{
|
{
|
||||||
super.OnMenuOpen();
|
super.OnMenuOpen();
|
||||||
foreach (string name : {"TTSTest", "TTSDisable", "TTSPasteEndpoint", "TTSPasteToken"})
|
foreach (string name : {"TTSTest", "TTSDisable", "TTSPasteEndpoint", "TTSPasteToken", "RecordingsOn", "RecordingsOff"})
|
||||||
{
|
{
|
||||||
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText(name, GetRootWidget());
|
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText(name, GetRootWidget());
|
||||||
if (!button) { Close(); return; }
|
if (!button) { Close(); return; }
|
||||||
@@ -95,9 +98,12 @@ modded class ERR_AdminMenu
|
|||||||
super.UpdateActions();
|
super.UpdateActions();
|
||||||
bool ready = m_Ready && m_SpeechInitialized;
|
bool ready = m_Ready && m_SpeechInitialized;
|
||||||
ERR_SelectionStyle.Pair(GetRootWidget(), "TTSTest", "TTSDisable", m_SpeechSelection);
|
ERR_SelectionStyle.Pair(GetRootWidget(), "TTSTest", "TTSDisable", m_SpeechSelection);
|
||||||
|
ERR_SelectionStyle.Pair(GetRootWidget(), "RecordingsOn", "RecordingsOff", m_RecordingsSelection);
|
||||||
foreach (SCR_ButtonTextComponent button : m_SpeechButtons)
|
foreach (SCR_ButtonTextComponent button : m_SpeechButtons)
|
||||||
{
|
{
|
||||||
bool enabled = ready && !m_SpeechBusy;
|
bool enabled = ready && !m_SpeechBusy;
|
||||||
|
string name = button.GetRootWidget().GetName();
|
||||||
|
if (name == "RecordingsOn" || name == "RecordingsOff") enabled = ready;
|
||||||
if (button.GetRootWidget().GetName() == "TTSDisable") enabled = ready && (m_SpeechEnabled || m_SpeechBusy);
|
if (button.GetRootWidget().GetName() == "TTSDisable") enabled = ready && (m_SpeechEnabled || m_SpeechBusy);
|
||||||
button.SetEnabled(enabled);
|
button.SetEnabled(enabled);
|
||||||
}
|
}
|
||||||
@@ -111,12 +117,13 @@ modded class ERR_AdminMenu
|
|||||||
override void ReceiveSnapshot(bool authorized, array<string> values, array<string> status)
|
override void ReceiveSnapshot(bool authorized, array<string> values, array<string> status)
|
||||||
{
|
{
|
||||||
string endpoint, message, tokenSaved; bool ready, present;
|
string endpoint, message, tokenSaved; bool ready, present;
|
||||||
m_SpeechBusy = false; m_SpeechEnabled = false;
|
m_SpeechBusy = false; m_SpeechEnabled = false; m_RecordingsSelection = -1;
|
||||||
if (authorized && values)
|
if (authorized && values)
|
||||||
foreach (string entry : values)
|
foreach (string entry : values)
|
||||||
{
|
{
|
||||||
int divider = entry.IndexOf("="); if (divider < 0) continue;
|
int divider = entry.IndexOf("="); if (divider < 0) continue;
|
||||||
string key = entry.Substring(0, divider), value = entry.Substring(divider + 1, entry.Length() - divider - 1);
|
string key = entry.Substring(0, divider), value = entry.Substring(divider + 1, entry.Length() - divider - 1);
|
||||||
|
if (key == "recordingsEnabled") { m_RecordingsSelection = 0; if (value == "1") m_RecordingsSelection = 1; }
|
||||||
if (key == "ttsUrl") { endpoint = value; present = true; }
|
if (key == "ttsUrl") { endpoint = value; present = true; }
|
||||||
if (key == "ttsStatus") message = value;
|
if (key == "ttsStatus") message = value;
|
||||||
if (key == "ttsTokenSaved") tokenSaved = value;
|
if (key == "ttsTokenSaved") tokenSaved = value;
|
||||||
@@ -152,8 +159,16 @@ modded class ERR_AdminMenu
|
|||||||
if (!m_SpeechBusy) PasteSpeechField(name == "TTSPasteToken");
|
if (!m_SpeechBusy) PasteSpeechField(name == "TTSPasteToken");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (name == "RecordingsOn" || name == "RecordingsOff")
|
||||||
|
{
|
||||||
|
int recordingsAction = 3; if (name == "RecordingsOn") recordingsAction = 2;
|
||||||
|
m_ClickDelay = 0.3;
|
||||||
|
SetText("Feedback", "Saving prerecorded speech setting...");
|
||||||
|
m_Controller.ERR_AdminSpeech(recordingsAction, "", "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
int action;
|
int action;
|
||||||
if (button.GetRootWidget().GetName() == "TTSDisable") action = 1;
|
if (name == "TTSDisable") action = 1;
|
||||||
SCR_EditBoxComponent endpoint = SCR_EditBoxComponent.GetEditBoxComponent("TTSEndpoint", GetRootWidget());
|
SCR_EditBoxComponent endpoint = SCR_EditBoxComponent.GetEditBoxComponent("TTSEndpoint", GetRootWidget());
|
||||||
SCR_EditBoxComponent token = SCR_EditBoxComponent.GetEditBoxComponent("TTSToken", GetRootWidget());
|
SCR_EditBoxComponent token = SCR_EditBoxComponent.GetEditBoxComponent("TTSToken", GetRootWidget());
|
||||||
if (!endpoint || !token) return;
|
if (!endpoint || !token) return;
|
||||||
|
|||||||
@@ -77,10 +77,11 @@ class ERR_SpeechService
|
|||||||
protected string m_Endpoint;
|
protected string m_Endpoint;
|
||||||
protected string m_Token;
|
protected string m_Token;
|
||||||
protected bool m_Enabled;
|
protected bool m_Enabled;
|
||||||
|
protected bool m_Bundled = true;
|
||||||
protected bool m_Ready;
|
protected bool m_Ready;
|
||||||
protected bool m_BootAttempted;
|
protected bool m_BootAttempted;
|
||||||
protected bool m_Stopped;
|
protected bool m_Stopped;
|
||||||
protected string m_Status = "Not configured. Existing audio is unchanged.";
|
protected string m_Status = "Bundled recordings are available. Live speech is not configured.";
|
||||||
protected ref ERR_SpeechRequest m_Request;
|
protected ref ERR_SpeechRequest m_Request;
|
||||||
protected float m_Deadline;
|
protected float m_Deadline;
|
||||||
protected float m_LastTest = -60000;
|
protected float m_LastTest = -60000;
|
||||||
@@ -88,6 +89,7 @@ class ERR_SpeechService
|
|||||||
protected string m_StaticURL;
|
protected string m_StaticURL;
|
||||||
string CacheNamespace() { return m_Endpoint; }
|
string CacheNamespace() { return m_Endpoint; }
|
||||||
string StaticURL() { return m_StaticURL; }
|
string StaticURL() { return m_StaticURL; }
|
||||||
|
bool Bundled() { return m_Bundled; }
|
||||||
bool Ready() { return !m_Stopped && m_Enabled && m_Ready; }
|
bool Ready() { return !m_Stopped && m_Enabled && m_Ready; }
|
||||||
bool Busy() { return m_Request != null; }
|
bool Busy() { return m_Request != null; }
|
||||||
bool IdleFor(float milliseconds)
|
bool IdleFor(float milliseconds)
|
||||||
@@ -109,7 +111,12 @@ class ERR_SpeechService
|
|||||||
if (ERR_SpeechRules.Token(token)) m_Token = token;
|
if (ERR_SpeechRules.Token(token)) m_Token = token;
|
||||||
m_Enabled = !m_Endpoint.IsEmpty() && !m_Token.IsEmpty();
|
m_Enabled = !m_Endpoint.IsEmpty() && !m_Token.IsEmpty();
|
||||||
json.ReadValue("ttsEnabled", m_Enabled);
|
json.ReadValue("ttsEnabled", m_Enabled);
|
||||||
if (m_Enabled) m_Status = "Configured; waiting to verify the speech service. Existing audio remains available.";
|
json.ReadValue("bundledSpeech", m_Bundled);
|
||||||
|
bool recordingsEnabled = true;
|
||||||
|
json.ReadValue("recordingsEnabled", recordingsEnabled);
|
||||||
|
m_Mode.ERR_SetRecordingsEnabled(recordingsEnabled);
|
||||||
|
if (m_Bundled) m_Status = "Prerecorded welcome selected. Test & enable to use a live welcome briefing.";
|
||||||
|
else if (m_Enabled) m_Status = "Live welcome configured; waiting to verify the speech service.";
|
||||||
else if (!m_Endpoint.IsEmpty()) m_Status = "Disabled. Existing audio is unchanged.";
|
else if (!m_Endpoint.IsEmpty()) m_Status = "Disabled. Existing audio is unchanged.";
|
||||||
}
|
}
|
||||||
void Snapshot(array<string> values)
|
void Snapshot(array<string> values)
|
||||||
@@ -123,23 +130,37 @@ class ERR_SpeechService
|
|||||||
values.Insert("ttsTokenSaved=" + saved); values.Insert("ttsReady=" + ready);
|
values.Insert("ttsTokenSaved=" + saved); values.Insert("ttsReady=" + ready);
|
||||||
values.Insert("ttsBusy=" + busy); values.Insert("ttsEnabled=" + enabled);
|
values.Insert("ttsBusy=" + busy); values.Insert("ttsEnabled=" + enabled);
|
||||||
values.Insert("ttsStatus=" + m_Status);
|
values.Insert("ttsStatus=" + m_Status);
|
||||||
|
string recordings = "0"; if (m_Mode.ERR_RecordingsEnabled()) recordings = "1";
|
||||||
|
values.Insert("recordingsEnabled=" + recordings);
|
||||||
}
|
}
|
||||||
protected bool Save(string endpoint, string token, bool enabled)
|
protected bool Save(string endpoint, string token, bool enabled, bool recordingsEnabled, bool preserveWelcome = false)
|
||||||
{
|
{
|
||||||
JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false);
|
JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false);
|
||||||
json.WriteValue("welcomeUrl", m_StaticURL); json.WriteValue("briefingUrl", endpoint);
|
json.WriteValue("welcomeUrl", m_StaticURL); json.WriteValue("briefingUrl", endpoint);
|
||||||
json.WriteValue("briefingToken", token); json.WriteValue("ttsEnabled", enabled);
|
json.WriteValue("briefingToken", token); json.WriteValue("ttsEnabled", enabled);
|
||||||
|
bool bundled = !enabled; if (preserveWelcome) bundled = m_Bundled;
|
||||||
|
json.WriteValue("bundledSpeech", bundled);
|
||||||
|
json.WriteValue("recordingsEnabled", recordingsEnabled);
|
||||||
return ERR_ProfileConfig.Save(ERR_ProfileConfig.VOICE, json);
|
return ERR_ProfileConfig.Save(ERR_ProfileConfig.VOICE, json);
|
||||||
}
|
}
|
||||||
|
void SetRecordings(ERR_AdminSession editor, bool enabled)
|
||||||
|
{
|
||||||
|
if (!Current() || !editor || !editor.Valid()) return;
|
||||||
|
if (!Save(m_Endpoint, m_Token, m_Enabled, enabled, true))
|
||||||
|
{ editor.SpeechReply("Could not save. Previous speech settings retained."); return; }
|
||||||
|
m_Mode.ERR_SetRecordingsEnabled(enabled);
|
||||||
|
if (enabled) editor.SpeechReply("Prerecorded speech enabled for everyone.");
|
||||||
|
else editor.SpeechReply("Prerecorded speech disabled for everyone. Text messages remain visible.");
|
||||||
|
}
|
||||||
void Disable(ERR_AdminSession editor)
|
void Disable(ERR_AdminSession editor)
|
||||||
{
|
{
|
||||||
if (!Current() || !editor || !editor.Valid()) return;
|
if (!Current() || !editor || !editor.Valid()) return;
|
||||||
if (!Save(m_Endpoint, m_Token, false)) { editor.SpeechReply("Could not save. Previous speech settings retained."); return; }
|
if (!Save(m_Endpoint, m_Token, false, m_Mode.ERR_RecordingsEnabled())) { editor.SpeechReply("Could not save. Previous speech settings retained."); return; }
|
||||||
if (m_Request) m_Request.Cancel();
|
if (m_Request) m_Request.Cancel();
|
||||||
m_Request = null; m_Enabled = false; m_Ready = false; m_BootAttempted = false;
|
m_Request = null; m_Enabled = false; m_Bundled = true; m_Ready = false; m_BootAttempted = false;
|
||||||
m_Status = "Disabled. Existing audio is unchanged.";
|
m_Status = "Live TTS disabled. Prerecorded speech follows its On / Off setting.";
|
||||||
m_Mode.ERR_RestoreStaticWelcome();
|
m_Mode.ERR_RestoreStaticWelcome();
|
||||||
editor.SpeechReply("TTS disabled. The original welcome remains available.");
|
editor.SpeechReply("Live TTS disabled. Prerecorded speech follows its On / Off setting.");
|
||||||
}
|
}
|
||||||
void TestAndEnable(ERR_AdminSession editor, string endpoint, string token)
|
void TestAndEnable(ERR_AdminSession editor, string endpoint, string token)
|
||||||
{
|
{
|
||||||
@@ -188,6 +209,7 @@ class ERR_SpeechService
|
|||||||
ERR_SpeechRequest expired = m_Request;
|
ERR_SpeechRequest expired = m_Request;
|
||||||
Finished(expired, ""); expired.Cancel();
|
Finished(expired, ""); expired.Cancel();
|
||||||
}
|
}
|
||||||
|
if (m_Mode.ERR_UsesBundledWelcome()) return;
|
||||||
if (Busy() || !m_Enabled || m_BootAttempted || !m_Mode.Active() || !ERR_WelcomeVoiceRules.HTTPReady()) return;
|
if (Busy() || !m_Enabled || m_BootAttempted || !m_Mode.Active() || !ERR_WelcomeVoiceRules.HTTPReady()) return;
|
||||||
m_BootAttempted = true;
|
m_BootAttempted = true;
|
||||||
if (m_Endpoint.IsEmpty() || m_Token.IsEmpty()) { m_Status = "Incomplete settings. Existing audio is unchanged."; return; }
|
if (m_Endpoint.IsEmpty() || m_Token.IsEmpty()) { m_Status = "Incomplete settings. Existing audio is unchanged."; return; }
|
||||||
@@ -213,9 +235,9 @@ class ERR_SpeechService
|
|||||||
}
|
}
|
||||||
if (request.Enable)
|
if (request.Enable)
|
||||||
{
|
{
|
||||||
if (!Save(request.Endpoint, request.Token, true))
|
if (!Save(request.Endpoint, request.Token, true, m_Mode.ERR_RecordingsEnabled()))
|
||||||
{ m_Status = "Test passed, but settings could not be saved. Previous settings retained."; request.Editor.SpeechReply(m_Status); return; }
|
{ m_Status = "Test passed, but settings could not be saved. Previous settings retained."; request.Editor.SpeechReply(m_Status); return; }
|
||||||
m_Endpoint = request.Endpoint; m_Token = request.Token; m_Enabled = true; m_BootAttempted = true;
|
m_Endpoint = request.Endpoint; m_Token = request.Token; m_Enabled = true; m_Bundled = false; m_BootAttempted = true;
|
||||||
}
|
}
|
||||||
m_Ready = true; m_Status = "Enabled. Speech generation verified; repeated text uses the service cache.";
|
m_Ready = true; m_Status = "Enabled. Speech generation verified; repeated text uses the service cache.";
|
||||||
if (request.Enable || request.Boot) m_Mode.ERR_PublishWelcome(url, request.Text);
|
if (request.Enable || request.Boot) m_Mode.ERR_PublishWelcome(url, request.Text);
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
// Exact, finite shared starting supplies. Edits affect the next preparation only.
|
||||||
|
class ERR_StartBackpack
|
||||||
|
{
|
||||||
|
static const string PATH = "$profile:EscapistsStartBackpack.json";
|
||||||
|
protected ref array<string> m_Records = {"default"};
|
||||||
|
protected bool m_ReadOnly;
|
||||||
|
void ERR_StartBackpack()
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer()) { m_ReadOnly = true; return; }
|
||||||
|
array<string> loaded;
|
||||||
|
if (Read(PATH, loaded) || Read(PATH + ".bak", loaded)) m_Records = loaded;
|
||||||
|
else if (ERR_ProfileConfig.Exists(PATH) || ERR_ProfileConfig.Exists(PATH + ".bak")) m_ReadOnly = true;
|
||||||
|
}
|
||||||
|
static string Destination(array<string> parts)
|
||||||
|
{
|
||||||
|
if (parts.Count() == 3) return parts[2];
|
||||||
|
return "backpack";
|
||||||
|
}
|
||||||
|
static bool Valid(array<string> records)
|
||||||
|
{
|
||||||
|
if (!records || records.IsEmpty() || records.Count() > 33) return false;
|
||||||
|
if (records[0] != "default" && records[0] != "custom") return false;
|
||||||
|
set<string> seen = new set<string>(); int total;
|
||||||
|
for (int i = 1; i < records.Count(); i++)
|
||||||
|
{
|
||||||
|
array<string> parts = {}; records[i].Split("|", parts, false);
|
||||||
|
if ((parts.Count() != 2 && parts.Count() != 3) || parts[0].Length() > 512 || !parts[0].EndsWith(".et")) return false;
|
||||||
|
string key = ERR_TerrainCatalog.Key(parts[0]); int count;
|
||||||
|
string destination = Destination(parts);
|
||||||
|
if (destination != "backpack" && destination != "ground") return false;
|
||||||
|
if (!ERR_ItemPreferences.Guid(key) || seen.Contains(key + "|" + destination)) return false;
|
||||||
|
if (!ERR_CommandArguments.Integer(parts[1], count) || count < 1 || count > 48) return false;
|
||||||
|
total += count; if (total > 64) return false;
|
||||||
|
seen.Insert(key + "|" + destination);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static bool Parse(JsonLoadContext json, out array<string> records)
|
||||||
|
{
|
||||||
|
int version; records = {};
|
||||||
|
return json.ReadValue("version", version) && (version == 1 || version == 2) && json.ReadValue("records", records) && Valid(records);
|
||||||
|
}
|
||||||
|
protected bool Read(string path, out array<string> records)
|
||||||
|
{
|
||||||
|
JsonLoadContext json = new JsonLoadContext();
|
||||||
|
return ERR_ProfileConfig.Load(path, json) && Parse(json, records);
|
||||||
|
}
|
||||||
|
protected bool Save(array<string> records)
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer() || m_ReadOnly || !Valid(records)) return false;
|
||||||
|
JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false);
|
||||||
|
json.WriteValue("version", 2); json.WriteValue("records", records);
|
||||||
|
if (!ERR_ProfileConfig.Save(PATH, json)) return false;
|
||||||
|
m_Records = records; return true;
|
||||||
|
}
|
||||||
|
bool Custom() { return m_Records[0] == "custom"; }
|
||||||
|
bool SetMode(bool custom)
|
||||||
|
{
|
||||||
|
array<string> next = {}; next.Copy(m_Records);
|
||||||
|
next[0] = "default"; if (custom) next[0] = "custom";
|
||||||
|
return Save(next);
|
||||||
|
}
|
||||||
|
bool Set(ResourceName prefab, int quantity, string destination = "backpack")
|
||||||
|
{
|
||||||
|
if (quantity < 0 || quantity > 48 || (destination != "backpack" && destination != "ground")) return false;
|
||||||
|
string key = ERR_TerrainCatalog.Key(prefab);
|
||||||
|
if (!ERR_ItemPreferences.Guid(key)) return false;
|
||||||
|
array<string> next = {m_Records[0]};
|
||||||
|
for (int i = 1; i < m_Records.Count(); i++)
|
||||||
|
{
|
||||||
|
array<string> parts = {}; m_Records[i].Split("|", parts, false);
|
||||||
|
if (ERR_TerrainCatalog.Key(parts[0]) != key || Destination(parts) != destination) next.Insert(m_Records[i]);
|
||||||
|
}
|
||||||
|
if (quantity > 0) next.Insert(prefab + "|" + quantity.ToString() + "|" + destination);
|
||||||
|
return Save(next);
|
||||||
|
}
|
||||||
|
bool Remove(string key, string destination = "backpack")
|
||||||
|
{
|
||||||
|
for (int i = 1; i < m_Records.Count(); i++)
|
||||||
|
{
|
||||||
|
array<string> parts = {}; m_Records[i].Split("|", parts, false);
|
||||||
|
if (ERR_TerrainCatalog.Key(parts[0]) == key && Destination(parts) == destination) return Set(parts[0], 0, destination);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
void Stock(array<ref ERR_SiteStock> stock)
|
||||||
|
{
|
||||||
|
for (int i = 1; i < m_Records.Count(); i++)
|
||||||
|
{
|
||||||
|
array<string> parts = {}; m_Records[i].Split("|", parts, false);
|
||||||
|
ERR_SiteStock item = new ERR_SiteStock(); item.Prefab = parts[0]; item.Count = parts[1].ToInt(); item.OnGround = Destination(parts) == "ground"; stock.Insert(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void Snapshot(array<string> values)
|
||||||
|
{
|
||||||
|
values.Insert("starterMode=" + m_Records[0]);
|
||||||
|
string state = "Saved for next round. The shared field map is always included. Oversized items are left nearby. Add matching magazines separately.";
|
||||||
|
if (m_ReadOnly) state = "Read-only: starting backpack configuration needs repair.";
|
||||||
|
values.Insert("starterState=" + state);
|
||||||
|
for (int i = 1; i < m_Records.Count(); i++)
|
||||||
|
{
|
||||||
|
array<string> parts = {}; m_Records[i].Split("|", parts, false);
|
||||||
|
string key = ERR_TerrainCatalog.Key(parts[0]);
|
||||||
|
ERR_ServerItem item = GetGame().ERR_ServerItems().Item(key);
|
||||||
|
string label = FilePath.StripPath(parts[0]); if (item) label = item.Label;
|
||||||
|
values.Insert("starterItem=" + key + "|" + ERR_ServerItemCatalog.Clean(label) + "|" + parts[1] + "|" + Destination(parts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modded class ArmaReforgerScripted
|
||||||
|
{
|
||||||
|
protected ref ERR_StartBackpack m_ERRStartBackpack;
|
||||||
|
ERR_StartBackpack ERR_StartingSupplies()
|
||||||
|
{
|
||||||
|
if (!m_ERRStartBackpack) m_ERRStartBackpack = new ERR_StartBackpack();
|
||||||
|
return m_ERRStartBackpack;
|
||||||
|
}
|
||||||
|
override protected void OnBeforeWorldCleanup()
|
||||||
|
{
|
||||||
|
m_ERRStartBackpack = null; super.OnBeforeWorldCleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
// Catalog choices come from the authenticated session, not client prefab paths.
|
||||||
|
modded class ERR_AdminMenu
|
||||||
|
{
|
||||||
|
protected ref array<SCR_ButtonTextComponent> m_StarterButtons = {};
|
||||||
|
protected ref array<string> m_StarterMods = {}, m_StarterCatalog = {}, m_StarterSaved = {};
|
||||||
|
protected string m_StarterCatalogSignature, m_StarterSavedSignature, m_StarterAddon;
|
||||||
|
protected int m_StarterMode = -1;
|
||||||
|
override void OnMenuOpen()
|
||||||
|
{
|
||||||
|
super.OnMenuOpen();
|
||||||
|
foreach (string name : {"StarterDefault", "StarterCustom", "StarterShow", "StarterPrevious", "StarterNext", "StarterAdd", "StarterRemove"})
|
||||||
|
{
|
||||||
|
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText(name, GetRootWidget());
|
||||||
|
if (!button) { Close(); return; }
|
||||||
|
button.m_OnClicked.Insert(OnStarterButton); m_StarterButtons.Insert(button);
|
||||||
|
}
|
||||||
|
SCR_EditBoxComponent.GetEditBoxComponent("StarterQuantity", GetRootWidget()).SetValue("1");
|
||||||
|
SCR_ComboBoxComponent destination = SCR_ComboBoxComponent.GetComboBoxComponent("StarterDestination", GetRootWidget());
|
||||||
|
destination.ClearAll(); destination.AddItem("Inside backpack"); destination.AddItem("On ground beside backpack");
|
||||||
|
destination.SetCurrentItem(0, false, false, false);
|
||||||
|
UpdateActions();
|
||||||
|
}
|
||||||
|
override void OnMenuClose()
|
||||||
|
{
|
||||||
|
foreach (SCR_ButtonTextComponent button : m_StarterButtons) if (button) button.m_OnClicked.Remove(OnStarterButton);
|
||||||
|
m_StarterButtons.Clear(); m_StarterMods.Clear(); m_StarterCatalog.Clear(); m_StarterSaved.Clear();
|
||||||
|
super.OnMenuClose();
|
||||||
|
}
|
||||||
|
protected override void UpdateActions()
|
||||||
|
{
|
||||||
|
super.UpdateActions();
|
||||||
|
ERR_SelectionStyle.Pair(GetRootWidget(), "StarterCustom", "StarterDefault", m_StarterMode);
|
||||||
|
foreach (SCR_ButtonTextComponent button : m_StarterButtons)
|
||||||
|
{
|
||||||
|
string name = button.GetRootWidget().GetName();
|
||||||
|
bool enabled = m_Ready && m_StarterMode >= 0;
|
||||||
|
if (name == "StarterDefault") enabled = enabled && m_StarterMode == 1;
|
||||||
|
if (name == "StarterCustom") enabled = enabled && m_StarterMode == 0;
|
||||||
|
if (name == "StarterShow") enabled = enabled && !m_StarterMods.IsEmpty();
|
||||||
|
if (name == "StarterAdd") enabled = enabled && !m_StarterCatalog.IsEmpty();
|
||||||
|
if (name == "StarterRemove") enabled = enabled && !m_StarterSaved.IsEmpty();
|
||||||
|
if (name == "StarterPrevious") enabled = enabled && !m_StarterAddon.IsEmpty() && m_ShownItemPage > 0;
|
||||||
|
if (name == "StarterNext") enabled = enabled && !m_StarterAddon.IsEmpty() && m_ShownItemPage < m_LastItemPage;
|
||||||
|
button.SetEnabled(enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Preserve selection across polls and only rebuild a dropdown when its data changes.
|
||||||
|
protected void StarterCombo(string name, array<string> keys, array<string> labels, array<string> previous)
|
||||||
|
{
|
||||||
|
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent(name, GetRootWidget());
|
||||||
|
if (!combo) return;
|
||||||
|
int index = combo.GetCurrentIndex(); string selected;
|
||||||
|
if (index >= 0 && index < previous.Count()) selected = previous[index];
|
||||||
|
combo.ClearAll(); foreach (string label : labels) combo.AddItem(WidgetManager.Translate(label));
|
||||||
|
combo.SetCurrentItem(Math.Max(0, keys.Find(selected)), false, false, false);
|
||||||
|
previous.Copy(keys);
|
||||||
|
}
|
||||||
|
override void ReceiveSnapshot(bool authorized, array<string> values, array<string> status)
|
||||||
|
{
|
||||||
|
super.ReceiveSnapshot(authorized, values, status);
|
||||||
|
if (!authorized || !values) return;
|
||||||
|
if (m_StarterMods.Count() != m_ServerAddonKeys.Count()) StarterCombo("StarterMod", m_ServerAddonKeys, m_ServerAddonLabels, m_StarterMods);
|
||||||
|
array<string> catalogKeys = {}, catalogLabels = {}, savedKeys = {}, savedLabels = {};
|
||||||
|
string catalogSignature = m_ShownItemAddon, savedSignature;
|
||||||
|
int total;
|
||||||
|
foreach (string entry : values)
|
||||||
|
{
|
||||||
|
int divider = entry.IndexOf("="); if (divider < 0) continue;
|
||||||
|
string field = entry.Substring(0, divider), value = entry.Substring(divider + 1, entry.Length() - divider - 1);
|
||||||
|
array<string> parts = {}; value.Split("|", parts, false);
|
||||||
|
if (field == "starterMode") { m_StarterMode = 0; if (value == "custom") m_StarterMode = 1; }
|
||||||
|
if (field == "starterState") SetText("StarterState", value);
|
||||||
|
if (field == "serverItem" && parts.Count() == 6)
|
||||||
|
{ catalogKeys.Insert(parts[0]); catalogLabels.Insert(parts[1]); catalogSignature += value; }
|
||||||
|
if (field == "starterItem" && parts.Count() == 4)
|
||||||
|
{
|
||||||
|
string location = "Backpack"; if (parts[3] == "ground") location = "Ground";
|
||||||
|
savedKeys.Insert(parts[0] + " " + parts[3]);
|
||||||
|
savedLabels.Insert(location + " | " + parts[2] + " x " + WidgetManager.Translate(parts[1]));
|
||||||
|
savedSignature += value; total += parts[2].ToInt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (catalogSignature != m_StarterCatalogSignature)
|
||||||
|
{
|
||||||
|
StarterCombo("StarterCatalogItem", catalogKeys, catalogLabels, m_StarterCatalog);
|
||||||
|
m_StarterCatalogSignature = catalogSignature; m_StarterAddon = m_ShownItemAddon;
|
||||||
|
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("StarterMod", GetRootWidget());
|
||||||
|
if (combo && !m_StarterAddon.IsEmpty()) combo.SetCurrentItem(Math.Max(0, m_StarterMods.Find(m_StarterAddon)), false, false, false);
|
||||||
|
}
|
||||||
|
if (savedSignature != m_StarterSavedSignature)
|
||||||
|
{ StarterCombo("StarterSaved", savedKeys, savedLabels, m_StarterSaved); m_StarterSavedSignature = savedSignature; }
|
||||||
|
string mode = "Default loadout selected"; if (m_StarterMode == 1) mode = "Custom loadout selected";
|
||||||
|
SetText("StarterSummary", mode + " | Custom: " + savedKeys.Count().ToString() + "/32 entries, " + total.ToString() + "/64 items + field map");
|
||||||
|
string page = "Choose a mod, then Show items. Quantity 0 removes an item.";
|
||||||
|
if (!m_StarterAddon.IsEmpty()) page = "Catalog page " + (m_ShownItemPage + 1).ToString() + "/" + (m_LastItemPage + 1).ToString() + " | Select an item above; enter its total desired quantity (0 removes).";
|
||||||
|
SetText("StarterCatalogState", page); UpdateActions();
|
||||||
|
}
|
||||||
|
protected void OnStarterButton(SCR_ButtonBaseComponent button)
|
||||||
|
{
|
||||||
|
if (!button || !m_Ready || !IsFocused() || !m_Controller || m_Age > 6 || m_ClickDelay > 0) return;
|
||||||
|
string name = button.GetRootWidget().GetName();
|
||||||
|
if (name == "StarterDefault") { SubmitStarter("starter mode default"); return; }
|
||||||
|
if (name == "StarterCustom") { SubmitStarter("starter mode custom"); return; }
|
||||||
|
if (name == "StarterRemove")
|
||||||
|
{
|
||||||
|
int selected = SCR_ComboBoxComponent.GetComboBoxComponent("StarterSaved", GetRootWidget()).GetCurrentIndex();
|
||||||
|
if (selected >= 0 && selected < m_StarterSaved.Count()) SubmitStarter("starter remove " + m_StarterSaved[selected]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (name == "StarterAdd")
|
||||||
|
{
|
||||||
|
int selected = SCR_ComboBoxComponent.GetComboBoxComponent("StarterCatalogItem", GetRootWidget()).GetCurrentIndex();
|
||||||
|
int count; string quantity = SCR_EditBoxComponent.GetEditBoxComponent("StarterQuantity", GetRootWidget()).GetValue().Trim();
|
||||||
|
if (!ERR_CommandArguments.Integer(quantity, count) || count < 0 || count > 48) { ReceiveReply("Enter a quantity from 0 to 48; 0 removes the item."); return; }
|
||||||
|
string destination = "backpack";
|
||||||
|
if (SCR_ComboBoxComponent.GetComboBoxComponent("StarterDestination", GetRootWidget()).GetCurrentIndex() == 1) destination = "ground";
|
||||||
|
if (selected >= 0 && selected < m_StarterCatalog.Count() && !m_StarterAddon.IsEmpty()) SubmitStarter("starter set " + m_StarterAddon + " " + m_StarterCatalog[selected] + " " + count.ToString() + " " + destination);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int index = SCR_ComboBoxComponent.GetComboBoxComponent("StarterMod", GetRootWidget()).GetCurrentIndex();
|
||||||
|
if (index < 0 || index >= m_StarterMods.Count()) return;
|
||||||
|
string addon = m_StarterMods[index]; int page;
|
||||||
|
if (name == "StarterPrevious" || name == "StarterNext")
|
||||||
|
{
|
||||||
|
addon = m_StarterAddon; page = m_ShownItemPage - 1;
|
||||||
|
if (name == "StarterNext") page = m_ShownItemPage + 1;
|
||||||
|
}
|
||||||
|
SubmitStarter("items list " + addon + " " + Math.Clamp(page, 0, m_LastItemPage).ToString());
|
||||||
|
}
|
||||||
|
protected void SubmitStarter(string command)
|
||||||
|
{
|
||||||
|
m_ClickDelay = 0.3; SetText("Feedback", "Updating starting backpack...");
|
||||||
|
m_Controller.ERR_AdminSubmit(command, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,443 @@
|
|||||||
|
// The mission only produces facts. Aggregation and public leaderboards belong to
|
||||||
|
// the receiver; retries retain event IDs so they cannot count a result twice.
|
||||||
|
[BaseContainerProps()]
|
||||||
|
class ERR_TelemetryConfig
|
||||||
|
{
|
||||||
|
[Attribute("")] string Endpoint;
|
||||||
|
static const string RESOURCE = "{E72B130684FA492C}Configs/ERR/Telemetry.conf";
|
||||||
|
static const string PATH = "$profile:EscapistsTelemetry.json";
|
||||||
|
string Token;
|
||||||
|
string ServerName;
|
||||||
|
static ERR_TelemetryConfig Load()
|
||||||
|
{
|
||||||
|
ERR_TelemetryConfig config = ERR_TelemetryConfig.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(RESOURCE));
|
||||||
|
if (!config) config = new ERR_TelemetryConfig();
|
||||||
|
if (FileIO.FileExists(PATH))
|
||||||
|
{
|
||||||
|
JsonLoadContext json = new JsonLoadContext();
|
||||||
|
if (!json.LoadFromFile(PATH)) { config.Endpoint = ""; return config; }
|
||||||
|
json.ReadValue("endpoint", config.Endpoint);
|
||||||
|
json.ReadValue("token", config.Token);
|
||||||
|
json.ReadValue("serverName", config.ServerName);
|
||||||
|
}
|
||||||
|
config.Endpoint.TrimInPlace();
|
||||||
|
if (!config.Endpoint.StartsWith("https://") || !ERR_SpeechRules.Endpoint(config.Endpoint)) config.Endpoint = "";
|
||||||
|
if (config.Token.Length() > 256 || config.ServerName.Length() > 120) config.Endpoint = "";
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ERR_TelemetryPlayer
|
||||||
|
{
|
||||||
|
string id;
|
||||||
|
string name;
|
||||||
|
bool authenticated;
|
||||||
|
bool connected;
|
||||||
|
bool participated;
|
||||||
|
bool escaped;
|
||||||
|
int connectedSeconds;
|
||||||
|
int kills;
|
||||||
|
int friendlyKills;
|
||||||
|
int deaths;
|
||||||
|
int downed;
|
||||||
|
int recoveries;
|
||||||
|
}
|
||||||
|
class ERR_TelemetryMod
|
||||||
|
{
|
||||||
|
string id;
|
||||||
|
string name;
|
||||||
|
string version;
|
||||||
|
}
|
||||||
|
class ERR_TelemetryState
|
||||||
|
{
|
||||||
|
int schemaVersion = 1;
|
||||||
|
string serverId;
|
||||||
|
string endpoint;
|
||||||
|
string previousMissionId;
|
||||||
|
string previousOutcome;
|
||||||
|
int droppedEvents;
|
||||||
|
ref array<string> pending = {};
|
||||||
|
}
|
||||||
|
class ERR_TelemetryRules
|
||||||
|
{
|
||||||
|
static bool Acknowledged(int status) { return status >= 200 && status < 300; }
|
||||||
|
static int RetrySeconds(int failures)
|
||||||
|
{
|
||||||
|
return Math.Min(300, 5 * Math.Pow(2, Math.Min(failures, 6)));
|
||||||
|
}
|
||||||
|
static string Outcome(int phase, bool emptyRestart, bool preparationFailed)
|
||||||
|
{
|
||||||
|
if (preparationFailed) return "preparation_failed";
|
||||||
|
if (emptyRestart) return "restarted_empty";
|
||||||
|
if (phase == ERR_Phase.VICTORY) return "victory";
|
||||||
|
if (phase == ERR_Phase.DEFEAT) return "defeat";
|
||||||
|
return "interrupted";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ERR_Telemetry
|
||||||
|
{
|
||||||
|
static const string STATE = "$profile:EscapistsTelemetryState.json";
|
||||||
|
protected ERR_GameMode m_Mode;
|
||||||
|
protected ref ERR_TelemetryConfig m_Config;
|
||||||
|
protected ref ERR_TelemetryState m_State;
|
||||||
|
protected ref array<ref ERR_TelemetryPlayer> m_Players = {};
|
||||||
|
protected ref array<ref ERR_TelemetryMod> m_Mods = {};
|
||||||
|
protected ref RestCallback m_Callback;
|
||||||
|
protected RestContext m_Context;
|
||||||
|
protected string m_MissionId;
|
||||||
|
protected string m_InFlight;
|
||||||
|
protected string m_FinalEventId;
|
||||||
|
protected string m_FinalOutcome;
|
||||||
|
protected string m_FinalTerrain;
|
||||||
|
protected bool m_CommunityReady;
|
||||||
|
protected int m_Started;
|
||||||
|
protected int m_LastSample;
|
||||||
|
protected int m_NextHeartbeat;
|
||||||
|
protected int m_NextAttempt;
|
||||||
|
protected int m_Peak;
|
||||||
|
protected int m_Failures;
|
||||||
|
protected int m_RequestStarted;
|
||||||
|
protected int m_Delivered;
|
||||||
|
protected bool m_Collected;
|
||||||
|
protected bool m_Partial;
|
||||||
|
protected bool m_Enabled;
|
||||||
|
protected bool m_Ended;
|
||||||
|
protected bool m_Closed;
|
||||||
|
protected bool m_Dirty;
|
||||||
|
protected bool m_TruncatedPlayers;
|
||||||
|
protected bool m_TruncatedMods;
|
||||||
|
protected bool m_CredentialBlocked;
|
||||||
|
|
||||||
|
void ERR_Telemetry(ERR_GameMode mode)
|
||||||
|
{
|
||||||
|
m_Mode = mode; m_Config = ERR_TelemetryConfig.Load();
|
||||||
|
m_State = new ERR_TelemetryState();
|
||||||
|
// A damaged primary can recover from the last fully written generation.
|
||||||
|
if (!LoadState(STATE) && !LoadState(STATE + ".bak"))
|
||||||
|
{
|
||||||
|
if (FileIO.FileExists(STATE) || FileIO.FileExists(STATE + ".bak"))
|
||||||
|
{
|
||||||
|
Print("Reintroduced [telemetry]: unreadable queue; telemetry suspended, files preserved", LogLevel.ERROR);
|
||||||
|
m_Closed = true; return;
|
||||||
|
}
|
||||||
|
m_State.serverId = UUID.GenV4();
|
||||||
|
}
|
||||||
|
if (m_State.endpoint != m_Config.Endpoint)
|
||||||
|
{
|
||||||
|
// Never redirect previously collected player data to a different receiver.
|
||||||
|
Purge(); m_State.endpoint = m_Config.Endpoint;
|
||||||
|
}
|
||||||
|
array<string> addons = {}; GameProject.GetLoadedAddons(addons);
|
||||||
|
foreach (string addon : addons)
|
||||||
|
{
|
||||||
|
if (GameProject.IsVanillaAddon(addon)) continue;
|
||||||
|
if (m_Mods.Count() >= 128) { m_TruncatedMods = true; break; }
|
||||||
|
ERR_TelemetryMod mod = new ERR_TelemetryMod();
|
||||||
|
mod.id = addon; mod.name = GameProject.GetAddonTitle(addon);
|
||||||
|
BackendApi backend = GetGame().GetBackendApi();
|
||||||
|
if (backend && backend.GetWorkshop())
|
||||||
|
{
|
||||||
|
WorkshopItem item = backend.GetWorkshop().FindItem(addon);
|
||||||
|
if (item && item.GetActiveRevision()) mod.version = item.GetActiveRevision().GetVersion();
|
||||||
|
}
|
||||||
|
m_Mods.Insert(mod);
|
||||||
|
}
|
||||||
|
m_MissionId = UUID.GenV4(); m_Started = System.GetTickCount();
|
||||||
|
Refresh();
|
||||||
|
GetGame().GetCallqueue().CallLater(Pulse, 5000, true);
|
||||||
|
}
|
||||||
|
protected bool ReadState(string path, out ERR_TelemetryState state)
|
||||||
|
{
|
||||||
|
string raw;
|
||||||
|
JsonLoadContext json = new JsonLoadContext();
|
||||||
|
if (!ERR_ProfileConfig.Text(path, raw) || !json.LoadFromString(raw) || !json.ReadValue("state", state) || !state || state.schemaVersion != 1 || !UUID.IsUUID(state.serverId) || !state.pending || state.pending.Count() > 64) return false;
|
||||||
|
foreach (string queuedEvent : state.pending)
|
||||||
|
{
|
||||||
|
JsonLoadContext queued = new JsonLoadContext(); string eventId, missionId, serverId;
|
||||||
|
if (queuedEvent.Length() > 65536 || !queued.LoadFromString(queuedEvent) || !queued.ReadValue("eventId", eventId) || !UUID.IsUUID(eventId) || !queued.ReadValue("missionId", missionId) || !UUID.IsUUID(missionId) || !queued.ReadValue("serverId", serverId) || serverId != state.serverId) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
protected bool LoadState(string path)
|
||||||
|
{
|
||||||
|
ERR_TelemetryState state;
|
||||||
|
if (!ReadState(path, state)) return false;
|
||||||
|
m_State = state; return true;
|
||||||
|
}
|
||||||
|
protected bool Save()
|
||||||
|
{
|
||||||
|
JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false);
|
||||||
|
json.WriteValue("state", m_State);
|
||||||
|
if (!json.SaveToFile(STATE + ".tmp")) return false;
|
||||||
|
string written;
|
||||||
|
if (!ERR_ProfileConfig.Text(STATE + ".tmp", written) || written != json.SaveToString()) return false;
|
||||||
|
ERR_TelemetryState previous;
|
||||||
|
if (ReadState(STATE, previous) && !FileIO.CopyFile(STATE, STATE + ".bak")) return false;
|
||||||
|
if (!FileIO.CopyFile(STATE + ".tmp", STATE)) return false;
|
||||||
|
string confirmed;
|
||||||
|
if (!ERR_ProfileConfig.Text(STATE, confirmed) || confirmed != written) return false;
|
||||||
|
FileIO.DeleteFile(STATE + ".tmp"); m_Dirty = false; return true;
|
||||||
|
}
|
||||||
|
protected void Purge()
|
||||||
|
{
|
||||||
|
m_State.pending.Clear(); m_State.previousMissionId = ""; m_State.previousOutcome = "";
|
||||||
|
m_Players.Clear(); m_InFlight = ""; m_Callback = null;
|
||||||
|
// Do not retain personal data in recovery files after opting out.
|
||||||
|
FileIO.DeleteFile(STATE); FileIO.DeleteFile(STATE + ".bak"); FileIO.DeleteFile(STATE + ".tmp");
|
||||||
|
m_Dirty = true;
|
||||||
|
}
|
||||||
|
protected bool Current() { return GetGame() && m_Mode && ERR_GameMode.Get() == m_Mode; }
|
||||||
|
string Status()
|
||||||
|
{
|
||||||
|
if (m_Closed) return "Telemetry suspended: queue recovery required.";
|
||||||
|
if (m_Mode.Settings().Get("telemetryEnabled") == 0) return "Disabled. Unsent records discarded.";
|
||||||
|
if (m_Config.Endpoint.IsEmpty()) return "Not sending: receiving URL is missing or invalid.";
|
||||||
|
if (m_Dirty) return "Waiting for profile storage; events have not been sent.";
|
||||||
|
if (m_CredentialBlocked) return "Telemetry credential storage needs attention; delivery suspended.";
|
||||||
|
if (m_Failures > 0) return "Delivery unavailable; pending records will retry.";
|
||||||
|
return string.Format("Configured. %1 delivered this session; %2 pending.", m_Delivered, m_State.pending.Count());
|
||||||
|
}
|
||||||
|
bool Enabled() { return m_Enabled && !m_Closed; }
|
||||||
|
void Refresh()
|
||||||
|
{
|
||||||
|
if (m_Closed || !Current() || !Replication.IsServer() || !m_Mode.Settings()) return;
|
||||||
|
bool enabled = m_Mode.Settings().Get("telemetryEnabled") == 1 && !m_Config.Endpoint.IsEmpty();
|
||||||
|
if (!enabled)
|
||||||
|
{
|
||||||
|
if (m_Enabled || !m_State.pending.IsEmpty() || !m_State.previousMissionId.IsEmpty())
|
||||||
|
{
|
||||||
|
m_Enabled = false; Purge(); Save();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m_Enabled || m_Ended) return;
|
||||||
|
m_Enabled = true;
|
||||||
|
int now = System.GetTickCount(); m_LastSample = now; m_NextHeartbeat = now + 300000;
|
||||||
|
m_Partial = m_Collected || now - m_Started > 1000;
|
||||||
|
m_Peak = 0; m_TruncatedPlayers = false;
|
||||||
|
string previous = m_State.previousMissionId;
|
||||||
|
string outcome = m_State.previousOutcome;
|
||||||
|
m_State.previousMissionId = m_MissionId; m_State.previousOutcome = "unfinished";
|
||||||
|
Sample();
|
||||||
|
if (!m_Collected && !previous.IsEmpty()) Event("mission_restarted", "", previous, outcome);
|
||||||
|
if (m_Collected) Event("telemetry_resumed");
|
||||||
|
else Event("mission_started", "", previous, outcome);
|
||||||
|
m_Collected = true;
|
||||||
|
}
|
||||||
|
protected void Sample()
|
||||||
|
{
|
||||||
|
if (!Enabled() || m_Ended) return;
|
||||||
|
int now = System.GetTickCount();
|
||||||
|
int elapsed = Math.Max(0, (now - m_LastSample) / 1000); m_LastSample = now;
|
||||||
|
foreach (ERR_TelemetryPlayer known : m_Players)
|
||||||
|
{
|
||||||
|
if (known.connected) known.connectedSeconds += elapsed;
|
||||||
|
known.connected = false;
|
||||||
|
}
|
||||||
|
array<int> connections = {}; GetGame().GetPlayerManager().GetPlayers(connections);
|
||||||
|
m_Peak = Math.Max(m_Peak, connections.Count());
|
||||||
|
foreach (int connection : connections)
|
||||||
|
{
|
||||||
|
UUID account = SCR_PlayerIdentityUtils.GetPlayerIdentityId(connection);
|
||||||
|
string key = account;
|
||||||
|
bool authenticated = !account.IsNull();
|
||||||
|
if (!authenticated) key = m_MissionId + ":connection:" + connection.ToString();
|
||||||
|
ERR_TelemetryPlayer player;
|
||||||
|
foreach (ERR_TelemetryPlayer candidate : m_Players) if (candidate.id == key) { player = candidate; break; }
|
||||||
|
if (!player)
|
||||||
|
{
|
||||||
|
if (m_Players.Count() >= 128) { m_TruncatedPlayers = true; continue; }
|
||||||
|
player = new ERR_TelemetryPlayer(); player.id = key; player.authenticated = authenticated; m_Players.Insert(player);
|
||||||
|
}
|
||||||
|
player.name = GetGame().GetPlayerManager().GetPlayerName(connection);
|
||||||
|
if (player.name.Length() > 128) player.name = player.name.Substring(0, 128);
|
||||||
|
player.connected = true;
|
||||||
|
ERR_PlayerLife life = m_Mode.Players().Get(connection);
|
||||||
|
if (life && life.Connected && life.UsedLife) player.participated = true;
|
||||||
|
if (authenticated && m_Mode.Extraction()) player.escaped = m_Mode.Extraction().DidEscape(account);
|
||||||
|
}
|
||||||
|
// Include disconnected escapees/participants with their final authoritative result.
|
||||||
|
array<ref ERR_PlayerLife> lives = {}; m_Mode.Players().Participants(lives);
|
||||||
|
foreach (ERR_PlayerLife participant : lives)
|
||||||
|
foreach (ERR_TelemetryPlayer tracked : m_Players)
|
||||||
|
if (tracked.authenticated && tracked.id == participant.AccountId)
|
||||||
|
{
|
||||||
|
tracked.participated = true;
|
||||||
|
ERR_RoundPlayerStats stats;
|
||||||
|
if (m_Mode.ERR_Statistics()) stats = m_Mode.ERR_Statistics().Find(participant.AccountId);
|
||||||
|
if (stats)
|
||||||
|
{
|
||||||
|
tracked.kills = stats.Kills; tracked.friendlyKills = stats.FriendlyKills;
|
||||||
|
tracked.deaths = stats.Deaths; tracked.downed = stats.Downed; tracked.recoveries = stats.Recoveries;
|
||||||
|
}
|
||||||
|
if (m_Mode.Extraction()) tracked.escaped = m_Mode.Extraction().DidEscape(participant.AccountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void RosterChanged(int playerId) { Sample(); }
|
||||||
|
void PhaseChanged(ERR_Phase previous, ERR_Phase current)
|
||||||
|
{
|
||||||
|
if (!Enabled() || m_Ended) return;
|
||||||
|
if (ERR_MissionRules.Terminal(current)) Finish(ERR_TelemetryRules.Outcome(current, false, false));
|
||||||
|
else Event("phase_changed");
|
||||||
|
}
|
||||||
|
void Finish(string outcome)
|
||||||
|
{
|
||||||
|
Refresh();
|
||||||
|
if (!Enabled() || m_Ended) return;
|
||||||
|
Sample(); m_Ended = true;
|
||||||
|
m_State.previousOutcome = outcome;
|
||||||
|
Event("mission_finished", outcome);
|
||||||
|
}
|
||||||
|
protected string Terrain()
|
||||||
|
{
|
||||||
|
// The loaded root scene identifies the island beneath GM/Conflict scenarios.
|
||||||
|
// Use the same terrain resolver as the mission title, on the reporting server.
|
||||||
|
string world = GetGame().GetWorldFile();
|
||||||
|
if (GetGame().GetWorld() && GetGame().GetWorld().GetNumSubScenes() > 0)
|
||||||
|
{
|
||||||
|
string root = GetGame().GetWorld().GetSubSceneName(0);
|
||||||
|
if (!root.IsEmpty()) world = root;
|
||||||
|
}
|
||||||
|
return ERR_TerrainDisplayName.Resolve(world);
|
||||||
|
}
|
||||||
|
protected void Event(string type, string outcome = "", string previous = "", string previousOutcome = "")
|
||||||
|
{
|
||||||
|
if (!Enabled()) return;
|
||||||
|
if (!m_Ended) Sample();
|
||||||
|
JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false);
|
||||||
|
json.WriteValue("schemaVersion", 1);
|
||||||
|
string eventId = UUID.GenV4();
|
||||||
|
json.WriteValue("eventId", eventId); json.WriteValue("missionId", m_MissionId);
|
||||||
|
json.WriteValue("serverId", m_State.serverId); json.WriteValue("serverName", ERR_TelemetryServerName.Resolve(m_Config.ServerName));
|
||||||
|
json.WriteValue("type", type); json.WriteValue("occurredAt", System.GetUnixTime());
|
||||||
|
json.WriteValue("partial", m_Partial);
|
||||||
|
int elapsedSeconds = Math.Max(0, (System.GetTickCount() - m_Started) / 1000);
|
||||||
|
json.WriteValue("elapsedSeconds", elapsedSeconds);
|
||||||
|
json.WriteValue("phase", m_Mode.Phase()); json.WriteValue("outcome", outcome);
|
||||||
|
json.WriteValue("previousMissionId", previous); json.WriteValue("previousOutcome", previousOutcome);
|
||||||
|
string terrain = Terrain();
|
||||||
|
json.WriteValue("terrain", terrain);
|
||||||
|
SCR_MissionHeader header = SCR_MissionHeader.Cast(GetGame().GetMissionHeader());
|
||||||
|
string scenario; if (header) scenario = header.m_sName;
|
||||||
|
json.WriteValue("gameVersion", GetGame().GetBuildVersion());
|
||||||
|
json.WriteValue("scenario", scenario); json.WriteValue("mods", m_Mods);
|
||||||
|
json.WriteValue("modsTruncated", m_TruncatedMods); json.WriteValue("playersTruncated", m_TruncatedPlayers);
|
||||||
|
json.WriteValue("connectedPlayers", GetGame().GetPlayerManager().GetPlayerCount());
|
||||||
|
json.WriteValue("peakPlayers", m_Peak); json.WriteValue("players", m_Players);
|
||||||
|
json.WriteValue("reason", m_Mode.EndReason()); json.WriteValue("preparationError", m_Mode.PreparationError());
|
||||||
|
map<string, int> settings = new map<string, int>();
|
||||||
|
foreach (string key : ERR_Settings.Keys()) settings.Set(key, m_Mode.Settings().Get(key));
|
||||||
|
json.WriteValue("settings", settings); json.WriteValue("droppedEvents", m_State.droppedEvents);
|
||||||
|
string payload = json.SaveToString();
|
||||||
|
if (payload.Length() > 65536) { m_State.droppedEvents++; m_Dirty = true; return; }
|
||||||
|
// Bound disk and memory during long outages. Prefer retaining result records.
|
||||||
|
if (m_State.pending.Count() >= 64)
|
||||||
|
{
|
||||||
|
int remove = -1;
|
||||||
|
for (int i = 0; i < m_State.pending.Count(); i++)
|
||||||
|
{
|
||||||
|
if (m_State.pending[i] == m_InFlight) continue;
|
||||||
|
JsonLoadContext queued = new JsonLoadContext(); string queuedType;
|
||||||
|
if (queued.LoadFromString(m_State.pending[i]) && queued.ReadValue("type", queuedType) && queuedType == "heartbeat") { remove = i; break; }
|
||||||
|
}
|
||||||
|
if (remove < 0) remove = 0;
|
||||||
|
if (remove == 0 && !m_InFlight.IsEmpty()) remove = 1;
|
||||||
|
m_State.pending.Remove(remove); m_State.droppedEvents++;
|
||||||
|
}
|
||||||
|
if (type == "mission_finished")
|
||||||
|
{
|
||||||
|
m_FinalEventId = eventId; m_FinalOutcome = outcome; m_FinalTerrain = terrain;
|
||||||
|
// Finish can arrive while a previous request is in flight. RemoveItem on its
|
||||||
|
// acknowledgement still removes that exact payload, leaving this final first.
|
||||||
|
m_State.pending.InsertAt(payload, 0);
|
||||||
|
if (outcome == "victory" || outcome == "defeat")
|
||||||
|
m_Mode.ERR_PublishCommunityStatistics("This map's community record: waiting for the round result to be recorded...");
|
||||||
|
GetGame().GetCallqueue().CallLater(Pulse, 1, false);
|
||||||
|
}
|
||||||
|
else m_State.pending.Insert(payload);
|
||||||
|
m_Dirty = true;
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
protected void Pulse()
|
||||||
|
{
|
||||||
|
if (m_Closed || ERR_GameMode.Get() != m_Mode) { Close(); return; }
|
||||||
|
Refresh();
|
||||||
|
if (!Enabled()) return;
|
||||||
|
int now = System.GetTickCount();
|
||||||
|
if (!m_Ended)
|
||||||
|
{
|
||||||
|
Sample();
|
||||||
|
if (now >= m_NextHeartbeat) { m_NextHeartbeat = now + 300000; Event("heartbeat"); }
|
||||||
|
}
|
||||||
|
if (m_Dirty && !Save()) { CommunityUnavailable(); return; }
|
||||||
|
if (m_Callback && now - m_RequestStarted > 30000) Failed(m_Callback);
|
||||||
|
if (m_Callback || m_State.pending.IsEmpty() || now < m_NextAttempt) return;
|
||||||
|
if (m_Config.Token.IsEmpty())
|
||||||
|
{
|
||||||
|
string token;
|
||||||
|
if (!ERR_TelemetryCredentials.Resolve(m_Config.Endpoint, m_State.serverId, token))
|
||||||
|
{
|
||||||
|
CommunityUnavailable();
|
||||||
|
m_CredentialBlocked = true; m_NextAttempt = now + 30000; return;
|
||||||
|
}
|
||||||
|
m_Config.Token = token; m_CredentialBlocked = false;
|
||||||
|
}
|
||||||
|
m_Context = GetGame().GetRestApi().GetContext(m_Config.Endpoint);
|
||||||
|
if (!m_Context) { CommunityUnavailable(); m_NextAttempt = now + 300000; return; }
|
||||||
|
m_Callback = new RestCallback(); m_Callback.SetOnSuccess(Delivered); m_Callback.SetOnError(Failed);
|
||||||
|
m_InFlight = m_State.pending[0]; m_RequestStarted = now;
|
||||||
|
JsonSaveContext request = new JsonSaveContext(); request.EnableTypeDiscriminator(false);
|
||||||
|
request.WriteValue("token", m_Config.Token);
|
||||||
|
// Embed the event object as JSON rather than double-encoding a JSON string.
|
||||||
|
string wrapper = request.SaveToString();
|
||||||
|
wrapper = wrapper.Substring(0, wrapper.Length() - 1) + ",\"event\":" + m_InFlight + "}";
|
||||||
|
m_Context.SetHeaders("Content-Type,application/json"); m_Context.SetTimeout(15);
|
||||||
|
m_Context.POST(m_Callback, "", wrapper);
|
||||||
|
}
|
||||||
|
protected void Delivered(RestCallback callback)
|
||||||
|
{
|
||||||
|
if (callback != m_Callback || !Enabled() || !Current()) return;
|
||||||
|
if (!ERR_TelemetryRules.Acknowledged(callback.GetHttpCode())) { Failed(callback); return; }
|
||||||
|
UpdateCommunity(callback);
|
||||||
|
m_State.pending.RemoveItem(m_InFlight); m_InFlight = ""; m_Callback = null;
|
||||||
|
m_Failures = 0; m_Delivered++; m_Dirty = true; Save();
|
||||||
|
if (m_Ended) GetGame().GetCallqueue().CallLater(Pulse, 1, false);
|
||||||
|
}
|
||||||
|
protected void CommunityUnavailable()
|
||||||
|
{
|
||||||
|
if (!m_CommunityReady && Current() && m_Ended && (m_FinalOutcome == "victory" || m_FinalOutcome == "defeat"))
|
||||||
|
m_Mode.ERR_PublishCommunityStatistics("Community map records unavailable. This round's local statistics are shown below.");
|
||||||
|
}
|
||||||
|
protected void UpdateCommunity(RestCallback callback)
|
||||||
|
{
|
||||||
|
if (!m_Ended || m_FinalEventId.IsEmpty() || !Current()) return;
|
||||||
|
JsonLoadContext sent = new JsonLoadContext(); string sentEvent;
|
||||||
|
if (!sent.LoadFromString(m_InFlight) || !sent.ReadValue("eventId", sentEvent) || sentEvent != m_FinalEventId) return;
|
||||||
|
if (m_FinalOutcome != "victory" && m_FinalOutcome != "defeat") return;
|
||||||
|
JsonLoadContext reply = new JsonLoadContext();
|
||||||
|
ERR_CommunityStatistics community;
|
||||||
|
if (callback.GetData().Length() > 8192 || !reply.LoadFromString(callback.GetData()) || !reply.ReadValue("community", community) || !community || !community.Matches(m_State.serverId, m_MissionId, m_FinalEventId, m_FinalTerrain, m_FinalOutcome))
|
||||||
|
{
|
||||||
|
CommunityUnavailable(); return;
|
||||||
|
}
|
||||||
|
m_CommunityReady = true;
|
||||||
|
m_Mode.ERR_PublishCommunityStatistics(community.Summary());
|
||||||
|
}
|
||||||
|
protected void Failed(RestCallback callback)
|
||||||
|
{
|
||||||
|
if (callback != m_Callback || !Enabled()) return;
|
||||||
|
m_Callback = null; m_InFlight = ""; m_Failures = Math.Min(100, m_Failures + 1);
|
||||||
|
m_NextAttempt = System.GetTickCount() + 1000 * ERR_TelemetryRules.RetrySeconds(m_Failures);
|
||||||
|
CommunityUnavailable();
|
||||||
|
if (m_Failures == 1) Print("Reintroduced [telemetry]: delivery failed; queued events will retry", LogLevel.WARNING);
|
||||||
|
}
|
||||||
|
void Close()
|
||||||
|
{
|
||||||
|
if (m_Closed) return;
|
||||||
|
if (Current()) Finish("interrupted");
|
||||||
|
m_Closed = true;
|
||||||
|
if (GetGame()) GetGame().GetCallqueue().Remove(Pulse);
|
||||||
|
m_Callback = null; m_Context = null; m_Mode = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// Each installation claims its random server ID with its own persistent secret.
|
||||||
|
// No receiver administrator secret is bundled with the mod or sent by players.
|
||||||
|
class ERR_TelemetryCredential
|
||||||
|
{
|
||||||
|
string endpoint;
|
||||||
|
string serverId;
|
||||||
|
string token;
|
||||||
|
}
|
||||||
|
class ERR_TelemetryCredentials
|
||||||
|
{
|
||||||
|
static const string PATH = "$profile:EscapistsTelemetryCredentials.json";
|
||||||
|
static bool Valid(ERR_TelemetryCredential entry)
|
||||||
|
{
|
||||||
|
return entry && !entry.endpoint.IsEmpty() && UUID.IsUUID(entry.serverId) && entry.token.Length() >= 32 && entry.token.Length() <= 256;
|
||||||
|
}
|
||||||
|
static bool Resolve(string endpoint, string serverId, out string token)
|
||||||
|
{
|
||||||
|
token = "";
|
||||||
|
ref array<ref ERR_TelemetryCredential> entries = {};
|
||||||
|
if (FileIO.FileExists(PATH))
|
||||||
|
{
|
||||||
|
JsonLoadContext loaded = new JsonLoadContext();
|
||||||
|
// Never silently replace a damaged credential: the receiver already owns its
|
||||||
|
// hash, and a new secret must not take over the same server's history.
|
||||||
|
if (!loaded.LoadFromFile(PATH) || !loaded.ReadValue("credentials", entries) || !entries || entries.Count() > 16) return false;
|
||||||
|
foreach (ERR_TelemetryCredential known : entries)
|
||||||
|
{
|
||||||
|
if (!Valid(known)) return false;
|
||||||
|
if (known.endpoint == endpoint && known.serverId == serverId) token = known.token;
|
||||||
|
}
|
||||||
|
if (!token.IsEmpty()) return true;
|
||||||
|
}
|
||||||
|
if (entries.Count() >= 16) return false;
|
||||||
|
ERR_TelemetryCredential created = new ERR_TelemetryCredential();
|
||||||
|
created.endpoint = endpoint; created.serverId = serverId;
|
||||||
|
string first = UUID.GenV4(); string second = UUID.GenV4();
|
||||||
|
created.token = first + second;
|
||||||
|
entries.Insert(created);
|
||||||
|
JsonSaveContext saved = new JsonSaveContext(); saved.EnableTypeDiscriminator(false);
|
||||||
|
saved.WriteValue("credentials", entries);
|
||||||
|
if (!saved.SaveToFile(PATH + ".tmp")) return false;
|
||||||
|
string written;
|
||||||
|
if (!ERR_ProfileConfig.Text(PATH + ".tmp", written) || written != saved.SaveToString()) return false;
|
||||||
|
if (!FileIO.CopyFile(PATH + ".tmp", PATH)) return false;
|
||||||
|
string confirmed;
|
||||||
|
if (!ERR_ProfileConfig.Text(PATH, confirmed) || confirmed != written) return false;
|
||||||
|
FileIO.DeleteFile(PATH + ".tmp");
|
||||||
|
// Publish to the HTTP sender only after the exact credential is durable.
|
||||||
|
token = created.token;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// Keep instrumentation outside mission rules: telemetry cannot decide an outcome
|
||||||
|
// or delay vanilla shutdown/restarts. Unsent results survive in the profile queue.
|
||||||
|
modded class ERR_GameMode
|
||||||
|
{
|
||||||
|
protected ref ERR_Telemetry m_ERRTelemetry;
|
||||||
|
protected override void EOnInit(IEntity owner)
|
||||||
|
{
|
||||||
|
super.EOnInit(owner);
|
||||||
|
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !Settings()) return;
|
||||||
|
#ifdef WORKBENCH
|
||||||
|
if (!System.IsCLIParam("errTelemetryTest")) return;
|
||||||
|
#endif
|
||||||
|
m_ERRTelemetry = new ERR_Telemetry(this);
|
||||||
|
Admin().Register(new ERR_TelemetryAdminProvider(m_ERRTelemetry));
|
||||||
|
OnPhaseChanged().Insert(m_ERRTelemetry.PhaseChanged);
|
||||||
|
Players().OnChanged().Insert(m_ERRTelemetry.RosterChanged);
|
||||||
|
}
|
||||||
|
void ERR_RefreshTelemetry() { if (m_ERRTelemetry) m_ERRTelemetry.Refresh(); }
|
||||||
|
protected override void FailPreparation(string detail)
|
||||||
|
{
|
||||||
|
super.FailPreparation(detail);
|
||||||
|
if (m_ERRTelemetry && !PreparationError().IsEmpty()) m_ERRTelemetry.Finish("preparation_failed");
|
||||||
|
}
|
||||||
|
protected override void OnGameModeEnd(SCR_GameModeEndData endData)
|
||||||
|
{
|
||||||
|
// Observe the phase BEFORE vanilla maps native interruption/empty restart to defeat.
|
||||||
|
if (m_ERRTelemetry) m_ERRTelemetry.Finish(ERR_TelemetryRules.Outcome(Phase(), m_EmptyRestartRequested, !PreparationError().IsEmpty()));
|
||||||
|
super.OnGameModeEnd(endData);
|
||||||
|
}
|
||||||
|
override void OnGameEnd()
|
||||||
|
{
|
||||||
|
if (m_ERRTelemetry) m_ERRTelemetry.Close();
|
||||||
|
super.OnGameEnd();
|
||||||
|
}
|
||||||
|
void ~ERR_GameMode() { if (m_ERRTelemetry) m_ERRTelemetry.Close(); }
|
||||||
|
}
|
||||||
|
modded class ERR_AdminService
|
||||||
|
{
|
||||||
|
override void Execute(ERR_GameMode mode, bool authorized, string arguments, array<string> replies)
|
||||||
|
{
|
||||||
|
super.Execute(mode, authorized, arguments, replies);
|
||||||
|
// Settings only change through the existing authenticated, save-before-apply path.
|
||||||
|
if (authorized && mode) mode.ERR_RefreshTelemetry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ERR_TelemetryAdminProvider : ERR_AdminProvider
|
||||||
|
{
|
||||||
|
protected ERR_Telemetry m_Service;
|
||||||
|
void ERR_TelemetryAdminProvider(ERR_Telemetry service) { m_Service = service; }
|
||||||
|
override string Id() { return "telemetry"; }
|
||||||
|
override void Capabilities(array<string> values)
|
||||||
|
{
|
||||||
|
if (m_Service) values.Insert("telemetryStatus=" + m_Service.Status());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Resolve on the reporting server for every new event: lobby metadata can be
|
||||||
|
// unavailable during initial startup. Never infer identity from the scenario.
|
||||||
|
class ERR_TelemetryServerName
|
||||||
|
{
|
||||||
|
static string Resolve(string configured)
|
||||||
|
{
|
||||||
|
string name = configured;
|
||||||
|
name.TrimInPlace();
|
||||||
|
if (name.IsEmpty())
|
||||||
|
{
|
||||||
|
BackendApi backend = GetGame().GetBackendApi();
|
||||||
|
SCR_DSConfig dedicated = new SCR_DSConfig();
|
||||||
|
if (backend && backend.GetRunningDSConfig(dedicated) && dedicated.game)
|
||||||
|
name = dedicated.game.name;
|
||||||
|
name.TrimInPlace();
|
||||||
|
}
|
||||||
|
if (name.IsEmpty())
|
||||||
|
{
|
||||||
|
ServerConfig config = ServerLobbyApi.GetServerConfig();
|
||||||
|
if (config) name = config.GetName();
|
||||||
|
name.TrimInPlace();
|
||||||
|
}
|
||||||
|
if (name.IsEmpty())
|
||||||
|
{
|
||||||
|
ServerInfo info = GetGame().GetServerInfo();
|
||||||
|
if (info) name = info.GetName();
|
||||||
|
name.TrimInPlace();
|
||||||
|
}
|
||||||
|
if (name.Length() > 120) name = name.Substring(0, 120);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,13 +7,21 @@ class ERR_TerrainSites : ERR_GeneratedSites
|
|||||||
protected vector m_Candidate;
|
protected vector m_Candidate;
|
||||||
protected float m_CandidateHeading;
|
protected float m_CandidateHeading;
|
||||||
protected int m_CandidateBegan;
|
protected int m_CandidateBegan;
|
||||||
protected ref ERR_ReconAirports m_Airports = new ERR_ReconAirports();
|
|
||||||
protected bool m_AirportsReady;
|
|
||||||
protected int m_GeometryRejected, m_NavigationRejected, m_PadRejected;
|
protected int m_GeometryRejected, m_NavigationRejected, m_PadRejected;
|
||||||
|
protected string m_NavigationReason;
|
||||||
|
// Keep the same work slice; difficult terrain gets more slices, not larger stalls.
|
||||||
|
static int SearchAttempts() { return 48000; }
|
||||||
|
static int SearchMilliseconds() { return 120000; }
|
||||||
|
static float PrisonAirportBuffer(vector low, vector high)
|
||||||
|
{
|
||||||
|
// The old 1.5km tactical separation could exclude an entire small island.
|
||||||
|
// Physical ground, scenery and navigation checks still apply independently.
|
||||||
|
return Math.Clamp(Math.Min(high[0] - low[0], high[2] - low[2]) * 0.15, 200, 1500);
|
||||||
|
}
|
||||||
static bool Bounds(vector low, vector high)
|
static bool Bounds(vector low, vector high)
|
||||||
{
|
{
|
||||||
float width = high[0] - low[0], depth = high[2] - low[2];
|
float width = high[0] - low[0], depth = high[2] - low[2];
|
||||||
return width >= 2000 && depth >= 2000 && width <= 100000 && depth <= 100000;
|
return width >= 1000 && depth >= 1000 && width <= 100000 && depth <= 100000;
|
||||||
}
|
}
|
||||||
static float Spacing(vector low, vector high)
|
static float Spacing(vector low, vector high)
|
||||||
{
|
{
|
||||||
@@ -23,12 +31,12 @@ class ERR_TerrainSites : ERR_GeneratedSites
|
|||||||
{
|
{
|
||||||
if (m_Started || !Replication.IsServer() || !definition || !definition.Valid() || !GetGame().GetWorldEntity()) return false;
|
if (m_Started || !Replication.IsServer() || !definition || !definition.Valid() || !GetGame().GetWorldEntity()) return false;
|
||||||
GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High);
|
GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High);
|
||||||
if (!Bounds(m_Low, m_High)) { m_Error = "Terrain bounds must span 2-100 km on each axis."; return false; }
|
if (!Bounds(m_Low, m_High)) { m_Error = "Terrain bounds must span 1-100 km on each axis."; return false; }
|
||||||
AIWorld world = GetGame().GetAIWorld();
|
AIWorld world = GetGame().GetAIWorld();
|
||||||
if (!world || !world.GetNavmeshWorldComponent("Soldiers")) { m_Error = "Terrain requires its own Soldiers navigation mesh."; return false; }
|
if (!world || !world.GetNavmeshWorldComponent("Soldiers")) { m_Error = "Terrain requires its own Soldiers navigation mesh."; return false; }
|
||||||
m_Started = true; m_Definition = definition; definition.TerrainGenerated = true;
|
m_Started = true; m_Definition = definition; definition.TerrainGenerated = true;
|
||||||
m_Spacing = Spacing(m_Low, m_High);
|
m_Spacing = Spacing(m_Low, m_High);
|
||||||
// Resolve the terrain's airport first, then keep the prison in escape range.
|
// Resolve a clear landing zone first, then keep the prison in escape range.
|
||||||
array<int> order = {ERR_LocationKind.EXTRACTION, ERR_LocationKind.PRISON, ERR_LocationKind.RADIO, ERR_LocationKind.DEPOT, ERR_LocationKind.HOSPITAL, ERR_LocationKind.MORTAR, ERR_LocationKind.OUTPOST};
|
array<int> order = {ERR_LocationKind.EXTRACTION, ERR_LocationKind.PRISON, ERR_LocationKind.RADIO, ERR_LocationKind.DEPOT, ERR_LocationKind.HOSPITAL, ERR_LocationKind.MORTAR, ERR_LocationKind.OUTPOST};
|
||||||
foreach (int kind : order)
|
foreach (int kind : order)
|
||||||
foreach (ERR_SiteDefinition site : definition.Sites)
|
foreach (ERR_SiteDefinition site : definition.Sites)
|
||||||
@@ -37,7 +45,7 @@ class ERR_TerrainSites : ERR_GeneratedSites
|
|||||||
site.PlacementResolved = false;
|
site.PlacementResolved = false;
|
||||||
// The queue, rather than RandomClear, owns placement including extraction.
|
// The queue, rather than RandomClear, owns placement including extraction.
|
||||||
site.RandomClear = false;
|
site.RandomClear = false;
|
||||||
if (kind == ERR_LocationKind.EXTRACTION) { site.RequireAirfield = true; site.AircraftOffset = vector.Zero; }
|
if (kind == ERR_LocationKind.EXTRACTION) { site.RequireAirfield = false; site.Label = "Extraction landing zone"; site.AircraftOffset = vector.Zero; }
|
||||||
if (kind == ERR_LocationKind.DEPOT) site.Label = "Supply cache";
|
if (kind == ERR_LocationKind.DEPOT) site.Label = "Supply cache";
|
||||||
m_Pending.Insert(site);
|
m_Pending.Insert(site);
|
||||||
}
|
}
|
||||||
@@ -62,89 +70,74 @@ class ERR_TerrainSites : ERR_GeneratedSites
|
|||||||
}
|
}
|
||||||
protected bool Navigable(vector position)
|
protected bool Navigable(vector position)
|
||||||
{
|
{
|
||||||
|
m_NavigationReason = "";
|
||||||
NavmeshWorldComponent mesh = GetGame().GetAIWorld().GetNavmeshWorldComponent("Soldiers");
|
NavmeshWorldComponent mesh = GetGame().GetAIWorld().GetNavmeshWorldComponent("Soldiers");
|
||||||
if (!mesh) return false;
|
if (!mesh) { m_NavigationReason = "Soldiers navmesh unavailable"; return false; }
|
||||||
if (!mesh.IsTileLoaded(position)) { m_WaitingNav = mesh.LoadTileIn(position); return false; }
|
if (!mesh.IsTileLoaded(position))
|
||||||
|
{
|
||||||
|
m_WaitingNav = mesh.LoadTileIn(position);
|
||||||
|
m_NavigationReason = "navmesh tile unavailable";
|
||||||
|
if (m_WaitingNav) m_NavigationReason = "waiting for navmesh tile";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
vector reachable;
|
vector reachable;
|
||||||
return mesh.GetReachablePoint(position, 10, reachable);
|
if (mesh.GetReachablePoint(position, 10, reachable)) return true;
|
||||||
|
m_NavigationReason = "no reachable AI ground";
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
protected bool Survey(ERR_SiteDefinition site, vector position, float heading, out float floor)
|
protected bool Survey(ERR_SiteDefinition site, vector position, float heading, out float floor)
|
||||||
{
|
{
|
||||||
if (!m_Terrain.Site(site, position, heading, floor)) { m_GeometryRejected++; return false; }
|
if (!m_Terrain.Site(site, position, heading, floor, PrisonAirportBuffer(m_Low, m_High))) { m_GeometryRejected++; RecordRejection(m_Terrain.Rejection()); return false; }
|
||||||
vector ground = Vector(position[0], floor, position[2]);
|
vector ground = Vector(position[0], floor, position[2]);
|
||||||
if (!Navigable(ground)) { m_NavigationRejected++; return false; }
|
// Aircraft spawn at the centre's terrain height, not the camp grid's minimum.
|
||||||
if (site.Guarded && !Navigable(ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(site.GuardOffset, heading)))) { m_NavigationRejected++; return false; }
|
// Using the minimum here falsely steepens the uphill edge of a valid pad.
|
||||||
if (site.ResponseCount > 0 && !Navigable(ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(site.ResponseOffset, heading)))) { m_NavigationRejected++; return false; }
|
if (site.Kind == ERR_LocationKind.EXTRACTION)
|
||||||
|
{
|
||||||
|
ground = ERR_Actors.Ground(position);
|
||||||
|
floor = ground[1];
|
||||||
|
}
|
||||||
|
if (!Navigable(ground)) { m_NavigationRejected++; RecordRejection(m_NavigationReason); return false; }
|
||||||
|
if (site.Guarded && !Navigable(ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(site.GuardOffset, heading)))) { m_NavigationRejected++; RecordRejection("guard area: " + m_NavigationReason); return false; }
|
||||||
|
if (site.ResponseCount > 0 && !Navigable(ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(site.ResponseOffset, heading)))) { m_NavigationRejected++; RecordRejection("patrol area: " + m_NavigationReason); return false; }
|
||||||
if (site.Kind != ERR_LocationKind.EXTRACTION) return true;
|
if (site.Kind != ERR_LocationKind.EXTRACTION) return true;
|
||||||
float padFloor;
|
float padFloor;
|
||||||
if (m_Terrain.Clear(ground, heading, 14, 0.8, padFloor))
|
if (m_Terrain.Clear(ground, heading, 14, 0.8, padFloor))
|
||||||
{
|
{
|
||||||
bool primaryClear;
|
bool primaryClear = ERR_AirfieldPlacement.ClearGround(ground);
|
||||||
if (site.RequireAirfield) primaryClear = ERR_AirfieldPlacement.Clear(ground);
|
if (!primaryClear) RecordRejection("primary aircraft clearance");
|
||||||
else primaryClear = ERR_AirfieldPlacement.ClearGround(ground);
|
else if (!ERR_RescueLanding.Fits(ground)) RecordRejection("secondary aircraft pad");
|
||||||
if (primaryClear && ERR_RescueLanding.Fits(ground)) return true;
|
else return true;
|
||||||
}
|
}
|
||||||
|
else RecordRejection("landing zone: " + m_Terrain.Rejection());
|
||||||
m_PadRejected++; return false;
|
m_PadRejected++; return false;
|
||||||
}
|
}
|
||||||
protected vector SamplePosition(ERR_SiteDefinition site)
|
protected vector SamplePosition(ERR_SiteDefinition site)
|
||||||
{
|
{
|
||||||
if (site.Kind != ERR_LocationKind.EXTRACTION || m_Airports.m_Positions.IsEmpty())
|
// Helicopters need a clear pad, not airport infrastructure or an airport-first pass.
|
||||||
return Vector(Math.RandomFloat(m_Low[0] + 60, m_High[0] - 60), 0, Math.RandomFloat(m_Low[2] + 60, m_High[2] - 60));
|
return Vector(Math.RandomFloat(m_Low[0] + 60, m_High[0] - 60), 0, Math.RandomFloat(m_Low[2] + 60, m_High[2] - 60));
|
||||||
vector centre = m_Airports.m_Positions[Math.RandomInt(0, m_Airports.m_Positions.Count())];
|
|
||||||
float angle = Math.RandomFloat(0, Math.PI2), radius = Math.RandomFloat(0, 450);
|
|
||||||
return centre + Vector(Math.Cos(angle) * radius, 0, Math.Sin(angle) * radius);
|
|
||||||
}
|
|
||||||
protected bool PrepareAirports()
|
|
||||||
{
|
|
||||||
if (m_AirportsReady) return true;
|
|
||||||
// The scenario ticks every 250ms. One 2km tile per tick made Chernarus
|
|
||||||
// spend roughly 48 seconds just waiting between airport queries. Batch
|
|
||||||
// inexpensive tiles, but yield after 4ms or eight queries. A native query
|
|
||||||
// itself cannot be interrupted, so check the budget after every call.
|
|
||||||
int started = System.GetTickCount();
|
|
||||||
for (int tile = 0; tile < 8; tile++)
|
|
||||||
{
|
|
||||||
m_Airports.Update();
|
|
||||||
if (m_Airports.Complete() || System.GetTickCount() - started >= 4) break;
|
|
||||||
}
|
|
||||||
if (!m_Airports.Complete()) return false;
|
|
||||||
if (m_Airports.m_Positions.IsEmpty())
|
|
||||||
{
|
|
||||||
// A painted runway or an island without an airport must not require a
|
|
||||||
// connector or administrator coordinates. Survey a helicopter landing zone
|
|
||||||
// with the same ground/navmesh/aircraft checks and finite placement budget.
|
|
||||||
foreach (ERR_SiteDefinition site : m_Pending)
|
|
||||||
if (site.Kind == ERR_LocationKind.EXTRACTION)
|
|
||||||
{
|
|
||||||
site.RequireAirfield = false;
|
|
||||||
site.Label = "Extraction landing zone";
|
|
||||||
}
|
|
||||||
Print("Reintroduced [terrain]: no airport infrastructure found; surveying a clear helicopter extraction landing zone");
|
|
||||||
}
|
|
||||||
PrintFormat("Reintroduced [generation]: airport survey ready in %1 ms; airports=%2", System.GetTickCount() - m_Began, m_Airports.m_Positions.Count());
|
|
||||||
m_AirportsReady = true; m_Began = System.GetTickCount();
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
override ERR_PrepareResult Tick()
|
override ERR_PrepareResult Tick()
|
||||||
{
|
{
|
||||||
if (m_Result != ERR_PrepareResult.PENDING) return m_Result;
|
if (m_Result != ERR_PrepareResult.PENDING) return m_Result;
|
||||||
if (!m_Started || !Replication.IsServer()) return ERR_PrepareResult.FAILED;
|
if (!m_Started || !Replication.IsServer()) return ERR_PrepareResult.FAILED;
|
||||||
if (!PrepareAirports()) return m_Result;
|
|
||||||
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;
|
if (m_StartupOnly && StartupReady()) return ERR_PrepareResult.PENDING;
|
||||||
ERR_SiteDefinition site = m_Pending[m_Index];
|
ERR_SiteDefinition site = m_Pending[m_Index];
|
||||||
if (m_Attempts >= 12000 || System.GetTickCount() - m_Began >= 30000)
|
ReportProgress(site);
|
||||||
|
if (m_Attempts >= SearchAttempts() || System.GetTickCount() - m_Began >= SearchMilliseconds())
|
||||||
{
|
{
|
||||||
PrintFormat("Reintroduced [terrain survey exhausted]: id=%1 attempts=%2 rejected geometry=%3 navigation=%4 aircraft=%5", site.Id, m_Attempts, m_GeometryRejected, m_NavigationRejected, m_PadRejected);
|
PrintFormat("Reintroduced [terrain survey exhausted]: id=%1 attempts=%2 rejected geometry=%3 navigation=%4 aircraft=%5", site.Id, m_Attempts, m_GeometryRejected, m_NavigationRejected, m_PadRejected);
|
||||||
|
ReportSearch(site, "exhausted");
|
||||||
if (!SkipOptional(site))
|
if (!SkipOptional(site))
|
||||||
{
|
{
|
||||||
m_Error = "Terrain has no safe navigable placement for " + site.Label + ". Check this map's Soldiers navmesh, terrain clearance and size; no foreign-map fallback was used.";
|
m_Error = "Terrain has no safe navigable placement for " + site.Label + " (" + site.Id + "). Checks: " + RejectionSummary();
|
||||||
m_Result = ERR_PrepareResult.FAILED; return m_Result;
|
m_Result = ERR_PrepareResult.FAILED; return m_Result;
|
||||||
}
|
}
|
||||||
m_Index++; m_Attempts = 0; m_Began = System.GetTickCount(); m_HaveCandidate = false;
|
m_Index++; m_Attempts = 0; m_Began = System.GetTickCount(); m_HaveCandidate = false;
|
||||||
m_GeometryRejected = 0; m_NavigationRejected = 0; m_PadRejected = 0;
|
m_GeometryRejected = 0; m_NavigationRejected = 0; m_PadRejected = 0;
|
||||||
|
ClearDiagnostics();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -157,11 +150,14 @@ class ERR_TerrainSites : ERR_GeneratedSites
|
|||||||
vector position = m_Candidate;
|
vector position = m_Candidate;
|
||||||
float heading = m_CandidateHeading, floor;
|
float heading = m_CandidateHeading, floor;
|
||||||
m_WaitingNav = false;
|
m_WaitingNav = false;
|
||||||
if (Separated(site, position, false) && Survey(site, position, heading, floor))
|
bool separated = Separated(site, position, false);
|
||||||
|
if (!separated) RecordRejection("site spacing / escape boundary");
|
||||||
|
if (separated && Survey(site, position, heading, floor))
|
||||||
{
|
{
|
||||||
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("Reintroduced [terrain site]: id=%1 position=%2 heading=%3 attempts=%4", site.Id, position, heading, m_Attempts);
|
PrintFormat("Reintroduced [terrain site]: id=%1 position=%2 heading=%3 attempts=%4", site.Id, position, heading, m_Attempts);
|
||||||
|
ReportSearch(site, "placed"); ClearDiagnostics();
|
||||||
m_Index++; m_Attempts = 0; m_Began = System.GetTickCount();
|
m_Index++; m_Attempts = 0; m_Began = System.GetTickCount();
|
||||||
m_GeometryRejected = 0; m_NavigationRejected = 0; m_PadRejected = 0;
|
m_GeometryRejected = 0; m_NavigationRejected = 0; m_PadRejected = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// World-authored pumps survive a game-mode takeover. Their native registrations
|
||||||
|
// belong to the discarded manager and must move to the replacement on each peer.
|
||||||
|
modded class SCR_SupportStationManagerComponent
|
||||||
|
{
|
||||||
|
void ERR_CollectTerrainStations(array<SCR_BaseSupportStationComponent> stations)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < m_mSupportStations.Count(); i++)
|
||||||
|
{
|
||||||
|
array<SCR_BaseSupportStationComponent> registered = m_mSupportStations.GetElement(i);
|
||||||
|
if (!registered) continue;
|
||||||
|
foreach (SCR_BaseSupportStationComponent station : registered)
|
||||||
|
if (station && !stations.Contains(station)) stations.Insert(station);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override void OnPostInit(IEntity owner)
|
||||||
|
{
|
||||||
|
super.OnPostInit(owner);
|
||||||
|
if (ERR_TerrainTakeoverMode.Cast(owner) && GetInstance() == this)
|
||||||
|
GetGame().ERR_RestoreTerrainSupportStations(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modded class SCR_BaseSupportStationComponent
|
||||||
|
{
|
||||||
|
bool ERR_CanRestoreTerrainStation()
|
||||||
|
{
|
||||||
|
IEntity owner = GetOwner();
|
||||||
|
return owner && GetGame() && owner.GetWorld() == GetGame().GetWorld()
|
||||||
|
&& UsesRange() && m_eCurrentDamageState != EDamageState.DESTROYED;
|
||||||
|
}
|
||||||
|
void ERR_TerrainStationRestored(SCR_SupportStationManagerComponent manager) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
modded class SCR_BaseItemSupportStationComponent
|
||||||
|
{
|
||||||
|
override void ERR_TerrainStationRestored(SCR_SupportStationManagerComponent manager)
|
||||||
|
{
|
||||||
|
super.ERR_TerrainStationRestored(manager);
|
||||||
|
// Item cleanup must remove the station from the manager it now belongs to.
|
||||||
|
if (m_ERRItemStationInitialized && UsesRange()) m_ERRItemStationManager = manager;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modded class ArmaReforgerScripted
|
||||||
|
{
|
||||||
|
protected ref array<SCR_BaseSupportStationComponent> m_ERRTerrainSupportStations = {};
|
||||||
|
void ERR_CaptureTerrainSupportStations(SCR_SupportStationManagerComponent original)
|
||||||
|
{
|
||||||
|
m_ERRTerrainSupportStations.Clear();
|
||||||
|
if (original) original.ERR_CollectTerrainStations(m_ERRTerrainSupportStations);
|
||||||
|
}
|
||||||
|
void ERR_RestoreTerrainSupportStations(SCR_SupportStationManagerComponent manager)
|
||||||
|
{
|
||||||
|
if (!manager) return;
|
||||||
|
int restored;
|
||||||
|
foreach (SCR_BaseSupportStationComponent station : m_ERRTerrainSupportStations)
|
||||||
|
{
|
||||||
|
if (!station || !station.ERR_CanRestoreTerrainStation()) continue;
|
||||||
|
manager.AddSupportStation(station);
|
||||||
|
station.ERR_TerrainStationRestored(manager);
|
||||||
|
restored++;
|
||||||
|
}
|
||||||
|
m_ERRTerrainSupportStations.Clear();
|
||||||
|
PrintFormat("Reintroduced [terrain support]: retained %1 existing support stations", restored);
|
||||||
|
}
|
||||||
|
override protected void OnBeforeWorldCleanup()
|
||||||
|
{
|
||||||
|
m_ERRTerrainSupportStations.Clear();
|
||||||
|
super.OnBeforeWorldCleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -243,6 +243,7 @@ modded class ArmaReforgerScripted
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Print("Reintroduced [terrain takeover]: stopping original scenario; preserving radio and chat managers");
|
Print("Reintroduced [terrain takeover]: stopping original scenario; preserving radio and chat managers");
|
||||||
|
ERR_CaptureTerrainSupportStations(SCR_SupportStationManagerComponent.Cast(m_ERRTerrainOriginal.FindComponent(SCR_SupportStationManagerComponent)));
|
||||||
m_ERRTerrainOriginal.OnGameEnd();
|
m_ERRTerrainOriginal.OnGameEnd();
|
||||||
if (m_DataCollectorComponent && m_DataCollectorComponent.GetOwner() == m_ERRTerrainOriginal) m_DataCollectorComponent = null;
|
if (m_DataCollectorComponent && m_DataCollectorComponent.GetOwner() == m_ERRTerrainOriginal) m_DataCollectorComponent = null;
|
||||||
SCR_EntityCatalogManagerComponent.ERR_ReleaseTerrainCatalog(m_ERRTerrainOriginal);
|
SCR_EntityCatalogManagerComponent.ERR_ReleaseTerrainCatalog(m_ERRTerrainOriginal);
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ modded class ArmaReforgerScripted
|
|||||||
m_ERRClientTerrainTakeover = true;
|
m_ERRClientTerrainTakeover = true;
|
||||||
SCR_BaseGameMode mode = SCR_BaseGameMode.Cast(m_ERRClientOldManagers[0]);
|
SCR_BaseGameMode mode = SCR_BaseGameMode.Cast(m_ERRClientOldManagers[0]);
|
||||||
PrintFormat("Reintroduced [terrain client]: drain retained mode=%1 incoming=%2", mode, incoming);
|
PrintFormat("Reintroduced [terrain client]: drain retained mode=%1 incoming=%2", mode, incoming);
|
||||||
|
ERR_CaptureTerrainSupportStations(m_ERROldSupport);
|
||||||
if (mode) mode.OnGameEnd();
|
if (mode) mode.OnGameEnd();
|
||||||
// Owner entities can already be invalid while native destruction is deferred.
|
// Owner entities can already be invalid while native destruction is deferred.
|
||||||
// Compare the captured component itself, never clear a newer singleton.
|
// Compare the captured component itself, never clear a newer singleton.
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ class ERR_TerrainDisplayName
|
|||||||
seen.Insert(world);
|
seen.Insert(world);
|
||||||
// Native subscene names can be plain paths. They are already the loaded
|
// Native subscene names can be plain paths. They are already the loaded
|
||||||
// terrain, not GUID resources that LoadContainer can traverse safely.
|
// terrain, not GUID resources that LoadContainer can traverse safely.
|
||||||
if (!world.IsExternal() && !world.IsInternal()) break;
|
// IsExternal/IsInternal also accept ordinary paths in the native runtime.
|
||||||
|
// Inspect the serialized GUID prefix without invoking resource resolution.
|
||||||
|
if (world.Length() < 18 || world.Substring(0, 1) != "{" || world.Substring(17, 1) != "}") break;
|
||||||
Resource resource = BaseContainerTools.LoadContainer(world);
|
Resource resource = BaseContainerTools.LoadContainer(world);
|
||||||
if (!resource || !resource.IsValid()) break;
|
if (!resource || !resource.IsValid()) break;
|
||||||
BaseContainer container = resource.GetResource().ToBaseContainer();
|
BaseContainer container = resource.GetResource().ToBaseContainer();
|
||||||
@@ -61,6 +63,18 @@ class ERR_TerrainDisplayName
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep the authored Escape header as the source of mission text.
|
||||||
|
class ERR_TerrainMissionText
|
||||||
|
{
|
||||||
|
static bool Load(out string description, out string details)
|
||||||
|
{
|
||||||
|
Resource resource = BaseContainerTools.LoadContainer("{C1E20667DCE324ED}Missions/ERR_Everon.conf");
|
||||||
|
if (!resource || !resource.IsValid()) return false;
|
||||||
|
BaseContainer header = resource.GetResource().ToBaseContainer();
|
||||||
|
return header && header.Get("m_sDescription", description) && header.Get("m_sDetails", details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Scenario selection still loads the terrain's header. Advertise the configured
|
// Scenario selection still loads the terrain's header. Advertise the configured
|
||||||
// Escape mode through the native DS name callback, then update local metadata
|
// Escape mode through the native DS name callback, then update local metadata
|
||||||
// only when the authority actually starts Escape or its bootstrap reaches a client.
|
// only when the authority actually starts Escape or its bootstrap reaches a client.
|
||||||
@@ -69,6 +83,9 @@ modded class ArmaReforgerScripted
|
|||||||
protected ref SCR_MissionHeader m_ERRPresentedTerrainHeader;
|
protected ref SCR_MissionHeader m_ERRPresentedTerrainHeader;
|
||||||
protected string m_ERROriginalTerrainTitle;
|
protected string m_ERROriginalTerrainTitle;
|
||||||
protected string m_ERROriginalTerrainGameMode;
|
protected string m_ERROriginalTerrainGameMode;
|
||||||
|
protected string m_ERROriginalTerrainDescription, m_ERROriginalTerrainDetails;
|
||||||
|
protected string m_ERRAppliedTerrainDescription, m_ERRAppliedTerrainDetails;
|
||||||
|
protected bool m_ERRTerrainTextApplied;
|
||||||
protected string m_ERRAppliedTerrainTitle;
|
protected string m_ERRAppliedTerrainTitle;
|
||||||
protected string m_ERRTitleWorld;
|
protected string m_ERRTitleWorld;
|
||||||
protected string m_ERRTerrainLabel;
|
protected string m_ERRTerrainLabel;
|
||||||
@@ -129,6 +146,14 @@ modded class ArmaReforgerScripted
|
|||||||
m_ERRAppliedTerrainTitle = ERR_TerrainMissionTitle(header);
|
m_ERRAppliedTerrainTitle = ERR_TerrainMissionTitle(header);
|
||||||
header.m_sName = m_ERRAppliedTerrainTitle;
|
header.m_sName = m_ERRAppliedTerrainTitle;
|
||||||
header.m_sGameMode = "Escape";
|
header.m_sGameMode = "Escape";
|
||||||
|
m_ERROriginalTerrainDescription = header.m_sDescription;
|
||||||
|
m_ERROriginalTerrainDetails = header.m_sDetails;
|
||||||
|
m_ERRTerrainTextApplied = ERR_TerrainMissionText.Load(m_ERRAppliedTerrainDescription, m_ERRAppliedTerrainDetails);
|
||||||
|
if (m_ERRTerrainTextApplied)
|
||||||
|
{
|
||||||
|
header.m_sDescription = m_ERRAppliedTerrainDescription;
|
||||||
|
header.m_sDetails = m_ERRAppliedTerrainDetails;
|
||||||
|
}
|
||||||
PrintFormat("Reintroduced [terrain presentation]: mission=%1; source scenario=%2", header.m_sName, header.GetHeaderResourceName());
|
PrintFormat("Reintroduced [terrain presentation]: mission=%1; source scenario=%2", header.m_sName, header.GetHeaderResourceName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,17 +168,30 @@ modded class ArmaReforgerScripted
|
|||||||
PrintFormat("Reintroduced [terrain presentation]: refreshed mission=%1; loaded world=%2", title, GetWorldFile());
|
PrintFormat("Reintroduced [terrain presentation]: refreshed mission=%1; loaded world=%2", title, GetWorldFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SCR_MissionHeader ERR_TerrainLoadingHeader(SCR_MissionHeader requested)
|
||||||
|
{
|
||||||
|
if (!requested) return null;
|
||||||
|
SCR_MissionHeader header = SCR_MissionHeader.Cast(GetMissionHeader());
|
||||||
|
// A loading screen may hold a copy. Only replace the confirmed scenario;
|
||||||
|
// another incoming map must never inherit the outgoing world's text.
|
||||||
|
if (!header || header != m_ERRPresentedTerrainHeader || m_ERRPresentedTerrainWorld != GetWorld()) return requested;
|
||||||
|
if (requested != header && (requested.GetHeaderResourceName().IsEmpty() || requested.GetHeaderResourceName() != header.GetHeaderResourceName())) return requested;
|
||||||
|
ERR_RefreshTerrainPresentation();
|
||||||
|
return header;
|
||||||
|
}
|
||||||
string ERR_TerrainLoadingTitle(SCR_MissionHeader requested)
|
string ERR_TerrainLoadingTitle(SCR_MissionHeader requested)
|
||||||
{
|
{
|
||||||
if (!requested) return "";
|
SCR_MissionHeader header = ERR_TerrainLoadingHeader(requested);
|
||||||
SCR_MissionHeader header = SCR_MissionHeader.Cast(GetMissionHeader());
|
if (!header) return "";
|
||||||
// The loading screen can hold a separate copy of the same scenario header.
|
|
||||||
// Never replace another incoming scenario's title with the outgoing world.
|
|
||||||
if (!header || header != m_ERRPresentedTerrainHeader || m_ERRPresentedTerrainWorld != GetWorld()) return requested.m_sName;
|
|
||||||
if (requested != header && (requested.GetHeaderResourceName().IsEmpty() || requested.GetHeaderResourceName() != header.GetHeaderResourceName())) return requested.m_sName;
|
|
||||||
ERR_RefreshTerrainPresentation();
|
|
||||||
return header.m_sName;
|
return header.m_sName;
|
||||||
}
|
}
|
||||||
|
string ERR_TerrainLoadingDescription(SCR_MissionHeader requested)
|
||||||
|
{
|
||||||
|
SCR_MissionHeader header = ERR_TerrainLoadingHeader(requested);
|
||||||
|
if (!header) return "";
|
||||||
|
if (!header.m_sDetails.IsEmpty()) return header.m_sDetails;
|
||||||
|
return header.m_sDescription;
|
||||||
|
}
|
||||||
|
|
||||||
override void OnUpdate(BaseWorld world, float timeslice)
|
override void OnUpdate(BaseWorld world, float timeslice)
|
||||||
{
|
{
|
||||||
@@ -171,6 +209,11 @@ modded class ArmaReforgerScripted
|
|||||||
{
|
{
|
||||||
// A later change by another system belongs to that system.
|
// A later change by another system belongs to that system.
|
||||||
if (m_ERRPresentedTerrainHeader.m_sName == m_ERRAppliedTerrainTitle) m_ERRPresentedTerrainHeader.m_sName = m_ERROriginalTerrainTitle;
|
if (m_ERRPresentedTerrainHeader.m_sName == m_ERRAppliedTerrainTitle) m_ERRPresentedTerrainHeader.m_sName = m_ERROriginalTerrainTitle;
|
||||||
|
if (m_ERRTerrainTextApplied)
|
||||||
|
{
|
||||||
|
if (m_ERRPresentedTerrainHeader.m_sDescription == m_ERRAppliedTerrainDescription) m_ERRPresentedTerrainHeader.m_sDescription = m_ERROriginalTerrainDescription;
|
||||||
|
if (m_ERRPresentedTerrainHeader.m_sDetails == m_ERRAppliedTerrainDetails) m_ERRPresentedTerrainHeader.m_sDetails = m_ERROriginalTerrainDetails;
|
||||||
|
}
|
||||||
if (m_ERRPresentedTerrainHeader.m_sGameMode == "Escape") m_ERRPresentedTerrainHeader.m_sGameMode = m_ERROriginalTerrainGameMode;
|
if (m_ERRPresentedTerrainHeader.m_sGameMode == "Escape") m_ERRPresentedTerrainHeader.m_sGameMode = m_ERROriginalTerrainGameMode;
|
||||||
}
|
}
|
||||||
m_ERRPresentedTerrainHeader = null;
|
m_ERRPresentedTerrainHeader = null;
|
||||||
@@ -178,6 +221,9 @@ modded class ArmaReforgerScripted
|
|||||||
m_ERROriginalTerrainTitle = "";
|
m_ERROriginalTerrainTitle = "";
|
||||||
m_ERROriginalTerrainGameMode = "";
|
m_ERROriginalTerrainGameMode = "";
|
||||||
m_ERRAppliedTerrainTitle = "";
|
m_ERRAppliedTerrainTitle = "";
|
||||||
|
m_ERRTerrainTextApplied = false;
|
||||||
|
m_ERROriginalTerrainDescription = ""; m_ERROriginalTerrainDetails = "";
|
||||||
|
m_ERRAppliedTerrainDescription = ""; m_ERRAppliedTerrainDetails = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
override protected void OnBeforeWorldCleanup()
|
override protected void OnBeforeWorldCleanup()
|
||||||
@@ -189,27 +235,37 @@ modded class ArmaReforgerScripted
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Vanilla copies the header title once, before the replicated Escape bootstrap
|
// Vanilla copies the header title once, before the replicated Escape bootstrap
|
||||||
// may arrive. Refresh only the title; do not recreate loading widgets/hints.
|
// may arrive. Refresh text in existing widgets; do not recreate widgets/hints.
|
||||||
modded class SCR_LoadingScreenComponent
|
modded class SCR_LoadingScreenComponent
|
||||||
{
|
{
|
||||||
protected ref SCR_MissionHeader m_ERRLoadingTerrainHeader;
|
protected ref SCR_MissionHeader m_ERRLoadingTerrainHeader;
|
||||||
protected string m_ERRLoadingTerrainTitle;
|
protected string m_ERRLoadingTerrainTitle, m_ERRLoadingTerrainDescription;
|
||||||
protected float m_ERRLoadingTerrainWait;
|
protected float m_ERRLoadingTerrainWait;
|
||||||
|
|
||||||
override void OnMissionDataRetrieved(SCR_MissionHeader header)
|
override void OnMissionDataRetrieved(SCR_MissionHeader header)
|
||||||
{
|
{
|
||||||
super.OnMissionDataRetrieved(header);
|
super.OnMissionDataRetrieved(header);
|
||||||
m_ERRLoadingTerrainHeader = header;
|
m_ERRLoadingTerrainHeader = header;
|
||||||
m_ERRLoadingTerrainTitle = ""; m_ERRLoadingTerrainWait = 0;
|
m_ERRLoadingTerrainTitle = ""; m_ERRLoadingTerrainDescription = ""; m_ERRLoadingTerrainWait = 0;
|
||||||
ERR_RefreshLoadingTerrainTitle();
|
ERR_RefreshLoadingTerrainTitle();
|
||||||
}
|
}
|
||||||
protected void ERR_RefreshLoadingTerrainTitle()
|
protected void ERR_RefreshLoadingTerrainTitle()
|
||||||
{
|
{
|
||||||
if (!m_ERRLoadingTerrainHeader || !m_Widgets || !m_Widgets.m_wTitle || !GetGame()) return;
|
if (!m_ERRLoadingTerrainHeader || !m_Widgets || !GetGame()) return;
|
||||||
string title = GetGame().ERR_TerrainLoadingTitle(m_ERRLoadingTerrainHeader);
|
string title = GetGame().ERR_TerrainLoadingTitle(m_ERRLoadingTerrainHeader);
|
||||||
if (title == m_ERRLoadingTerrainTitle) return;
|
if (m_Widgets.m_wTitle && title != m_ERRLoadingTerrainTitle)
|
||||||
m_ERRLoadingTerrainTitle = title;
|
{
|
||||||
m_Widgets.m_wTitle.SetText(title);
|
m_ERRLoadingTerrainTitle = title;
|
||||||
|
m_Widgets.m_wTitle.SetText(title);
|
||||||
|
}
|
||||||
|
// Match vanilla details/description precedence, translation and length limit.
|
||||||
|
string description = WidgetManager.Translate(GetGame().ERR_TerrainLoadingDescription(m_ERRLoadingTerrainHeader));
|
||||||
|
if (description.Length() > MAX_DESCRIPTION_LENGTH) description = description.Substring(0, MAX_DESCRIPTION_LENGTH) + "...";
|
||||||
|
if (m_Widgets.m_wDescription && description != m_ERRLoadingTerrainDescription)
|
||||||
|
{
|
||||||
|
m_ERRLoadingTerrainDescription = description;
|
||||||
|
m_Widgets.m_wDescription.SetText(description);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
override void Update(float timeSlice, float progress = 0, float minDurationRatio = 0)
|
override void Update(float timeSlice, float progress = 0, float minDurationRatio = 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,11 +35,10 @@ modded class SCR_PlayerController
|
|||||||
RplComponent component = RplComponent.Cast(target.FindComponent(RplComponent));
|
RplComponent component = RplComponent.Cast(target.FindComponent(RplComponent));
|
||||||
if (!component) return false;
|
if (!component) return false;
|
||||||
float now = GetGame().GetWorld().GetWorldTime();
|
float now = GetGame().GetWorld().GetWorldTime();
|
||||||
if (!m_ERRProbeViewMissing) m_ERRProbeViewBuffer.Push(component.Id(), now, now, "0 1.6 0", "0 0 1", "0 1 0", 45, 0.0125, RplId.Invalid(), vector.Zero);
|
// Feed complete sampled world poses, as the real receiver does. The target
|
||||||
vector local[4], world[4];
|
// remains a fixture stand-in; this does not qualify live multiplayer relay.
|
||||||
if (!m_ERRProbeViewBuffer.Read(component.Id(), now, seconds, local, fov, nearPlane)) return false;
|
if (!m_ERRProbeViewMissing) m_ERRProbeViewBuffer.Push(component.Id(), now, now, target.GetOrigin() + "0 1.6 0", "0 0 1", "0 1 0", 45, 0.0125, RplId.Invalid(), vector.Zero);
|
||||||
target.GetWorldTransform(world); Math3D.MatrixMultiply4(world, local, transform);
|
return m_ERRProbeViewBuffer.Read(component.Id(), now, seconds, transform, fov, nearPlane);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
string ERR_ProbeAcceptedRecoveryStatus() { return m_ERRShownRecoveryStatus; }
|
string ERR_ProbeAcceptedRecoveryStatus() { return m_ERRShownRecoveryStatus; }
|
||||||
@@ -65,7 +64,7 @@ modded class SCR_PlayerController
|
|||||||
bool ERR_ProbeOrbitFrame(float yaw, float pitch, float zoom, bool enabled, out vector transform[4])
|
bool ERR_ProbeOrbitFrame(float yaw, float pitch, float zoom, bool enabled, out vector transform[4])
|
||||||
{
|
{
|
||||||
string outcome;
|
string outcome;
|
||||||
if (!System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "unconscious-camera" || this != GetGame().GetPlayerController() || !ERR_ProbeCameraActive() || ERR_ObservedEntity()) return false;
|
if (!System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "unconscious-camera" || this != GetGame().GetPlayerController() || !ERR_ProbeCameraActive() || ERR_ObservedEntity() != GetControlledEntity() || ERR_Actors.CanFight(GetControlledEntity())) return false;
|
||||||
ERR_FollowSurvivorCameraComponent follow = ERR_ProbeFollowComponent();
|
ERR_FollowSurvivorCameraComponent follow = ERR_ProbeFollowComponent();
|
||||||
InputManager input = GetGame().GetInputManager();
|
InputManager input = GetGame().GetInputManager();
|
||||||
if (!follow || !input) return false;
|
if (!follow || !input) return false;
|
||||||
@@ -163,17 +162,30 @@ modded class ERR_GameMode
|
|||||||
foreach (SCR_PersistentDamageEffect effect : effects) m_ERRCameraDamage.TerminateDamageEffect(effect);
|
foreach (SCR_PersistentDamageEffect effect : effects) m_ERRCameraDamage.TerminateDamageEffect(effect);
|
||||||
m_ERRCameraDamage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, true);
|
m_ERRCameraDamage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, true);
|
||||||
}
|
}
|
||||||
protected bool ERR_CameraNoTargetOrbit(SCR_PlayerController pc)
|
protected bool ERR_CameraOwnBodyOrbit(SCR_PlayerController pc)
|
||||||
{
|
{
|
||||||
vector body = m_ERRCameraBody.GetOrigin();
|
vector body = m_ERRCameraBody.GetOrigin();
|
||||||
|
pc.ERR_UpdateObserverServer(1);
|
||||||
|
pc.ERR_UpdateObserverServer(-1);
|
||||||
|
if (!ERR_CameraCheck(pc.ERR_ObservedEntity() == m_ERRCameraBody, "solo forward/backward cycling retains the actual own body")) return false;
|
||||||
|
ERR_FollowSurvivorCameraComponent follow = pc.ERR_ProbeFollowComponent();
|
||||||
|
follow.Input(true, false, false, true);
|
||||||
|
follow.Input(true, false, false, false);
|
||||||
|
if (!ERR_CameraCheck(follow.IsPlayerView(), "own-body orbit is tested with player-view mode selected")) return false;
|
||||||
vector baseline[4], yaw[4], pitch[4], zoom[4], blocked[4];
|
vector baseline[4], yaw[4], pitch[4], zoom[4], blocked[4];
|
||||||
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(0, 0, 0, true, baseline), "no-target frame accepts native orbit action values")) return false;
|
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(0, 0, 0, true, baseline), "own-body frame accepts native orbit action values")) return false;
|
||||||
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(1, 0, 0, true, yaw) && vector.Distance(baseline[2], yaw[2]) > 0.05, "no-target yaw action changes production camera direction")) return false;
|
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(1, 0, 0, true, yaw) && vector.Distance(baseline[2], yaw[2]) > 0.05, "own-body yaw action changes production camera direction")) return false;
|
||||||
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(0, -1, 0, true, pitch) && Math.AbsFloat(yaw[2][1] - pitch[2][1]) > 0.03, "no-target pitch action changes production camera elevation")) return false;
|
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(0, -1, 0, true, pitch) && Math.AbsFloat(yaw[2][1] - pitch[2][1]) > 0.03, "own-body pitch action changes production camera elevation")) return false;
|
||||||
vector aim = pc.ERR_ObserverAnchor() + "0 1.5 0";
|
vector aim = m_ERRCameraBody.GetOrigin() + "0 0.4 0";
|
||||||
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(0, 0, -1, true, zoom) && vector.Distance(zoom[3], aim) < vector.Distance(pitch[3], aim) - 0.05, "no-target zoom action moves production camera closer to casualty anchor")) return false;
|
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(0, 0, -1, true, zoom) && vector.Distance(zoom[3], aim) < vector.Distance(pitch[3], aim) - 0.05, "own-body zoom action moves production camera closer to casualty anchor")) return false;
|
||||||
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(1, -1, -1, false, blocked) && vector.Distance(zoom[3], blocked[3]) < 0.001 && vector.Distance(zoom[2], blocked[2]) < 0.001, "disabled no-target input preserves camera pose")) return false;
|
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(1, -1, -1, false, blocked) && vector.Distance(zoom[3], blocked[3]) < 0.001 && vector.Distance(zoom[2], blocked[2]) < 0.001, "disabled own-body input preserves camera pose")) return false;
|
||||||
if (!ERR_CameraCheck(vector.Distance(body, m_ERRCameraBody.GetOrigin()) < 0.001 && pc.GetControlledEntity() == m_ERRCameraBody && !pc.ERR_ObservedEntity(), "no-target orbit leaves casualty and possession unchanged")) return false;
|
if (!ERR_CameraCheck(vector.Distance(body, m_ERRCameraBody.GetOrigin()) < 0.001 && pc.GetControlledEntity() == m_ERRCameraBody && pc.ERR_ObservedEntity() == m_ERRCameraBody, "own-body orbit leaves casualty and possession unchanged")) return false;
|
||||||
|
// The disabled frame starts the normal two-frame input-resume gate.
|
||||||
|
follow.Input(true, false, false, false);
|
||||||
|
follow.Input(true, false, false, false);
|
||||||
|
follow.Input(true, false, false, true);
|
||||||
|
follow.Input(true, false, false, false);
|
||||||
|
if (!ERR_CameraCheck(!follow.IsPlayerView(), "own-body camera returns to ordinary third-person selection")) return false;
|
||||||
Math3D.MatrixCopy(zoom, m_ERRCameraOrbitPose);
|
Math3D.MatrixCopy(zoom, m_ERRCameraOrbitPose);
|
||||||
Print("ERR CAMERA ORBIT: native action/frame regression; physical mouse delivery and multiplayer are not tested");
|
Print("ERR CAMERA ORBIT: native action/frame regression; physical mouse delivery and multiplayer are not tested");
|
||||||
return true;
|
return true;
|
||||||
@@ -193,6 +205,9 @@ modded class ERR_GameMode
|
|||||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(m_ERRCameraBody.FindComponent(InventoryStorageManagerComponent));
|
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(m_ERRCameraBody.FindComponent(InventoryStorageManagerComponent));
|
||||||
m_ERRCameraWeapon = ERR_Actors.Spawn("{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et", m_ERRCameraBody.GetOrigin()); Own(m_ERRCameraWeapon);
|
m_ERRCameraWeapon = ERR_Actors.Spawn("{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et", m_ERRCameraBody.GetOrigin()); Own(m_ERRCameraWeapon);
|
||||||
if (!ERR_CameraCheck(m_ERRCameraWeapon && inventory && inventory.TryInsertItem(m_ERRCameraWeapon, EStoragePurpose.PURPOSE_WEAPON_PROXY), "one physical rifle granted to the player")) return;
|
if (!ERR_CameraCheck(m_ERRCameraWeapon && inventory && inventory.TryInsertItem(m_ERRCameraWeapon, EStoragePurpose.PURPOSE_WEAPON_PROXY), "one physical rifle granted to the player")) return;
|
||||||
|
// Keep this deliberately unconscious solo fixture recoverable under the all-down rules.
|
||||||
|
IEntity injector = ERR_Actors.Spawn(ERR_EnemyMedicalCatalog.EPI, m_ERRCameraBody.GetOrigin()); Own(injector);
|
||||||
|
if (!ERR_CameraCheck(injector && inventory.TryInsertItem(injector) && Settings().Set("selfEpinephrine", 1), "carried injector keeps the camera fixture recoverable")) return;
|
||||||
m_ERRCameraStage = -1; m_ERRCameraWait = now + 1500; return;
|
m_ERRCameraStage = -1; m_ERRCameraWait = now + 1500; return;
|
||||||
}
|
}
|
||||||
SCR_CharacterControllerComponent character = SCR_CharacterControllerComponent.Cast(m_ERRCameraBody.FindComponent(SCR_CharacterControllerComponent));
|
SCR_CharacterControllerComponent character = SCR_CharacterControllerComponent.Cast(m_ERRCameraBody.FindComponent(SCR_CharacterControllerComponent));
|
||||||
@@ -209,22 +224,24 @@ modded class ERR_GameMode
|
|||||||
m_ERRCameraBody = pc.GetControlledEntity();
|
m_ERRCameraBody = pc.GetControlledEntity();
|
||||||
m_ERRCameraDamage = SCR_CharacterDamageManagerComponent.Cast(m_ERRCameraBody.FindComponent(SCR_CharacterDamageManagerComponent));
|
m_ERRCameraDamage = SCR_CharacterDamageManagerComponent.Cast(m_ERRCameraBody.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||||
if (!ERR_CameraCheck(m_ERRCameraDamage && m_ERRCameraDamage.ERR_IsRevivablePlayer(), "admitted player retains treatable body")) return;
|
if (!ERR_CameraCheck(m_ERRCameraDamage && m_ERRCameraDamage.ERR_IsRevivablePlayer(), "admitted player retains treatable body")) return;
|
||||||
ERR_CameraKnockout(); m_ERRCameraStage = 1; m_ERRCameraWait = now + 3000; return;
|
// Allow the server medical sample plus the 3s spectator hint settling window.
|
||||||
|
ERR_CameraKnockout(); m_ERRCameraStage = 1; m_ERRCameraWait = now + 6000; return;
|
||||||
}
|
}
|
||||||
if (m_ERRCameraStage == 1)
|
if (m_ERRCameraStage == 1)
|
||||||
{
|
{
|
||||||
if (!ERR_CameraCheck(pc.ERR_ObserverState() == ERR_ObserverReason.UNCONSCIOUS && pc.ERR_ProbeCameraActive(), "native knockout automatically opens the manual third-person camera")) return;
|
if (!ERR_CameraCheck(pc.ERR_ObserverState() == ERR_ObserverReason.UNCONSCIOUS && pc.ERR_ProbeCameraActive(), "native knockout automatically opens the manual third-person camera")) return;
|
||||||
if (!ERR_CameraCheck(pc.GetControlledEntity() == m_ERRCameraBody && ERR_Actors.Alive(m_ERRCameraBody) && !ERR_Actors.CanFight(m_ERRCameraBody), "possession and living unconscious body unchanged")) return;
|
if (!ERR_CameraCheck(pc.GetControlledEntity() == m_ERRCameraBody && ERR_Actors.Alive(m_ERRCameraBody) && !ERR_Actors.CanFight(m_ERRCameraBody), "possession and living unconscious body unchanged")) return;
|
||||||
if (!ERR_CameraCheck(!pc.ERR_ObservedEntity() && vector.DistanceXZ(pc.ERR_ObserverAnchor(), m_ERRCameraBody.GetOrigin()) < 1, "no teammate falls back to the casualty body")) return;
|
if (!ERR_CameraCheck(pc.ERR_ObservedEntity() == m_ERRCameraBody && vector.DistanceXZ(pc.ERR_ObserverAnchor(), m_ERRCameraBody.GetOrigin()) < 1, "own casualty body is a real selected camera target")) return;
|
||||||
if (!ERR_CameraCheck(playerInventory.Contains(m_ERRCameraWeapon), "enabled retention keeps the exact rifle with the unconscious player")) return;
|
if (!ERR_CameraCheck(playerInventory.Contains(m_ERRCameraWeapon), "enabled retention keeps the exact rifle with the unconscious player")) return;
|
||||||
if (!ERR_CameraCheck(!RecoveryStatus().IsEmpty() && RecoveryStatus().Contains("Treatment or natural recovery") && pc.ERR_ProbeAcceptedRecoveryStatus() == RecoveryStatus(), "accepted observer hint contains the current ongoing recovery status")) return;
|
string expectedRecovery = RecoveryStatus() + "\n" + pc.ERR_SelfEpinephrineHint();
|
||||||
if (!ERR_CameraNoTargetOrbit(pc)) return;
|
if (!ERR_CameraCheck(RecoveryStatus().Contains("Self epinephrine is available") && pc.ERR_ProbeAcceptedRecoveryStatus() == expectedRecovery, "accepted observer hint contains the current recovery status and self-injection controls")) return;
|
||||||
|
if (!ERR_CameraOwnBodyOrbit(pc)) return;
|
||||||
m_ERRCameraStage = 11; m_ERRCameraWait = now + 500; return;
|
m_ERRCameraStage = 11; m_ERRCameraWait = now + 500; return;
|
||||||
}
|
}
|
||||||
if (m_ERRCameraStage == 11)
|
if (m_ERRCameraStage == 11)
|
||||||
{
|
{
|
||||||
vector applied[4]; GetGame().GetCameraManager().CurrentCamera().GetWorldTransform(applied);
|
vector applied[4]; GetGame().GetCameraManager().CurrentCamera().GetWorldTransform(applied);
|
||||||
if (!ERR_CameraCheck(pc.ERR_ProbeCameraActive() && vector.Distance(applied[2], m_ERRCameraOrbitPose[2]) < 0.01, "native camera applies and retains no-target orbit direction on subsequent frames")) return;
|
if (!ERR_CameraCheck(pc.ERR_ProbeCameraActive() && vector.Distance(applied[2], m_ERRCameraOrbitPose[2]) < 0.01, "native camera applies and retains own-body orbit direction on subsequent frames")) return;
|
||||||
if (!ERR_CameraOpenChat(pc, false)) return;
|
if (!ERR_CameraOpenChat(pc, false)) return;
|
||||||
m_ERRCameraStage = 111; m_ERRCameraWait = now + 500; return;
|
m_ERRCameraStage = 111; m_ERRCameraWait = now + 500; return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ modded class SCR_PlayerController
|
|||||||
protected void ERR_RequestWelcomePermit(int serial)
|
protected void ERR_RequestWelcomePermit(int serial)
|
||||||
{
|
{
|
||||||
ERR_GameMode mode = ERR_GameMode.Get();
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
if (!Replication.IsServer() || !mode || !mode.Active() || serial <= 0 || mode.ERR_WelcomePending() || !ERR_WelcomeVoiceRules.URL(mode.ERR_WelcomeVoiceURL())) return;
|
if (!Replication.IsServer() || !mode || !mode.Active() || serial <= 0 || mode.ERR_WelcomePending() || !ERR_WelcomeVoiceRules.Source(mode.ERR_WelcomeVoiceURL())) return;
|
||||||
ERR_PlayerLife life = mode.Players().CurrentActor(GetControlledEntity());
|
ERR_PlayerLife life = mode.Players().CurrentActor(GetControlledEntity());
|
||||||
if (!life || GetGame().GetPlayerManager().GetPlayerController(life.ConnectionId) != this) return;
|
if (!life || GetGame().GetPlayerManager().GetPlayerController(life.ConnectionId) != this) return;
|
||||||
float now = GetGame().GetWorld().GetWorldTime();
|
float now = GetGame().GetWorld().GetWorldTime();
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ class ERR_WelcomeVoiceRules
|
|||||||
if (state == "fog" || state == "foggy") return "fog";
|
if (state == "fog" || state == "foggy") return "fog";
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
static bool Source(string value)
|
||||||
|
{
|
||||||
|
return URL(value) || !ERR_BakedSpeech.Graph(value).IsEmpty();
|
||||||
|
}
|
||||||
static string Briefing(string terrain, string current, string next)
|
static string Briefing(string terrain, string current, string next)
|
||||||
{
|
{
|
||||||
string text = "Listen up. Welcome to Escape Reintroduced.";
|
string text = "Listen up. Welcome to Escape Reintroduced.";
|
||||||
@@ -75,6 +79,28 @@ class ERR_WelcomeVoiceRules
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The native REST body limit is below our maximum base64 WAV size. Fetch bounded
|
||||||
|
// parts sequentially, checking offsets and the advertised total before decoding.
|
||||||
|
class ERR_VoiceTransfer
|
||||||
|
{
|
||||||
|
protected string m_Data;
|
||||||
|
protected int m_Total;
|
||||||
|
int Part() { return m_Data.Length() / 262144; }
|
||||||
|
string Data() { return m_Data; }
|
||||||
|
void Clear() { m_Data = ""; m_Total = 0; }
|
||||||
|
// -1 rejects the transfer, 0 requests another part, 1 completes it.
|
||||||
|
int Accept(int offset, int total, string data)
|
||||||
|
{
|
||||||
|
if (total < 60 || total > 2796204 || total % 4 != 0) return -1;
|
||||||
|
if (offset != m_Data.Length() || offset >= total || offset % 262144 != 0) return -1;
|
||||||
|
if (m_Total != 0 && total != m_Total) return -1;
|
||||||
|
if (data.Length() != Math.Min(262144, total - offset)) return -1;
|
||||||
|
m_Total = total; m_Data += data;
|
||||||
|
if (m_Data.Length() == m_Total) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// REST callbacks run on the game thread. Keep reconstruction out of the callback
|
// REST callbacks run on the game thread. Keep reconstruction out of the callback
|
||||||
// and yield between bounded pieces, including when several clips arrive at once.
|
// and yield between bounded pieces, including when several clips arrive at once.
|
||||||
class ERR_WelcomeVoiceDecoder
|
class ERR_WelcomeVoiceDecoder
|
||||||
@@ -164,6 +190,13 @@ modded class ERR_GameMode
|
|||||||
[RplProp()] protected string m_ERRWelcomeVoiceURL;
|
[RplProp()] protected string m_ERRWelcomeVoiceURL;
|
||||||
[RplProp()] protected string m_ERRWelcomeVoiceText;
|
[RplProp()] protected string m_ERRWelcomeVoiceText;
|
||||||
[RplProp()] protected bool m_ERRWelcomePending;
|
[RplProp()] protected bool m_ERRWelcomePending;
|
||||||
|
[RplProp()] protected bool m_ERRRecordingsEnabled = true;
|
||||||
|
bool ERR_RecordingsEnabled() { return m_ERRRecordingsEnabled; }
|
||||||
|
void ERR_SetRecordingsEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer()) return;
|
||||||
|
m_ERRRecordingsEnabled = enabled; Replication.BumpMe();
|
||||||
|
}
|
||||||
protected ref ERR_SpeechService m_ERRSpeech;
|
protected ref ERR_SpeechService m_ERRSpeech;
|
||||||
protected string m_ERRWelcomeAttempt;
|
protected string m_ERRWelcomeAttempt;
|
||||||
protected float m_ERRWelcomeWait;
|
protected float m_ERRWelcomeWait;
|
||||||
@@ -171,6 +204,7 @@ modded class ERR_GameMode
|
|||||||
string ERR_WelcomeVoiceURL() { return m_ERRWelcomeVoiceURL; }
|
string ERR_WelcomeVoiceURL() { return m_ERRWelcomeVoiceURL; }
|
||||||
string ERR_WelcomeVoiceText() { return m_ERRWelcomeVoiceText; }
|
string ERR_WelcomeVoiceText() { return m_ERRWelcomeVoiceText; }
|
||||||
bool ERR_WelcomePending() { return m_ERRWelcomePending; }
|
bool ERR_WelcomePending() { return m_ERRWelcomePending; }
|
||||||
|
bool ERR_UsesBundledWelcome() { return m_ERRSpeech && m_ERRSpeech.Bundled(); }
|
||||||
void ERR_SetWelcomePending(bool pending) { m_ERRWelcomePending = pending; Replication.BumpMe(); }
|
void ERR_SetWelcomePending(bool pending) { m_ERRWelcomePending = pending; Replication.BumpMe(); }
|
||||||
protected override void EOnInit(IEntity owner)
|
protected override void EOnInit(IEntity owner)
|
||||||
{
|
{
|
||||||
@@ -185,8 +219,8 @@ modded class ERR_GameMode
|
|||||||
{
|
{
|
||||||
if (!Replication.IsServer() || !m_ERRSpeech) return;
|
if (!Replication.IsServer() || !m_ERRSpeech) return;
|
||||||
m_ERRWelcomePending = false;
|
m_ERRWelcomePending = false;
|
||||||
m_ERRWelcomeVoiceURL = m_ERRSpeech.StaticURL();
|
m_ERRWelcomeVoiceURL = ERR_BakedSpeech.Welcome();
|
||||||
m_ERRWelcomeVoiceText = "Welcome to Escape Reintroduced. Work together, stay alert, and find your way off the island. Good luck.";
|
m_ERRWelcomeVoiceText = ERR_BakedSpeech.WelcomeText();
|
||||||
Replication.BumpMe();
|
Replication.BumpMe();
|
||||||
}
|
}
|
||||||
void ERR_PublishWelcome(string url, string text)
|
void ERR_PublishWelcome(string url, string text)
|
||||||
@@ -222,6 +256,7 @@ modded class ERR_GameMode
|
|||||||
{
|
{
|
||||||
if (!Replication.IsServer() || !m_ERRSpeech) return;
|
if (!Replication.IsServer() || !m_ERRSpeech) return;
|
||||||
m_ERRSpeech.Tick();
|
m_ERRSpeech.Tick();
|
||||||
|
if (ERR_UsesBundledWelcome()) return;
|
||||||
if (!Active() || !m_ERRSpeech.Ready()) return;
|
if (!Active() || !m_ERRSpeech.Ready()) return;
|
||||||
m_ERRWelcomeWait -= seconds;
|
m_ERRWelcomeWait -= seconds;
|
||||||
if (m_ERRWelcomeWait > 0) return;
|
if (m_ERRWelcomeWait > 0) return;
|
||||||
@@ -261,6 +296,9 @@ class ERR_WelcomeVoiceClient : Managed
|
|||||||
protected bool m_Stopped;
|
protected bool m_Stopped;
|
||||||
protected bool m_Downloaded;
|
protected bool m_Downloaded;
|
||||||
protected ref RestCallback m_Callback;
|
protected ref RestCallback m_Callback;
|
||||||
|
protected ref ERR_VoiceTransfer m_Transfer;
|
||||||
|
protected string m_DownloadURL;
|
||||||
|
protected ResourceName m_BakedGraph;
|
||||||
protected ref ERR_WelcomeVoiceDecoder m_Decoder;
|
protected ref ERR_WelcomeVoiceDecoder m_Decoder;
|
||||||
protected ref FileHandle m_Output;
|
protected ref FileHandle m_Output;
|
||||||
protected int m_PreparationBytes;
|
protected int m_PreparationBytes;
|
||||||
@@ -287,28 +325,63 @@ class ERR_WelcomeVoiceClient : Managed
|
|||||||
void Tick(float seconds, bool ready, string url, string text)
|
void Tick(float seconds, bool ready, string url, string text)
|
||||||
{
|
{
|
||||||
if (m_Stopped) return;
|
if (m_Stopped) return;
|
||||||
|
// Check before playing prepared audio or claiming the once-only welcome.
|
||||||
|
if (m_Mode && !m_Mode.ERR_RecordingsEnabled() && (!m_BakedGraph.IsEmpty() || !ERR_BakedSpeech.Graph(url).IsEmpty()))
|
||||||
|
{
|
||||||
|
m_Wait = 0;
|
||||||
|
if (m_Started) Stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (m_Downloaded && ready && Current()) { m_Downloaded = false; Play(); }
|
if (m_Downloaded && ready && Current()) { m_Downloaded = false; Play(); }
|
||||||
if (m_Started || m_Connection.Attempted()) return;
|
if (m_Started || m_Connection.Attempted()) return;
|
||||||
if (!ready || !ERR_WelcomeVoiceRules.URL(url) || !ERR_WelcomeVoiceRules.HTTPReady()) { m_Wait = 0; return; }
|
ResourceName baked = ERR_BakedSpeech.Graph(url);
|
||||||
|
if (!ready || !ERR_WelcomeVoiceRules.Source(url)) { m_Wait = 0; return; }
|
||||||
|
if (baked.IsEmpty() && !ERR_WelcomeVoiceRules.HTTPReady()) { m_Wait = 0; return; }
|
||||||
m_Wait += seconds;
|
m_Wait += seconds;
|
||||||
if (m_Wait < 3 || !Permit()) return;
|
if (m_Wait < 3 || !Permit()) return;
|
||||||
if (!m_Connection.Claim()) return;
|
if (!m_Connection.Claim()) return;
|
||||||
m_Started = true; m_Text = text; // One attempt per connection, including failures.
|
m_Started = true; m_Text = text; // One attempt per connection, including failures.
|
||||||
|
m_BakedGraph = baked;
|
||||||
|
if (!m_BakedGraph.IsEmpty()) { m_Downloaded = true; return; }
|
||||||
FileIO.DeleteFile(m_Path);
|
FileIO.DeleteFile(m_Path);
|
||||||
|
m_DownloadURL = url;
|
||||||
|
m_Transfer = new ERR_VoiceTransfer();
|
||||||
|
RequestPart();
|
||||||
|
Print("Reintroduced [welcome voice]: downloading startup clip in bounded parts");
|
||||||
|
}
|
||||||
|
protected void RequestPart()
|
||||||
|
{
|
||||||
|
if (!Current() || !m_Transfer) { CancelDownload(); return; }
|
||||||
|
string separator = "?";
|
||||||
|
if (m_DownloadURL.IndexOf("?") >= 0) separator = "&";
|
||||||
|
string url = m_DownloadURL + separator + "part=" + m_Transfer.Part().ToString();
|
||||||
m_Callback = new RestCallback();
|
m_Callback = new RestCallback();
|
||||||
m_Callback.SetOnSuccess(Downloaded); m_Callback.SetOnError(Failed);
|
m_Callback.SetOnSuccess(Downloaded); m_Callback.SetOnError(Failed);
|
||||||
RestContext context = GetGame().GetRestApi().GetContext(url);
|
RestContext context = GetGame().GetRestApi().GetContext(url);
|
||||||
if (!context) { m_Callback = null; Print("Reintroduced [welcome voice]: HTTP unavailable; no retry", LogLevel.WARNING); return; }
|
if (!context) { m_Callback = null; CancelDownload(); Print("Reintroduced [welcome voice]: HTTP unavailable; no retry", LogLevel.WARNING); return; }
|
||||||
context.SetTimeout(20);
|
context.SetTimeout(20);
|
||||||
context.GET(m_Callback, "");
|
context.GET(m_Callback, "");
|
||||||
Print("Reintroduced [welcome voice]: downloading startup clip");
|
|
||||||
}
|
}
|
||||||
protected void Downloaded(RestCallback callback)
|
protected void Downloaded(RestCallback callback)
|
||||||
{
|
{
|
||||||
m_Callback = null;
|
m_Callback = null;
|
||||||
if (!Current()) { FileIO.DeleteFile(m_Path); return; }
|
if (!Current() || !m_Transfer) { CancelDownload(); return; }
|
||||||
|
JsonLoadContext response = new JsonLoadContext();
|
||||||
|
int offset, total; string data;
|
||||||
|
bool valid = callback.GetData().Length() <= 262272 && response.LoadFromString(callback.GetData());
|
||||||
|
if (valid) valid = response.ReadValue("offset", offset) && response.ReadValue("total", total) && response.ReadValue("data", data);
|
||||||
|
int result = -1;
|
||||||
|
if (valid) result = m_Transfer.Accept(offset, total, data);
|
||||||
|
if (result < 0) { CancelDownload(); Print("Reintroduced [welcome voice]: invalid audio part; no retry", LogLevel.WARNING); return; }
|
||||||
|
if (result == 0) { RequestPart(); return; }
|
||||||
|
BeginPreparation(m_Transfer.Data());
|
||||||
|
m_Transfer = null;
|
||||||
|
}
|
||||||
|
protected void BeginPreparation(string encoded)
|
||||||
|
{
|
||||||
|
if (!Current()) { CancelDownload(); return; }
|
||||||
m_Decoder = new ERR_WelcomeVoiceDecoder();
|
m_Decoder = new ERR_WelcomeVoiceDecoder();
|
||||||
if (!m_Decoder.Begin(callback.GetData()))
|
if (!m_Decoder.Begin(encoded))
|
||||||
{ m_Decoder = null; Print("Reintroduced [welcome voice]: invalid base64 clip", LogLevel.WARNING); return; }
|
{ m_Decoder = null; Print("Reintroduced [welcome voice]: invalid base64 clip", LogLevel.WARNING); return; }
|
||||||
GetGame().ERR_QueueVoiceDownload(this);
|
GetGame().ERR_QueueVoiceDownload(this);
|
||||||
}
|
}
|
||||||
@@ -316,6 +389,7 @@ class ERR_WelcomeVoiceClient : Managed
|
|||||||
{
|
{
|
||||||
if (m_Output) { m_Output.Close(); m_Output = null; }
|
if (m_Output) { m_Output.Close(); m_Output = null; }
|
||||||
m_Decoder = null; m_Downloaded = false;
|
m_Decoder = null; m_Downloaded = false;
|
||||||
|
m_Transfer = null; m_DownloadURL = "";
|
||||||
FileIO.DeleteFile(m_Path);
|
FileIO.DeleteFile(m_Path);
|
||||||
}
|
}
|
||||||
// Called for exactly one queued client per game update, shared by greetings,
|
// Called for exactly one queued client per game update, shared by greetings,
|
||||||
@@ -354,7 +428,14 @@ class ERR_WelcomeVoiceClient : Managed
|
|||||||
}
|
}
|
||||||
protected void Play()
|
protected void Play()
|
||||||
{
|
{
|
||||||
m_Handle = AudioSystem.PlaySound(m_Path);
|
if (!m_BakedGraph.IsEmpty())
|
||||||
|
{
|
||||||
|
vector transform[4]; Math3D.MatrixIdentity4(transform);
|
||||||
|
CameraBase camera = GetGame().GetCameraManager().CurrentCamera();
|
||||||
|
if (camera) camera.GetWorldTransform(transform);
|
||||||
|
if (AudioSystem.PlayEventInitialize(m_BakedGraph)) m_Handle = AudioSystem.PlayEvent(m_BakedGraph, "ERR_SPEECH", transform);
|
||||||
|
}
|
||||||
|
else m_Handle = AudioSystem.PlaySound(m_Path);
|
||||||
if (m_Handle == AudioHandle.Invalid) Print("Reintroduced [welcome voice]: playback unavailable", LogLevel.WARNING);
|
if (m_Handle == AudioHandle.Invalid) Print("Reintroduced [welcome voice]: playback unavailable", LogLevel.WARNING);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -366,8 +447,8 @@ class ERR_WelcomeVoiceClient : Managed
|
|||||||
protected void Failed(RestCallback callback)
|
protected void Failed(RestCallback callback)
|
||||||
{
|
{
|
||||||
m_Callback = null;
|
m_Callback = null;
|
||||||
FileIO.DeleteFile(m_Path);
|
CancelDownload();
|
||||||
if (!m_Stopped) PrintFormat("Reintroduced [welcome voice]: download failed (HTTP %1); no retry", callback.GetHttpCode());
|
if (!m_Stopped) PrintFormat("Reintroduced [welcome voice]: download failed (HTTP %1, REST %2); no retry", callback.GetHttpCode(), callback.GetRestResult());
|
||||||
}
|
}
|
||||||
void Stop()
|
void Stop()
|
||||||
{
|
{
|
||||||
@@ -411,7 +492,7 @@ modded class ArmaReforgerScripted
|
|||||||
void ERR_PlaySpeechPreview(string url, string text, string title = "Speech test")
|
void ERR_PlaySpeechPreview(string url, string text, string title = "Speech test")
|
||||||
{
|
{
|
||||||
ERR_GameMode mode = ERR_GameMode.Get();
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
if (!mode || !ERR_WelcomeVoiceRules.URL(url) || !ERR_SpeechRules.Text(text)) return;
|
if (!mode || !ERR_WelcomeVoiceRules.Source(url) || !ERR_SpeechRules.Text(text)) return;
|
||||||
if (m_ERRSpeechPreview) m_ERRSpeechPreview.Stop();
|
if (m_ERRSpeechPreview) m_ERRSpeechPreview.Stop();
|
||||||
m_ERRSpeechPreview = new ERR_WelcomeVoiceClient(mode, ++m_ERRWelcomeSerial, new ERR_WelcomeConnection(), false, title);
|
m_ERRSpeechPreview = new ERR_WelcomeVoiceClient(mode, ++m_ERRWelcomeSerial, new ERR_WelcomeConnection(), false, title);
|
||||||
// Mission announcements and admin previews do not reset the welcome.
|
// Mission announcements and admin previews do not reset the welcome.
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#ifdef WORKBENCH
|
||||||
|
class ERR_BakedSpeechProbeClient : ERR_WelcomeVoiceClient
|
||||||
|
{
|
||||||
|
bool Played;
|
||||||
|
bool ValidPlayback;
|
||||||
|
bool RequestedHTTP;
|
||||||
|
override protected void RequestPart() { RequestedHTTP = true; super.RequestPart(); }
|
||||||
|
override protected void Play()
|
||||||
|
{
|
||||||
|
super.Play();
|
||||||
|
Played = true;
|
||||||
|
ValidPlayback = m_Handle != AudioHandle.Invalid && !m_BakedGraph.IsEmpty();
|
||||||
|
}
|
||||||
|
bool Stopped() { return m_Stopped && m_Handle == AudioHandle.Invalid; }
|
||||||
|
bool Offline() { return !RequestedHTTP && !m_Callback && !m_Transfer && !m_Decoder && m_PreparationBytes == 0; }
|
||||||
|
}
|
||||||
|
modded class ERR_GameMode
|
||||||
|
{
|
||||||
|
protected ref ERR_BakedSpeechProbeClient m_ERRBakedProbe;
|
||||||
|
protected int m_ERRBakedDeadline;
|
||||||
|
protected int m_ERRBakedIndex;
|
||||||
|
protected override void EOnInit(IEntity owner)
|
||||||
|
{
|
||||||
|
super.EOnInit(owner);
|
||||||
|
string outcome;
|
||||||
|
if (SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "bundled-speech") return;
|
||||||
|
m_ERRBakedDeadline = System.GetTickCount() + 240000;
|
||||||
|
OnStop().Insert(ERR_StopBakedProbe);
|
||||||
|
GetGame().GetCallqueue().CallLater(ERR_TickBakedProbe, 100, true);
|
||||||
|
}
|
||||||
|
protected void ERR_TickBakedProbe()
|
||||||
|
{
|
||||||
|
if (System.GetTickCount() >= m_ERRBakedDeadline || !PreparationError().IsEmpty())
|
||||||
|
{ Print("ERR SMOKE FAIL: bundled speech preparation/deadline", LogLevel.ERROR); ERR_StopBakedProbe(); return; }
|
||||||
|
if (!Active()) return;
|
||||||
|
if (!GetGame().GetPlayerController() || !GetGame().GetPlayerController().GetControlledEntity() || ArmaReforgerLoadingAnim.IsOpen()) return;
|
||||||
|
if (!GetGame().GetCameraManager().CurrentCamera()) return;
|
||||||
|
SCR_RespawnSystemComponent respawn = SCR_RespawnSystemComponent.GetInstance();
|
||||||
|
if (respawn && !respawn.ERR_WelcomeAudioReady()) return;
|
||||||
|
array<string> sources = ERR_BakedSpeech.Sources();
|
||||||
|
if (!m_ERRBakedProbe)
|
||||||
|
{
|
||||||
|
if (!ERR_UsesBundledWelcome() || ERR_Speech().Ready())
|
||||||
|
{ Print("ERR SMOKE FAIL: bundled speech must work without a verified live service", LogLevel.ERROR); ERR_StopBakedProbe(); return; }
|
||||||
|
m_ERRBakedProbe = new ERR_BakedSpeechProbeClient(this, 1000 + m_ERRBakedIndex, new ERR_WelcomeConnection(), false);
|
||||||
|
ERR_SetRecordingsEnabled(false);
|
||||||
|
m_ERRBakedProbe.Tick(3, true, sources[m_ERRBakedIndex], "Muted test");
|
||||||
|
m_ERRBakedProbe.Tick(0.1, true, sources[m_ERRBakedIndex], "Muted test");
|
||||||
|
if (m_ERRBakedProbe.Played || m_ERRBakedProbe.PreparingDownload())
|
||||||
|
{ Print("ERR SMOKE FAIL: disabled bundled recording started", LogLevel.ERROR); ERR_StopBakedProbe(); return; }
|
||||||
|
ERR_SetRecordingsEnabled(true);
|
||||||
|
m_ERRBakedProbe.Tick(3, true, sources[m_ERRBakedIndex], "Bundled speech test");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_ERRBakedProbe.Tick(0.1, true, sources[m_ERRBakedIndex], "Bundled speech test");
|
||||||
|
if (!m_ERRBakedProbe.Played) return;
|
||||||
|
if (!m_ERRBakedProbe.ValidPlayback || !m_ERRBakedProbe.Offline())
|
||||||
|
{ Print("ERR SMOKE FAIL: bundled graph playback without HTTP or decoding " + sources[m_ERRBakedIndex], LogLevel.ERROR); ERR_StopBakedProbe(); return; }
|
||||||
|
Print("ERR SMOKE PASS: bundled graph playback without HTTP or decoding " + sources[m_ERRBakedIndex]);
|
||||||
|
ERR_SetRecordingsEnabled(false);
|
||||||
|
m_ERRBakedProbe.Tick(0.1, true, sources[m_ERRBakedIndex], "Mute playing audio");
|
||||||
|
if (!m_ERRBakedProbe.Stopped())
|
||||||
|
{ Print("ERR SMOKE FAIL: disabling bundled recording did not terminate playback", LogLevel.ERROR); ERR_StopBakedProbe(); return; }
|
||||||
|
Print("ERR SMOKE PASS: recording toggle blocks, re-enables and stops " + sources[m_ERRBakedIndex]);
|
||||||
|
ERR_SetRecordingsEnabled(true);
|
||||||
|
m_ERRBakedProbe = null;
|
||||||
|
m_ERRBakedIndex++;
|
||||||
|
if (m_ERRBakedIndex < sources.Count()) return;
|
||||||
|
Print("ERR SMOKE COMPLETE: all bundled native speech graphs play without live TTS, downloads or PCM reconstruction; not audible multiplayer proof");
|
||||||
|
ERR_StopBakedProbe();
|
||||||
|
}
|
||||||
|
protected void ERR_StopBakedProbe()
|
||||||
|
{
|
||||||
|
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_TickBakedProbe);
|
||||||
|
if (m_ERRBakedProbe) m_ERRBakedProbe.Stop();
|
||||||
|
OnStop().Remove(ERR_StopBakedProbe);
|
||||||
|
}
|
||||||
|
void ~ERR_GameMode() { ERR_StopBakedProbe(); }
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
#ifdef WORKBENCH
|
||||||
|
modded class ERR_AdminMenu
|
||||||
|
{
|
||||||
|
bool ERR_StarterProbeBrowse()
|
||||||
|
{
|
||||||
|
ERR_ServerAddonItems addon = GetGame().ERR_ServerItems().Find("58d0fb3206b6f859");
|
||||||
|
if (!addon) return false;
|
||||||
|
foreach (int index, ERR_ServerItem item : addon.Items)
|
||||||
|
if (item.Prefab == ERR_Supplies.Pistol())
|
||||||
|
{ m_Controller.ERR_AdminSubmit("items list " + addon.Id + " " + (index / ERR_ServerItemCatalog.PAGE_SIZE).ToString(), true); return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool ERR_StarterProbeQuantity()
|
||||||
|
{
|
||||||
|
int index = m_StarterCatalog.Find(ERR_TerrainCatalog.Key(ERR_Supplies.Pistol()));
|
||||||
|
if (index < 0 || !m_Ready) return false;
|
||||||
|
SCR_ComboBoxComponent.GetComboBoxComponent("StarterCatalogItem", GetRootWidget()).SetCurrentItem(index, false, false, false);
|
||||||
|
SCR_EditBoxComponent.GetEditBoxComponent("StarterQuantity", GetRootWidget()).SetValue("1");
|
||||||
|
OnStarterButton(SCR_ButtonTextComponent.GetButtonText("StarterAdd", GetRootWidget())); return true;
|
||||||
|
}
|
||||||
|
bool ERR_StarterProbeReady(int mode) { return m_Ready && m_StarterMode == mode; }
|
||||||
|
bool ERR_StarterProbeSelect(bool custom)
|
||||||
|
{
|
||||||
|
ShowPage(20);
|
||||||
|
string name = "StarterDefault"; if (custom) name = "StarterCustom";
|
||||||
|
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText(name, GetRootWidget());
|
||||||
|
if (!button || !button.IsEnabled()) return false;
|
||||||
|
OnStarterButton(button); return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modded class ERR_GameMode
|
||||||
|
{
|
||||||
|
protected int m_ERRStarterStage, m_ERRStarterDeadline;
|
||||||
|
protected float m_ERRStarterWait;
|
||||||
|
protected IEntity m_ERRStarterContainer;
|
||||||
|
protected override void EOnInit(IEntity owner)
|
||||||
|
{
|
||||||
|
super.EOnInit(owner);
|
||||||
|
string outcome;
|
||||||
|
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "starter-backpack") return;
|
||||||
|
m_ERRStarterDeadline = System.GetTickCount() + 360000;
|
||||||
|
GetGame().GetCallqueue().CallLater(ERR_StarterProbeTick, 500, true);
|
||||||
|
OnStop().Insert(ERR_StopStarterProbe);
|
||||||
|
}
|
||||||
|
protected void ERR_StopStarterProbe() { GetGame().GetCallqueue().Remove(ERR_StarterProbeTick); }
|
||||||
|
protected bool ERR_StarterCheck(bool ok, string detail)
|
||||||
|
{
|
||||||
|
if (ok) { Print("ERR SMOKE PASS: starting backpack " + detail); return true; }
|
||||||
|
Print("ERR SMOKE FAIL: starting backpack " + detail, LogLevel.ERROR); ERR_StopStarterProbe(); return false;
|
||||||
|
}
|
||||||
|
protected bool ERR_StarterInventory()
|
||||||
|
{
|
||||||
|
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(m_ERRStarterContainer.FindComponent(InventoryStorageManagerComponent));
|
||||||
|
if (!inventory) return false;
|
||||||
|
array<IEntity> items = {}; inventory.GetItems(items);
|
||||||
|
int pistols, magazines, epinephrine, other;
|
||||||
|
foreach (IEntity item : items)
|
||||||
|
{
|
||||||
|
if (!item || !item.GetPrefabData()) return false;
|
||||||
|
ResourceName prefab = item.GetPrefabData().GetPrefabName();
|
||||||
|
if (prefab == ERR_Supplies.Pistol()) pistols++;
|
||||||
|
else if (prefab == ERR_Supplies.Magazine()) magazines++;
|
||||||
|
else if (prefab == ERR_EnemyMedicalCatalog.EPI) epinephrine++;
|
||||||
|
else other++;
|
||||||
|
}
|
||||||
|
PrintFormat("ERR STARTER STOCK: pistols=%1 magazines=%2 epinephrine=%3 other=%4", pistols, magazines, epinephrine, other);
|
||||||
|
// Native enumeration includes the magazine loaded in each pistol, plus four spares.
|
||||||
|
return pistols == 2 && magazines == 6 && epinephrine == 3 && other == 1;
|
||||||
|
}
|
||||||
|
protected void ERR_StarterProbeTick()
|
||||||
|
{
|
||||||
|
if (System.GetTickCount() > m_ERRStarterDeadline || !PreparationError().IsEmpty()) { ERR_StarterCheck(false, "deadline or preparation failure"); return; }
|
||||||
|
if (!Active() || GetWorld().GetWorldTime() < m_ERRStarterWait) return;
|
||||||
|
SCR_PlayerController pc = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||||
|
if (!pc || !pc.GetControlledEntity() || !Players().CurrentActor(pc.GetControlledEntity())) return;
|
||||||
|
if (m_ERRStarterStage == 0)
|
||||||
|
{
|
||||||
|
ERR_SiteRuntime prison = Sites().Find("everon-prison-1");
|
||||||
|
if (!ERR_StarterCheck(prison && prison.Container, "physical shared backpack exists")) return;
|
||||||
|
m_ERRStarterContainer = prison.Container;
|
||||||
|
if (!ERR_StarterCheck(GetGame().ERR_StartingSupplies().Custom() && ERR_StarterInventory(), "saved exact custom loadout replaced default stock and retained one map")) return;
|
||||||
|
ERR_AdminMenu.Open(); m_ERRStarterStage = 1; return;
|
||||||
|
}
|
||||||
|
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu();
|
||||||
|
if (!menu) return;
|
||||||
|
if (m_ERRStarterStage == 1)
|
||||||
|
{
|
||||||
|
if (!menu.ERR_StarterProbeReady(1)) return;
|
||||||
|
if (!ERR_StarterCheck(menu.ERR_StarterProbeSelect(false), "real menu default button submitted authenticated command")) return;
|
||||||
|
m_ERRStarterStage = 2; m_ERRStarterWait = GetWorld().GetWorldTime() + 1500; return;
|
||||||
|
}
|
||||||
|
if (m_ERRStarterStage == 2)
|
||||||
|
{
|
||||||
|
if (!menu.ERR_StarterProbeReady(0)) return;
|
||||||
|
ERR_StartBackpack reload = new ERR_StartBackpack();
|
||||||
|
if (!ERR_StarterCheck(!reload.Custom() && ERR_StarterInventory(), "default choice persisted without refilling current backpack")) return;
|
||||||
|
if (!ERR_StarterCheck(menu.ERR_StarterProbeSelect(true), "real menu custom button submitted authenticated command")) return;
|
||||||
|
m_ERRStarterStage = 3; m_ERRStarterWait = GetWorld().GetWorldTime() + 1500; return;
|
||||||
|
}
|
||||||
|
if (m_ERRStarterStage == 3)
|
||||||
|
{
|
||||||
|
if (!menu.ERR_StarterProbeReady(1) || !menu.ERR_StarterProbeBrowse()) return;
|
||||||
|
m_ERRStarterStage = 4; m_ERRStarterWait = GetWorld().GetWorldTime() + 1500; return;
|
||||||
|
}
|
||||||
|
if (m_ERRStarterStage == 4)
|
||||||
|
{
|
||||||
|
if (!menu.ERR_StarterProbeQuantity()) return;
|
||||||
|
m_ERRStarterStage = 5; m_ERRStarterWait = GetWorld().GetWorldTime() + 1500; return;
|
||||||
|
}
|
||||||
|
ERR_StartBackpack reload = new ERR_StartBackpack();
|
||||||
|
array<ref ERR_SiteStock> planned = {}; reload.Stock(planned);
|
||||||
|
int pistols;
|
||||||
|
foreach (ERR_SiteStock item : planned) if (item.Prefab == ERR_Supplies.Pistol()) pistols = item.Count;
|
||||||
|
if (!ERR_StarterCheck(reload.Custom() && planned.Count() == 3 && pistols == 1 && ERR_StarterInventory(), "actual quantity menu saved next-round stock to disk without changing current backpack")) return;
|
||||||
|
if (!ERR_StarterCheck(!reload.Set(ERR_Supplies.Pistol(), 49), "oversized quantity rejected")) return;
|
||||||
|
menu.Close(); ERR_StopStarterProbe(); Transition(ERR_Phase.DEFEAT);
|
||||||
|
Print("ERR SMOKE COMPLETE: starting backpack exact physical stock, map, menu mode RPC, disk reload and current-round isolation; scripted local fixture");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// Bound native AI horn requests without changing steering, braking or player input.
|
||||||
|
class ERR_AIDriverHornLimit
|
||||||
|
{
|
||||||
|
protected int m_End = -1;
|
||||||
|
protected int m_Next = 0;
|
||||||
|
protected int m_Last = -1;
|
||||||
|
bool Allow(bool requested, int now)
|
||||||
|
{
|
||||||
|
if (m_Last >= 0 && now < m_Last) { m_End = -1; m_Next = now + 30000; }
|
||||||
|
m_Last = now;
|
||||||
|
if (!requested) { m_End = -1; return false; }
|
||||||
|
if (m_End >= 0)
|
||||||
|
{
|
||||||
|
if (now < m_End) return true;
|
||||||
|
m_End = -1;
|
||||||
|
}
|
||||||
|
if (now < m_Next) return false;
|
||||||
|
m_End = now + 500;
|
||||||
|
m_Next = now + 30000;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modded class SCR_CharacterControllerComponent
|
||||||
|
{
|
||||||
|
protected ref ERR_AIDriverHornLimit m_ERRHornLimit;
|
||||||
|
protected override void OnApplyControls(IEntity owner, float timeSlice)
|
||||||
|
{
|
||||||
|
super.OnApplyControls(owner, timeSlice);
|
||||||
|
if (!Replication.IsServer() || !owner || ERR_Actors.Player(owner)) return;
|
||||||
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
|
if (!mode) return;
|
||||||
|
IEntity vehicle = CompartmentAccessComponent.GetVehicleIn(owner);
|
||||||
|
if (!vehicle || !vehicle.FindComponent(AICarMovementComponent)) return;
|
||||||
|
BaseVehicleControllerComponent controls = BaseVehicleControllerComponent.Cast(vehicle.FindComponent(BaseVehicleControllerComponent));
|
||||||
|
if (!controls || !controls.GetPilotCompartmentSlot() || controls.GetPilotCompartmentSlot().GetOccupant() != owner) return;
|
||||||
|
CharacterInputContext input = GetInputContext();
|
||||||
|
if (!input) return;
|
||||||
|
if (!m_ERRHornLimit) m_ERRHornLimit = new ERR_AIDriverHornLimit();
|
||||||
|
bool permitted = m_ERRHornLimit.Allow(mode.Active() && input.GetVehicleHorn() != 0, System.GetTickCount());
|
||||||
|
if (!permitted) input.SetVehicleHorn(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
// Optional RCD 2.3.7 adapter. Foreign types are compiled only after the addon
|
||||||
|
// is mounted. No workshop asset or player controller override is shipped here.
|
||||||
|
class ERR_DroneBridge
|
||||||
|
{
|
||||||
|
static const string ADDON = "65AD60E204191D37";
|
||||||
|
protected static ref ScriptModule s_Module;
|
||||||
|
protected static bool s_Attempted;
|
||||||
|
static bool Available()
|
||||||
|
{
|
||||||
|
array<string> loaded = {}; GameProject.GetLoadedAddons(loaded);
|
||||||
|
return loaded.Contains(ADDON);
|
||||||
|
}
|
||||||
|
static bool Initialize()
|
||||||
|
{
|
||||||
|
if (s_Module) return true;
|
||||||
|
if (s_Attempted || !Replication.IsServer() || !Available()) return false;
|
||||||
|
s_Attempted = true;
|
||||||
|
#ifndef WORKBENCH
|
||||||
|
if (!System.IsConsoleApp()) return false;
|
||||||
|
#endif
|
||||||
|
string code;
|
||||||
|
code += "class ERR_OptionalDroneAPI {\n";
|
||||||
|
code += "static bool ERR_DroneReady() {\n";
|
||||||
|
code += "return SAL_DroneConnectionManager.GetInstance()!=null;\n";
|
||||||
|
code += "}\n";
|
||||||
|
code += "static int ERR_DroneState(IEntity entity) {\n";
|
||||||
|
code += "int state=0;\n";
|
||||||
|
code += "if(entity) {\n";
|
||||||
|
code += "SAL_DroneControllerComponent control=SAL_DroneControllerComponent.Cast(entity.FindComponent(SAL_DroneControllerComponent));\n";
|
||||||
|
code += "if(control) {\n";
|
||||||
|
code += "if(!control.m_bIsDestroyed) state=1;\n";
|
||||||
|
code += "if(control.m_iOwner != -1 || control.m_bIsConnected || entity.GetParent()) state=2;\n";
|
||||||
|
code += "}\n";
|
||||||
|
code += "}\n";
|
||||||
|
code += "return state;\n";
|
||||||
|
code += "}\n";
|
||||||
|
code += "static bool ERR_DroneStart(IEntity e) {\n";
|
||||||
|
code += "SAL_DroneControllerComponent c=SAL_DroneControllerComponent.Cast(e.FindComponent(SAL_DroneControllerComponent));\n";
|
||||||
|
code += "SAL_DroneCleanUpComponent clean=SAL_DroneCleanUpComponent.Cast(e.FindComponent(SAL_DroneCleanUpComponent));\n";
|
||||||
|
code += "SAL_DroneBatteryComponent b=SAL_DroneBatteryComponent.Cast(e.FindComponent(SAL_DroneBatteryComponent));\n";
|
||||||
|
code += "if(!c || !clean || !b || !c.m_DroneManager || !c.m_CameraManager || !c.m_Drone || !e.GetPhysics() || ERR_DroneState(e)!=1) return false;\n";
|
||||||
|
code += "c.ClearEventMask(e,EntityEvent.SIMULATE|EntityEvent.FIXEDFRAME);\n";
|
||||||
|
code += "b.ClearEventMask(e,EntityEvent.FIXEDFRAME);\n";
|
||||||
|
code += "clean.m_bIsConnected=true;\n";
|
||||||
|
code += "c.m_bIsArmed=true;\n";
|
||||||
|
code += "c.m_bIsActive=true;\n";
|
||||||
|
code += "SAL_DroneNetworkPacket p=new SAL_DroneNetworkPacket();\n";
|
||||||
|
code += "p.SetDrone(SAL_DroneConnectionManager.GetRplId(e));\n";
|
||||||
|
code += "p.SetIsArmed(true);\n";
|
||||||
|
code += "c.m_DroneManager.DisarmDrone(p);\n";
|
||||||
|
code += "return true;\n";
|
||||||
|
code += "}\n";
|
||||||
|
code += "static bool ERR_DroneStep(IEntity e, vector controls, float dt, bool send) {\n";
|
||||||
|
code += "if(ERR_DroneState(e)!=1) return false;\n";
|
||||||
|
code += "SAL_DroneControllerComponent c=SAL_DroneControllerComponent.Cast(e.FindComponent(SAL_DroneControllerComponent));\n";
|
||||||
|
code += "if(!c.m_BatteryComponent || c.m_BatteryComponent.m_fCurrentBattery<=0) return false;\n";
|
||||||
|
code += "c.m_iThrottle=controls[0];\n";
|
||||||
|
code += "c.m_iPitch=controls[1];\n";
|
||||||
|
code += "c.m_iRoll=controls[2];\n";
|
||||||
|
code += "c.m_iYaw=0;\n";
|
||||||
|
code += "e.GetPhysics().EnableGravity(true);\n";
|
||||||
|
code += "e.GetPhysics().SetActive(true);\n";
|
||||||
|
code += "int old=c.m_iOwner;\n";
|
||||||
|
code += "c.m_iOwner=SCR_PlayerController.GetLocalPlayerId();\n";
|
||||||
|
code += "c.EOnSimulate(e,dt);\n";
|
||||||
|
code += "c.m_iOwner=old;\n";
|
||||||
|
code += "c.m_BatteryComponent.m_fCurrentBattery=Math.Max(0,c.m_BatteryComponent.m_fCurrentBattery-dt*(1+Math.AbsFloat(controls[1])+Math.AbsFloat(controls[2])));\n";
|
||||||
|
code += "if(send) c.SendPacketServer(e,dt);\n";
|
||||||
|
code += "return true;\n";
|
||||||
|
code += "}\n";
|
||||||
|
code += "static void ERR_DroneRelease(IEntity e) {\n";
|
||||||
|
code += "if(!e) return;\n";
|
||||||
|
code += "SAL_DroneControllerComponent c=SAL_DroneControllerComponent.Cast(e.FindComponent(SAL_DroneControllerComponent));\n";
|
||||||
|
code += "SAL_DroneBatteryComponent b=SAL_DroneBatteryComponent.Cast(e.FindComponent(SAL_DroneBatteryComponent));\n";
|
||||||
|
code += "SAL_DroneCleanUpComponent clean=SAL_DroneCleanUpComponent.Cast(e.FindComponent(SAL_DroneCleanUpComponent));\n";
|
||||||
|
code += "if(!c) return;\n";
|
||||||
|
code += "c.SetEventMask(e,EntityEvent.SIMULATE|EntityEvent.FIXEDFRAME);\n";
|
||||||
|
code += "if(b) b.SetEventMask(e,EntityEvent.FIXEDFRAME);\n";
|
||||||
|
code += "if(ERR_DroneState(e)==2) return;\n";
|
||||||
|
code += "if(clean) clean.m_bIsConnected=false;\n";
|
||||||
|
code += "c.m_bIsArmed=false;\n";
|
||||||
|
code += "c.m_iThrottle=0;\n";
|
||||||
|
code += "SAL_DroneNetworkPacket p=new SAL_DroneNetworkPacket();\n";
|
||||||
|
code += "p.SetDrone(SAL_DroneConnectionManager.GetRplId(e));\n";
|
||||||
|
code += "p.SetIsArmed(false);\n";
|
||||||
|
code += "if(c.m_DroneManager)c.m_DroneManager.DisarmDrone(p);\n";
|
||||||
|
code += "}\n";
|
||||||
|
code += "static bool ERR_DroneJammed(IEntity e) {\n";
|
||||||
|
code += "SAL_DroneConnectionManager m=SAL_DroneConnectionManager.GetInstance();\n";
|
||||||
|
code += "if(!m)return true;\n";
|
||||||
|
code += "int count=0;\n";
|
||||||
|
code += "foreach(RplId id:m.m_aJammers) {\n";
|
||||||
|
code += "if(count++>=64)break;\n";
|
||||||
|
code += "RplComponent r=RplComponent.Cast(Replication.FindItem(id));\n";
|
||||||
|
code += "if(!r)continue;\n";
|
||||||
|
code += "IEntity jammer=r.GetEntity();\n";
|
||||||
|
code += "if(!jammer)continue;\n";
|
||||||
|
code += "SAL_DroneJammerComponent j=SAL_DroneJammerComponent.Cast(jammer.FindComponent(SAL_DroneJammerComponent));\n";
|
||||||
|
code += "if(j && vector.Distance(e.GetOrigin(),jammer.GetOrigin())<j.m_fJammingRange)return true;\n";
|
||||||
|
code += "}\n";
|
||||||
|
code += "return false;\n";
|
||||||
|
code += "}\n";
|
||||||
|
code += "}\n";
|
||||||
|
code += "bool ERR_DroneReady() {\n";
|
||||||
|
code += "return ERR_OptionalDroneAPI.ERR_DroneReady();\n";
|
||||||
|
code += "}\n";
|
||||||
|
code += "int ERR_DroneState(IEntity e) {\n";
|
||||||
|
code += "return ERR_OptionalDroneAPI.ERR_DroneState(e);\n";
|
||||||
|
code += "}\n";
|
||||||
|
code += "bool ERR_DroneStart(IEntity e) {\n";
|
||||||
|
code += "return ERR_OptionalDroneAPI.ERR_DroneStart(e);\n";
|
||||||
|
code += "}\n";
|
||||||
|
code += "bool ERR_DroneStep(IEntity e, vector controls, float dt, bool send) {\n";
|
||||||
|
code += "return ERR_OptionalDroneAPI.ERR_DroneStep(e,controls,dt,send);\n";
|
||||||
|
code += "}\n";
|
||||||
|
code += "void ERR_DroneRelease(IEntity e) {\n";
|
||||||
|
code += "ERR_OptionalDroneAPI.ERR_DroneRelease(e);\n";
|
||||||
|
code += "}\n";
|
||||||
|
code += "bool ERR_DroneJammed(IEntity e) {\n";
|
||||||
|
code += "return ERR_OptionalDroneAPI.ERR_DroneJammed(e);\n";
|
||||||
|
code += "}\n";
|
||||||
|
string error; int line;
|
||||||
|
s_Module = ScriptModule.CompileScript(GetGame().GetScriptModule(), code, error, line);
|
||||||
|
if (!s_Module)
|
||||||
|
{
|
||||||
|
PrintFormat("Reintroduced [drone adapter]: line=%1 error=%2", line, error);
|
||||||
|
ERR_HQ.Event("DRONE UNAVAILABLE", string.Format("optional adapter line=%1: %2", line, error));
|
||||||
|
}
|
||||||
|
return s_Module != null;
|
||||||
|
}
|
||||||
|
static bool Ready()
|
||||||
|
{
|
||||||
|
bool result;
|
||||||
|
return s_Module && s_Module.Call(null, "ERR_DroneReady", false, result) && result;
|
||||||
|
}
|
||||||
|
static int State(IEntity entity)
|
||||||
|
{
|
||||||
|
int result;
|
||||||
|
if (!entity || !s_Module || !s_Module.Call(null, "ERR_DroneState", false, result, entity)) return 0;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
static bool Start(IEntity entity)
|
||||||
|
{
|
||||||
|
bool result;
|
||||||
|
return s_Module && s_Module.Call(null, "ERR_DroneStart", false, result, entity) && result;
|
||||||
|
}
|
||||||
|
static bool Step(IEntity entity, vector controls, float dt, bool send)
|
||||||
|
{
|
||||||
|
bool result;
|
||||||
|
return s_Module && s_Module.Call(null, "ERR_DroneStep", false, result, entity, controls, dt, send) && result;
|
||||||
|
}
|
||||||
|
static void Release(IEntity entity)
|
||||||
|
{
|
||||||
|
int ignored;
|
||||||
|
if (entity && s_Module) s_Module.Call(null, "ERR_DroneRelease", false, ignored, entity);
|
||||||
|
}
|
||||||
|
static bool Jammed(IEntity entity)
|
||||||
|
{
|
||||||
|
bool result;
|
||||||
|
return !s_Module || !s_Module.Call(null, "ERR_DroneJammed", false, result, entity) || result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ERR_DroneCatalog
|
||||||
|
{
|
||||||
|
ResourceName Prefab;
|
||||||
|
void Find()
|
||||||
|
{
|
||||||
|
if (!ERR_DroneBridge.Available()) return;
|
||||||
|
SearchResourcesFilter filter = new SearchResourcesFilter();
|
||||||
|
filter.fileExtensions = {"et"}; filter.recursive = true;
|
||||||
|
filter.rootPath = "$" + GameProject.GetAddonID(ERR_DroneBridge.ADDON) + ":Prefabs/Drones/DJIBase";
|
||||||
|
ResourceDatabase.SearchResources(filter, Add);
|
||||||
|
}
|
||||||
|
protected void Add(ResourceName resource, string path = "")
|
||||||
|
{
|
||||||
|
string name = resource; name.ToLower();
|
||||||
|
// Exact inspected unarmed airframe; never a dropper, FPV, placeholder or wreck.
|
||||||
|
if (name.EndsWith("/djibase.et")) Prefab = resource;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
// Bounded, delivered-report-driven reconnaissance. This scheduler never follows
|
||||||
|
// a target entity: only the camera sensor may sample a visible actor's position.
|
||||||
|
class ERR_DroneSchedule
|
||||||
|
{
|
||||||
|
float NextLaunch;
|
||||||
|
float LastObservation = -1;
|
||||||
|
int Launched, Failures;
|
||||||
|
bool Stopped;
|
||||||
|
bool Due(float now, int count, int limit) { return !Stopped && limit > 0 && count < limit && now >= NextLaunch && Launched < 64 && Failures < 3; }
|
||||||
|
bool Fresh(float observed, float now) { return observed > LastObservation && observed >= 0 && observed <= now && now - observed <= 60000; }
|
||||||
|
void Attempt(float observed, float now, int cooldown) { LastObservation = observed; NextLaunch = now + cooldown * 1000; }
|
||||||
|
void Result(bool success) { if (success) Failures = 0; else Failures++; }
|
||||||
|
}
|
||||||
|
class ERR_DroneFlightRules
|
||||||
|
{
|
||||||
|
static vector Controls(vector velocity, vector desired, vector forward, vector right)
|
||||||
|
{
|
||||||
|
vector error = desired - velocity;
|
||||||
|
return Vector(Math.Clamp(0.49 + error[1] * 0.12, 0.15, 0.85), Math.Clamp(-vector.Dot(error, forward) * 0.12, -0.5, 0.5), Math.Clamp(-vector.Dot(error, right) * 0.12, -0.5, 0.5));
|
||||||
|
}
|
||||||
|
static bool CameraCone(vector origin, vector aim, vector point)
|
||||||
|
{
|
||||||
|
vector offset = point - origin;
|
||||||
|
float distance = offset.Length();
|
||||||
|
return distance >= 5 && distance <= 350 && vector.Dot(offset.Normalized(), (aim - origin).Normalized()) >= 0.8660254;
|
||||||
|
}
|
||||||
|
static bool HitsTarget(IEntity hit, IEntity target)
|
||||||
|
{
|
||||||
|
if (!target) return false;
|
||||||
|
// Helmets and other attached equipment have their own collision entities.
|
||||||
|
// Accept only the intended character's descendants, not an unrelated blocker.
|
||||||
|
for (int depth = 0; hit && depth < 16; depth++)
|
||||||
|
{
|
||||||
|
if (hit == target) return true;
|
||||||
|
hit = hit.GetParent();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
static bool Clear(vector from, vector to, IEntity exclude, IEntity target = null)
|
||||||
|
{
|
||||||
|
TraceParam trace = new TraceParam(); trace.Start = from; trace.End = to;
|
||||||
|
trace.Exclude = exclude; trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
||||||
|
float fraction = GetGame().GetWorld().TraceMove(trace, null);
|
||||||
|
return fraction >= 1 || HitsTarget(trace.TraceEnt, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ERR_DroneFlight
|
||||||
|
{
|
||||||
|
IEntity Drone;
|
||||||
|
IEntity Operator;
|
||||||
|
SCR_AIGroup Group;
|
||||||
|
vector Home, Search;
|
||||||
|
float Created, LastStep, NextPacket, NextScan, NextReport, SearchStarted = -1;
|
||||||
|
bool Started, Returning, Finished, Released, Counted;
|
||||||
|
int Candidate;
|
||||||
|
float NextLink; bool LinkGood;
|
||||||
|
string Reason = "preparing";
|
||||||
|
bool Link()
|
||||||
|
{
|
||||||
|
float now = GetGame().GetWorld().GetWorldTime();
|
||||||
|
if (now < NextLink) return LinkGood;
|
||||||
|
NextLink = now + 1000;
|
||||||
|
LinkGood = ERR_Actors.CanFight(Operator) && Drone && vector.Distance(Drone.GetOrigin(), Operator.GetOrigin()) <= 1800
|
||||||
|
&& !ERR_DroneBridge.Jammed(Drone)
|
||||||
|
&& ERR_DroneFlightRules.Clear(Drone.GetOrigin(), Operator.GetOrigin() + Vector(0, 1.5, 0), Drone, Operator);
|
||||||
|
return LinkGood;
|
||||||
|
}
|
||||||
|
void Finish(string reason)
|
||||||
|
{
|
||||||
|
if (Finished) return;
|
||||||
|
Finished = true; Reason = reason;
|
||||||
|
ERR_DroneBridge.Release(Drone); Released = true;
|
||||||
|
ERR_HQ.Event("DRONE RETIRED", reason);
|
||||||
|
}
|
||||||
|
void Advance(float now)
|
||||||
|
{
|
||||||
|
if (Finished) return;
|
||||||
|
if (!Drone) { Finish("airframe removed"); return; }
|
||||||
|
int state = ERR_DroneBridge.State(Drone);
|
||||||
|
if (state == 2) { Finish("player takeover; control released"); return; }
|
||||||
|
if (state == 0 || !ERR_Actors.Alive(Drone)) { Finish("airframe destroyed"); return; }
|
||||||
|
if (!Started)
|
||||||
|
{
|
||||||
|
if (now - Created < 1000) return;
|
||||||
|
if (!ERR_DroneBridge.Start(Drone))
|
||||||
|
{
|
||||||
|
if (now - Created >= 5000) Finish("flight adapter initialization failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Started = true; LastStep = now; Reason = "taking off";
|
||||||
|
}
|
||||||
|
float dt = Math.Clamp((now - LastStep) / 1000, 0, 0.1); LastStep = now;
|
||||||
|
if (dt <= 0) return;
|
||||||
|
vector position = Drone.GetOrigin();
|
||||||
|
if (now - Created >= 600000) { Finish("flight time limit reached"); return; }
|
||||||
|
if (!ERR_Actors.CanFight(Operator)) Returning = true;
|
||||||
|
if (now - Created > 15000 && !Link()) Returning = true;
|
||||||
|
if (vector.DistanceXZ(position, Search) < 120 && SearchStarted < 0) SearchStarted = now;
|
||||||
|
if (SearchStarted >= 0 && now - SearchStarted >= 120000) Returning = true;
|
||||||
|
if (now - Created >= 360000) Returning = true;
|
||||||
|
vector goal = Search;
|
||||||
|
if (Returning) goal = Home;
|
||||||
|
float ground = GetGame().GetWorld().GetSurfaceY(position[0], position[2]);
|
||||||
|
float altitude = Math.Max(ground, GetGame().GetWorld().GetSurfaceY(goal[0], goal[2])) + 70;
|
||||||
|
vector horizontal = goal - position; horizontal[1] = 0;
|
||||||
|
float distance = horizontal.Length();
|
||||||
|
vector desired = horizontal.Normalized() * Math.Min(8, distance * 0.2);
|
||||||
|
// Clear obstacles vertically before departing; return to the original clear pad.
|
||||||
|
if (now - Created < 20000 && position[1] < Home[1] + 30) desired = vector.Zero;
|
||||||
|
if (Returning && distance < 3) { altitude = Home[1] + 0.15; desired = horizontal * 0.3; }
|
||||||
|
vector ahead = position + desired * 3;
|
||||||
|
if (!(Returning && distance < 3)) altitude = Math.Max(altitude, GetGame().GetWorld().GetSurfaceY(ahead[0], ahead[2]) + 40);
|
||||||
|
desired[1] = Math.Clamp((altitude - position[1]) * 0.6, -2, 3);
|
||||||
|
if (Returning && distance < 3 && position[1] < Home[1] + 0.7 && now - Created > 10000) { Finish("returned to launch site"); return; }
|
||||||
|
if (!Returning && now - Created > 45000 && position[1] < ground + 2) { Finish("unable to take off or flight obstructed"); return; }
|
||||||
|
Physics physics = Drone.GetPhysics();
|
||||||
|
if (!physics) { Finish("physics unavailable"); return; }
|
||||||
|
vector controls = ERR_DroneFlightRules.Controls(physics.GetVelocity(), desired, Drone.GetTransformAxis(2), Drone.GetTransformAxis(0));
|
||||||
|
bool send = now >= NextPacket;
|
||||||
|
if (send) NextPacket = now + 100;
|
||||||
|
if (!ERR_DroneBridge.Step(Drone, controls, dt, send)) { Finish("battery exhausted or control lost"); return; }
|
||||||
|
if (Returning) Reason = "returning";
|
||||||
|
else Reason = "searching reported area";
|
||||||
|
}
|
||||||
|
void Observe(ERR_HQ hq, float now)
|
||||||
|
{
|
||||||
|
if (!Started || Finished || Returning || now < NextScan || now < NextReport) return;
|
||||||
|
NextScan = now + 1000;
|
||||||
|
if (!Link()) return;
|
||||||
|
array<IEntity> candidates = {}; ERR_Actors.Players(candidates, true);
|
||||||
|
if (candidates.IsEmpty()) return;
|
||||||
|
IEntity actor = candidates[Candidate++ % candidates.Count()];
|
||||||
|
// Mounted occupants are not visible through a vehicle's hull. A later
|
||||||
|
// infantry sighting can still dispatch another search at its reported sector.
|
||||||
|
if (!actor || CompartmentAccessComponent.GetVehicleIn(actor)) return;
|
||||||
|
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(actor.FindComponent(FactionAffiliationComponent));
|
||||||
|
if (!affiliation || affiliation.GetAffiliatedFaction() != ERR_GameMode.Get().PlayerFaction()) return;
|
||||||
|
vector point = actor.GetOrigin() + Vector(0, 1.2, 0);
|
||||||
|
if (!ERR_DroneFlightRules.CameraCone(Drone.GetOrigin(), Search, point)) return;
|
||||||
|
if (!ERR_DroneFlightRules.Clear(Drone.GetOrigin(), point, Drone, actor)) return;
|
||||||
|
ERR_HQReport report = new ERR_HQReport();
|
||||||
|
report.Target = actor; report.Position = point; report.ObservedAt = now; report.Source = Group;
|
||||||
|
report.TravelMode = "on foot"; report.SeenEscapists = 1; report.DetailsObservedAt = now; report.DroneObservation = true;
|
||||||
|
if (hq.QueueDroneObservation(Group, report, now)) NextReport = now + ERR_GameMode.Get().Settings().Runtime("reportIntervalSeconds") * 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ERR_DroneRecon
|
||||||
|
{
|
||||||
|
protected ERR_GameMode m_Mode;
|
||||||
|
protected ref ERR_DroneSchedule m_Schedule = new ERR_DroneSchedule();
|
||||||
|
protected ref ERR_DroneCatalog m_Catalog = new ERR_DroneCatalog();
|
||||||
|
protected ref array<ref ERR_DroneFlight> m_Flights = {};
|
||||||
|
protected int m_Limit, m_Cooldown;
|
||||||
|
protected bool m_Checked, m_Ready, m_Running;
|
||||||
|
protected string m_Reason = "disabled";
|
||||||
|
void ERR_DroneRecon(ERR_GameMode mode)
|
||||||
|
{
|
||||||
|
m_Mode = mode; m_Limit = mode.Settings().Get("droneReconCount"); m_Cooldown = mode.Settings().Get("droneCooldownSeconds");
|
||||||
|
if (m_Limit > 0) m_Reason = "waiting for prison escape";
|
||||||
|
}
|
||||||
|
bool Ready() { return m_Ready; }
|
||||||
|
string Status() { return string.Format("Recon drones: %1/%2 active; %3", m_Flights.Count(), m_Limit, m_Reason); }
|
||||||
|
protected bool Prepare()
|
||||||
|
{
|
||||||
|
if (m_Checked) return m_Ready;
|
||||||
|
m_Checked = true;
|
||||||
|
if (!ERR_DroneBridge.Available()) { m_Reason = "Realistic Combat Drones missing"; return false; }
|
||||||
|
if (!ERR_DroneBridge.Initialize()) { m_Reason = "flight adapter unavailable on this host/version"; return false; }
|
||||||
|
if (!ERR_DroneBridge.Ready()) { m_Reason = "drone manager unavailable"; return false; }
|
||||||
|
m_Catalog.Find();
|
||||||
|
if (m_Catalog.Prefab.IsEmpty()) { m_Reason = "supported unarmed DJI prefab unavailable"; return false; }
|
||||||
|
m_Ready = true; m_Reason = "waiting for a recent HQ sighting";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
void Tick(float now)
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer() || m_Schedule.Stopped || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) return;
|
||||||
|
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
|
||||||
|
for (int i = m_Flights.Count() - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
ERR_DroneFlight flight = m_Flights[i];
|
||||||
|
if (!flight.Counted && flight.Started && flight.Drone && flight.Drone.GetOrigin()[1] > flight.Home[1] + 10)
|
||||||
|
{ flight.Counted = true; m_Schedule.Result(true); }
|
||||||
|
if (flight.Finished)
|
||||||
|
{
|
||||||
|
if (!flight.Counted && flight.Reason != "player takeover; control released") m_Schedule.Result(false);
|
||||||
|
m_Reason = flight.Reason;
|
||||||
|
if (m_Schedule.Failures >= 3) m_Reason = "paused after three launch failures";
|
||||||
|
m_Flights.Remove(i); continue;
|
||||||
|
}
|
||||||
|
flight.Observe(hq, now);
|
||||||
|
}
|
||||||
|
if (m_Limit == 0 || m_Mode.Phase() < ERR_Phase.RADIO_SEARCH) return;
|
||||||
|
array<IEntity> players = {}; ERR_Actors.Players(players, true);
|
||||||
|
if (players.IsEmpty() || !Prepare() || !m_Schedule.Due(now, m_Flights.Count(), m_Limit)) return;
|
||||||
|
array<ref ERR_HQReport> contacts = {}; hq.SupportContacts(contacts, now);
|
||||||
|
ERR_HQReport chosen;
|
||||||
|
foreach (ERR_HQReport report : contacts)
|
||||||
|
{
|
||||||
|
if (!report || report.DroneObservation || report.Distress || !report.Source || !m_Schedule.Fresh(report.ObservedAt, now)) continue;
|
||||||
|
bool assigned;
|
||||||
|
foreach (ERR_DroneFlight active : m_Flights) if (active.Group == report.Source || vector.DistanceXZ(active.Search, report.Position) < 250) assigned = true;
|
||||||
|
if (assigned || !ERR_Actors.CanFight(report.Source.GetLeaderEntity())) continue;
|
||||||
|
if (!chosen || report.ObservedAt > chosen.ObservedAt) chosen = report;
|
||||||
|
}
|
||||||
|
if (!chosen) return;
|
||||||
|
m_Schedule.Attempt(chosen.ObservedAt, now, m_Cooldown);
|
||||||
|
bool spawned = Launch(chosen, now, players);
|
||||||
|
if (spawned) m_Schedule.Launched++;
|
||||||
|
else m_Schedule.Result(false);
|
||||||
|
if (m_Schedule.Failures >= 3) m_Reason = "paused after three launch failures";
|
||||||
|
if (m_Schedule.Launched >= 64) m_Reason = "round launch limit reached";
|
||||||
|
}
|
||||||
|
protected bool Launch(ERR_HQReport report, float now, array<IEntity> players)
|
||||||
|
{
|
||||||
|
IEntity caller = report.Source.GetLeaderEntity();
|
||||||
|
if (!ERR_Actors.CanFight(caller) || ERR_HQRadio.Capability(caller) == 0 || vector.DistanceXZ(caller.GetOrigin(), report.Position) > 1200) { m_Reason = "reporting squad has no suitable drone operator"; return false; }
|
||||||
|
vector pad; bool found;
|
||||||
|
for (int attempt = 0; attempt < 8; attempt++)
|
||||||
|
{
|
||||||
|
float angle = Math.PI2 * attempt / 8;
|
||||||
|
vector point = ERR_Actors.Ground(caller.GetOrigin() + Vector(Math.Cos(angle) * 8, 0, Math.Sin(angle) * 8));
|
||||||
|
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point)) continue;
|
||||||
|
bool near;
|
||||||
|
foreach (IEntity player : players) if (vector.DistanceXZ(point, player.GetOrigin()) < 100) near = true;
|
||||||
|
if (near) continue;
|
||||||
|
TraceBox trace = new TraceBox(); trace.Start = point + Vector(0, 0.5, 0); trace.End = point + Vector(0, 80, 0);
|
||||||
|
trace.Mins = "-0.5 -0.25 -0.5"; trace.Maxs = "0.5 0.25 0.5"; trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
||||||
|
if (GetGame().GetWorld().TraceMove(trace, null) < 1) continue;
|
||||||
|
pad = trace.Start; found = true; break;
|
||||||
|
}
|
||||||
|
if (!found) { m_Reason = "reporting squad has no clear launch pad"; return false; }
|
||||||
|
IEntity drone = ERR_Actors.Spawn(m_Catalog.Prefab, pad);
|
||||||
|
if (!drone) { m_Reason = "airframe creation failed"; return false; }
|
||||||
|
if (!ERR_Actors.SetFaction(drone, m_Mode.EnemyFaction())) { ERR_Actors.DeleteSpawned(drone); m_Reason = "airframe faction unavailable"; return false; }
|
||||||
|
m_Mode.Own(drone);
|
||||||
|
ERR_DroneFlight flight = new ERR_DroneFlight();
|
||||||
|
flight.Drone = drone; flight.Operator = caller; flight.Group = report.Source; flight.Home = pad; flight.Search = report.Position; flight.Created = now;
|
||||||
|
m_Flights.Insert(flight); m_Reason = "responding to reported sightings";
|
||||||
|
if (!m_Running) { m_Running = true; GetGame().GetCallqueue().CallLater(Advance, 50, true); }
|
||||||
|
ERR_HQ.Event("DRONE LAUNCH", string.Format("report M%1; launch=%2; search=%3", report.MessageId, pad, report.Position));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
protected void Advance()
|
||||||
|
{
|
||||||
|
if (m_Schedule.Stopped || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) { Stop(); return; }
|
||||||
|
float now = GetGame().GetWorld().GetWorldTime();
|
||||||
|
foreach (ERR_DroneFlight flight : m_Flights) flight.Advance(now);
|
||||||
|
if (m_Flights.IsEmpty()) { GetGame().GetCallqueue().Remove(Advance); m_Running = false; }
|
||||||
|
}
|
||||||
|
void Stop()
|
||||||
|
{
|
||||||
|
if (m_Schedule.Stopped) return;
|
||||||
|
m_Schedule.Stopped = true; m_Reason = "mission stopped";
|
||||||
|
GetGame().GetCallqueue().Remove(Advance); m_Running = false;
|
||||||
|
foreach (ERR_DroneFlight flight : m_Flights) flight.Finish("mission stopped");
|
||||||
|
m_Flights.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
// Nearby garages are discovered incrementally. Each callback performs one
|
||||||
|
// survey, maintenance, planning, resource-load or native-spawn step.
|
||||||
|
class ERR_GarageVehicleRules
|
||||||
|
{
|
||||||
|
static bool Garage(string path)
|
||||||
|
{
|
||||||
|
path.ToLower();
|
||||||
|
return path.EndsWith("/garage_e_01.et") || path.EndsWith("/houseaddon_garage_e_01.et");
|
||||||
|
}
|
||||||
|
static bool Envelope(vector low, vector high)
|
||||||
|
{
|
||||||
|
return ERR_ForceRules.FiniteVector(low) && ERR_ForceRules.FiniteVector(high)
|
||||||
|
&& low[0] >= -1.05 && high[0] <= 1.05 && low[2] >= -2.4 && high[2] <= 2.4
|
||||||
|
&& low[1] >= -0.3 && high[1] <= 1.9
|
||||||
|
&& high[0] > low[0] && high[1] > low[1] && high[2] > low[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ERR_GarageSite
|
||||||
|
{
|
||||||
|
IEntity Garage;
|
||||||
|
bool Consumed;
|
||||||
|
bool Selected;
|
||||||
|
float NextTry;
|
||||||
|
}
|
||||||
|
class ERR_ParkedVehicle
|
||||||
|
{
|
||||||
|
Vehicle Entity;
|
||||||
|
ref ERR_GarageSite Site;
|
||||||
|
vector Origin;
|
||||||
|
float HiddenSince = -1;
|
||||||
|
}
|
||||||
|
class ERR_GarageVehicles
|
||||||
|
{
|
||||||
|
protected ERR_GameMode m_Mode;
|
||||||
|
protected ref array<ref ERR_GarageSite> m_Sites = {};
|
||||||
|
protected ref array<ref ERR_ParkedVehicle> m_Parked = {};
|
||||||
|
protected ref map<string, bool> m_KnownCells = new map<string, bool>();
|
||||||
|
protected ref ERR_MotorbikeCatalog m_Bikes = new ERR_MotorbikeCatalog();
|
||||||
|
protected ref array<ResourceName> m_LoadedNames = {};
|
||||||
|
protected ref array<ref Resource> m_LoadedResources = {};
|
||||||
|
protected ref ERR_GarageSite m_Pending;
|
||||||
|
protected vector m_Position, m_Angles;
|
||||||
|
protected ResourceName m_Prefab;
|
||||||
|
protected bool m_PendingBike;
|
||||||
|
protected int m_Stage, m_Work, m_SiteCursor, m_MaintainCursor, m_PlayerCursor;
|
||||||
|
protected int m_Requested, m_Spawned, m_Claimed, m_BikeCandidate;
|
||||||
|
protected int m_GarageChance, m_SpawnRadius, m_SpawnInterval, m_CleanupRadius;
|
||||||
|
protected float m_NextSpawn, m_NextStep;
|
||||||
|
protected bool m_Stopped, m_Started;
|
||||||
|
int Spawned() { return m_Spawned; }
|
||||||
|
void ERR_GarageVehicles(ERR_GameMode mode) { m_Mode = mode; }
|
||||||
|
protected float Now() { return GetGame().GetWorld().GetWorldTime(); }
|
||||||
|
protected bool Running() { return m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active(); }
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer() || m_Stopped || m_Started) return;
|
||||||
|
m_Started = true; m_Requested = m_Mode.Settings().Get("garageVehicleCount");
|
||||||
|
m_GarageChance = m_Mode.Settings().Get("garageChancePercent");
|
||||||
|
m_SpawnRadius = m_Mode.Settings().Get("garageRadiusMetres");
|
||||||
|
m_SpawnInterval = m_Mode.Settings().Get("garageSpawnIntervalSeconds") * 1000;
|
||||||
|
m_CleanupRadius = Math.Max(1400, m_SpawnRadius + 400);
|
||||||
|
if (m_Requested <= 0 || m_GarageChance <= 0) { Stop(); return; }
|
||||||
|
m_Bikes.Find();
|
||||||
|
GetGame().GetCallqueue().CallLater(Step, 250, true);
|
||||||
|
}
|
||||||
|
protected bool Observers(array<vector> positions, array<IEntity> actors)
|
||||||
|
{
|
||||||
|
positions.Clear(); actors.Clear();
|
||||||
|
array<int> ids = {}; GetGame().GetPlayerManager().GetPlayers(ids);
|
||||||
|
SCR_EditorManagerCore editors = SCR_EditorManagerCore.Cast(SCR_EditorManagerCore.GetInstance(SCR_EditorManagerCore));
|
||||||
|
foreach (int id : ids)
|
||||||
|
{
|
||||||
|
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
|
||||||
|
if (!controller) return false;
|
||||||
|
SCR_EditorManagerEntity editor; if (editors) editor = editors.GetEditorManager(id);
|
||||||
|
if (editor && editor.IsOpened()) return false;
|
||||||
|
IEntity body = controller.GetControlledEntity();
|
||||||
|
if (body)
|
||||||
|
{
|
||||||
|
if (!ERR_ForceRules.FiniteVector(body.GetOrigin())) return false;
|
||||||
|
positions.Insert(body.GetOrigin()); actors.Insert(body);
|
||||||
|
}
|
||||||
|
if (controller.ERR_ObserverState() != ERR_ObserverReason.NONE)
|
||||||
|
{
|
||||||
|
IEntity watched = controller.ERR_ObservedEntity();
|
||||||
|
if (!watched || !ERR_ForceRules.FiniteVector(watched.GetOrigin())) return false;
|
||||||
|
positions.Insert(watched.GetOrigin()); actors.Insert(watched);
|
||||||
|
}
|
||||||
|
else if (!body) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
protected bool Nearby(vector position, array<vector> observers, float radius)
|
||||||
|
{
|
||||||
|
foreach (vector observer : observers) if (vector.DistanceXZ(position, observer) <= radius) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Sample both ends/sides and roof, not just the hidden centre of a garage.
|
||||||
|
// Open doors and windows can reveal a bay; no distance alone proves it hidden.
|
||||||
|
protected bool Hidden(vector position, vector angles, array<IEntity> actors, IEntity ignored = null)
|
||||||
|
{
|
||||||
|
vector matrix[4]; Math3D.AnglesToMatrix(angles, matrix);
|
||||||
|
foreach (IEntity actor : actors)
|
||||||
|
{
|
||||||
|
if (!actor) return false;
|
||||||
|
array<IEntity> excluded = {actor};
|
||||||
|
IEntity ride = CompartmentAccessComponent.GetVehicleIn(actor); if (ride) excluded.Insert(ride);
|
||||||
|
for (int height = 0; height < 2; height++)
|
||||||
|
for (int x = -1; x <= 1; x++)
|
||||||
|
for (int z = -1; z <= 1; z++)
|
||||||
|
{
|
||||||
|
TraceParam ray = new TraceParam();
|
||||||
|
ray.Start = actor.GetOrigin() + "0 1.7 0";
|
||||||
|
ray.End = position + matrix[0] * (x * 1.1) + matrix[2] * (z * 2.5) + Vector(0, 0.5 + height * 1.3, 0);
|
||||||
|
ray.Flags = TraceFlags.WORLD | TraceFlags.ENTS; ray.LayerMask = TRACE_LAYER_CAMERA;
|
||||||
|
ray.ExcludeArray = excluded;
|
||||||
|
float fraction = GetGame().GetWorld().TraceMove(ray, null);
|
||||||
|
if (!ERR_ForceRules.Finite(fraction) || fraction >= 0.995 || (ignored && ERR_ConvoyRules.PartOf(ray.TraceEnt, ignored))) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
protected bool Filter(IEntity entity) { return entity && SCR_DestructibleBuildingEntity.Cast(entity); }
|
||||||
|
protected bool Remember(IEntity entity)
|
||||||
|
{
|
||||||
|
if (m_Sites.Count() >= 2048) return false;
|
||||||
|
if (!entity.GetPrefabData() || !ERR_GarageVehicleRules.Garage(entity.GetPrefabData().GetPrefabName())) return true;
|
||||||
|
foreach (ERR_GarageSite site : m_Sites) if (site.Garage == entity) return true;
|
||||||
|
ERR_GarageSite added = new ERR_GarageSite(); added.Garage = entity;
|
||||||
|
added.Selected = Math.RandomInt(0, 100) < m_GarageChance;
|
||||||
|
m_Sites.Insert(added);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
protected string CellKey(vector point) { return Math.Floor(point[0] / 500).ToString() + ":" + Math.Floor(point[2] / 500).ToString(); }
|
||||||
|
protected void Discover(array<vector> observers, float now)
|
||||||
|
{
|
||||||
|
if (observers.IsEmpty() || m_Sites.Count() >= 2048 || m_KnownCells.Count() >= 16384) return;
|
||||||
|
// Round-robin observers, nearest unvisited cell first. Each update queries
|
||||||
|
// at most one 500m cell; teammates share already scanned cells.
|
||||||
|
if (m_PlayerCursor >= observers.Count()) m_PlayerCursor = 0;
|
||||||
|
vector observer = observers[m_PlayerCursor++];
|
||||||
|
int cx = Math.Floor(observer[0] / 500), cz = Math.Floor(observer[2] / 500);
|
||||||
|
for (int ring = 0; ring <= Math.Ceil(m_SpawnRadius / 500.0); ring++)
|
||||||
|
for (int x = -ring; x <= ring; x++)
|
||||||
|
for (int z = -ring; z <= ring; z++)
|
||||||
|
{
|
||||||
|
if (Math.Max(Math.AbsInt(x), Math.AbsInt(z)) != ring) continue;
|
||||||
|
vector low = Vector((cx + x) * 500, 0, (cz + z) * 500);
|
||||||
|
string key = CellKey(low);
|
||||||
|
if (m_KnownCells.Contains(key) || !Nearby(low + "250 0 250", observers, m_CleanupRadius)) continue;
|
||||||
|
m_KnownCells.Set(key, true);
|
||||||
|
vector boundsLow, boundsHigh; GetGame().GetWorldEntity().GetWorldBounds(boundsLow, boundsHigh);
|
||||||
|
low[1] = boundsLow[1] - 100; vector high = low + "500 0 500"; high[1] = boundsHigh[1] + 100;
|
||||||
|
GetGame().GetWorld().QueryEntitiesByAABB(low, high, Remember, Filter, EQueryEntitiesFlags.ALL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected bool InUse(ERR_ParkedVehicle parked)
|
||||||
|
{
|
||||||
|
if (!parked.Entity) return false;
|
||||||
|
// Movement also protects a towed, pushed or otherwise used car, even if a
|
||||||
|
// compartment callback was missed. Never reclaim somebody's transport.
|
||||||
|
if (vector.Distance(parked.Entity.GetOrigin(), parked.Origin) > 3) return true;
|
||||||
|
array<BaseCompartmentSlot> seats = {};
|
||||||
|
if (!ERR_ConvoyRules.CollectSeats(parked.Entity, seats)) return true;
|
||||||
|
foreach (BaseCompartmentSlot seat : seats) if (seat.GetOccupant()) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
protected void Claim(int index)
|
||||||
|
{
|
||||||
|
ERR_ParkedVehicle parked = m_Parked[index];
|
||||||
|
parked.Site.Consumed = true;
|
||||||
|
if (parked.Entity) m_Mode.Own(parked.Entity);
|
||||||
|
m_Claimed++; m_Parked.Remove(index);
|
||||||
|
}
|
||||||
|
void PlayerEntered(IEntity actor, IEntity entered)
|
||||||
|
{
|
||||||
|
if (m_Stopped || !Replication.IsServer() || !ERR_Actors.Player(actor)) return;
|
||||||
|
for (int i = m_Parked.Count() - 1; i >= 0; i--)
|
||||||
|
if (m_Parked[i].Entity && ERR_ConvoyRules.PartOf(entered, m_Parked[i].Entity)) Claim(i);
|
||||||
|
}
|
||||||
|
protected void Maintain(array<vector> observers, array<IEntity> actors, float now, bool known)
|
||||||
|
{
|
||||||
|
if (m_Parked.IsEmpty()) return;
|
||||||
|
if (m_MaintainCursor >= m_Parked.Count()) m_MaintainCursor = 0;
|
||||||
|
int index = m_MaintainCursor++; ERR_ParkedVehicle parked = m_Parked[index];
|
||||||
|
if (!parked.Entity) { parked.Site.Consumed = true; m_Parked.Remove(index); return; }
|
||||||
|
if (InUse(parked)) { Claim(index); return; }
|
||||||
|
if (!known || !ERR_CivilianTrafficRules.Far(parked.Entity.GetOrigin(), observers, m_CleanupRadius)
|
||||||
|
|| !Hidden(parked.Entity.GetOrigin(), parked.Entity.GetYawPitchRoll(), actors, parked.Entity)) { parked.HiddenSince = -1; return; }
|
||||||
|
if (parked.HiddenSince < 0) parked.HiddenSince = now;
|
||||||
|
if (!ERR_CivilianTrafficRules.CleanupDue(now, parked.HiddenSince)) return;
|
||||||
|
// Recheck occupancy immediately before deletion. Claimed cars are no longer
|
||||||
|
// in this collection and cannot pass through the retirement path.
|
||||||
|
if (InUse(parked)) { Claim(index); return; }
|
||||||
|
SCR_GarbageSystem garbage = SCR_GarbageSystem.GetByEntityWorld(parked.Entity);
|
||||||
|
if (garbage) garbage.UpdateBlacklist(parked.Entity, false);
|
||||||
|
ERR_Actors.DeleteSpawned(parked.Entity); m_Parked.Remove(index);
|
||||||
|
}
|
||||||
|
protected bool Occupied(ERR_GarageSite site)
|
||||||
|
{
|
||||||
|
foreach (ERR_ParkedVehicle parked : m_Parked) if (parked.Site == site) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
protected void Plan(array<vector> observers, array<IEntity> actors, float now)
|
||||||
|
{
|
||||||
|
if (observers.IsEmpty() || now < m_NextSpawn || m_Parked.Count() >= m_Requested || m_Parked.Count() + m_Claimed >= 128 || m_Sites.IsEmpty()) return;
|
||||||
|
// Bounded planning work, even after a long tour of a large terrain.
|
||||||
|
for (int candidate = 0; candidate < 8; candidate++)
|
||||||
|
{
|
||||||
|
if (m_SiteCursor >= m_Sites.Count()) m_SiteCursor = 0;
|
||||||
|
ERR_GarageSite site = m_Sites[m_SiteCursor++];
|
||||||
|
if (!site.Selected || site.Consumed || !site.Garage || now < site.NextTry || Occupied(site)) continue;
|
||||||
|
vector position = site.Garage.CoordToParent("0 0 -0.5");
|
||||||
|
if (!Nearby(position, observers, m_SpawnRadius)) continue;
|
||||||
|
site.NextTry = now + 15000;
|
||||||
|
if (!ERR_CivilianTrafficRules.Far(position, observers, 35) || !ERR_Actors.Alive(site.Garage)) continue;
|
||||||
|
vector transform[4]; site.Garage.GetWorldTransform(transform);
|
||||||
|
TraceParam floor = new TraceParam(); floor.Start = position + "0 1 0"; floor.End = position - "0 1 0";
|
||||||
|
floor.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
|
||||||
|
float fraction = GetGame().GetWorld().TraceMove(floor, null);
|
||||||
|
if (!ERR_ForceRules.Finite(fraction) || fraction >= 1 || floor.TraceNorm[1] < 0.98) return;
|
||||||
|
position = vector.Lerp(floor.Start, floor.End, fraction) + "0 0.08 0";
|
||||||
|
vector angles = site.Garage.GetYawPitchRoll();
|
||||||
|
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), position) || !Clear(position, transform) || !Hidden(position, angles, actors)) return;
|
||||||
|
m_Pending = site; m_Position = position; m_Angles = angles;
|
||||||
|
m_Prefab = ERR_CivilianTraffic.CarPrefab(Math.RandomInt(0, 2));
|
||||||
|
m_PendingBike = m_Spawned % 4 == 3 && m_BikeCandidate < m_Bikes.Prefabs.Count();
|
||||||
|
if (m_PendingBike) m_Prefab = m_Bikes.Prefabs[m_BikeCandidate];
|
||||||
|
m_Stage = 1; return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected void Pending(array<vector> observers, array<IEntity> actors)
|
||||||
|
{
|
||||||
|
if (m_Stage == 1)
|
||||||
|
{
|
||||||
|
if (!m_LoadedNames.Contains(m_Prefab))
|
||||||
|
{
|
||||||
|
Resource resource = Resource.Load(m_Prefab);
|
||||||
|
if (!resource || !resource.IsValid()) { if (m_PendingBike) m_BikeCandidate++; m_Pending = null; return; }
|
||||||
|
m_LoadedNames.Insert(m_Prefab); m_LoadedResources.Insert(resource);
|
||||||
|
}
|
||||||
|
m_Stage = 2; return;
|
||||||
|
}
|
||||||
|
ERR_GarageSite site = m_Pending; m_Pending = null;
|
||||||
|
if (!site.Garage || site.Consumed || !ERR_Actors.Alive(site.Garage) || !Nearby(m_Position, observers, m_SpawnRadius)
|
||||||
|
|| !ERR_CivilianTrafficRules.Far(m_Position, observers, 35) || !Hidden(m_Position, m_Angles, actors)) return;
|
||||||
|
vector transform[4]; Math3D.AnglesToMatrix(m_Angles, transform);
|
||||||
|
if (!Clear(m_Position, transform)) return;
|
||||||
|
m_NextSpawn = Now() + m_SpawnInterval;
|
||||||
|
IEntity entity = ERR_Actors.Spawn(m_Prefab, m_Position, m_Angles);
|
||||||
|
Vehicle vehicle = Vehicle.Cast(entity);
|
||||||
|
vector low, high; if (vehicle) vehicle.GetBounds(low, high);
|
||||||
|
array<BaseCompartmentSlot> seats = {}; bool driver; bool armed;
|
||||||
|
if (vehicle && ERR_ConvoyRules.CollectSeats(vehicle, seats))
|
||||||
|
foreach (BaseCompartmentSlot seat : seats)
|
||||||
|
{
|
||||||
|
if (seat.GetType() == ECompartmentType.TURRET) armed = true;
|
||||||
|
if (seat.GetType() == ECompartmentType.PILOT && seat.IsCompartmentAccessible() && !seat.GetOccupant()) driver = true;
|
||||||
|
}
|
||||||
|
if (!driver || armed || !ERR_GarageVehicleRules.Envelope(low, high) || !Clear(m_Position, transform, entity))
|
||||||
|
{
|
||||||
|
ERR_Actors.DeleteSpawned(entity); if (m_PendingBike) m_BikeCandidate++; return;
|
||||||
|
}
|
||||||
|
Faction civilian = GetGame().GetFactionManager().GetFactionByKey("CIV");
|
||||||
|
if (civilian) ERR_Actors.SetFaction(vehicle, civilian);
|
||||||
|
SCR_FuelManagerComponent fuel = SCR_FuelManagerComponent.Cast(vehicle.FindComponent(SCR_FuelManagerComponent));
|
||||||
|
if (fuel) fuel.SetTotalFuelPercentage(Math.RandomFloat(0.3, 0.8));
|
||||||
|
ERR_ParkedVehicle parked = new ERR_ParkedVehicle(); parked.Entity = vehicle; parked.Site = site; parked.Origin = m_Position;
|
||||||
|
m_Parked.Insert(parked); m_Spawned++;
|
||||||
|
SCR_GarbageSystem garbage = SCR_GarbageSystem.GetByEntityWorld(vehicle);
|
||||||
|
if (garbage) garbage.UpdateBlacklist(vehicle, true);
|
||||||
|
}
|
||||||
|
protected void Step()
|
||||||
|
{
|
||||||
|
if (m_Stopped || !Replication.IsServer() || !Running()) return;
|
||||||
|
float now = Now();
|
||||||
|
if (now < m_NextStep) return;
|
||||||
|
m_NextStep = now + 250; // Do not catch up missed work in one slow frame.
|
||||||
|
array<vector> observers = {}; array<IEntity> actors = {};
|
||||||
|
bool known = Observers(observers, actors);
|
||||||
|
if (!known)
|
||||||
|
{
|
||||||
|
m_Pending = null;
|
||||||
|
// Unknown cameras invalidate every retirement grace period immediately.
|
||||||
|
foreach (ERR_ParkedVehicle parked : m_Parked) parked.HiddenSince = -1;
|
||||||
|
Maintain(observers, actors, now, false); return;
|
||||||
|
}
|
||||||
|
if (m_Pending) { Pending(observers, actors); return; }
|
||||||
|
m_Work = (m_Work + 1) % 3;
|
||||||
|
if (m_Work == 0) Maintain(observers, actors, now, true);
|
||||||
|
else if (m_Work == 1) Discover(observers, now);
|
||||||
|
else Plan(observers, actors, now);
|
||||||
|
}
|
||||||
|
// Filled oriented body volume includes furniture, doors, other vehicles and
|
||||||
|
// players. The garage itself is deliberately NOT excluded from collision.
|
||||||
|
protected bool Clear(vector position, vector transform[4], IEntity ignored = null)
|
||||||
|
{
|
||||||
|
TraceOBB body = new TraceOBB();
|
||||||
|
body.Start = position; body.End = position + "0 0.01 0";
|
||||||
|
body.Mins = "-1.1 0.12 -2.5"; body.Maxs = "1.1 2 2.5";
|
||||||
|
for (int i = 0; i < 3; i++) body.Mat[i] = transform[i];
|
||||||
|
body.Flags = TraceFlags.WORLD | TraceFlags.ENTS | TraceFlags.OCEAN;
|
||||||
|
body.Exclude = ignored;
|
||||||
|
float fraction = GetGame().GetWorld().TraceMove(body, null);
|
||||||
|
return ERR_ForceRules.Finite(fraction) && fraction >= 1;
|
||||||
|
}
|
||||||
|
void Stop()
|
||||||
|
{
|
||||||
|
m_Stopped = true;
|
||||||
|
if (GetGame()) GetGame().GetCallqueue().Remove(Step);
|
||||||
|
m_Pending = null; m_Sites.Clear();
|
||||||
|
// No visible deletion at round end. World teardown owns remaining vehicles.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -124,6 +124,8 @@ class ERR_HQ : ERR_CommandService
|
|||||||
ERR_AirAssaultSystem AirAssault() { return m_AirAssault; }
|
ERR_AirAssaultSystem AirAssault() { return m_AirAssault; }
|
||||||
protected ref ERR_AirReconSystem m_Recon;
|
protected ref ERR_AirReconSystem m_Recon;
|
||||||
protected ref ERR_PlaneRecon m_PlaneRecon;
|
protected ref ERR_PlaneRecon m_PlaneRecon;
|
||||||
|
protected ref ERR_DroneRecon m_DroneRecon;
|
||||||
|
ERR_DroneRecon DroneRecon() { return m_DroneRecon; }
|
||||||
ERR_PlaneRecon PlaneRecon() { return m_PlaneRecon; }
|
ERR_PlaneRecon PlaneRecon() { return m_PlaneRecon; }
|
||||||
ERR_AirReconSystem Recon() { return m_Recon; }
|
ERR_AirReconSystem Recon() { return m_Recon; }
|
||||||
ERR_ScoutSystem Scouts() { return m_Scouts; }
|
ERR_ScoutSystem Scouts() { return m_Scouts; }
|
||||||
@@ -173,12 +175,14 @@ class ERR_HQ : ERR_CommandService
|
|||||||
m_Hunters = new ERR_HunterService(mode, null);
|
m_Hunters = new ERR_HunterService(mode, null);
|
||||||
m_Hunters.ConfigureOpening(mode.Settings().Get("hunterSpawnDistanceMetres"));
|
m_Hunters.ConfigureOpening(mode.Settings().Get("hunterSpawnDistanceMetres"));
|
||||||
m_Hunters.ConfigureMotorbikes(mode.Settings().Get("motorbikeCount"));
|
m_Hunters.ConfigureMotorbikes(mode.Settings().Get("motorbikeCount"));
|
||||||
|
m_Hunters.ConfigureSarka();
|
||||||
m_ObjectiveDefence = new ERR_ObjectiveDefence(mode, this);
|
m_ObjectiveDefence = new ERR_ObjectiveDefence(mode, this);
|
||||||
m_Towns = new ERR_TownGarrisons(mode);
|
m_Towns = new ERR_TownGarrisons(mode);
|
||||||
m_Roadblocks = new ERR_Roadblocks(mode);
|
m_Roadblocks = new ERR_Roadblocks(mode);
|
||||||
m_Scouts = new ERR_ScoutSystem(mode);
|
m_Scouts = new ERR_ScoutSystem(mode);
|
||||||
m_Recon = new ERR_AirReconSystem(mode);
|
m_Recon = new ERR_AirReconSystem(mode);
|
||||||
m_PlaneRecon = new ERR_PlaneRecon(mode);
|
m_PlaneRecon = new ERR_PlaneRecon(mode);
|
||||||
|
m_DroneRecon = new ERR_DroneRecon(mode);
|
||||||
m_AirAssault = new ERR_AirAssaultSystem(this, mode);
|
m_AirAssault = new ERR_AirAssaultSystem(this, mode);
|
||||||
m_Airfield = new ERR_AirfieldSupport(this, mode); m_Recovery = new ERR_BodyRecovery(this, mode);
|
m_Airfield = new ERR_AirfieldSupport(this, mode); m_Recovery = new ERR_BodyRecovery(this, mode);
|
||||||
bool legacyFixture;
|
bool legacyFixture;
|
||||||
@@ -503,6 +507,24 @@ class ERR_HQ : ERR_CommandService
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
// A camera observation goes through the physical operator's existing radio
|
||||||
|
// route and delivery queue. It never writes directly into HQ memory.
|
||||||
|
bool QueueDroneObservation(SCR_AIGroup group, ERR_HQReport report, float now)
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer() || !group || !report || !report.DroneObservation || m_Mode.Phase() < ERR_Phase.RADIO_SEARCH) return false;
|
||||||
|
foreach (ERR_HQUnit unit : m_Units)
|
||||||
|
{
|
||||||
|
if (unit.Group != group || unit.Uplink || !unit.Radio || !ERR_Actors.CanFight(unit.Radio)) continue;
|
||||||
|
ERR_HQRoute route = ObservationRoute(unit);
|
||||||
|
if (!route) return false;
|
||||||
|
report.MessageId = m_Journal.Message(); report.Source = group;
|
||||||
|
route.MessageId = report.MessageId; route.Payload = "drone camera; " + report.Description(now);
|
||||||
|
unit.PendingReport = report.Snapshot(); unit.Uplink = route;
|
||||||
|
m_Journal.Write("DRONE REPORT", route.Payload, report.MessageId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
void SupportContacts(out array<ref ERR_HQReport> contacts, float now)
|
void SupportContacts(out array<ref ERR_HQReport> contacts, float now)
|
||||||
{
|
{
|
||||||
contacts.Clear();
|
contacts.Clear();
|
||||||
@@ -536,10 +558,11 @@ class ERR_HQ : ERR_CommandService
|
|||||||
ERR_HQJournal Journal() { return m_Journal; }
|
ERR_HQJournal Journal() { return m_Journal; }
|
||||||
string Status()
|
string Status()
|
||||||
{
|
{
|
||||||
return string.Format("Enemy HQ: %1 registered groups; %2 received sightings; %3 assigned responses.", m_Units.Count(), m_Contacts.Count(), Assigned())
|
string status = string.Format("Enemy HQ: %1 registered groups; %2 received sightings; %3 assigned responses.", m_Units.Count(), m_Contacts.Count(), Assigned())
|
||||||
+ string.Format("\nHQ memory: %1 tracked reports; %2 searched areas.\nAggression: %3 of 30, based on %4 identified bodies (one level per three bodies).", m_Memory.Count(), m_Coverage.Count(), m_Watch.Aggression(), m_Watch.IdentifiedBodies())
|
+ string.Format("\nHQ memory: %1 tracked reports; %2 searched areas.\nAggression: %3 of 30, based on %4 identified bodies (one level per three bodies).", m_Memory.Count(), m_Coverage.Count(), m_Watch.Aggression(), m_Watch.IdentifiedBodies())
|
||||||
+ "\n" + m_Hunters.Status() + "\n" + m_Mode.Forces().Status() + "\n" + m_Mortars.Status()
|
+ "\n" + m_Hunters.Status() + "\n" + m_Mode.Forces().Status() + "\n" + m_Mortars.Status()
|
||||||
+ "\n" + m_Logistics.Status() + "\n" + m_Resupply.Status() + "\n" + m_Recon.Status() + "\n" + m_PlaneRecon.Status();
|
+ "\n" + m_Logistics.Status() + "\n" + m_Resupply.Status() + "\n" + m_Recon.Status() + "\n" + m_PlaneRecon.Status();
|
||||||
|
return status + "\n" + m_DroneRecon.Status();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void BindPhysical(ERR_HQUnit unit)
|
protected void BindPhysical(ERR_HQUnit unit)
|
||||||
@@ -1741,9 +1764,11 @@ class ERR_HQ : ERR_CommandService
|
|||||||
|
|
||||||
override void Maintain(float now)
|
override void Maintain(float now)
|
||||||
{
|
{
|
||||||
|
m_Hunters.PauseSarkaIfEmpty();
|
||||||
// Aircraft already airborne keep flying even when the server is empty.
|
// Aircraft already airborne keep flying even when the server is empty.
|
||||||
m_Recon.Tick(now);
|
m_Recon.Tick(now);
|
||||||
m_PlaneRecon.Tick(now);
|
m_PlaneRecon.Tick(now);
|
||||||
|
m_DroneRecon.Tick(now);
|
||||||
m_AirAssault.Maintain(now);
|
m_AirAssault.Maintain(now);
|
||||||
m_Airfield.Maintain(now);
|
m_Airfield.Maintain(now);
|
||||||
}
|
}
|
||||||
@@ -1878,6 +1903,7 @@ class ERR_HQ : ERR_CommandService
|
|||||||
m_AirAssault.Stop();
|
m_AirAssault.Stop();
|
||||||
m_Recon.Stop();
|
m_Recon.Stop();
|
||||||
m_PlaneRecon.Stop();
|
m_PlaneRecon.Stop();
|
||||||
|
m_DroneRecon.Stop();
|
||||||
m_Scouts.Stop();
|
m_Scouts.Stop();
|
||||||
m_ConvoyStartup.Stop();
|
m_ConvoyStartup.Stop();
|
||||||
if (m_SupplyConvoys) m_SupplyConvoys.Stop();
|
if (m_SupplyConvoys) m_SupplyConvoys.Stop();
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ class ERR_HQSuppressActivity : SCR_AISuppressActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[BaseContainerProps()]
|
||||||
modded class SCR_AIGoalReaction_Suppress
|
modded class SCR_AIGoalReaction_Suppress
|
||||||
{
|
{
|
||||||
override void PerformReaction(notnull SCR_AIUtilityComponent utility, SCR_AIMessageBase message)
|
override void PerformReaction(notnull SCR_AIUtilityComponent utility, SCR_AIMessageBase message)
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ class ERR_HQReport
|
|||||||
bool HasVelocity;
|
bool HasVelocity;
|
||||||
bool Engaged;
|
bool Engaged;
|
||||||
bool Distress;
|
bool Distress;
|
||||||
|
bool DroneObservation;
|
||||||
int SeenEscapists = -1;
|
int SeenEscapists = -1;
|
||||||
string TravelMode = "unknown";
|
string TravelMode = "unknown";
|
||||||
string RoadStatus = "road unknown";
|
string RoadStatus = "road unknown";
|
||||||
@@ -72,6 +73,7 @@ class ERR_HQReport
|
|||||||
copy.ObservedAt = ObservedAt;
|
copy.ObservedAt = ObservedAt;
|
||||||
copy.Engaged = Engaged;
|
copy.Engaged = Engaged;
|
||||||
copy.Distress = Distress;
|
copy.Distress = Distress;
|
||||||
|
copy.DroneObservation = DroneObservation;
|
||||||
copy.HasVelocity = HasVelocity;
|
copy.HasVelocity = HasVelocity;
|
||||||
copy.SeenEscapists = SeenEscapists;
|
copy.SeenEscapists = SeenEscapists;
|
||||||
copy.TravelMode = TravelMode;
|
copy.TravelMode = TravelMode;
|
||||||
|
|||||||
@@ -14,6 +14,20 @@ class ERR_VehicleBehaviorCleanup
|
|||||||
|
|
||||||
// Preserve original hunter actors. Reboarding uses native AI tasks, never new
|
// Preserve original hunter actors. Reboarding uses native AI tasks, never new
|
||||||
// crew, vehicle teleports, health restoration, or ammunition replacement.
|
// crew, vehicle teleports, health restoration, or ammunition replacement.
|
||||||
|
// Readiness failures share one deadline even if the reason changes or combat
|
||||||
|
// briefly interrupts reboarding. Native combat and moving vehicles defer exit.
|
||||||
|
class ERR_HunterRecoveryWait
|
||||||
|
{
|
||||||
|
float Since = -1;
|
||||||
|
void Reset() { Since = -1; }
|
||||||
|
bool Expired(bool registered, bool ready, bool combat, float speed, float now)
|
||||||
|
{
|
||||||
|
if (!registered || ready) { Reset(); return false; }
|
||||||
|
if (Since < 0 || now < Since) Since = now;
|
||||||
|
return now - Since >= 120000 && !combat && speed >= 0 && speed <= 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ERR_HunterCrewRecovery
|
class ERR_HunterCrewRecovery
|
||||||
{
|
{
|
||||||
protected ERR_HunterVehicle m_Unit;
|
protected ERR_HunterVehicle m_Unit;
|
||||||
@@ -22,6 +36,7 @@ class ERR_HunterCrewRecovery
|
|||||||
protected float m_NextAttempt;
|
protected float m_NextAttempt;
|
||||||
protected string m_LastReason;
|
protected string m_LastReason;
|
||||||
protected bool m_Stopped;
|
protected bool m_Stopped;
|
||||||
|
ref ERR_HunterRecoveryWait Wait = new ERR_HunterRecoveryWait();
|
||||||
string Reason;
|
string Reason;
|
||||||
void ERR_HunterCrewRecovery(ERR_HunterVehicle unit) { m_Unit = unit; }
|
void ERR_HunterCrewRecovery(ERR_HunterVehicle unit) { m_Unit = unit; }
|
||||||
void SuspendBoarding()
|
void SuspendBoarding()
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// Wave size is sampled from delivered body-driven aggression every ten minutes.
|
||||||
|
// Failed placements remain pending in this wave, never accumulate into a backlog.
|
||||||
|
class ERR_SarkaWaves
|
||||||
|
{
|
||||||
|
float NextWave = -1;
|
||||||
|
float NextAttempt;
|
||||||
|
int Pending;
|
||||||
|
int Target;
|
||||||
|
int Wave;
|
||||||
|
int Spawned;
|
||||||
|
int AngerPerCar = 3;
|
||||||
|
void Configure(int points)
|
||||||
|
{
|
||||||
|
if (AngerPerCar == points) return;
|
||||||
|
AngerPerCar = points;
|
||||||
|
if (points == 0) Pause();
|
||||||
|
}
|
||||||
|
void Pause() { NextWave = -1; Pending = 0; Target = 0; }
|
||||||
|
bool Due(float now, int aggression, bool playing)
|
||||||
|
{
|
||||||
|
if (!playing || AngerPerCar == 0) { Pause(); return false; }
|
||||||
|
if (NextWave < 0) { NextWave = now + 600000; return false; }
|
||||||
|
if (now >= NextWave)
|
||||||
|
{
|
||||||
|
NextWave = now + 600000; NextAttempt = now;
|
||||||
|
// Whole thresholds only: five points per car means zero at four, one at five.
|
||||||
|
Target = Math.Clamp(aggression, 0, 30) / AngerPerCar; Pending = Target; Wave++;
|
||||||
|
}
|
||||||
|
if (Pending == 0 || now < NextAttempt) return false;
|
||||||
|
NextAttempt = now + 5000;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
void Commit() { if (Pending > 0) { Pending--; Spawned++; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
class ERR_SarkaRules
|
||||||
|
{
|
||||||
|
static ResourceName Prefab() { return "{AD80FB47D868C775}Prefabs/Vehicles/Wheeled/S105/S105_green.et"; }
|
||||||
|
static bool Seats(int drivers, int turrets, int cargo, int total)
|
||||||
|
{
|
||||||
|
return drivers == 1 && turrets == 0 && cargo == 3 && total == 4;
|
||||||
|
}
|
||||||
|
static bool InBand(vector position, array<vector> players)
|
||||||
|
{
|
||||||
|
if (players.IsEmpty() || !ERR_ForceRules.FiniteVector(position)) return false;
|
||||||
|
float nearest = 1000000;
|
||||||
|
foreach (vector player : players)
|
||||||
|
{
|
||||||
|
if (!ERR_ForceRules.FiniteVector(player)) return false;
|
||||||
|
nearest = Math.Min(nearest, vector.DistanceXZ(position, player));
|
||||||
|
}
|
||||||
|
return nearest >= 1000 && nearest <= 1250;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,7 @@ class ERR_HunterVehicle : ERR_ConvoyVehicle
|
|||||||
float PendingObservedAt = -1;
|
float PendingObservedAt = -1;
|
||||||
bool TroopTransport;
|
bool TroopTransport;
|
||||||
bool Motorbike;
|
bool Motorbike;
|
||||||
|
bool Sarka;
|
||||||
bool OpeningPatrol;
|
bool OpeningPatrol;
|
||||||
vector PatrolHome;
|
vector PatrolHome;
|
||||||
bool RadioRequested;
|
bool RadioRequested;
|
||||||
@@ -80,6 +81,7 @@ class ERR_HunterVehicle : ERR_ConvoyVehicle
|
|||||||
bool DeliveredContact;
|
bool DeliveredContact;
|
||||||
ref ERR_HunterCrewRecovery Recovery;
|
ref ERR_HunterCrewRecovery Recovery;
|
||||||
ref ERR_HunterTactics Tactics;
|
ref ERR_HunterTactics Tactics;
|
||||||
|
ref ERR_HunterApproachMemory FailedApproaches = new ERR_HunterApproachMemory();
|
||||||
ref ERR_HunterCloseContact CloseContact;
|
ref ERR_HunterCloseContact CloseContact;
|
||||||
bool CloseContactHeld;
|
bool CloseContactHeld;
|
||||||
bool CombatHeld;
|
bool CombatHeld;
|
||||||
@@ -136,6 +138,98 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
protected int m_ReserveSpent;
|
protected int m_ReserveSpent;
|
||||||
protected float m_NextReserve;
|
protected float m_NextReserve;
|
||||||
protected ref ERR_HunterVehicle m_PendingReserve;
|
protected ref ERR_HunterVehicle m_PendingReserve;
|
||||||
|
protected ref ERR_SarkaWaves m_SarkaWaves = new ERR_SarkaWaves();
|
||||||
|
protected string m_SarkaStatus = "waiting for first wave after prison escape";
|
||||||
|
void ConfigureSarka()
|
||||||
|
{
|
||||||
|
int points = m_Mode.Settings().Get("sarkaAngerPerCar");
|
||||||
|
bool changed = points != m_SarkaWaves.AngerPerCar;
|
||||||
|
m_SarkaWaves.Configure(points);
|
||||||
|
if (changed) m_SarkaStatus = "saved anger threshold applies to the next wave";
|
||||||
|
if (points == 0) m_SarkaStatus = "new waves disabled; existing teams remain";
|
||||||
|
}
|
||||||
|
string SarkaStatus()
|
||||||
|
{
|
||||||
|
string ratio = "New waves disabled.";
|
||||||
|
if (m_SarkaWaves.AngerPerCar > 0) ratio = string.Format("One car per %1 anger points (rounded down).", m_SarkaWaves.AngerPerCar);
|
||||||
|
return string.Format("Sarka waves: %1 deployed, %2/%3 pending in wave %4; %5. %6", m_SarkaWaves.Spawned, m_SarkaWaves.Pending, m_SarkaWaves.Target, m_SarkaWaves.Wave, m_SarkaStatus, ratio);
|
||||||
|
}
|
||||||
|
// HQ maintenance runs even while tactical ticks pause in an empty round.
|
||||||
|
void PauseSarkaIfEmpty()
|
||||||
|
{
|
||||||
|
if (m_Stopped || !m_Mode || !m_Mode.Active()) return;
|
||||||
|
array<IEntity> players = {}; ERR_Actors.Players(players);
|
||||||
|
if (players.IsEmpty()) m_SarkaWaves.Pause();
|
||||||
|
}
|
||||||
|
protected void TickSarka(ERR_HQ hq, float now)
|
||||||
|
{
|
||||||
|
ConfigureSarka();
|
||||||
|
// Released crews keep their physical/HQ ownership; discard only wave tracking.
|
||||||
|
for (int retired = m_Hunters.Count() - 1; retired >= 0; retired--)
|
||||||
|
if (m_Hunters[retired].Sarka && m_Hunters[retired].Released) m_Hunters.Remove(retired);
|
||||||
|
array<IEntity> players = {}; ERR_Actors.Players(players);
|
||||||
|
int previousWave = m_SarkaWaves.Wave;
|
||||||
|
bool due = m_SarkaWaves.Due(now, hq.Watch().Aggression(), !players.IsEmpty());
|
||||||
|
if (previousWave != m_SarkaWaves.Wave)
|
||||||
|
{
|
||||||
|
m_SarkaStatus = "placing four-person special-forces teams on roads";
|
||||||
|
if (m_SarkaWaves.Target == 0) m_SarkaStatus = "no aggression; waiting for the next interval";
|
||||||
|
ERR_HQ.Event("SARKA WAVE", string.Format("wave=%1 aggression=%2 cars=%3 identified bodies=%4", m_SarkaWaves.Wave, hq.Watch().Aggression(), m_SarkaWaves.Target, hq.Watch().IdentifiedBodies()));
|
||||||
|
}
|
||||||
|
if (!due) return;
|
||||||
|
int awaiting;
|
||||||
|
foreach (ERR_HunterVehicle pending : m_Hunters) if (!pending.Released && !pending.Registered) awaiting++;
|
||||||
|
if (m_Mode.Forces().Count() + awaiting >= 250) { m_SarkaStatus = "waiting for formation budget"; return; }
|
||||||
|
if (!ERR_EnemyLoadoutCatalog.Available(ERR_EnemyMedicalCatalog.Crew(m_Mode, false)) || !ERR_EnemyLoadoutCatalog.Available(ERR_EnemyMedicalCatalog.Crew(m_Mode, true)))
|
||||||
|
{ m_SarkaStatus = "selected faction lacks response crew"; return; }
|
||||||
|
array<vector> positions = {};
|
||||||
|
foreach (IEntity player : players) positions.Insert(player.GetOrigin());
|
||||||
|
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||||
|
for (int attempt = 0; attempt < 8; attempt++)
|
||||||
|
{
|
||||||
|
vector offset = Vector(Math.RandomFloat(-1, 1), 0, Math.RandomFloat(-1, 1));
|
||||||
|
if (offset.Length() < 0.1) continue;
|
||||||
|
vector seed = positions[Math.RandomInt(0, positions.Count())] + offset.Normalized() * 1125;
|
||||||
|
vector position, angles;
|
||||||
|
if (!ERR_SupplyConvoySystem.ApproachDirected(seed, position, angles, 150) || !ERR_SarkaRules.InBand(position, positions) || !placement.Clear(position, angles[0])) continue;
|
||||||
|
bool occupied;
|
||||||
|
foreach (ERR_HunterVehicle other : m_Hunters)
|
||||||
|
if (other.Entity && vector.DistanceXZ(position, other.Entity.GetOrigin()) < 60) occupied = true;
|
||||||
|
if (occupied) continue;
|
||||||
|
if (SpawnSarka(now, position, angles))
|
||||||
|
{
|
||||||
|
m_SarkaWaves.Commit();
|
||||||
|
m_SarkaStatus = "four-person teams join HQ hunters; departures spaced five seconds apart";
|
||||||
|
}
|
||||||
|
else m_SarkaStatus = "vehicle or crew setup failed; retrying this wave";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_SarkaStatus = "waiting for a clear road away from players";
|
||||||
|
}
|
||||||
|
protected bool SpawnSarka(float now, vector position, vector angles)
|
||||||
|
{
|
||||||
|
ERR_HunterVehicle unit = new ERR_HunterVehicle();
|
||||||
|
unit.Sarka = true; unit.TroopTransport = true; unit.OpeningPatrol = true; unit.PatrolHome = position;
|
||||||
|
unit.Source = "aggression-sarka-" + (m_SarkaWaves.Spawned + 1).ToString();
|
||||||
|
IEntity spawned = ERR_Actors.Spawn(ERR_SarkaRules.Prefab(), position, angles);
|
||||||
|
unit.Entity = Vehicle.Cast(spawned);
|
||||||
|
if (!unit.Entity || !unit.Entity.FindComponent(AICarMovementComponent) || !unit.Entity.FindComponent(SCR_AIVehicleUsageComponent))
|
||||||
|
{ ERR_Actors.DeleteSpawned(spawned); return false; }
|
||||||
|
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), position));
|
||||||
|
if (!unit.Group || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit))
|
||||||
|
{
|
||||||
|
foreach (IEntity failed : unit.Members) ERR_Actors.DeleteSpawned(failed);
|
||||||
|
ERR_Actors.DeleteSpawned(unit.Group); ERR_Actors.DeleteSpawned(unit.Entity); return false;
|
||||||
|
}
|
||||||
|
m_Mode.Own(unit.Entity); m_Mode.Own(unit.Group);
|
||||||
|
ERR_Actors.SetFaction(unit.Entity, m_Mode.EnemyFaction());
|
||||||
|
unit.LaunchedAt = now; unit.Deadline = now + 120000; m_Hunters.Insert(unit);
|
||||||
|
unit.Recovery = new ERR_HunterCrewRecovery(unit); unit.Recovery.Update(m_Mode, now);
|
||||||
|
AICarMovementComponent movement = AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent));
|
||||||
|
movement.SetCruiseSpeed(45);
|
||||||
|
ERR_HQ.Event("SARKA SPAWN", string.Format("%1 wave=%2 crew=%3 position=%4; four original response soldiers including driver and medic", unit.Source, m_SarkaWaves.Wave, unit.Members.Count(), position));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
protected int m_OpeningDistance = 1000;
|
protected int m_OpeningDistance = 1000;
|
||||||
// Called once when the round's HQ is created; menu edits affect the next HQ.
|
// Called once when the round's HQ is created; menu edits affect the next HQ.
|
||||||
void ConfigureOpening(int distance)
|
void ConfigureOpening(int distance)
|
||||||
@@ -275,7 +369,6 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
if (!m_Mode.Active() || !actor || !entered || !m_Mode.Players()) return;
|
if (!m_Mode.Active() || !actor || !entered || !m_Mode.Players()) return;
|
||||||
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
|
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
|
||||||
if (!hq || hq.Hunters() != this || !m_Mode.Players().CurrentActor(actor)) return;
|
if (!hq || hq.Hunters() != this || !m_Mode.Players().CurrentActor(actor)) return;
|
||||||
if (m_Hunters.Count() > 21) return;
|
|
||||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||||
{
|
{
|
||||||
// Released survivors already have independent HQ ownership. Do not revoke
|
// Released survivors already have independent HQ ownership. Do not revoke
|
||||||
@@ -304,7 +397,7 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
int Deployed()
|
int Deployed()
|
||||||
{
|
{
|
||||||
int count;
|
int count;
|
||||||
foreach (ERR_HunterVehicle unit : m_Hunters) if (!unit.Motorbike) count++;
|
foreach (ERR_HunterVehicle unit : m_Hunters) if (!unit.Motorbike && !unit.Sarka) count++;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
int ActiveHunters()
|
int ActiveHunters()
|
||||||
@@ -324,7 +417,7 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
{
|
{
|
||||||
int count;
|
int count;
|
||||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||||
if (!unit.Motorbike && !ERR_HunterFleetRules.Unavailable(hq.Watch().Find(unit.Source), unit.LaunchedAt, now)) count++;
|
if (!unit.Motorbike && !unit.Sarka && !ERR_HunterFleetRules.Unavailable(hq.Watch().Find(unit.Source), unit.LaunchedAt, now)) count++;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
protected ERR_HunterVehicle ReserveSource(ERR_HQ hq, float now)
|
protected ERR_HunterVehicle ReserveSource(ERR_HQ hq, float now)
|
||||||
@@ -332,7 +425,7 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
if (!ERR_HunterFleetRules.ReserveDue(Deployed(), m_Target, m_ReserveSpent, AvailableToHQ(hq, now), m_NextReserve, now)) return null;
|
if (!ERR_HunterFleetRules.ReserveDue(Deployed(), m_Target, m_ReserveSpent, AvailableToHQ(hq, now), m_NextReserve, now)) return null;
|
||||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||||
{
|
{
|
||||||
if (unit.Motorbike || unit.ReserveCovered || now - unit.LaunchedAt < 480000) continue;
|
if (unit.Motorbike || unit.Sarka || unit.ReserveCovered || now - unit.LaunchedAt < 480000) continue;
|
||||||
if (ERR_HunterFleetRules.Unavailable(hq.Watch().Find(unit.Source), unit.LaunchedAt, now)) return unit;
|
if (ERR_HunterFleetRules.Unavailable(hq.Watch().Find(unit.Source), unit.LaunchedAt, now)) return unit;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -344,7 +437,13 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
ERR_HQ.Event("HUNTER RESERVE", string.Format("%1 replaces unavailable patrol %2; reserve=%3/3; next departure in at least 480s; silence is not a confirmed death", unit.Source, m_PendingReserve.Source, m_ReserveSpent));
|
ERR_HQ.Event("HUNTER RESERVE", string.Format("%1 replaces unavailable patrol %2; reserve=%3/3; next departure in at least 480s; silence is not a confirmed death", unit.Source, m_PendingReserve.Source, m_ReserveSpent));
|
||||||
m_PendingReserve = null;
|
m_PendingReserve = null;
|
||||||
}
|
}
|
||||||
override string Status() { return string.Format("HQ hunting patrols: %1 active including bikes; %2 UAZ/trucks deployed; aggression allowance %3, reserve used %4/3 (15 total maximum). Reserve departures at least eight minutes apart. Motorbikes have a separate allowance.", ActiveHunters(), Deployed(), m_Target, m_ReserveSpent); }
|
override string Status()
|
||||||
|
{
|
||||||
|
// Keep the formatted patrol summary before calling another formatting method.
|
||||||
|
string patrols = string.Format("HQ hunting patrols: %1 active including bikes and Sarkas; %2 UAZ/trucks deployed; aggression allowance %3, reserve used %4/3 (15 total maximum). Reserve departures at least eight minutes apart. Motorbikes have a separate allowance.", ActiveHunters(), Deployed(), m_Target, m_ReserveSpent);
|
||||||
|
string sarka = SarkaStatus();
|
||||||
|
return patrols + "\n" + sarka;
|
||||||
|
}
|
||||||
protected bool StartHunter(ERR_HQ hq, float now)
|
protected bool StartHunter(ERR_HQ hq, float now)
|
||||||
{
|
{
|
||||||
if ((!m_PendingReserve && Deployed() >= m_Target + m_ReserveSpent) || Deployed() >= 15 || m_Mode.Forces().Count() >= 250) return false;
|
if ((!m_PendingReserve && Deployed() >= m_Target + m_ReserveSpent) || Deployed() >= 15 || m_Mode.Forces().Count() >= 250) return false;
|
||||||
@@ -410,6 +509,10 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
{
|
{
|
||||||
if (!ERR_MotorbikeRules.Seats(drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false;
|
if (!ERR_MotorbikeRules.Seats(drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false;
|
||||||
}
|
}
|
||||||
|
else if (hunter.Sarka)
|
||||||
|
{
|
||||||
|
if (!ERR_SarkaRules.Seats(drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false;
|
||||||
|
}
|
||||||
else if (!ERR_HunterFleetRules.Seats(hunter.TroopTransport, drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false;
|
else if (!ERR_HunterFleetRules.Seats(hunter.TroopTransport, drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false;
|
||||||
unit.Seats.Insert(drivers[0]);
|
unit.Seats.Insert(drivers[0]);
|
||||||
foreach (BaseCompartmentSlot turret : turrets) unit.Seats.Insert(turret);
|
foreach (BaseCompartmentSlot turret : turrets) unit.Seats.Insert(turret);
|
||||||
@@ -653,6 +756,7 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
}
|
}
|
||||||
m_PendingReserve = null;
|
m_PendingReserve = null;
|
||||||
TickMotorbikes(hq, now);
|
TickMotorbikes(hq, now);
|
||||||
|
TickSarka(hq, now);
|
||||||
if (!m_Coordination) m_Coordination = new ERR_HunterCoordination(m_Mode);
|
if (!m_Coordination) m_Coordination = new ERR_HunterCoordination(m_Mode);
|
||||||
m_Coordination.Tick(hq, m_Hunters, now);
|
m_Coordination.Tick(hq, m_Hunters, now);
|
||||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||||
@@ -667,10 +771,22 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
// A delivered manoeuvre owns dismount/reboarding. The ordinary recovery
|
// A delivered manoeuvre owns dismount/reboarding. The ordinary recovery
|
||||||
// must not re-add its vehicle while the team is moving on foot.
|
// must not re-add its vehicle while the team is moving on foot.
|
||||||
if (!unit.CloseContact) unit.CloseContact = new ERR_HunterCloseContact(unit);
|
if (!unit.CloseContact) unit.CloseContact = new ERR_HunterCloseContact(unit);
|
||||||
if (unit.CloseContact.Tick(m_Mode, now)) continue;
|
if (unit.CloseContact.Tick(m_Mode, now))
|
||||||
if (unit.Tactics && unit.Tactics.Tick(hq, now)) continue;
|
{ if (unit.Recovery) unit.Recovery.Wait.Reset(); continue; }
|
||||||
|
if (unit.Tactics && unit.Tactics.Tick(hq, now))
|
||||||
|
{ if (unit.Recovery) unit.Recovery.Wait.Reset(); continue; }
|
||||||
UpdateCombat(unit, now);
|
UpdateCombat(unit, now);
|
||||||
if (unit.Recovery) unit.Recovery.Update(m_Mode, now);
|
if (unit.Recovery) unit.Recovery.Update(m_Mode, now);
|
||||||
|
// Crew failures bypass the route-progress observer. Give them their own
|
||||||
|
// bounded fallback, including a casualty occupying the driver's seat.
|
||||||
|
if (unit.Recovery)
|
||||||
|
{
|
||||||
|
float recoverySpeed = -1;
|
||||||
|
Physics recoveryPhysics = unit.Entity.GetPhysics();
|
||||||
|
if (recoveryPhysics) recoverySpeed = recoveryPhysics.GetVelocity().Length();
|
||||||
|
if (unit.Recovery.Wait.Expired(unit.Registered, unit.CrewValid(m_Mode), unit.CombatHeld, recoverySpeed, now))
|
||||||
|
{ AbandonStuckVehicle(unit, "driver recovery timed out after two minutes"); continue; }
|
||||||
|
}
|
||||||
if (unit.CombatHeld) continue;
|
if (unit.CombatHeld) continue;
|
||||||
if (!unit.Registered)
|
if (!unit.Registered)
|
||||||
{
|
{
|
||||||
@@ -757,7 +873,7 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
if (unit.Waypoint) { unit.Waypoint.SetCompletionRadius(50); unit.Group.AddWaypoint(unit.Waypoint); }
|
if (unit.Waypoint) { unit.Waypoint.SetCompletionRadius(50); unit.Group.AddWaypoint(unit.Waypoint); }
|
||||||
ERR_HQ.Event("HUNTER REPATH", unit.Source + ": no movement for 60 seconds; native route requested again, attempt " + unit.Repaths.ToString());
|
ERR_HQ.Event("HUNTER REPATH", unit.Source + ": no movement for 60 seconds; native route requested again, attempt " + unit.Repaths.ToString());
|
||||||
}
|
}
|
||||||
protected void AbandonStuckVehicle(ERR_HunterVehicle unit)
|
protected void AbandonStuckVehicle(ERR_HunterVehicle unit, string reason = "three route retries failed")
|
||||||
{
|
{
|
||||||
if (unit.Released || unit.PlayerCaptureObserved || ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) return;
|
if (unit.Released || unit.PlayerCaptureObserved || ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) return;
|
||||||
// Stop reboarding/tactics before detaching the vehicle. Preserve the original
|
// Stop reboarding/tactics before detaching the vehicle. Preserve the original
|
||||||
@@ -775,7 +891,7 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
if (!agent || !agent.m_UtilityComponent || !agent.m_UtilityComponent.m_AIInfo) continue;
|
if (!agent || !agent.m_UtilityComponent || !agent.m_UtilityComponent.m_AIInfo) continue;
|
||||||
agent.m_UtilityComponent.AddAction(new SCR_AIGetOutVehicle(agent.m_UtilityComponent, null, unit.Entity, priority: 1000));
|
agent.m_UtilityComponent.AddAction(new SCR_AIGetOutVehicle(agent.m_UtilityComponent, null, unit.Entity, priority: 1000));
|
||||||
}
|
}
|
||||||
ERR_HQ.Event("HUNTER RELEASE", unit.Source + ": three route retries failed; original crew ordered out for on-foot HQ duties, vehicle retained");
|
ERR_HQ.Event("HUNTER RELEASE", unit.Source + ": " + reason + "; original crew ordered out for on-foot HQ duties, vehicle retained");
|
||||||
}
|
}
|
||||||
override void Stop()
|
override void Stop()
|
||||||
{
|
{
|
||||||
@@ -799,6 +915,7 @@ modded class SCR_CompartmentAccessComponent
|
|||||||
if (mode) hq = ERR_HQ.Cast(mode.Command());
|
if (mode) hq = ERR_HQ.Cast(mode.Command());
|
||||||
if (hq && hq.Hunters()) hq.Hunters().PlayerEntered(GetOwner(), targetEntity);
|
if (hq && hq.Hunters()) hq.Hunters().PlayerEntered(GetOwner(), targetEntity);
|
||||||
if (mode && mode.CivilianTraffic()) mode.CivilianTraffic().PlayerEntered(GetOwner(), targetEntity);
|
if (mode && mode.CivilianTraffic()) mode.CivilianTraffic().PlayerEntered(GetOwner(), targetEntity);
|
||||||
|
if (mode && mode.GarageVehicles()) mode.GarageVehicles().PlayerEntered(GetOwner(), targetEntity);
|
||||||
}
|
}
|
||||||
super.OnCompartmentEntered(targetEntity, manager, mgrID, slotID, move);
|
super.OnCompartmentEntered(targetEntity, manager, mgrID, slotID, move);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,33 @@
|
|||||||
// A bounded response by original hunter crews. Enemy positions are immutable
|
// A bounded response by original hunter crews. Enemy positions are immutable
|
||||||
// radio-delivered snapshots; native perception still owns aiming and firing.
|
// radio-delivered snapshots; native perception still owns aiming and firing.
|
||||||
|
// Failures belong to the physical crew, not a disposable tactical assignment.
|
||||||
|
// Bound both memory and lifetime so changed terrain/contact can be tried later.
|
||||||
|
class ERR_HunterApproachMemory
|
||||||
|
{
|
||||||
|
protected ref array<vector> m_Positions = {};
|
||||||
|
protected ref array<float> m_Expires = {};
|
||||||
|
float RetryAfter;
|
||||||
|
void Prune(float now)
|
||||||
|
{
|
||||||
|
for (int i = m_Expires.Count() - 1; i >= 0; i--)
|
||||||
|
if (now >= m_Expires[i]) { m_Expires.Remove(i); m_Positions.Remove(i); }
|
||||||
|
}
|
||||||
|
void Reject(vector position, float now)
|
||||||
|
{
|
||||||
|
Prune(now); RetryAfter = now + 60000;
|
||||||
|
for (int i = 0; i < m_Positions.Count(); i++)
|
||||||
|
if (vector.DistanceXZ(position, m_Positions[i]) < 85)
|
||||||
|
{ m_Expires[i] = now + 300000; return; }
|
||||||
|
if (m_Positions.Count() >= 8) { m_Positions.Remove(0); m_Expires.Remove(0); }
|
||||||
|
m_Positions.Insert(position); m_Expires.Insert(now + 300000);
|
||||||
|
}
|
||||||
|
void Append(array<vector> excluded, float now)
|
||||||
|
{
|
||||||
|
Prune(now);
|
||||||
|
foreach (vector position : m_Positions) excluded.Insert(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ERR_HunterAttackRules
|
class ERR_HunterAttackRules
|
||||||
{
|
{
|
||||||
static float ManeuverBudget(float metres) { return Math.Clamp(30 + metres / 2, 90, 240) * 1000; }
|
static float ManeuverBudget(float metres) { return Math.Clamp(30 + metres / 2, 90, 240) * 1000; }
|
||||||
@@ -328,12 +356,22 @@ class ERR_HunterTactics
|
|||||||
bool Plan(array<vector> occupied)
|
bool Plan(array<vector> occupied)
|
||||||
{
|
{
|
||||||
if (!Living()) return false;
|
if (!Living()) return false;
|
||||||
|
float now = GetGame().GetWorld().GetWorldTime();
|
||||||
|
if (!Delivered && now < Unit.FailedApproaches.RetryAfter) return false;
|
||||||
|
array<vector> excluded = {};
|
||||||
|
foreach (vector reservedPosition : occupied) excluded.Insert(reservedPosition);
|
||||||
|
Unit.FailedApproaches.Append(excluded, now);
|
||||||
SCR_AIGroupUtilityComponent utility = Unit.Group.GetGroupUtilityComponent();
|
SCR_AIGroupUtilityComponent utility = Unit.Group.GetGroupUtilityComponent();
|
||||||
if (!utility) return false;
|
if (!utility) return false;
|
||||||
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||||
float currentRange = vector.DistanceXZ(Unit.Entity.GetOrigin(), Attack.ReportPosition);
|
float currentRange = vector.DistanceXZ(Unit.Entity.GetOrigin(), Attack.ReportPosition);
|
||||||
if (Role == 0 && currentRange >= 140 && currentRange <= 500 && VisualCover())
|
if (Role == 0 && currentRange >= 140 && currentRange <= 500 && VisualCover())
|
||||||
{ Stage = Unit.Entity.GetOrigin(); return true; }
|
{
|
||||||
|
bool rejected;
|
||||||
|
foreach (vector failedPosition : excluded)
|
||||||
|
if (vector.DistanceXZ(Unit.Entity.GetOrigin(), failedPosition) < 85) rejected = true;
|
||||||
|
if (!rejected) { Stage = Unit.Entity.GetOrigin(); return true; }
|
||||||
|
}
|
||||||
// Approach from this crew's side of the contact. A common cover axis used
|
// Approach from this crew's side of the contact. A common cover axis used
|
||||||
// to send the opposite-side truck around the entire fight before dismount.
|
// to send the opposite-side truck around the entire fight before dismount.
|
||||||
vector approachForward = ERR_MountedCover.Approach(Unit.Entity.GetOrigin(), Attack.ReportPosition, Attack.Forward);
|
vector approachForward = ERR_MountedCover.Approach(Unit.Entity.GetOrigin(), Attack.ReportPosition, Attack.Forward);
|
||||||
@@ -348,7 +386,7 @@ class ERR_HunterTactics
|
|||||||
if (attempt % 2 == 0) lateral = -lateral;
|
if (attempt % 2 == 0) lateral = -lateral;
|
||||||
vector seed = desired - approachForward * (attempt * 15) + approachLeft * lateral;
|
vector seed = desired - approachForward * (attempt * 15) + approachLeft * lateral;
|
||||||
if (!ERR_SupplyConvoySystem.ApproachDirected(seed, point, angles, 100)) continue;
|
if (!ERR_SupplyConvoySystem.ApproachDirected(seed, point, angles, 100)) continue;
|
||||||
if (!ERR_HunterAttackRules.Stage(point, Attack.ReportPosition, approachForward, occupied) || !utility.IsPositionAllowed(point) || !placement.Clear(point, angles[0], 2.5)) continue;
|
if (!ERR_HunterAttackRules.Stage(point, Attack.ReportPosition, approachForward, excluded) || !utility.IsPositionAllowed(point) || !placement.Clear(point, angles[0], 2.5)) continue;
|
||||||
if (Role == 0 && !ERR_MountedCover.ClearLane(Unit.Entity, point, Attack.ReportPosition)) continue;
|
if (Role == 0 && !ERR_MountedCover.ClearLane(Unit.Entity, point, Attack.ReportPosition)) continue;
|
||||||
Stage = point; found = true; break;
|
Stage = point; found = true; break;
|
||||||
}
|
}
|
||||||
@@ -359,7 +397,7 @@ class ERR_HunterTactics
|
|||||||
vector point = desired;
|
vector point = desired;
|
||||||
if (offroad > 0) point += Vector(Math.RandomFloat(-90, 90), 0, Math.RandomFloat(-90, 90));
|
if (offroad > 0) point += Vector(Math.RandomFloat(-90, 90), 0, Math.RandomFloat(-90, 90));
|
||||||
point = ERR_Actors.Ground(point);
|
point = ERR_Actors.Ground(point);
|
||||||
if (!ERR_HunterAttackRules.Stage(point, Attack.ReportPosition, approachForward, occupied) || !utility.IsPositionAllowed(point)) continue;
|
if (!ERR_HunterAttackRules.Stage(point, Attack.ReportPosition, approachForward, excluded) || !utility.IsPositionAllowed(point)) continue;
|
||||||
if (!placement.Clear(point, ERR_ForceRules.Heading(point, Attack.ReportPosition, 0), 2.5)) continue;
|
if (!placement.Clear(point, ERR_ForceRules.Heading(point, Attack.ReportPosition, 0), 2.5)) continue;
|
||||||
if (Role == 0 && !ERR_MountedCover.ClearLane(Unit.Entity, point, Attack.ReportPosition)) continue;
|
if (Role == 0 && !ERR_MountedCover.ClearLane(Unit.Entity, point, Attack.ReportPosition)) continue;
|
||||||
Stage = point; found = true;
|
Stage = point; found = true;
|
||||||
@@ -371,7 +409,7 @@ class ERR_HunterTactics
|
|||||||
vector parked = Unit.Entity.GetOrigin();
|
vector parked = Unit.Entity.GetOrigin();
|
||||||
float range = vector.DistanceXZ(parked, Attack.ReportPosition);
|
float range = vector.DistanceXZ(parked, Attack.ReportPosition);
|
||||||
bool separate = true;
|
bool separate = true;
|
||||||
foreach (vector reserved : occupied) if (vector.DistanceXZ(parked, reserved) < 85) separate = false;
|
foreach (vector reserved : excluded) if (vector.DistanceXZ(parked, reserved) < 85) separate = false;
|
||||||
if (range >= 140 && range <= 500 && separate) { Stage = parked; found = true; }
|
if (range >= 140 && range <= 500 && separate) { Stage = parked; found = true; }
|
||||||
}
|
}
|
||||||
if (!found) { Note("no separated safe approach; keeping current orders"); return false; }
|
if (!found) { Note("no separated safe approach; keeping current orders"); return false; }
|
||||||
@@ -410,6 +448,7 @@ class ERR_HunterTactics
|
|||||||
}
|
}
|
||||||
protected bool AlternateStage(float now)
|
protected bool AlternateStage(float now)
|
||||||
{
|
{
|
||||||
|
RejectStage(now);
|
||||||
// Reserve failed positions as well as the other teams. Do not repeatedly
|
// Reserve failed positions as well as the other teams. Do not repeatedly
|
||||||
// choose the same screened corner or send two vehicles to one firing point.
|
// choose the same screened corner or send two vehicles to one firing point.
|
||||||
array<vector> occupied = {};
|
array<vector> occupied = {};
|
||||||
@@ -426,9 +465,13 @@ class ERR_HunterTactics
|
|||||||
Note("alternate approach assigned=" + Stage.ToString());
|
Note("alternate approach assigned=" + Stage.ToString());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
protected void RejectStage(float now)
|
||||||
|
{
|
||||||
|
Unit.FailedApproaches.Reject(Stage, now);
|
||||||
|
}
|
||||||
protected bool RecoverApproach(float now)
|
protected bool RecoverApproach(float now)
|
||||||
{
|
{
|
||||||
if (m_ApproachRetries >= 2) { Note("approach still blocked after two recoveries; releasing assignment"); Stop(); return false; }
|
if (m_ApproachRetries >= 2) { Note("approach still blocked after two recoveries; releasing assignment"); RejectStage(now); Stop(); return false; }
|
||||||
m_ApproachRetries++;
|
m_ApproachRetries++;
|
||||||
bool assigned;
|
bool assigned;
|
||||||
if (m_ApproachRetries == 1) assigned = Move(Stage, 35);
|
if (m_ApproachRetries == 1) assigned = Move(Stage, 35);
|
||||||
@@ -444,7 +487,7 @@ class ERR_HunterTactics
|
|||||||
if (VisualCover()) { m_BlindSince = -1; return true; }
|
if (VisualCover()) { m_BlindSince = -1; return true; }
|
||||||
if (m_BlindSince < 0) m_BlindSince = now;
|
if (m_BlindSince < 0) m_BlindSince = now;
|
||||||
if (now - m_BlindSince < 20000) return true;
|
if (now - m_BlindSince < 20000) return true;
|
||||||
if (m_CoverRetries >= 3) { Note("no visual cover after three alternate positions; releasing assignment"); Stop(); return false; }
|
if (m_CoverRetries >= 3) { Note("no visual cover after three alternate positions; releasing assignment"); RejectStage(now); Stop(); return false; }
|
||||||
m_CoverRetries++; m_BlindSince = now;
|
m_CoverRetries++; m_BlindSince = now;
|
||||||
Note("cover position has no visual contact; looking for another firing angle");
|
Note("cover position has no visual contact; looking for another firing angle");
|
||||||
AlternateStage(now);
|
AlternateStage(now);
|
||||||
@@ -500,7 +543,7 @@ class ERR_HunterTactics
|
|||||||
{
|
{
|
||||||
if (ERR_HunterAttackRules.ExtendApproach(m_ApproachStarted, m_ApproachProgress, now))
|
if (ERR_HunterAttackRules.ExtendApproach(m_ApproachStarted, m_ApproachProgress, now))
|
||||||
{ Deadline = Math.Min(now + 30000, m_ApproachStarted + 300000); Note("approach still progressing; allowing another bounded route interval"); }
|
{ Deadline = Math.Min(now + 30000, m_ApproachStarted + 300000); Note("approach still progressing; allowing another bounded route interval"); }
|
||||||
else { Note("approach made no recent progress or exhausted five-minute budget; release this assignment"); Stop(); return false; }
|
else { Note("approach made no recent progress or exhausted five-minute budget; release this assignment"); RejectStage(now); Stop(); return false; }
|
||||||
}
|
}
|
||||||
else if (now - m_ApproachProgress >= 30000 && !RecoverApproach(now)) return false;
|
else if (now - m_ApproachProgress >= 30000 && !RecoverApproach(now)) return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class ERR_MortarSystem
|
|||||||
protected bool CreateBattery(vector authoredPosition, int id)
|
protected bool CreateBattery(vector authoredPosition, int id)
|
||||||
{
|
{
|
||||||
vector position = ERR_Actors.Ground(authoredPosition);
|
vector position = ERR_Actors.Ground(authoredPosition);
|
||||||
if (position[1] < 1) { PrintFormat("Reintroduced [mortars]: below land at %1", position); return false; }
|
if (!ERR_Actors.DryGround(position)) { PrintFormat("Reintroduced [mortars]: submerged or invalid ground at %1", position); return false; }
|
||||||
IEntity gun = ERR_Actors.Spawn(ERR_MortarBattery.GUN, position);
|
IEntity gun = ERR_Actors.Spawn(ERR_MortarBattery.GUN, position);
|
||||||
SCR_AIGroup group = SCR_AIGroup.Cast(ERR_Actors.Spawn(MortarGroup(), ERR_Actors.Ground(position + "0 0 4")));
|
SCR_AIGroup group = SCR_AIGroup.Cast(ERR_Actors.Spawn(MortarGroup(), ERR_Actors.Ground(position + "0 0 4")));
|
||||||
if (!gun || !group)
|
if (!gun || !group)
|
||||||
|
|||||||
@@ -62,10 +62,24 @@ class ERR_RecommendedAddons
|
|||||||
static void Snapshot(ERR_GameMode mode, array<string> values)
|
static void Snapshot(ERR_GameMode mode, array<string> values)
|
||||||
{
|
{
|
||||||
array<string> loaded = {}; GameProject.GetLoadedAddons(loaded);
|
array<string> loaded = {}; GameProject.GetLoadedAddons(loaded);
|
||||||
Entry(values, "bikes", loaded, {"6503091A7B264FC3", "61B84088181A8FA4", "629B2BA37EFFD577"}, {"WCS Motorbikes", "Motorbike", "WCS Armaments"}, mode.Settings().Get("motorbikeCount") > 0);
|
Entry(values, "bikes", loaded, {"6503091A7B264FC3", "61B84088181A8FA4", "629B2BA37EFFD577"}, {"WCS Motorbikes", "Motorbike", "WCS Armaments"}, mode.Settings().Get("motorbikeCount") > 0 || (mode.Settings().Get("garageVehicleCount") > 0 && mode.Settings().Get("garageChancePercent") > 0));
|
||||||
Entry(values, "plane", loaded, {"69BAEE0D439966FE", "69A8A34027DA65C5"}, {"Cessna 172", "Propeller Flight Core"}, mode.Settings().Get("planeReconEnabled") == 1);
|
Entry(values, "plane", loaded, {"69BAEE0D439966FE", "69A8A34027DA65C5"}, {"Cessna 172", "Propeller Flight Core"}, mode.Settings().Get("planeReconEnabled") == 1);
|
||||||
Entry(values, "weapons", loaded, {"BFC890A6F1603EC0"}, {"Phaseline Marksman Rifles"}, mode.Settings().Get("automaticEquipmentChancePercent") != 0);
|
Entry(values, "weapons", loaded, {"BFC890A6F1603EC0"}, {"Phaseline Marksman Rifles"}, mode.Settings().Get("automaticEquipmentChancePercent") != 0);
|
||||||
ERR_HQ hq = ERR_HQ.Cast(mode.Command());
|
ERR_HQ hq = ERR_HQ.Cast(mode.Command());
|
||||||
if (hq) values.Insert("recommendedBikeRuntime=" + hq.Hunters().MotorbikeStatus());
|
if (hq) values.Insert("recommendedBikeRuntime=" + hq.Hunters().MotorbikeStatus());
|
||||||
|
string droneState = "Missing: Realistic Combat Drones";
|
||||||
|
if (ERR_DroneBridge.Available())
|
||||||
|
{
|
||||||
|
droneState = "Loaded - disabled in mission settings";
|
||||||
|
if (mode.Settings().Get("droneReconCount") > 0) droneState = "Loaded - enabled for next round; see current-round status below";
|
||||||
|
}
|
||||||
|
values.Insert("recommended_drones=" + droneState);
|
||||||
|
int droneReady;
|
||||||
|
if (hq && hq.DroneRecon())
|
||||||
|
{
|
||||||
|
values.Insert("droneRuntime=" + hq.DroneRecon().Status());
|
||||||
|
if (hq.DroneRecon().Ready()) droneReady = 1;
|
||||||
|
}
|
||||||
|
values.Insert("recommendedReady_drones=" + droneReady.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,47 @@ class ERR_RescueFlightComponentClass : ERR_ReconFlightComponentClass {}
|
|||||||
class ERR_RescueFlightComponent : ERR_ReconFlightComponent
|
class ERR_RescueFlightComponent : ERR_ReconFlightComponent
|
||||||
{
|
{
|
||||||
protected bool m_Escort, m_Departing, m_LandingConfirmed;
|
protected bool m_Escort, m_Departing, m_LandingConfirmed;
|
||||||
|
[RplProp(onRplName: "ApplyResultsHold")] protected bool m_ResultsHeld;
|
||||||
|
// Contact plus real clearance excludes boarding, engine spin-up, and the
|
||||||
|
// controller's ten-second grounded takeoff hold. Every transport must lift.
|
||||||
|
bool AirborneAfterPickup()
|
||||||
|
{
|
||||||
|
if (!m_Departing || m_Lost || !m_Aircraft) return false;
|
||||||
|
VehicleHelicopterSimulation sim = VehicleHelicopterSimulation.Cast(m_Aircraft.FindComponent(VehicleHelicopterSimulation));
|
||||||
|
if (!sim || sim.HasAnyGroundContact()) return false;
|
||||||
|
float ground = SCR_TerrainHelper.GetTerrainY(m_Aircraft.GetOrigin(), GetGame().GetWorld(), noUnderwater: true);
|
||||||
|
return m_Aircraft.GetOrigin()[1] - ground >= 3;
|
||||||
|
}
|
||||||
|
void HoldForResults()
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer() || m_Lost || !ERR_Actors.Alive(GetOwner()) || m_ResultsHeld) return;
|
||||||
|
m_ResultsHeld = true;
|
||||||
|
ApplyResultsHold();
|
||||||
|
Replication.BumpMe();
|
||||||
|
}
|
||||||
|
protected void ApplyResultsHold()
|
||||||
|
{
|
||||||
|
if (!m_ResultsHeld || !GetOwner()) return;
|
||||||
|
Physics physics = GetOwner().GetPhysics();
|
||||||
|
if (physics)
|
||||||
|
{
|
||||||
|
physics.SetVelocity(vector.Zero);
|
||||||
|
physics.SetAngularVelocity(vector.Zero);
|
||||||
|
physics.ChangeSimulationState(SimulationState.COLLISION);
|
||||||
|
}
|
||||||
|
if (Replication.IsServer())
|
||||||
|
{
|
||||||
|
DamageManagerComponent damage = DamageManagerComponent.Cast(GetOwner().FindComponent(DamageManagerComponent));
|
||||||
|
if (damage) damage.EnableDamageHandling(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override void Release(bool captured)
|
||||||
|
{
|
||||||
|
// Terminal presentation keeps the engines/rotors; physics is already held.
|
||||||
|
// Ordinary lost/captured/aborted flights retain their original cleanup.
|
||||||
|
super.Release(captured || m_ResultsHeld);
|
||||||
|
}
|
||||||
|
|
||||||
protected float m_BoardingReadySince = -1;
|
protected float m_BoardingReadySince = -1;
|
||||||
protected vector m_Pickup;
|
protected vector m_Pickup;
|
||||||
protected float m_RescueDeadline, m_NextThreat;
|
protected float m_RescueDeadline, m_NextThreat;
|
||||||
@@ -168,6 +209,7 @@ class ERR_RescueFlightComponent : ERR_ReconFlightComponent
|
|||||||
}
|
}
|
||||||
override void EOnSimulate(IEntity owner, float timeSlice)
|
override void EOnSimulate(IEntity owner, float timeSlice)
|
||||||
{
|
{
|
||||||
|
if (m_ResultsHeld) { ApplyResultsHold(); return; }
|
||||||
// Initial airborne crew allocation has a fixed 45-second deadline.
|
// Initial airborne crew allocation has a fixed 45-second deadline.
|
||||||
if (Replication.IsServer() && m_Initialized && m_Phase == ERR_ReconPhase.CREW)
|
if (Replication.IsServer() && m_Initialized && m_Phase == ERR_ReconPhase.CREW)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,22 @@ class ERR_RoadblockRules
|
|||||||
{
|
{
|
||||||
// Admission never waits longer for a high count. Remaining posts are built
|
// Admission never waits longer for a high count. Remaining posts are built
|
||||||
// by the same safe placement path in background, including after prison unlock.
|
// by the same safe placement path in background, including after prison unlock.
|
||||||
|
static int PropCount(bool large = false)
|
||||||
|
{
|
||||||
|
if (large) return 14;
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
static int GuardCount(bool large)
|
||||||
|
{
|
||||||
|
if (large) return 8;
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
static bool LargeRoad(float width, float threshold) { return width >= threshold; }
|
||||||
|
static bool Needed(bool large, int smallPlaced, int smallTarget, int largePlaced, int largeTarget)
|
||||||
|
{
|
||||||
|
if (large) return largePlaced < largeTarget;
|
||||||
|
return smallPlaced < smallTarget;
|
||||||
|
}
|
||||||
static int SearchMilliseconds() { return 12000; }
|
static int SearchMilliseconds() { return 12000; }
|
||||||
static int SearchAttempts() { return 300; }
|
static int SearchAttempts() { return 300; }
|
||||||
static bool Road(float width, float length, float heightDifference)
|
static bool Road(float width, float length, float heightDifference)
|
||||||
@@ -50,6 +66,8 @@ class ERR_Roadblocks
|
|||||||
protected int m_Target, m_Attempts, m_Started, m_CandidateStarted, m_Candidates, m_Planned;
|
protected int m_Target, m_Attempts, m_Started, m_CandidateStarted, m_Candidates, m_Planned;
|
||||||
protected vector m_Low, m_High, m_Centre, m_Forward, m_Right;
|
protected vector m_Low, m_High, m_Centre, m_Forward, m_Right;
|
||||||
protected float m_Shoulder;
|
protected float m_Shoulder;
|
||||||
|
protected bool m_Large;
|
||||||
|
protected int m_SmallTarget, m_LargeTarget, m_SmallPlaced, m_LargePlaced, m_MainRoadWidth;
|
||||||
protected SCR_AIGroup m_BuildGroup;
|
protected SCR_AIGroup m_BuildGroup;
|
||||||
protected AIWaypoint m_BuildHold;
|
protected AIWaypoint m_BuildHold;
|
||||||
protected ref array<IEntity> m_BuildSpawned = {};
|
protected ref array<IEntity> m_BuildSpawned = {};
|
||||||
@@ -60,7 +78,6 @@ class ERR_Roadblocks
|
|||||||
protected ref array<ref Resource> m_Loaded = {};
|
protected ref array<ref Resource> m_Loaded = {};
|
||||||
static const ResourceName COVER = "{21D83950DF36F250}Prefabs/Props/Military/Sandbags/Sandbag_01_long_burlap.et";
|
static const ResourceName COVER = "{21D83950DF36F250}Prefabs/Props/Military/Sandbags/Sandbag_01_long_burlap.et";
|
||||||
static const ResourceName BARRIER = "{64C9C1D1C2C1B38D}Prefabs/Props/Infrastructure/Roadblocks/RoadBlock_01.et";
|
static const ResourceName BARRIER = "{64C9C1D1C2C1B38D}Prefabs/Props/Infrastructure/Roadblocks/RoadBlock_01.et";
|
||||||
static const ResourceName OBSTACLE = "{A46D45922F8CAD6C}Prefabs/Props/Military/Fortification/CzechHedgehog_01_painted.et";
|
|
||||||
static const ResourceName SIGN = "{C1977A326F2AA011}Prefabs/Structures/Signs/Military/SignCheckpoint_01_stop.et";
|
static const ResourceName SIGN = "{C1977A326F2AA011}Prefabs/Structures/Signs/Military/SignCheckpoint_01_stop.et";
|
||||||
void ERR_Roadblocks(ERR_GameMode mode) { m_Mode = mode; m_Mining = new ERR_RoadblockMining(mode); }
|
void ERR_Roadblocks(ERR_GameMode mode) { m_Mode = mode; m_Mining = new ERR_RoadblockMining(mode); }
|
||||||
void MineTick(array<ref ERR_HQUnit> units, array<IEntity> attempted, float now) { if (!m_Stopped) m_Mining.Tick(units, attempted, now); }
|
void MineTick(array<ref ERR_HQUnit> units, array<IEntity> attempted, float now) { if (!m_Stopped) m_Mining.Tick(units, attempted, now); }
|
||||||
@@ -72,7 +89,7 @@ class ERR_Roadblocks
|
|||||||
if (m_Ready && m_Positions.Count() < m_Target) progress = "placement stopped; see server log";
|
if (m_Ready && m_Positions.Count() < m_Target) progress = "placement stopped; see server log";
|
||||||
if (m_Background) progress = "background placement pending";
|
if (m_Background) progress = "background placement pending";
|
||||||
else if (!m_Ready) progress = "initial placement in progress";
|
else if (!m_Ready) progress = "initial placement in progress";
|
||||||
return string.Format("Roadblocks: %1/%2 placed this round; next-round setting %3. Four guards per post, no replacements. %4.", m_Positions.Count(), m_Target, m_Mode.Settings().Get("roadblockCount"), progress);
|
return string.Format("Checkpoints: small %1/%2 (4 guards), large %3/%4 (8 guards). Main-road cutoff %5 m. No replacements. %6.", m_SmallPlaced, m_SmallTarget, m_LargePlaced, m_LargeTarget, m_MainRoadWidth, progress);
|
||||||
}
|
}
|
||||||
protected ERR_PrepareResult Finish(string reason)
|
protected ERR_PrepareResult Finish(string reason)
|
||||||
{
|
{
|
||||||
@@ -90,7 +107,7 @@ class ERR_Roadblocks
|
|||||||
if (profile && profile.IsValid()) source = profile.GetResource().ToEntitySource();
|
if (profile && profile.IsValid()) source = profile.GetResource().ToEntitySource();
|
||||||
if (!source || !source.Get("m_aUnitPrefabSlots", m_Roster) || m_Roster.IsEmpty()) return false;
|
if (!source || !source.Get("m_aUnitPrefabSlots", m_Roster) || m_Roster.IsEmpty()) return false;
|
||||||
// Preload before physical creation. A failed prefab never leaves half a post.
|
// Preload before physical creation. A failed prefab never leaves half a post.
|
||||||
array<ResourceName> resources = {COVER, BARRIER, OBSTACLE, SIGN, "{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", ERR_Resources.DefendWaypoint()};
|
array<ResourceName> resources = {COVER, BARRIER, SIGN, "{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", ERR_Resources.DefendWaypoint()};
|
||||||
foreach (ResourceName actor : m_Roster) resources.Insert(actor);
|
foreach (ResourceName actor : m_Roster) resources.Insert(actor);
|
||||||
foreach (ResourceName prefab : resources)
|
foreach (ResourceName prefab : resources)
|
||||||
{
|
{
|
||||||
@@ -119,11 +136,15 @@ class ERR_Roadblocks
|
|||||||
if (i > 0) length += vector.DistanceXZ(points[i - 1], points[i]);
|
if (i > 0) length += vector.DistanceXZ(points[i - 1], points[i]);
|
||||||
}
|
}
|
||||||
if (!ERR_RoadblockRules.Road(road.GetWidth(), length, 0)) return false;
|
if (!ERR_RoadblockRules.Road(road.GetWidth(), length, 0)) return false;
|
||||||
float distance = Math.RandomFloat(20, length - 20);
|
m_Large = ERR_RoadblockRules.LargeRoad(road.GetWidth(), m_MainRoadWidth);
|
||||||
|
if (!ERR_RoadblockRules.Needed(m_Large, m_SmallPlaced, m_SmallTarget, m_LargePlaced, m_LargeTarget)) return false;
|
||||||
|
float margin = 20;
|
||||||
|
if (m_Large) { if (length < 60) return false; margin = 23; }
|
||||||
|
float distance = Math.RandomFloat(margin, length - margin);
|
||||||
vector centre, before, after, beforeDirection, afterDirection;
|
vector centre, before, after, beforeDirection, afterDirection;
|
||||||
if (!ERR_ConvoyRoadPlan.Sample(points, distance, centre, m_Forward)
|
if (!ERR_ConvoyRoadPlan.Sample(points, distance, centre, m_Forward)
|
||||||
|| !ERR_ConvoyRoadPlan.Sample(points, distance - 15, before, beforeDirection)
|
|| !ERR_ConvoyRoadPlan.Sample(points, distance - margin, before, beforeDirection)
|
||||||
|| !ERR_ConvoyRoadPlan.Sample(points, distance + 15, after, afterDirection)) return false;
|
|| !ERR_ConvoyRoadPlan.Sample(points, distance + margin, after, afterDirection)) return false;
|
||||||
if (vector.Dot(m_Forward, beforeDirection) < 0.95 || vector.Dot(m_Forward, afterDirection) < 0.95) return false;
|
if (vector.Dot(m_Forward, beforeDirection) < 0.95 || vector.Dot(m_Forward, afterDirection) < 0.95) return false;
|
||||||
m_Centre = ERR_Actors.Ground(centre);
|
m_Centre = ERR_Actors.Ground(centre);
|
||||||
if (!ERR_RoadblockRules.Road(road.GetWidth(), length, m_Centre[1] - centre[1])) return false;
|
if (!ERR_RoadblockRules.Road(road.GetWidth(), length, m_Centre[1] - centre[1])) return false;
|
||||||
@@ -139,7 +160,7 @@ class ERR_Roadblocks
|
|||||||
m_Right = Vector(m_Forward[2], 0, -m_Forward[0]);
|
m_Right = Vector(m_Forward[2], 0, -m_Forward[0]);
|
||||||
// Protected guard positions sit on the shoulders; approach barriers narrow
|
// Protected guard positions sit on the shoulders; approach barriers narrow
|
||||||
// wider roads while preserving a straight vehicle passage on every width.
|
// wider roads while preserving a straight vehicle passage on every width.
|
||||||
m_Shoulder = Math.Max(road.GetWidth() * 0.5 + 1.5, 4);
|
m_Shoulder = Math.Max(road.GetWidth() * 0.5 + 0.5, 3.2);
|
||||||
m_Candidates++; m_CandidateStarted = System.GetTickCount();
|
m_Candidates++; m_CandidateStarted = System.GetTickCount();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -184,18 +205,32 @@ class ERR_Roadblocks
|
|||||||
protected void Layout(out array<ref ERR_RoadblockProp> covers)
|
protected void Layout(out array<ref ERR_RoadblockProp> covers)
|
||||||
{
|
{
|
||||||
covers.Clear();
|
covers.Clear();
|
||||||
|
if (m_Large)
|
||||||
|
{
|
||||||
|
// Four protected guard bays on each shoulder, with two barriers per approach.
|
||||||
|
// Extend along the road instead of closing the traffic lane.
|
||||||
|
for (int largeSide = -1; largeSide <= 1; largeSide += 2)
|
||||||
|
{
|
||||||
|
for (int bay = 0; bay < 4; bay++)
|
||||||
|
AddProp(covers, COVER, largeSide * m_Shoulder, bay * 6 - 9, m_Right, 1.9, 0.7, 1);
|
||||||
|
AddProp(covers, BARRIER, largeSide * 3.3, largeSide * 14, m_Forward, 1.2, 0.4, 1.3);
|
||||||
|
AddProp(covers, BARRIER, largeSide * 3.3, largeSide * 18, m_Forward, 1.2, 0.4, 1.3);
|
||||||
|
AddProp(covers, SIGN, largeSide * 3, largeSide * 21, m_Forward * (largeSide * 1.0), 0.45, 0.4, 0.8);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Footprints enclose the native model bounds, including small placement margins.
|
// Footprints enclose the native model bounds, including small placement margins.
|
||||||
for (int side = -1; side <= 1; side += 2)
|
for (int side = -1; side <= 1; side += 2)
|
||||||
{
|
{
|
||||||
// Two long sandbag walls shelter each pair from the road and its approach.
|
// A short sandbag screen protects each pair without a wide outer bay.
|
||||||
AddProp(covers, COVER, side * m_Shoulder, 0, m_Right, 1.9, 0.7, 1);
|
AddProp(covers, COVER, side * m_Shoulder, -2, m_Right, 1.9, 0.7, 1);
|
||||||
AddProp(covers, COVER, side * (m_Shoulder + 2.8), side * 2.8, m_Forward, 1.9, 0.7, 1);
|
AddProp(covers, COVER, side * m_Shoulder, 2, m_Right, 1.9, 0.7, 1);
|
||||||
// Opposing, staggered stop lines: the inner ends leave a >=4m centre lane.
|
// One barrier and sign per approach preserve a >=4m centre passage.
|
||||||
AddProp(covers, BARRIER, side * 3.3, side * 8, m_Forward, 1.2, 0.4, 1.3);
|
AddProp(covers, BARRIER, side * 3.3, side * 6, m_Forward, 1.2, 0.4, 1.3);
|
||||||
AddProp(covers, OBSTACLE, side * (m_Shoulder + 4), side * 8, m_Forward, 1, 1, 1.6);
|
AddProp(covers, SIGN, side * 3, side * 8.5, m_Forward * (side * 1.0), 0.45, 0.4, 0.8);
|
||||||
AddProp(covers, SIGN, side * 3, side * 12, m_Forward * (side * 1.0), 0.45, 0.4, 0.8);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void AddProp(array<ref ERR_RoadblockProp> covers, ResourceName prefab, float across, float along, vector forward, float halfWidth, float halfDepth, float height)
|
protected void AddProp(array<ref ERR_RoadblockProp> covers, ResourceName prefab, float across, float along, vector forward, float halfWidth, float halfDepth, float height)
|
||||||
{
|
{
|
||||||
vector point = ERR_Actors.Ground(m_Centre + m_Right * across + m_Forward * along);
|
vector point = ERR_Actors.Ground(m_Centre + m_Right * across + m_Forward * along);
|
||||||
@@ -209,7 +244,7 @@ class ERR_Roadblocks
|
|||||||
// Start a transaction; physical pieces are created separately in BuildPost.
|
// Start a transaction; physical pieces are created separately in BuildPost.
|
||||||
protected bool SpawnPost(array<vector> positions, array<ref ERR_RoadblockProp> covers)
|
protected bool SpawnPost(array<vector> positions, array<ref ERR_RoadblockProp> covers)
|
||||||
{
|
{
|
||||||
if (!PlayersClear() || m_Mode.Forces().Count() >= 250 || m_BuildGroup || positions.Count() != 4 || covers.Count() != 10) return false;
|
if (!PlayersClear() || m_Mode.Forces().Count() >= 250 || m_BuildGroup || positions.Count() != ERR_RoadblockRules.GuardCount(m_Large) || covers.Count() != ERR_RoadblockRules.PropCount(m_Large)) return false;
|
||||||
m_BuildGroup = SCR_AIGroup.Cast(ERR_Actors.Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", m_Centre));
|
m_BuildGroup = SCR_AIGroup.Cast(ERR_Actors.Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", m_Centre));
|
||||||
if (!m_BuildGroup) return false;
|
if (!m_BuildGroup) return false;
|
||||||
m_BuildGroup.SetFaction(m_Mode.EnemyFaction()); m_BuildGroup.SetSpawnImmediately(false); m_BuildGroup.SetDeleteWhenEmpty(false); m_BuildGroup.SetPermanentLOD(0);
|
m_BuildGroup.SetFaction(m_Mode.EnemyFaction()); m_BuildGroup.SetSpawnImmediately(false); m_BuildGroup.SetDeleteWhenEmpty(false); m_BuildGroup.SetPermanentLOD(0);
|
||||||
@@ -224,6 +259,27 @@ class ERR_Roadblocks
|
|||||||
m_BuildSpawned.Clear(); m_BuildGroup = null; m_BuildHold = null;
|
m_BuildSpawned.Clear(); m_BuildGroup = null; m_BuildHold = null;
|
||||||
m_BuildPositions.Clear(); m_BuildCovers.Clear();
|
m_BuildPositions.Clear(); m_BuildCovers.Clear();
|
||||||
}
|
}
|
||||||
|
// A free Game Master camera does not force-stream bare, non-editable props
|
||||||
|
// with the nearby editable guards. Keep this bounded bank (<=660 props across both quotas) on
|
||||||
|
// clients, including late joiners, rather than tying it to the player's body.
|
||||||
|
// Do not report a completed post if any physical prop lacks a network node.
|
||||||
|
protected bool ShareProps()
|
||||||
|
{
|
||||||
|
if (m_BuildCovers.Count() != ERR_RoadblockRules.PropCount(m_Large) || m_BuildSpawned.Count() != m_BuildPositions.Count() + m_BuildCovers.Count() + 1) return false;
|
||||||
|
for (int i = m_BuildPositions.Count(); i < m_BuildPositions.Count() + m_BuildCovers.Count(); i++)
|
||||||
|
{
|
||||||
|
IEntity entity = m_BuildSpawned[i];
|
||||||
|
RplComponent component;
|
||||||
|
if (entity) component = RplComponent.Cast(entity.FindComponent(RplComponent));
|
||||||
|
if (!component || component.Id() == RplId.Invalid()) return false;
|
||||||
|
}
|
||||||
|
for (int i = m_BuildPositions.Count(); i < m_BuildPositions.Count() + m_BuildCovers.Count(); i++)
|
||||||
|
{
|
||||||
|
RplComponent component = RplComponent.Cast(m_BuildSpawned[i].FindComponent(RplComponent));
|
||||||
|
component.EnableStreaming(false);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
protected void BuildPost()
|
protected void BuildPost()
|
||||||
{
|
{
|
||||||
if (!PlayersClear() || m_Mode.Forces().Count() >= 250) { CancelPost(); return; }
|
if (!PlayersClear() || m_Mode.Forces().Count() >= 250) { CancelPost(); return; }
|
||||||
@@ -256,19 +312,31 @@ class ERR_Roadblocks
|
|||||||
m_BuildSpawned.Insert(m_BuildHold); m_BuildHold.SetCompletionRadius(25);
|
m_BuildSpawned.Insert(m_BuildHold); m_BuildHold.SetCompletionRadius(25);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!ShareProps())
|
||||||
|
{
|
||||||
|
PrintFormat("Reintroduced [roadblocks]: rejected post at %1; checkpoint props missing replication nodes", m_Centre);
|
||||||
|
CancelPost(); return;
|
||||||
|
}
|
||||||
string id = "roadblock-" + m_Positions.Count().ToString();
|
string id = "roadblock-" + m_Positions.Count().ToString();
|
||||||
if (!m_Mode.Forces().Register(id, ERR_ForceKind.CHECKPOINT, m_Centre, 4, null, m_BuildGroup, GetGame().GetWorld().GetWorldTime()))
|
if (!m_Mode.Forces().Register(id, ERR_ForceKind.CHECKPOINT, m_Centre, m_BuildPositions.Count(), null, m_BuildGroup, GetGame().GetWorld().GetWorldTime()))
|
||||||
{ CancelPost(); return; }
|
{ CancelPost(); return; }
|
||||||
foreach (IEntity ownedEntity : m_BuildSpawned) m_Mode.Own(ownedEntity);
|
foreach (IEntity ownedEntity : m_BuildSpawned) m_Mode.Own(ownedEntity);
|
||||||
m_Mode.Own(m_BuildGroup); m_Groups.Insert(m_BuildGroup); m_Positions.Insert(m_Centre);
|
m_Mode.Own(m_BuildGroup); m_Groups.Insert(m_BuildGroup); m_Positions.Insert(m_Centre);
|
||||||
|
if (m_Large) m_LargePlaced++; else m_SmallPlaced++;
|
||||||
m_BuildGroup.AddWaypoint(m_BuildHold);
|
m_BuildGroup.AddWaypoint(m_BuildHold);
|
||||||
m_Mode.RegisterGroup(m_BuildGroup); m_BuildGroup.ActivateAI();
|
m_Mode.RegisterGroup(m_BuildGroup); m_BuildGroup.ActivateAI();
|
||||||
m_Mining.Register(m_BuildGroup, m_BuildHold, m_Right);
|
m_Mining.Register(m_BuildGroup, m_BuildHold, m_Right);
|
||||||
PrintFormat("Reintroduced [roadblocks]: %1 at %2 guards=4 props=10 roadPassage=open HQ=registered", id, m_Centre);
|
PrintFormat("Reintroduced [roadblocks]: %1 at %2 large=%3 guards=%4 props=%5 networkedProps=%5 propStreaming=always roadPassage=open HQ=registered", id, m_Centre, m_Large, m_BuildPositions.Count(), m_BuildCovers.Count());
|
||||||
m_BuildSpawned.Clear(); m_BuildGroup = null; m_BuildHold = null;
|
m_BuildSpawned.Clear(); m_BuildGroup = null; m_BuildHold = null;
|
||||||
m_BuildPositions.Clear(); m_BuildCovers.Clear();
|
m_BuildPositions.Clear(); m_BuildCovers.Clear();
|
||||||
m_NextSearch = System.GetTickCount() + 2000;
|
m_NextSearch = System.GetTickCount() + 2000;
|
||||||
}
|
}
|
||||||
|
protected vector GuardPosition(int side, int row)
|
||||||
|
{
|
||||||
|
float along = side * (row * 2 - 2.5);
|
||||||
|
if (m_Large) along = row * 6 - 9;
|
||||||
|
return ERR_Actors.Ground(m_Centre + m_Right * (side * (m_Shoulder + 1.5)) + m_Forward * along);
|
||||||
|
}
|
||||||
// 0: reject, 1: awaiting native tile, 2: ready.
|
// 0: reject, 1: awaiting native tile, 2: ready.
|
||||||
protected int Plan(out array<vector> positions, out array<ref ERR_RoadblockProp> covers)
|
protected int Plan(out array<vector> positions, out array<ref ERR_RoadblockProp> covers)
|
||||||
{
|
{
|
||||||
@@ -281,9 +349,9 @@ class ERR_Roadblocks
|
|||||||
map<int, vector> reserved = new map<int, vector>();
|
map<int, vector> reserved = new map<int, vector>();
|
||||||
for (int side = -1; side <= 1; side += 2)
|
for (int side = -1; side <= 1; side += 2)
|
||||||
{
|
{
|
||||||
for (int row = -1; row <= 1; row += 2)
|
for (int row = 0; row < ERR_RoadblockRules.GuardCount(m_Large) / 2; row++)
|
||||||
{
|
{
|
||||||
vector point = ERR_Actors.Ground(m_Centre + m_Right * (side * (m_Shoulder + 2.5)) + m_Forward * (side * (row - 1.5)));
|
vector point = GuardPosition(side, row);
|
||||||
if (!mesh.IsTileLoaded(point)) { mesh.LoadTileIn(point); return 1; }
|
if (!mesh.IsTileLoaded(point)) { mesh.LoadTileIn(point); return 1; }
|
||||||
vector reachable;
|
vector reachable;
|
||||||
if (!mesh.GetReachablePoint(point, 1, reachable) || vector.DistanceXZ(point, reachable) > 1 || !m_Placement.Clear(point, reserved)) return 0;
|
if (!mesh.GetReachablePoint(point, 1, reachable) || vector.DistanceXZ(point, reachable) > 1 || !m_Placement.Clear(point, reserved)) return 0;
|
||||||
@@ -298,7 +366,10 @@ class ERR_Roadblocks
|
|||||||
if (m_Ready) return ERR_PrepareResult.READY;
|
if (m_Ready) return ERR_PrepareResult.READY;
|
||||||
if (!m_Begun)
|
if (!m_Begun)
|
||||||
{
|
{
|
||||||
m_Begun = true; m_Started = System.GetTickCount(); m_Target = m_Mode.Settings().Get("roadblockCount");
|
m_Begun = true; m_Started = System.GetTickCount(); m_SmallTarget = m_Mode.Settings().Get("roadblockCount");
|
||||||
|
m_LargeTarget = m_Mode.Settings().Get("largeRoadblockCount");
|
||||||
|
m_MainRoadWidth = m_Mode.Settings().Get("mainRoadWidthMetres");
|
||||||
|
m_Target = m_SmallTarget + m_LargeTarget;
|
||||||
if (m_Target == 0) return Finish("disabled");
|
if (m_Target == 0) return Finish("disabled");
|
||||||
#ifdef WORKBENCH
|
#ifdef WORKBENCH
|
||||||
// The legacy convoy/hq-memory fixtures use the original 60-body allowance and six-squad layout.
|
// The legacy convoy/hq-memory fixtures use the original 60-body allowance and six-squad layout.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user