Compare commits
27
Commits
372fca7a68
...
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 | ||
|
|
7a7ac59edf |
+134
-1
@@ -1,6 +1,139 @@
|
||||
# Changelog
|
||||
|
||||
Latest published Workshop version: **0.0.112**
|
||||
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
|
||||
|
||||
|
||||
@@ -50,6 +50,15 @@ WCS Armaments and WCS BMP-1 (including their dependencies) remain separate,
|
||||
author-supplied mods; see [external dependencies](docs/external-addons.md).
|
||||
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).
|
||||
Earlier five-addon build reports below are historical evidence, not validation of
|
||||
the merged project. Nothing has been published to Workshop.
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
MenuManager {
|
||||
MenuPresets {
|
||||
MenuPreset ERR_PreparationFailure {
|
||||
Layout "{28DD8E27B1CF40C5}UI/layouts/ERR/PreparationFailure.layout"
|
||||
ActionContext "MenuContext"
|
||||
Class "ERR_PreparationFailureMenu"
|
||||
}
|
||||
MenuPreset ERR_AdminPanel {
|
||||
Layout "{AE761C0439D25FB8}UI/layouts/ERR/AdminMenu.layout"
|
||||
ActionContext "MenuContext"
|
||||
|
||||
@@ -70,12 +70,13 @@ ERR_ScenarioDefinition {
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-extraction-1"
|
||||
Label "Initial helicopter pickup site"
|
||||
Label "Extraction landing zone"
|
||||
Kind 2
|
||||
Prefab "{033503110AC34EAC}Prefabs/ERR/Extraction.et"
|
||||
Position 4800.953 0 11487.412
|
||||
AircraftOffset 0 0 0
|
||||
RequireAirfield 1
|
||||
RequireAirfield 0
|
||||
RandomClear 1
|
||||
Guarded 1
|
||||
GuardOffset 30 0 0
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ SCR_GameOverScreenConfig : "{651583FFFB7FC765}Configs/GameOverScreen/BaseGameOve
|
||||
m_aGameOverScreens {
|
||||
ERR_EscapeGameOverScreenInfo "{6B193DC8DDC63507}" {
|
||||
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_sTitle "Escape complete"
|
||||
m_sAudioOneShot "SOUND_ONVICTORY"
|
||||
@@ -10,7 +10,7 @@ SCR_GameOverScreenConfig : "{651583FFFB7FC765}Configs/GameOverScreen/BaseGameOve
|
||||
}
|
||||
ERR_FailureGameOverScreenInfo "{E66F29A7A8AD7438}" {
|
||||
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_sTitle "Escape failed"
|
||||
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" {
|
||||
Contexts {
|
||||
ActionContext ERR_PassengerBrakeContext {
|
||||
Priority 60
|
||||
Flags 0xa 0
|
||||
Actions {
|
||||
Action ERR_PassengerBrake {
|
||||
InputSource InputSourceValue "{A7B19F416E541030}" {
|
||||
Input "keyboard:KC_SPACE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ActionContext ERR_FieldMapContext {
|
||||
Priority 60
|
||||
Flags 0xa 0
|
||||
|
||||
@@ -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_fAutoReloadTime 10
|
||||
m_fAutoReloadTime 120
|
||||
m_bUseSpawnPreload 1
|
||||
}
|
||||
|
||||
@@ -78,6 +78,15 @@ class ERR_Actors
|
||||
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)
|
||||
{
|
||||
players.Clear();
|
||||
|
||||
@@ -46,7 +46,7 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
m_Mode = ERR_GameMode.Cast(GetGame().GetGameMode());
|
||||
if (!m_Controller) { Close(); return; }
|
||||
array<string> names = {"PlaneOn", "PlaneOff", "Close", "PauseJournal", "JournalDetails", "LatestJournal", "KeepWeaponOn", "KeepWeaponOff", "SelfEpiOn", "SelfEpiOff", "IntelSummary", "RerollIntel", "RandomDay_startTimeMinutes", "Default_viewDistanceMetres", "RestartEmptyOn", "RestartEmptyOff", "RecommendedPlane", "RecommendedWeapons"};
|
||||
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"};
|
||||
foreach (string mainButton : ERR_AdminMenuPages.MainButtons()) names.Insert(mainButton);
|
||||
foreach (string pageButton : Navigation()) names.Insert(pageButton);
|
||||
for (int density = 1; density <= 4; density++) names.Insert("GuardEvery_" + density.ToString());
|
||||
@@ -154,9 +154,13 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
}
|
||||
protected int m_KeepWeaponState = -1;
|
||||
protected int m_SelfEpiState = -1;
|
||||
protected int m_SitesState = -1;
|
||||
protected int m_RestartEmptyState = -1;
|
||||
protected int m_TelemetryState = -1;
|
||||
protected void UpdateActions()
|
||||
{
|
||||
ERR_SelectionStyle.Pair(GetRootWidget(), "TelemetryOn", "TelemetryOff", m_TelemetryState);
|
||||
ERR_SelectionStyle.Pair(GetRootWidget(), "SitesOn", "SitesOff", m_SitesState);
|
||||
ERR_SelectionStyle.Pair(GetRootWidget(), "SelfEpiOn", "SelfEpiOff", m_SelfEpiState);
|
||||
ERR_SelectionStyle.Pair(GetRootWidget(), "KeepWeaponOn", "KeepWeaponOff", m_KeepWeaponState);
|
||||
ERR_SelectionStyle.Pair(GetRootWidget(), "RestartEmptyOn", "RestartEmptyOff", m_RestartEmptyState);
|
||||
@@ -174,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 == "RerollIntel" || name == "IntelSummary") button.SetEnabled(m_Ready && m_Intel);
|
||||
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 == "RestartEmptyOff") button.SetEnabled(m_Ready && m_RestartEmptyState == 1);
|
||||
else if (name == "SitesOn") button.SetEnabled(m_Ready && m_SitesState == 0);
|
||||
else if (name == "SitesOff") button.SetEnabled(m_Ready && m_SitesState == 1);
|
||||
else if (name == "SelfEpiOn") button.SetEnabled(m_Ready && m_SelfEpiState == 0);
|
||||
else if (name == "SelfEpiOff") button.SetEnabled(m_Ready && m_SelfEpiState == 1);
|
||||
else if (name == "KeepWeaponOn") button.SetEnabled(m_Ready && m_KeepWeaponState == 0);
|
||||
@@ -186,7 +194,7 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
{
|
||||
if (!authorized) { Close(); 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)
|
||||
{
|
||||
if (entry == "supports=hq") { m_HQ = true; continue; }
|
||||
@@ -194,6 +202,8 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
int divider = entry.IndexOf("=");
|
||||
if (divider <= 0) continue;
|
||||
string key = entry.Substring(0, divider);
|
||||
if (key == "radioIntelProgress") { SetText("RadioIntelProgress", entry.Substring(divider + 1, entry.Length() - divider - 1)); continue; }
|
||||
if (key == "droneRuntime") { SetText("DroneRuntime", entry.Substring(divider + 1, entry.Length() - divider - 1)); continue; }
|
||||
if (key == "recommendedBikeRuntime") { SetText("RecommendedBikeRuntime", entry.Substring(divider + 1, entry.Length() - divider - 1)); continue; }
|
||||
if (key.IndexOf("recommended_") == 0)
|
||||
{
|
||||
@@ -222,6 +232,15 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
ERR_SelectionStyle.Pair(GetRootWidget(), "PlaneOn", "PlaneOff", planeState);
|
||||
m_AvailableFields.Insert(key); continue;
|
||||
}
|
||||
if (key == "telemetryStatus") { SetText("TelemetryStatus", entry.Substring(divider + 1, entry.Length() - divider - 1)); continue; }
|
||||
if (key == "telemetryEnabled")
|
||||
{
|
||||
m_TelemetryState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
|
||||
string telemetryState = "Disabled";
|
||||
if (m_TelemetryState == 1) telemetryState = "Enabled";
|
||||
SetText("Saved_telemetryEnabled", "Saved: " + telemetryState);
|
||||
continue;
|
||||
}
|
||||
if (key == "restartOnEmpty")
|
||||
{
|
||||
m_RestartEmptyState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
|
||||
@@ -230,6 +249,11 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
SetText("Saved_restartOnEmpty", "Saved: " + restartState);
|
||||
continue;
|
||||
}
|
||||
if (key == "showAllSites")
|
||||
{
|
||||
m_SitesState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
|
||||
continue;
|
||||
}
|
||||
if (key == "selfEpinephrine")
|
||||
{
|
||||
m_SelfEpiState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
|
||||
@@ -330,6 +354,7 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
if (name == "Close") { Close(); return; }
|
||||
int section = ERR_AdminMenuPages.MainButtons().Find(name);
|
||||
if (section >= 0) { ShowPage(ERR_AdminMenuPages.MainPage(section)); return; }
|
||||
if (name == "RecommendedDrones") { ShowPage(ERR_AdminPage.DRONES); return; }
|
||||
if (name == "RecommendedPlane") { ShowPage(ERR_AdminPage.PLANE); return; }
|
||||
if (name == "RecommendedWeapons") { ShowPage(ERR_AdminPage.ITEMS); return; }
|
||||
int page = Navigation().Find(name);
|
||||
@@ -380,6 +405,12 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
m_ClickDelay = 0.3; SetText("Feedback", "Saving reconnaissance plane selection for next round...");
|
||||
m_Controller.ERR_AdminSubmit("mission plane set " + enabled, true); return;
|
||||
}
|
||||
if (name == "TelemetryOn" || name == "TelemetryOff")
|
||||
{
|
||||
if (m_TelemetryState < 0) return;
|
||||
string enabled = "0"; if (name == "TelemetryOn") enabled = "1";
|
||||
m_Controller.ERR_AdminSubmit("mission telemetry set " + enabled, true); return;
|
||||
}
|
||||
if (name == "RestartEmptyOn" || name == "RestartEmptyOff")
|
||||
{
|
||||
if (m_RestartEmptyState < 0) return;
|
||||
@@ -387,6 +418,13 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
m_ClickDelay = 0.3; SetText("Feedback", "Saving restart after last disconnect...");
|
||||
m_Controller.ERR_AdminSubmit("mission restartempty set " + restartEnabled, true); return;
|
||||
}
|
||||
if (name == "SitesOn" || name == "SitesOff")
|
||||
{
|
||||
if (m_SitesState < 0) return;
|
||||
string sitesEnabled = "0"; if (name == "SitesOn") sitesEnabled = "1";
|
||||
m_ClickDelay = 0.3; SetText("Feedback", "Saving site map visibility...");
|
||||
m_Controller.ERR_AdminSubmit("intel sites set " + sitesEnabled, true); return;
|
||||
}
|
||||
if (name == "SelfEpiOn" || name == "SelfEpiOff")
|
||||
{
|
||||
if (m_SelfEpiState < 0) return;
|
||||
|
||||
@@ -3,12 +3,12 @@ class ERR_AdminMenuFields
|
||||
{
|
||||
static array<string> Keys()
|
||||
{
|
||||
return {"aiDoctrine", "aiShadow", "automaticEquipmentChancePercent", "sarkaAngerPerCar", "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"};
|
||||
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") 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);
|
||||
@@ -48,6 +48,8 @@ class ERR_AdminMenuFields
|
||||
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";
|
||||
@@ -61,12 +63,19 @@ class ERR_AdminMenuFields
|
||||
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";
|
||||
|
||||
@@ -23,17 +23,22 @@ enum ERR_AdminPage
|
||||
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"}; }
|
||||
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"}; }
|
||||
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};
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ class ERR_CommandArguments
|
||||
{
|
||||
static string CoreSetting(string section, string option)
|
||||
{
|
||||
if (section == "mission" && option == "telemetry") return "telemetryEnabled";
|
||||
if (section == "mission" && option == "doctrine") return "aiDoctrine";
|
||||
if (section == "mission" && option == "aishadow") return "aiShadow";
|
||||
if (section == "mission" && option == "sarkapoints") return "sarkaAngerPerCar";
|
||||
@@ -11,6 +12,8 @@ class ERR_CommandArguments
|
||||
if (section == "mission" && option == "planealtitude") return "planeAltitudeMetres";
|
||||
if (section == "mission" && option == "planereplacement") return "planeReplacementSeconds";
|
||||
if (section == "mission" && option == "planesector") return "planeSectorSeconds";
|
||||
if (section == "mission" && option == "drones") return "droneReconCount";
|
||||
if (section == "mission" && option == "dronecooldown") return "droneCooldownSeconds";
|
||||
if (section == "mission" && option == "plane") return "planeReconEnabled";
|
||||
if (section == "mission" && option == "camps") return "fieldCampCount";
|
||||
if (section == "mission" && option == "hospitals") return "fieldHospitalCount";
|
||||
@@ -21,6 +24,12 @@ class ERR_CommandArguments
|
||||
if (section == "mission" && option == "objectguards") return "objectGuardCount";
|
||||
if (section == "mission" && option == "hunterdistance") return "hunterSpawnDistanceMetres";
|
||||
if (section == "mission" && option == "roadblocks") return "roadblockCount";
|
||||
if (section == "mission" && option == "largeroadblocks") return "largeRoadblockCount";
|
||||
if (section == "mission" && option == "mainroadwidth") return "mainRoadWidthMetres";
|
||||
if (section == "mission" && option == "garagechance") return "garageChancePercent";
|
||||
if (section == "mission" && option == "garageradius") return "garageRadiusMetres";
|
||||
if (section == "mission" && option == "garageinterval") return "garageSpawnIntervalSeconds";
|
||||
if (section == "mission" && option == "garagevehicles") return "garageVehicleCount";
|
||||
if (section == "mission" && option == "traffic") return "civilianTrafficCount";
|
||||
if (section == "mission" && option == "start") return "startMode";
|
||||
if (section == "mission" && option == "selfepi") return "selfEpinephrine";
|
||||
@@ -28,6 +37,8 @@ class ERR_CommandArguments
|
||||
if (section == "mission" && option == "restartempty") return "restartOnEmpty";
|
||||
if (section == "mission" && option == "emptyhours") return "emptyRestartHours";
|
||||
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";
|
||||
return "";
|
||||
}
|
||||
@@ -35,8 +46,8 @@ class ERR_CommandArguments
|
||||
{
|
||||
if (key == "aiDoctrine" && text == "current") { value = 0; 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") && text == "on") { value = 1; return true; }
|
||||
if ((key == "aiShadow" || key == "restartOnEmpty" || key == "showAllSites") && text == "off") { value = 0; return true; }
|
||||
if ((key == "aiShadow" || key == "restartOnEmpty" || key == "showAllSites") && text == "on") { value = 1; return true; }
|
||||
if (key == "startMode" && text == "prison") { value = 0; return true; }
|
||||
if (key == "startMode" && text == "crash") { value = 1; return true; }
|
||||
if (text == "random" && ERR_Settings.Randomizable(key)) { value = -1; return true; }
|
||||
@@ -173,6 +184,8 @@ class ERR_AdminService
|
||||
if (key == "startMagazineCount") return "0..48 spare pistol magazines in the shared prison/crash supply backpack; default 8. Applies next round.";
|
||||
if (key == "sarkaAngerPerCar") return "1-30 anger points per car; 0 disables new waves. Default 3. Whole thresholds only: 5 means one car at 5 anger, two at 10. Four special-forces crew per car. Applies to new waves; existing teams remain.";
|
||||
if (key == "motorbikeCount") return "0-6; default 2; 0 disabled. Optional WCS Motorbikes, two response specialists per bike. Applies next round; losses are not replaced.";
|
||||
if (key == "droneReconCount") return "0-3; default 0. Applies next round.";
|
||||
if (key == "droneCooldownSeconds") return "60-1800; default 300. Applies next round.";
|
||||
if (key == "planeAltitudeMetres") return "150-500; default 250. Applies next round.";
|
||||
if (key == "planeReplacementSeconds") return "30-1800; default 60. Applies next round.";
|
||||
if (key == "planeSectorSeconds") return "30-600; default 120. Applies next round.";
|
||||
@@ -183,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 == "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 == "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 == "emptyRestartHours") return "0 disables idle restarts; 1..168 real hours continuously empty. Default 5. Applies immediately, measured from mission start or the last disconnect. Connected players, including spectators, prevent idle restarts.";
|
||||
if (key == "garageChancePercent") return "Garage selection chance (0-100%). Default 70%; 100 selects every suitable garage. Next round.";
|
||||
if (key == "garageRadiusMetres") return "Spawn radius (250-1500 metres). Distance around players; default 1000 m. Next round.";
|
||||
if (key == "garageSpawnIntervalSeconds") return "Time between spawns (5-60 seconds). Default 5 s. Increase to spread out spawning. Next round.";
|
||||
if (key == "garageVehicleCount") return "0..24 unclaimed garage vehicles near players; default 16. Used vehicles are kept; distant unused vehicles retire. Optional bikes. Applies next round.";
|
||||
if (key == "civilianTrafficCount") return "Set 0..6 civilian cars (default 3). Zero disables new traffic. Unclaimed cars retire only far from all players and out of sight; player-entered cars are preserved.";
|
||||
if (key == "showAllSites") return "0 disabled (default), 1 enabled. Show prepared sites at their positions as question marks; recovered intel identifies their types. Applies immediately to everyone.";
|
||||
if (key == "selfEpinephrine") return "0 disabled (default), 1 enabled. Unconscious players can press Space to use a carried epinephrine injector. Applies immediately; consumes the item and keeps its normal medical effect.";
|
||||
if (key == "keepWeaponOnUnconscious") return "Set 1 to retain the held firearm during unconsciousness and equip it after recovery; 0 keeps normal dropping. Applies to the next knockout.";
|
||||
if (key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent")
|
||||
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")
|
||||
return "Use /esc intel reveal set N (0-100) or random. This is the chance that a first document identifies its location exactly; otherwise it gives a rough location. A second document for that location always identifies it. Changes affect newly recovered documents immediately; known locations and spent documents remain unchanged.";
|
||||
return "Use /esc intel reveal set N (0-100) or random. This is the chance that a first document identifies its location exactly; otherwise it gives a rough location. A second document for that location always identifies it. With all sites shown, the first document always identifies the type. Changes affect newly recovered documents immediately; known locations and spent documents remain unchanged.";
|
||||
return "";
|
||||
}
|
||||
protected string RoadblockTiming(string key)
|
||||
{
|
||||
if (key == "aiDoctrine" || key == "aiShadow") 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 == "radioIntelSiteCount" || key == "garageVehicleCount" || key == "garageChancePercent" || key == "garageRadiusMetres" || key == "garageSpawnIntervalSeconds") return " for next round";
|
||||
if (key == "aiDoctrine" || key == "aiShadow" || key == "droneReconCount" || key == "droneCooldownSeconds") return " for next round";
|
||||
if (key == "motorbikeCount" || key == "planeAltitudeMetres" || key == "planeReplacementSeconds" || key == "planeSectorSeconds" || key == "planeReconEnabled" || key == "fieldCampCount" || key == "fieldHospitalCount" || key == "startWeaponCount" || key == "startMagazineCount" || key == "objectGuardCount" || key == "objectGuardEvery" || key == "objectGuardHQMode" || key == "hunterSpawnDistanceMetres" || key == "roadblockCount" || key == "largeRoadblockCount" || key == "mainRoadWidthMetres") return " for next round";
|
||||
return "";
|
||||
}
|
||||
protected void SettingCommand(ERR_Settings settings, string key, array<string> parts, array<string> replies)
|
||||
@@ -240,6 +263,7 @@ class ERR_AdminService
|
||||
}
|
||||
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("AI carrying and reroll commands require the Intel addon.");
|
||||
return;
|
||||
@@ -329,6 +353,7 @@ class ERR_AdminService
|
||||
if (!key.IsEmpty())
|
||||
{
|
||||
SettingCommand(mode.Settings(), key, parts, replies);
|
||||
if (key == "showAllSites") mode.PublishSiteVisibility();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,19 +80,24 @@ class ERR_AdminSession
|
||||
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[1] == "remove") saved = supplies.Remove(parts[2]);
|
||||
else if (parts.Count() == 5 && parts[1] == "set")
|
||||
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]);
|
||||
if (item) saved = supplies.Set(item.Prefab, count);
|
||||
string destination = "backpack"; if (parts.Count() == 6) destination = parts[5];
|
||||
if (item) saved = supplies.Set(item.Prefab, count, destination);
|
||||
}
|
||||
}
|
||||
if (saved) replies.Insert("Starting backpack 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 item types and 64 items total. Check configuration if read-only.");
|
||||
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.
|
||||
@@ -154,6 +159,7 @@ class ERR_AdminSession
|
||||
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.Discovery()) values.Insert("radioIntelProgress=" + m_Mode.Discovery().RadioIntelStatus());
|
||||
m_Mode.Admin().Capabilities(values);
|
||||
ERR_RecommendedAddons.Snapshot(m_Mode, values);
|
||||
GetGame().ERR_ServerItems().Snapshot(m_Mode, values, m_ItemAddon, m_ItemPage);
|
||||
|
||||
@@ -36,7 +36,7 @@ class ERR_AirfieldPlacement
|
||||
}
|
||||
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();
|
||||
array<vector> authored = {};
|
||||
AuthoredAirports(authored);
|
||||
@@ -49,12 +49,12 @@ class ERR_AirfieldPlacement
|
||||
// Same aircraft footprint/vertical sweep, without requiring airport scenery.
|
||||
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++)
|
||||
{
|
||||
float side = edge * Math.PI / 2;
|
||||
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;
|
||||
TraceBox clearance = new TraceBox();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,19 @@
|
||||
// A first clue identifies a stable search square, not the exact objective.
|
||||
// Stable grid centers prevent repeated updates from triangulating the site.
|
||||
// Normal clues use stable search squares to prevent triangulating the site.
|
||||
// The optional anonymous-site mode intentionally exposes positions, never types.
|
||||
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)
|
||||
{
|
||||
if (knowledge >= 2) return Math.Round(coordinate);
|
||||
if (knowledge == 0 || knowledge >= 2) return Math.Round(coordinate);
|
||||
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
|
||||
// locations which have not been recovered through gameplay.
|
||||
// Read replicated discovery and the administrator-selected visibility locally.
|
||||
// Anonymous markers never expose a site label or type before discovery.
|
||||
modded class ERR_LocationComponent
|
||||
{
|
||||
protected ref ERR_DiscoveryMarker m_ERRMarker;
|
||||
protected int m_ERRShown;
|
||||
protected int m_ERRShown = -1;
|
||||
protected string m_ERRShownLabel;
|
||||
|
||||
protected override void EOnInit(IEntity owner)
|
||||
@@ -126,20 +135,24 @@ modded class ERR_LocationComponent
|
||||
protected void ERR_UpdateMarker()
|
||||
{
|
||||
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();
|
||||
if (m_ERRMarker && oldManager) oldManager.ERR_RemoveDiscoveryMarker(m_ERRMarker);
|
||||
else if (m_ERRMarker) m_ERRMarker.OnDelete();
|
||||
m_ERRMarker = null; m_ERRShown = 0; m_ERRShownLabel = "";
|
||||
m_ERRMarker = null; m_ERRShown = -1; m_ERRShownLabel = "";
|
||||
return;
|
||||
}
|
||||
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();
|
||||
if (!manager || !manager.GetMarkerConfig()) return;
|
||||
string label = "Unknown location (500 m area)";
|
||||
if (Knowledge() == 2)
|
||||
if (shown == 0) label = "Unknown site";
|
||||
if (shown == 2)
|
||||
{
|
||||
if (!Label().IsEmpty()) label = Label();
|
||||
else if (Kind() == ERR_LocationKind.RADIO) label = "Radio station";
|
||||
@@ -152,18 +165,18 @@ modded class ERR_LocationComponent
|
||||
{
|
||||
m_ERRMarker = new ERR_DiscoveryMarker();
|
||||
m_ERRMarker.SetType(SCR_EMapMarkerType.PLACED_CUSTOM);
|
||||
m_ERRMarker.SetDiscoveryPosition(Position(), Knowledge());
|
||||
m_ERRMarker.SetDiscoveryPosition(Position(), shown);
|
||||
m_ERRMarker.SetMarkerConfigID(0);
|
||||
m_ERRMarker.SetLocationIcon(Kind(), Id(), Knowledge());
|
||||
m_ERRMarker.SetLocationIcon(Kind(), Id(), shown);
|
||||
m_ERRMarker.SetCustomText(label);
|
||||
manager.ERR_InsertDiscoveryMarker(m_ERRMarker);
|
||||
}
|
||||
m_ERRMarker.SetDiscoveryPosition(Position(), Knowledge());
|
||||
m_ERRMarker.SetLocationIcon(Kind(), Id(), Knowledge());
|
||||
m_ERRMarker.SetDiscoveryPosition(Position(), shown);
|
||||
m_ERRMarker.SetLocationIcon(Kind(), Id(), shown);
|
||||
m_ERRMarker.SetCustomText(label);
|
||||
// The objective presentation performs its own normal viewport culling.
|
||||
m_ERRMarker.RefreshPresentation();
|
||||
m_ERRShown = Knowledge();
|
||||
m_ERRShown = shown;
|
||||
m_ERRShownLabel = Label();
|
||||
}
|
||||
protected override void OnDelete(IEntity owner)
|
||||
|
||||
@@ -1,10 +1,86 @@
|
||||
// Base mission discovery: recovering physical documents works without companions.
|
||||
// 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
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
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.
|
||||
bool IsSpent(IEntity folder) { return folder && m_Spent.Contains(folder); }
|
||||
bool CanMarkSpent(IEntity folder)
|
||||
@@ -37,14 +113,74 @@ class ERR_DiscoveryService
|
||||
|
||||
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 = {};
|
||||
ERR_Actors.Players(players);
|
||||
m_CleanupNow = now;
|
||||
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)
|
||||
{
|
||||
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;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(player.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory) return;
|
||||
@@ -60,10 +196,22 @@ class ERR_DiscoveryService
|
||||
array<ERR_LocationComponent> eligible = {};
|
||||
ERR_DocumentComponent document = ERR_DocumentComponent.Cast(folder.FindComponent(ERR_DocumentComponent));
|
||||
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)
|
||||
{
|
||||
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 (location.Id() == document.TargetId()) eligible.Insert(location);
|
||||
@@ -72,15 +220,24 @@ class ERR_DiscoveryService
|
||||
}
|
||||
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;
|
||||
}
|
||||
ERR_LocationComponent chosen = eligible.GetRandomElement();
|
||||
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);
|
||||
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
|
||||
{
|
||||
protected bool m_ERREscapeContent;
|
||||
protected ERR_GameMode m_ERRStatisticsMode;
|
||||
protected ERR_GameMode m_ERRFailureMode;
|
||||
override void InitContent(SCR_GameOverScreenUIContentData endScreenUIContent)
|
||||
{
|
||||
SCR_PlayerController.ERR_OnEscapeResultChanged().Remove(ERR_RefreshEscapeContent);
|
||||
if (m_ERRFailureMode) m_ERRFailureMode.OnEndReasonChanged().Remove(ERR_RefreshFailureContent);
|
||||
m_ERRFailureMode = null;
|
||||
if (m_ERRStatisticsMode) m_ERRStatisticsMode.ERR_OnStatisticsChanged().Remove(ERR_RefreshStatistics);
|
||||
m_ERRStatisticsMode = null;
|
||||
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))
|
||||
{
|
||||
m_ERRFailureMode = ERR_GameMode.Get();
|
||||
@@ -71,10 +80,21 @@ modded class SCR_GameOverScreenContentUIComponent
|
||||
TextWidget debrief = TextWidget.Cast(m_wRoot.FindAnyWidget(m_sDebriefingName));
|
||||
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)
|
||||
{
|
||||
SCR_PlayerController.ERR_OnEscapeResultChanged().Remove(ERR_RefreshEscapeContent);
|
||||
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);
|
||||
m_ERRFailureMode = null;
|
||||
super.HandlerDeattached(w);
|
||||
|
||||
@@ -8,6 +8,16 @@ class ERR_RescueRules
|
||||
if (since < 0 || now < since) return now;
|
||||
return since;
|
||||
}
|
||||
static float AirborneSince(bool airborne, float since, float now)
|
||||
{
|
||||
if (!airborne || now < 0) return -1;
|
||||
if (since < 0 || now < since) return now;
|
||||
return since;
|
||||
}
|
||||
static bool FlightComplete(float since, float now)
|
||||
{
|
||||
return since >= 0 && now >= since && now - since >= 30000;
|
||||
}
|
||||
static bool CanDepart(float since, float now)
|
||||
{
|
||||
return since >= 0 && now >= since && now - since >= 8000;
|
||||
@@ -234,6 +244,7 @@ class ERR_ExtractionService
|
||||
protected bool m_Stopped;
|
||||
protected vector m_Center;
|
||||
protected float m_Radius;
|
||||
protected float m_AirborneSince = -1;
|
||||
protected ERR_ExtractionState m_State;
|
||||
protected ref set<UUID> m_Escapees = new set<UUID>();
|
||||
protected int m_ParticipantCount;
|
||||
@@ -328,8 +339,12 @@ class ERR_ExtractionService
|
||||
if (m_State != ERR_ExtractionState.WAITING || !m_Assigned) return;
|
||||
if (!FleetAlive()) { Resolve(ERR_ExtractionState.LOST); return; }
|
||||
if (m_Stage != 5 || phase != ERR_Phase.EXTRACTION) return;
|
||||
bool airborne = !m_Transports.IsEmpty();
|
||||
foreach (ERR_RescueFlightComponent departing : m_Transports)
|
||||
if (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 = {};
|
||||
m_Roster.Participants(lives);
|
||||
foreach (ERR_PlayerLife life : lives)
|
||||
@@ -359,6 +374,13 @@ class ERR_ExtractionService
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
// Capture the final aircraft state before terminal cleanup releases autopilots
|
||||
// and removes the AI groups. Keep the same occupied airframes until reload.
|
||||
if (m_State == ERR_ExtractionState.ESCAPED)
|
||||
{
|
||||
foreach (ERR_RescueFlightComponent safeTransport : m_Transports) if (safeTransport) safeTransport.HoldForResults();
|
||||
foreach (ERR_RescueFlightComponent safeEscort : m_Escorts) if (safeEscort && !safeEscort.Lost()) safeEscort.HoldForResults();
|
||||
}
|
||||
m_Stopped = true; m_RadioPending = ""; m_PreviousSignals.Clear();
|
||||
foreach (ERR_RescueFlightComponent transport : m_Transports) if (transport) transport.Release(false);
|
||||
foreach (ERR_RescueFlightComponent escort : m_Escorts) if (escort) escort.Release(false);
|
||||
|
||||
@@ -26,6 +26,16 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
m_SelectedPlayerFaction = player; m_SelectedEnemyFaction = enemy; Replication.BumpMe();
|
||||
}
|
||||
protected ref ERR_Settings m_Settings;
|
||||
[RplProp()] protected bool m_ShowAllSites;
|
||||
bool ShowAllSites() { return m_ShowAllSites; }
|
||||
void PublishSiteVisibility()
|
||||
{
|
||||
if (!Replication.IsServer() || !m_Settings || m_Stopped || m_NativeEndHandled) return;
|
||||
bool enabled = m_Settings.Get("showAllSites") == 1;
|
||||
if (m_ShowAllSites == enabled) return;
|
||||
m_ShowAllSites = enabled;
|
||||
Replication.BumpMe();
|
||||
}
|
||||
[RplProp()] protected int m_StartMode;
|
||||
protected bool m_StartModeFrozen;
|
||||
protected bool m_StartPrepared;
|
||||
@@ -55,6 +65,7 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
protected ref ERR_Performance m_Performance;
|
||||
ERR_Performance Performance() { return m_Performance; }
|
||||
protected ref ERR_CivilianTraffic m_CivilianTraffic;
|
||||
protected ref ERR_GarageVehicles m_GarageVehicles;
|
||||
protected ref ERR_ForceLedger m_Forces = new ERR_ForceLedger();
|
||||
protected ref ERR_ForceSimulation m_ForceSimulation;
|
||||
protected ref array<AIAgent> m_ForceAgents = {};
|
||||
@@ -133,6 +144,7 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
ERR_ForceLedger Forces() { return m_Forces; }
|
||||
ERR_CommandService Command() { return m_Command; }
|
||||
ERR_CivilianTraffic CivilianTraffic() { return m_CivilianTraffic; }
|
||||
ERR_GarageVehicles GarageVehicles() { return m_GarageVehicles; }
|
||||
ERR_Objectives Objectives() { return m_Objectives; }
|
||||
ERR_ExtractionService Extraction() { return m_Extraction; }
|
||||
string EndReason() { return m_EndReason; }
|
||||
@@ -498,6 +510,7 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
if (!Replication.IsServer()) return;
|
||||
m_EmptySince = System.GetTickCount();
|
||||
m_Settings = new ERR_Settings(SettingsPath());
|
||||
PublishSiteVisibility();
|
||||
if (!System.IsCLIParam("errNoPerf")) m_Performance = new ERR_Performance();
|
||||
PublishViewDistance();
|
||||
m_ContentSettings = new ERR_ContentSettings();
|
||||
@@ -535,6 +548,9 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
m_Command = CreateCommandService();
|
||||
m_CivilianTraffic = new ERR_CivilianTraffic(this);
|
||||
OnStop().Insert(m_CivilianTraffic.Stop);
|
||||
m_GarageVehicles = new ERR_GarageVehicles(this);
|
||||
OnStop().Insert(m_GarageVehicles.Stop);
|
||||
m_GarageVehicles.Start();
|
||||
GetGame().GetCallqueue().CallLater(ServerTick, 1000, true);
|
||||
GetGame().GetCallqueue().CallLater(StartScenario, 1500, false);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
class ERR_CampTerrain
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
m_Rejection = "";
|
||||
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.
|
||||
for (float dx = -halfSize; dx <= halfSize; dx += 2)
|
||||
for (float dz = -halfSize; dz <= halfSize; dz += 2)
|
||||
{
|
||||
vector sample = center + Rotate(Vector(dx, 0, dz), heading);
|
||||
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);
|
||||
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 cz = -halfSize + 1; cz < halfSize; cz += 2)
|
||||
@@ -53,33 +54,42 @@ class ERR_CampTerrain
|
||||
// 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.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;
|
||||
}
|
||||
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;
|
||||
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());
|
||||
if (!aiWorld || !aiWorld.GetRoadNetworkManager()) return false;
|
||||
BaseRoad road; float distance;
|
||||
aiWorld.GetRoadNetworkManager().GetClosestRoad(position, road, distance, skipNavlinks: true);
|
||||
if (road && distance < 12) return false;
|
||||
if (!Clear(position, heading, 10, 0.6, baseHeight)) return false;
|
||||
bool extraction = site.Kind == ERR_LocationKind.EXTRACTION;
|
||||
// Camps must stay off roads; a helicopter pad may use clear paved ground.
|
||||
// Its physical terrain, obstacle and fleet checks still decide acceptance.
|
||||
if (!extraction)
|
||||
{
|
||||
SCR_AIWorld aiWorld = SCR_AIWorld.Cast(GetGame().GetAIWorld());
|
||||
if (!aiWorld || !aiWorld.GetRoadNetworkManager()) return Reject("road network unavailable");
|
||||
BaseRoad road; float distance;
|
||||
aiWorld.GetRoadNetworkManager().GetClosestRoad(position, road, distance, skipNavlinks: true);
|
||||
if (road && distance < 12) return Reject("road proximity");
|
||||
}
|
||||
float maximumRange = 0.6;
|
||||
if (extraction) maximumRange = 0.8;
|
||||
if (!Clear(position, heading, 10, maximumRange, baseHeight)) return false;
|
||||
if (ERR_StartSetup.IsCrash(site))
|
||||
{
|
||||
vector offset, center; float halfSize, height;
|
||||
if (!ERR_StartSetup.Geometry(offset, center, halfSize, height)) return false;
|
||||
if (!ERR_StartSetup.Geometry(offset, center, halfSize, height)) return Reject("crash footprint unavailable");
|
||||
float wreckFloor;
|
||||
if (!Clear(position + Rotate(center, heading), heading, halfSize, 0.6, wreckFloor, height)) return false;
|
||||
if (!Clear(position + Rotate(center, heading), heading, halfSize, 0.6, wreckFloor, height)) return Reject("wreck area: " + m_Rejection);
|
||||
}
|
||||
float guardHeight;
|
||||
if (site.Guarded && !Clear(position + Rotate(site.GuardOffset, heading), heading, 6, 0.6, guardHeight)) return false;
|
||||
if (site.ResponseCount > 0 && !Clear(position + Rotate(site.ResponseOffset, 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 Reject("patrol area: " + m_Rejection);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -90,6 +100,38 @@ class ERR_GeneratedSites
|
||||
protected ref array<ERR_SiteDefinition> m_Pending = {};
|
||||
protected vector m_Low, m_High;
|
||||
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 ERR_PrepareResult m_Result = ERR_PrepareResult.PENDING;
|
||||
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 (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.EXTRACTION && other.Kind == ERR_LocationKind.PRISON && vector.DistanceXZ(position, other.Position) + 100 >= ERR_GameMode.Get().EscapeRadius()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -169,6 +212,10 @@ class ERR_GeneratedSites
|
||||
{
|
||||
if (site.Kind != ERR_LocationKind.EXTRACTION) return true;
|
||||
vector primary = ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(site.AircraftOffset, heading));
|
||||
// Validate the primary pad before committing a random candidate, so an
|
||||
// obstructed clearing is retried instead of failing later during site creation.
|
||||
if (site.RequireAirfield) { if (!ERR_AirfieldPlacement.Clear(primary)) return false; }
|
||||
else if (!ERR_AirfieldPlacement.ClearGround(primary)) return false;
|
||||
return ERR_RescueLanding.Fits(primary);
|
||||
}
|
||||
ERR_PrepareResult Tick()
|
||||
@@ -180,6 +227,7 @@ class ERR_GeneratedSites
|
||||
{
|
||||
if (m_StartupOnly && StartupReady()) return ERR_PrepareResult.PENDING;
|
||||
ERR_SiteDefinition site = m_Pending[m_Index];
|
||||
ReportProgress(site);
|
||||
bool fallback = m_Attempts >= 12000 || System.GetTickCount() - m_Began >= 30000;
|
||||
vector position = site.Position; float heading = site.Heading;
|
||||
if (!fallback)
|
||||
@@ -188,22 +236,30 @@ class ERR_GeneratedSites
|
||||
heading = Math.RandomFloat(0, 360); m_Attempts++;
|
||||
}
|
||||
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;
|
||||
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);
|
||||
ReportSearch(site, "placed"); ClearDiagnostics();
|
||||
m_Index++; m_Attempts = 0; m_Began = System.GetTickCount();
|
||||
}
|
||||
else if (fallback)
|
||||
{
|
||||
ReportSearch(site, "exhausted; authored fallback rejected");
|
||||
if (SkipOptional(site))
|
||||
{
|
||||
ClearDiagnostics();
|
||||
m_Index++; m_Attempts = 0; m_Began = System.GetTickCount();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,10 +50,11 @@ class ERR_LocationComponent : ScriptComponent
|
||||
bool Discover(int roll, int chance)
|
||||
{
|
||||
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;
|
||||
m_Knowledge = ERR_MissionRules.Discovery(m_Knowledge, roll, chance);
|
||||
Replication.BumpMe();
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode) mode.LocationDiscovered(this, previous, m_Knowledge);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ modded class ERR_GameMode
|
||||
if (!ERR_MortarIntelReady()) return;
|
||||
if (!ERR_AirfieldCheck(!Sites().Find("everon-supply-2"), "guarded car-and-crate site absent")) return;
|
||||
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(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;
|
||||
|
||||
@@ -775,6 +775,10 @@ modded class ERR_GameMode
|
||||
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(!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;
|
||||
}
|
||||
else if (m_ERRBaseStep == 1 || m_ERRBaseStep == 2)
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
modded class ERR_CampTerrain
|
||||
{
|
||||
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;
|
||||
if ((site.Id == "everon-camp-5" || site.Id == "everon-outpost-1") && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "optional-site-skip")
|
||||
{
|
||||
s_ERROptionalSiteRefused = true; return false;
|
||||
}
|
||||
return super.Site(site, position, heading, baseHeight);
|
||||
return super.Site(site, position, heading, baseHeight, airportBuffer);
|
||||
}
|
||||
}
|
||||
modded class ERR_GameMode
|
||||
|
||||
@@ -56,7 +56,7 @@ modded class ERR_GameMode
|
||||
float now = GetWorld().GetWorldTime();
|
||||
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;
|
||||
}
|
||||
if (!PreparationError().IsEmpty() || Phase() == ERR_Phase.DEFEAT) { ERR_RescueCheck(false, "mission failed: " + PreparationError()); return; }
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
#ifdef WORKBENCH
|
||||
class ERR_RoadblockProbeRules
|
||||
{
|
||||
static int Target()
|
||||
static int SmallTarget()
|
||||
{
|
||||
string value;
|
||||
if (System.GetCLIParam("errRoadblockProbeCount", value)) return Math.Clamp(value.ToInt(), 1, 30);
|
||||
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
|
||||
{
|
||||
@@ -23,7 +33,11 @@ modded class ERR_Roadblocks
|
||||
{
|
||||
string outcome;
|
||||
if (!m_Begun && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "roadblocks")
|
||||
m_Mode.Settings().Set("roadblockCount", ERR_RoadblockProbeRules.Target());
|
||||
{
|
||||
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);
|
||||
}
|
||||
protected int m_ERRLayoutPosts;
|
||||
@@ -40,7 +54,7 @@ modded class ERR_Roadblocks
|
||||
for (int i = 0; i < m_BuildCovers.Count() && valid; 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);
|
||||
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
|
||||
@@ -102,28 +116,29 @@ modded class ERR_Roadblocks
|
||||
foreach (SCR_AIGroup group : m_Groups)
|
||||
{
|
||||
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 (m_SmallPlaced != ERR_RoadblockProbeRules.SmallTarget() || m_LargePlaced != ERR_RoadblockProbeRules.LargeTarget()) return false;
|
||||
int count = m_Positions.Count();
|
||||
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);
|
||||
return m_Positions.Count() == count && m_BackgroundSteps > 0;
|
||||
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 || ERR_RoadblockProbeRules.Target() <= 4);
|
||||
}
|
||||
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();
|
||||
foreach (SCR_AIGroup group : m_Groups)
|
||||
{
|
||||
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;
|
||||
}
|
||||
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf"));
|
||||
// Repeated preparation cannot replenish or create another squad.
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -156,13 +171,13 @@ modded class ERR_GameMode
|
||||
IEntity player; if (controller) player = controller.GetControlledEntity();
|
||||
if (!player || !Players().CurrentActor(player)) return;
|
||||
DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
if (!posts.ERR_ProbeBackgroundPending() || posts.ERR_ProbePostCount() >= ERR_RoadblockProbeRules.Target() || !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();
|
||||
PrintFormat("ERR SMOKE PASS: roadblocks prison unlocked with %1/%2 posts; background still pending", m_ERRRoadblockInitial, ERR_RoadblockProbeRules.Target());
|
||||
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_ProbeCompleteRoadblocks() || posts.ERR_ProbePostCount() <= m_ERRRoadblockInitial) { ERR_RoadblockProbeFail("physical four-guard posts, replicated props, ledger and HQ admission"); return; }
|
||||
PrintFormat("ERR SMOKE PASS: roadblocks %1 physical posts / %2 guards / %3 replicated checkpoint props with distance streaming disabled; verified model footprints and centre lane; background completed after prison unlock, no extra posts on repeat tick", posts.ERR_ProbePostCount(), posts.ERR_ProbePostCount() * 4, posts.ERR_ProbePostCount() * ERR_RoadblockRules.PropCount());
|
||||
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; }
|
||||
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();
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
Print("ERR SMOKE COMPLETE: roadblocks background completion on Everon; scripted local fixture");
|
||||
|
||||
@@ -35,7 +35,7 @@ class ERR_ObjectiveText
|
||||
ERR_Phase phase = mode.Phase();
|
||||
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.";
|
||||
}
|
||||
if (phase == ERR_Phase.PRISON)
|
||||
|
||||
@@ -52,7 +52,7 @@ class ERR_ObserverGuidance
|
||||
}
|
||||
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 + " 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)
|
||||
{
|
||||
@@ -225,7 +225,7 @@ class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
|
||||
{
|
||||
target.GetWorldTransform(transform);
|
||||
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)
|
||||
{
|
||||
@@ -242,7 +242,7 @@ class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
|
||||
m_Target = null; m_Vehicle = null;
|
||||
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_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 bool m_ERRShownObserverAvailable;
|
||||
protected string m_ERRShownRecoveryStatus;
|
||||
protected string m_ERRPendingRecoveryStatus;
|
||||
protected int m_ERRRecoveryStatusSince;
|
||||
protected float m_ERRLastCycle = -1000;
|
||||
protected IEntity m_ERRRecoveryBody;
|
||||
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);
|
||||
// Dead teammates remain selectable alongside survivors, including our own body.
|
||||
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);
|
||||
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;
|
||||
}
|
||||
else index += direction;
|
||||
@@ -468,13 +473,24 @@ modded class SCR_PlayerController
|
||||
string recovery = mode.RecoveryStatus();
|
||||
string selfEpiHint = ERR_SelfEpinephrineHint();
|
||||
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();
|
||||
if (!hints) return; // retry on the existing monitor; do not lose the first hint
|
||||
string message = ERR_ObserverGuidance.Message(m_ERRObserverReason, survivorAvailable);
|
||||
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_ERRShownObserverReason = m_ERRObserverReason;
|
||||
m_ERRShownObserverAvailable = survivorAvailable;
|
||||
@@ -510,6 +526,8 @@ modded class SCR_PlayerController
|
||||
{
|
||||
m_ERRObserverReady = false;
|
||||
m_ERRShownRecoveryStatus = "";
|
||||
m_ERRPendingRecoveryStatus = "";
|
||||
m_ERRRecoveryStatusSince = 0;
|
||||
if (!m_ERRObserverCamera) return;
|
||||
if (GetGame().GetPlayerController() == this) m_ERRObserverCamera.TrySwitchToControlledEntityCamera();
|
||||
m_ERRObserverCamera.Terminate();
|
||||
|
||||
@@ -3,30 +3,44 @@
|
||||
class ERR_ObserverViewRules
|
||||
{
|
||||
// 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,
|
||||
// nine for PiP: scope pose/lens, eye pose/lens, native lens geometry.
|
||||
// pair a new eye pose with an old aiming pose. Five vectors for ordinary/2D,
|
||||
// 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)
|
||||
{
|
||||
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 (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 (!Origin(WorldPosition(origin, packet[4]))) 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;
|
||||
}
|
||||
// The body and camera arrive through independently timed replication streams.
|
||||
// Keep the sampled world axes and world-axis eye offset together. Applying
|
||||
// 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.
|
||||
// Sample axes, world-axis eye offset and body origin on the owner together.
|
||||
// Neither the proxy body rotation nor its position is used during playback.
|
||||
static void Encode(vector body[4], vector view[4], out vector pose[4])
|
||||
{
|
||||
for (int axis = 0; axis < 3; axis++) pose[axis] = view[axis];
|
||||
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];
|
||||
view[3] = body[3] + pose[3];
|
||||
// Finite coordinate bounds cover large terrains while rejecting NaN/infinity.
|
||||
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)
|
||||
{
|
||||
@@ -42,6 +56,7 @@ class ERR_ObserverViewRules
|
||||
class ERR_ObserverViewSample
|
||||
{
|
||||
int Stamp;
|
||||
// Complete sampled world pose, not an offset from the replicated proxy.
|
||||
vector Position, Forward, Up;
|
||||
float Fov, Near;
|
||||
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)
|
||||
{
|
||||
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 || 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)
|
||||
{
|
||||
if (!m_ERRHasPIP || this != GetGame().GetPlayerController() || !target || !ERR_Actors.CanFight(target)) return false;
|
||||
vector pose[4], world[4];
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
// Use the exact aiming playhead even after a streamed optic temporarily
|
||||
// fails resolution and this eye buffer is not read for several frames.
|
||||
m_ERREyeBuffer.Align(m_ERRViewBuffer.Playback(), now);
|
||||
if (!m_ERREyeBuffer.Read(m_ERRObservedId, now, seconds, pose, fov, nearPlane)) return false;
|
||||
target.GetWorldTransform(world); ERR_ObserverViewRules.Decode(world, pose, transform);
|
||||
if (!m_ERREyeBuffer.Read(m_ERRObservedId, now, seconds, transform, fov, nearPlane)) return false;
|
||||
m_ERRViewBuffer.PIP(pipFov, geometry, vignette);
|
||||
return true;
|
||||
}
|
||||
@@ -160,13 +173,10 @@ modded class SCR_PlayerController
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (now - m_ERRViewRequestAt >= 500)
|
||||
{ m_ERRViewRequestAt = now; Rpc(ERR_RequestPlayerView); }
|
||||
vector pose[4], world[4];
|
||||
if (!m_ERRViewBuffer.Read(m_ERRObservedId, now, seconds, pose, fov, nearPlane)) return false;
|
||||
if (!m_ERRViewBuffer.Read(m_ERRObservedId, now, seconds, transform, fov, nearPlane)) return false;
|
||||
RplId scopeWeapon; vector scopeData;
|
||||
m_ERRViewBuffer.Scope(scopeWeapon, scopeData);
|
||||
if (scopeWeapon != RplId.Invalid() && !ERR_ObserverScope.Resolve(target, scopeWeapon, scopeData)) return false;
|
||||
target.GetWorldTransform(world);
|
||||
ERR_ObserverViewRules.Decode(world, pose, transform);
|
||||
return true;
|
||||
}
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
|
||||
@@ -229,12 +239,14 @@ modded class SCR_PlayerController
|
||||
packet[3] = Vector(fov, nearPlane, pip.GetPIPCamera().GetVerticalFOV());
|
||||
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(world[3]);
|
||||
if (!ERR_ObserverViewRules.Packet(packet))
|
||||
{
|
||||
// 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;
|
||||
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)
|
||||
{
|
||||
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();
|
||||
IEntity actor = GetControlledEntity();
|
||||
if (!mode || !mode.Active() || !ERR_Actors.CanFight(actor)) return;
|
||||
ERR_PlayerLife life = mode.Players().CurrentActor(actor);
|
||||
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();
|
||||
if (actor != m_ERRViewSourceBody) { m_ERRViewSourceBody = actor; m_ERRViewInputSequence = 0; }
|
||||
if (sequence <= m_ERRViewInputSequence || now - m_ERRViewLastPacket < 35) return;
|
||||
@@ -275,19 +288,20 @@ modded class SCR_PlayerController
|
||||
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 (!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.
|
||||
if (!m_ERRViewBuffer.Accepts(target, stamp)) return;
|
||||
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(); }
|
||||
vector geometry; float vignette;
|
||||
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;
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
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);
|
||||
if (message.IsEmpty()) 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);
|
||||
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)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
@@ -54,11 +70,21 @@ modded class SCR_ReconnectComponent
|
||||
UUID identity = SCR_PlayerIdentityUtils.GetPlayerIdentityId(playerId);
|
||||
if (!life || !life.Connected || !life.UsedLife || identity.IsNull() || life.AccountId != identity) return false;
|
||||
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;
|
||||
int owner;
|
||||
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();
|
||||
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))
|
||||
{
|
||||
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;
|
||||
}
|
||||
bool restored = super.HandlePlayerReconnect(playerId);
|
||||
|
||||
@@ -13,6 +13,7 @@ class ERR_PlayerLife
|
||||
int ConnectionId;
|
||||
bool Connected;
|
||||
bool UsedLife;
|
||||
bool ReconnectPending;
|
||||
IEntity Actor;
|
||||
}
|
||||
|
||||
@@ -66,6 +67,18 @@ class ERR_PlayerRoster
|
||||
ERR_PlayerLife life = Get(playerId);
|
||||
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)
|
||||
{
|
||||
if (!Replication.IsServer() || !actor) return;
|
||||
@@ -73,13 +86,18 @@ class ERR_PlayerRoster
|
||||
if (!life) return;
|
||||
life.Actor = actor;
|
||||
life.UsedLife = true;
|
||||
life.ReconnectPending = false;
|
||||
m_OnChanged.Invoke(playerId);
|
||||
}
|
||||
void Disconnect(int playerId)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
ERR_PlayerLife life = Get(playerId);
|
||||
if (life) life.Connected = false;
|
||||
if (life)
|
||||
{
|
||||
life.Connected = false;
|
||||
life.ReconnectPending = life.UsedLife;
|
||||
}
|
||||
m_Connections.Remove(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);
|
||||
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()
|
||||
@@ -130,6 +134,7 @@ modded class SCR_RespawnSystemComponent
|
||||
// Inspect the actual production-created widgets, without creating or changing them.
|
||||
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 (!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;
|
||||
|
||||
@@ -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 string m_MedicalDetails;
|
||||
protected bool m_DefeatLogged;
|
||||
protected float m_NextForecast;
|
||||
protected ref ScriptInvoker m_OnChanged = new ScriptInvoker();
|
||||
void ERR_RecoveryService(ERR_GameMode mode) { m_Mode = mode; }
|
||||
ScriptInvoker OnChanged() { return m_OnChanged; }
|
||||
@@ -55,7 +56,7 @@ class ERR_RecoveryService
|
||||
if (due && !m_DefeatLogged)
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -70,9 +71,11 @@ class ERR_RecoveryService
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped || !m_Mode || !m_Mode.Active()) return;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (now >= m_NextForecast - 1000 && now < m_NextForecast) return;
|
||||
m_NextForecast = now + 1000;
|
||||
bool logMedical = now >= m_NextMedicalLog;
|
||||
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>();
|
||||
array<ref ERR_PlayerLife> 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 treatment = medical.ERR_TreatmentInProgress();
|
||||
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);
|
||||
nextSamples.Insert(life.Actor, sample);
|
||||
}
|
||||
@@ -122,9 +130,11 @@ class ERR_RecoveryService
|
||||
{
|
||||
message = "No conscious prisoner is currently available to help.";
|
||||
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 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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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 (historicalFixture != "sarka-hunters" && historicalFixture != "self-epinephrine" && historicalFixture != "starter-backpack" && historicalFixture != "objective-map" && historicalFixture != "speech-download" && historicalFixture != "bundled-speech") 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
|
||||
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
|
||||
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.
|
||||
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 (fixture != "sarka-hunters" && fixture != "self-epinephrine" && fixture != "starter-backpack" && fixture != "objective-map" && fixture != "speech-download" && fixture != "bundled-speech") 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; }
|
||||
#endif
|
||||
if (m_Progressive && !m_RuntimeDefinition.ConfigurePopulation(m_Definition, mode.Settings().Get("fieldCampCount"), mode.Settings().Get("fieldHospitalCount")))
|
||||
|
||||
@@ -51,10 +51,34 @@ modded class SCR_PlayerController
|
||||
if (mode && mode.Players()) life = mode.Players().Get(GetPlayerId());
|
||||
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 "";
|
||||
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));
|
||||
array<IEntity> 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.
|
||||
for (int kind = 0; kind < 2; kind++)
|
||||
foreach (IEntity injector : items)
|
||||
@@ -68,18 +92,8 @@ modded class SCR_PlayerController
|
||||
if (kind == 1 && !ERR_ConsumableCombatEpinephrine.Cast(effect)) continue;
|
||||
SCR_EConsumableFailReason reason;
|
||||
if (!effect.CanApplyEffect(actor, actor, reason)) continue;
|
||||
// An unconscious player cannot perform the standing injection animation.
|
||||
// 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 injector;
|
||||
}
|
||||
return "No usable epinephrine injector in your inventory, or treatment is already 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);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,17 @@ class ERR_Settings
|
||||
|
||||
static array<string> Keys()
|
||||
{
|
||||
return {"aiDoctrine", "aiShadow", "sarkaAngerPerCar", "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)
|
||||
{
|
||||
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 == "droneReconCount") return value >= 0 && value <= 3;
|
||||
if (key == "droneCooldownSeconds") return value >= 60 && value <= 1800;
|
||||
if (key == "planeAltitudeMetres") return value >= 150 && value <= 500;
|
||||
if (key == "planeReplacementSeconds") return value >= 30 && value <= 1800;
|
||||
if (key == "planeSectorSeconds") return value >= 30 && value <= 600;
|
||||
@@ -30,8 +33,15 @@ class ERR_Settings
|
||||
if (key == "objectGuardCount") return value >= 0 && value <= 16;
|
||||
if (key == "hunterSpawnDistanceMetres") return value >= 800 && value <= 3000;
|
||||
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 == "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 == "emptyRestartHours") return value >= 0 && value <= 168;
|
||||
if (key == "heliTimerMinutes") return value >= 1 && value <= 1440;
|
||||
@@ -118,8 +128,11 @@ class ERR_Settings
|
||||
static int AddedDefault(string key)
|
||||
{
|
||||
if (key == "aiDoctrine" || key == "aiShadow") return 0;
|
||||
if (key == "radioIntelSiteCount") return 5;
|
||||
if (key == "sarkaAngerPerCar") return 3;
|
||||
if (key == "motorbikeCount") return 2;
|
||||
if (key == "droneReconCount") return 0;
|
||||
if (key == "droneCooldownSeconds") return 300;
|
||||
if (key == "planeAltitudeMetres") return 250;
|
||||
if (key == "planeReplacementSeconds") return 60;
|
||||
if (key == "planeSectorSeconds") return 120;
|
||||
@@ -133,9 +146,16 @@ class ERR_Settings
|
||||
if (key == "objectGuardCount") return 8;
|
||||
if (key == "hunterSpawnDistanceMetres") return 1000;
|
||||
if (key == "roadblockCount") return 4;
|
||||
if (key == "largeRoadblockCount") return 4;
|
||||
if (key == "mainRoadWidthMetres") return 6;
|
||||
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 == "keepWeaponOnUnconscious" || key == "restartOnEmpty") return 1;
|
||||
if (key == "telemetryEnabled" || key == "keepWeaponOnUnconscious" || key == "restartOnEmpty") return 1;
|
||||
if (key == "emptyRestartHours") return 5;
|
||||
if (key == "heliTimerMinutes") return 15;
|
||||
if (key == "convoyCount") return 3;
|
||||
@@ -152,10 +172,17 @@ class ERR_Settings
|
||||
void ERR_Settings(string path = PATH)
|
||||
{
|
||||
m_Path = path;
|
||||
m_Values.Set("telemetryEnabled", AddedDefault("telemetryEnabled"));
|
||||
m_Values.Set("aiDoctrine", AddedDefault("aiDoctrine"));
|
||||
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("droneReconCount", AddedDefault("droneReconCount"));
|
||||
m_Values.Set("droneCooldownSeconds", AddedDefault("droneCooldownSeconds"));
|
||||
m_Values.Set("planeAltitudeMetres", AddedDefault("planeAltitudeMetres"));
|
||||
m_Values.Set("planeReplacementSeconds", AddedDefault("planeReplacementSeconds"));
|
||||
m_Values.Set("planeSectorSeconds", AddedDefault("planeSectorSeconds"));
|
||||
@@ -169,6 +196,8 @@ class ERR_Settings
|
||||
m_Values.Set("objectGuardHQMode", AddedDefault("objectGuardHQMode"));
|
||||
m_Values.Set("hunterSpawnDistanceMetres", AddedDefault("hunterSpawnDistanceMetres"));
|
||||
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("startMode", AddedDefault("startMode"));
|
||||
m_Values.Set("keepWeaponOnUnconscious", AddedDefault("keepWeaponOnUnconscious"));
|
||||
@@ -180,6 +209,8 @@ class ERR_Settings
|
||||
m_Values.Set("responseRadiusMetres", 2000);
|
||||
m_Values.Set("aiIntelChancePercent", 60);
|
||||
m_Values.Set("intelRevealChancePercent", 15);
|
||||
m_Values.Set("radioIntelSiteCount", 5);
|
||||
m_Values.Set("showAllSites", AddedDefault("showAllSites"));
|
||||
m_Values.Set("radioDefenceSeconds", 180);
|
||||
m_Values.Set("startTimeMinutes", AddedDefault("startTimeMinutes"));
|
||||
m_Values.Set("timeMultiplier", AddedDefault("timeMultiplier"));
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
[BaseContainerProps()]
|
||||
class ERR_SiteStock
|
||||
{
|
||||
// Runtime starting-loadout choice; authored objective stock remains in storage.
|
||||
bool OnGround;
|
||||
[Attribute("", UIWidgets.ResourceNamePicker, "Physical inventory item", "et")]
|
||||
ResourceName Prefab;
|
||||
[Attribute("1", UIWidgets.EditBox, "Finite initial count")]
|
||||
@@ -59,7 +61,7 @@ class ERR_SiteDefinition
|
||||
ResourceName Prefab;
|
||||
[Attribute("0 0 0", UIWidgets.Coords)]
|
||||
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;
|
||||
[Attribute("0", UIWidgets.EditBox, "Composition heading in degrees")]
|
||||
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.RequireAirfield && site.Kind != ERR_LocationKind.EXTRACTION) 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 (!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;
|
||||
|
||||
@@ -7,6 +7,7 @@ class ERR_StockPlacement
|
||||
bool Accepted;
|
||||
bool Optional;
|
||||
bool Skipped;
|
||||
bool OnGround;
|
||||
}
|
||||
|
||||
class ERR_SiteRuntime
|
||||
@@ -389,7 +390,7 @@ class ERR_SiteService
|
||||
if (only && spec != only) continue;
|
||||
if (startupOnly && !ERR_GeneratedSites.StartupSite(spec)) continue;
|
||||
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;
|
||||
if (spec.PlacementResolved && spec.Kind == ERR_LocationKind.PRISON) rootPosition = spec.Position;
|
||||
rootPosition[1] = rootPosition[1] + spec.RootHeight;
|
||||
@@ -397,7 +398,7 @@ class ERR_SiteService
|
||||
{
|
||||
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 (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.
|
||||
IEntity root = ERR_Actors.Spawn(spec.Prefab, rootPosition, Vector(spec.Heading, 0, 0));
|
||||
@@ -473,6 +474,7 @@ class ERR_SiteService
|
||||
&& 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.Inventory = inventory;
|
||||
m_Placements.Insert(placement);
|
||||
@@ -628,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()
|
||||
{
|
||||
if (!Replication.IsServer() || !m_Begun || m_Stopped) return ERR_PrepareResult.FAILED;
|
||||
@@ -636,7 +707,7 @@ class ERR_SiteService
|
||||
ERR_PrepareResult aircraftPlacement = ERR_PrepareResult.READY;
|
||||
if (m_ExtractionPlacement) aircraftPlacement = m_ExtractionPlacement.Poll(GetGame().GetWorld().GetWorldTime());
|
||||
if (aircraftPlacement == ERR_PrepareResult.FAILED) { Fail(m_ExtractionPlacement.FailureReason()); return m_State; }
|
||||
int pending, accepted, pendingGuards;
|
||||
int pendingGuards;
|
||||
foreach (ERR_SiteRuntime site : m_Sites)
|
||||
{
|
||||
if (site.Definition.Transport && !site.Definition.Transport.Optional && !TransportReady(site))
|
||||
@@ -654,29 +725,7 @@ class ERR_SiteService
|
||||
if (mobile == ERR_PrepareResult.PENDING) pendingGuards++;
|
||||
}
|
||||
}
|
||||
string firstRejected;
|
||||
foreach (ERR_StockPlacement placement : m_Placements)
|
||||
{
|
||||
if (placement.Skipped) continue;
|
||||
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;
|
||||
if (placement.Optional && GetGame().GetWorld().GetWorldTime() > m_Deadline)
|
||||
{
|
||||
Print("Reintroduced [starting backpack]: item did not fit; omitted " + placement.Item.GetPrefabData().GetPrefabName(), LogLevel.WARNING);
|
||||
ERR_Actors.DeleteSpawned(placement.Item); placement.Skipped = true; 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 (PrepareStock() != ERR_PrepareResult.READY) return m_State;
|
||||
if (pendingGuards > 0 || aircraftPlacement == ERR_PrepareResult.PENDING) return m_State;
|
||||
// All required scenery, stock and physical guards now exist. Survey optional
|
||||
// parking once against that real population, then bound only vehicle settling.
|
||||
|
||||
@@ -29,13 +29,13 @@ class ERR_SpawnPlacement
|
||||
foreach (int id, vector reserved : reservations)
|
||||
if (id != ownReservation && vector.DistanceXZ(position, reserved) < 1.2 && Math.AbsFloat(position[1] - reserved[1]) < 2) return false;
|
||||
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.
|
||||
for (int x = -1; x <= 1; x++)
|
||||
for (int z = -1; z <= 1; z++)
|
||||
{
|
||||
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_Candidate = ground; m_Blocked = false;
|
||||
|
||||
@@ -11,6 +11,11 @@ class ERR_StartBackpack
|
||||
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;
|
||||
@@ -19,19 +24,21 @@ class ERR_StartBackpack
|
||||
for (int i = 1; i < records.Count(); i++)
|
||||
{
|
||||
array<string> parts = {}; records[i].Split("|", parts, false);
|
||||
if (parts.Count() != 2 || parts[0].Length() > 512 || !parts[0].EndsWith(".et")) return 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;
|
||||
if (!ERR_ItemPreferences.Guid(key) || seen.Contains(key)) return false;
|
||||
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);
|
||||
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 && json.ReadValue("records", records) && Valid(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)
|
||||
{
|
||||
@@ -42,7 +49,7 @@ class ERR_StartBackpack
|
||||
{
|
||||
if (!Replication.IsServer() || m_ReadOnly || !Valid(records)) return false;
|
||||
JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false);
|
||||
json.WriteValue("version", 1); json.WriteValue("records", records);
|
||||
json.WriteValue("version", 2); json.WriteValue("records", records);
|
||||
if (!ERR_ProfileConfig.Save(PATH, json)) return false;
|
||||
m_Records = records; return true;
|
||||
}
|
||||
@@ -53,26 +60,26 @@ class ERR_StartBackpack
|
||||
next[0] = "default"; if (custom) next[0] = "custom";
|
||||
return Save(next);
|
||||
}
|
||||
bool Set(ResourceName prefab, int quantity)
|
||||
bool Set(ResourceName prefab, int quantity, string destination = "backpack")
|
||||
{
|
||||
if (quantity < 0 || quantity > 48) return false;
|
||||
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) next.Insert(m_Records[i]);
|
||||
if (ERR_TerrainCatalog.Key(parts[0]) != key || Destination(parts) != destination) next.Insert(m_Records[i]);
|
||||
}
|
||||
if (quantity > 0) next.Insert(prefab + "|" + quantity.ToString());
|
||||
if (quantity > 0) next.Insert(prefab + "|" + quantity.ToString() + "|" + destination);
|
||||
return Save(next);
|
||||
}
|
||||
bool Remove(string key)
|
||||
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) return Set(parts[0], 0);
|
||||
if (ERR_TerrainCatalog.Key(parts[0]) == key && Destination(parts) == destination) return Set(parts[0], 0, destination);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -81,13 +88,13 @@ class ERR_StartBackpack
|
||||
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(); stock.Insert(item);
|
||||
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. Add matching magazines separately.";
|
||||
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++)
|
||||
@@ -96,7 +103,7 @@ class ERR_StartBackpack
|
||||
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]);
|
||||
values.Insert("starterItem=" + key + "|" + ERR_ServerItemCatalog.Clean(label) + "|" + parts[1] + "|" + Destination(parts));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ modded class ERR_AdminMenu
|
||||
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()
|
||||
@@ -69,8 +72,13 @@ modded class ERR_AdminMenu
|
||||
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() == 3)
|
||||
{ savedKeys.Insert(parts[0]); savedLabels.Insert(parts[2] + " x " + WidgetManager.Translate(parts[1])); savedSignature += value; total += parts[2].ToInt(); }
|
||||
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)
|
||||
{
|
||||
@@ -82,7 +90,7 @@ modded class ERR_AdminMenu
|
||||
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 types, " + total.ToString() + "/64 items + field map");
|
||||
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();
|
||||
@@ -104,7 +112,9 @@ modded class ERR_AdminMenu
|
||||
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; }
|
||||
if (selected >= 0 && selected < m_StarterCatalog.Count() && !m_StarterAddon.IsEmpty()) SubmitStarter("starter set " + m_StarterAddon + " " + m_StarterCatalog[selected] + " " + count.ToString());
|
||||
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();
|
||||
|
||||
@@ -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 float m_CandidateHeading;
|
||||
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 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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -23,12 +31,12 @@ class ERR_TerrainSites : ERR_GeneratedSites
|
||||
{
|
||||
if (m_Started || !Replication.IsServer() || !definition || !definition.Valid() || !GetGame().GetWorldEntity()) return false;
|
||||
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();
|
||||
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_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};
|
||||
foreach (int kind : order)
|
||||
foreach (ERR_SiteDefinition site : definition.Sites)
|
||||
@@ -37,7 +45,7 @@ class ERR_TerrainSites : ERR_GeneratedSites
|
||||
site.PlacementResolved = false;
|
||||
// The queue, rather than RandomClear, owns placement including extraction.
|
||||
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";
|
||||
m_Pending.Insert(site);
|
||||
}
|
||||
@@ -62,89 +70,74 @@ class ERR_TerrainSites : ERR_GeneratedSites
|
||||
}
|
||||
protected bool Navigable(vector position)
|
||||
{
|
||||
m_NavigationReason = "";
|
||||
NavmeshWorldComponent mesh = GetGame().GetAIWorld().GetNavmeshWorldComponent("Soldiers");
|
||||
if (!mesh) return false;
|
||||
if (!mesh.IsTileLoaded(position)) { m_WaitingNav = mesh.LoadTileIn(position); return false; }
|
||||
if (!mesh) { m_NavigationReason = "Soldiers navmesh unavailable"; 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;
|
||||
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)
|
||||
{
|
||||
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]);
|
||||
if (!Navigable(ground)) { m_NavigationRejected++; return false; }
|
||||
if (site.Guarded && !Navigable(ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(site.GuardOffset, heading)))) { m_NavigationRejected++; return false; }
|
||||
if (site.ResponseCount > 0 && !Navigable(ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(site.ResponseOffset, heading)))) { m_NavigationRejected++; return false; }
|
||||
// Aircraft spawn at the centre's terrain height, not the camp grid's minimum.
|
||||
// Using the minimum here falsely steepens the uphill edge of a valid pad.
|
||||
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;
|
||||
float padFloor;
|
||||
if (m_Terrain.Clear(ground, heading, 14, 0.8, padFloor))
|
||||
{
|
||||
bool primaryClear;
|
||||
if (site.RequireAirfield) primaryClear = ERR_AirfieldPlacement.Clear(ground);
|
||||
else primaryClear = ERR_AirfieldPlacement.ClearGround(ground);
|
||||
if (primaryClear && ERR_RescueLanding.Fits(ground)) return true;
|
||||
bool primaryClear = ERR_AirfieldPlacement.ClearGround(ground);
|
||||
if (!primaryClear) RecordRejection("primary aircraft clearance");
|
||||
else if (!ERR_RescueLanding.Fits(ground)) RecordRejection("secondary aircraft pad");
|
||||
else return true;
|
||||
}
|
||||
else RecordRejection("landing zone: " + m_Terrain.Rejection());
|
||||
m_PadRejected++; return false;
|
||||
}
|
||||
protected vector SamplePosition(ERR_SiteDefinition site)
|
||||
{
|
||||
if (site.Kind != ERR_LocationKind.EXTRACTION || m_Airports.m_Positions.IsEmpty())
|
||||
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;
|
||||
// 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));
|
||||
}
|
||||
override ERR_PrepareResult Tick()
|
||||
{
|
||||
if (m_Result != ERR_PrepareResult.PENDING) return m_Result;
|
||||
if (!m_Started || !Replication.IsServer()) return ERR_PrepareResult.FAILED;
|
||||
if (!PrepareAirports()) return m_Result;
|
||||
int tick = System.GetTickCount();
|
||||
while (m_Index < m_Pending.Count())
|
||||
{
|
||||
if (m_StartupOnly && StartupReady()) return ERR_PrepareResult.PENDING;
|
||||
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);
|
||||
ReportSearch(site, "exhausted");
|
||||
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_Index++; m_Attempts = 0; m_Began = System.GetTickCount(); m_HaveCandidate = false;
|
||||
m_GeometryRejected = 0; m_NavigationRejected = 0; m_PadRejected = 0;
|
||||
ClearDiagnostics();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -157,11 +150,14 @@ class ERR_TerrainSites : ERR_GeneratedSites
|
||||
vector position = m_Candidate;
|
||||
float heading = m_CandidateHeading, floor;
|
||||
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;
|
||||
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);
|
||||
ReportSearch(site, "placed"); ClearDiagnostics();
|
||||
m_Index++; m_Attempts = 0; m_Began = System.GetTickCount();
|
||||
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");
|
||||
ERR_CaptureTerrainSupportStations(SCR_SupportStationManagerComponent.Cast(m_ERRTerrainOriginal.FindComponent(SCR_SupportStationManagerComponent)));
|
||||
m_ERRTerrainOriginal.OnGameEnd();
|
||||
if (m_DataCollectorComponent && m_DataCollectorComponent.GetOwner() == m_ERRTerrainOriginal) m_DataCollectorComponent = null;
|
||||
SCR_EntityCatalogManagerComponent.ERR_ReleaseTerrainCatalog(m_ERRTerrainOriginal);
|
||||
|
||||
@@ -102,6 +102,7 @@ modded class ArmaReforgerScripted
|
||||
m_ERRClientTerrainTakeover = true;
|
||||
SCR_BaseGameMode mode = SCR_BaseGameMode.Cast(m_ERRClientOldManagers[0]);
|
||||
PrintFormat("Reintroduced [terrain client]: drain retained mode=%1 incoming=%2", mode, incoming);
|
||||
ERR_CaptureTerrainSupportStations(m_ERROldSupport);
|
||||
if (mode) mode.OnGameEnd();
|
||||
// Owner entities can already be invalid while native destruction is deferred.
|
||||
// Compare the captured component itself, never clear a newer singleton.
|
||||
|
||||
@@ -63,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
|
||||
// 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.
|
||||
@@ -71,6 +83,9 @@ modded class ArmaReforgerScripted
|
||||
protected ref SCR_MissionHeader m_ERRPresentedTerrainHeader;
|
||||
protected string m_ERROriginalTerrainTitle;
|
||||
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_ERRTitleWorld;
|
||||
protected string m_ERRTerrainLabel;
|
||||
@@ -131,6 +146,14 @@ modded class ArmaReforgerScripted
|
||||
m_ERRAppliedTerrainTitle = ERR_TerrainMissionTitle(header);
|
||||
header.m_sName = m_ERRAppliedTerrainTitle;
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -145,17 +168,30 @@ modded class ArmaReforgerScripted
|
||||
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)
|
||||
{
|
||||
if (!requested) return "";
|
||||
SCR_MissionHeader header = SCR_MissionHeader.Cast(GetMissionHeader());
|
||||
// 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();
|
||||
SCR_MissionHeader header = ERR_TerrainLoadingHeader(requested);
|
||||
if (!header) return "";
|
||||
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)
|
||||
{
|
||||
@@ -173,6 +209,11 @@ modded class ArmaReforgerScripted
|
||||
{
|
||||
// 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_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;
|
||||
}
|
||||
m_ERRPresentedTerrainHeader = null;
|
||||
@@ -180,6 +221,9 @@ modded class ArmaReforgerScripted
|
||||
m_ERROriginalTerrainTitle = "";
|
||||
m_ERROriginalTerrainGameMode = "";
|
||||
m_ERRAppliedTerrainTitle = "";
|
||||
m_ERRTerrainTextApplied = false;
|
||||
m_ERROriginalTerrainDescription = ""; m_ERROriginalTerrainDetails = "";
|
||||
m_ERRAppliedTerrainDescription = ""; m_ERRAppliedTerrainDetails = "";
|
||||
}
|
||||
|
||||
override protected void OnBeforeWorldCleanup()
|
||||
@@ -191,27 +235,37 @@ modded class ArmaReforgerScripted
|
||||
}
|
||||
|
||||
// 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
|
||||
{
|
||||
protected ref SCR_MissionHeader m_ERRLoadingTerrainHeader;
|
||||
protected string m_ERRLoadingTerrainTitle;
|
||||
protected string m_ERRLoadingTerrainTitle, m_ERRLoadingTerrainDescription;
|
||||
protected float m_ERRLoadingTerrainWait;
|
||||
|
||||
override void OnMissionDataRetrieved(SCR_MissionHeader header)
|
||||
{
|
||||
super.OnMissionDataRetrieved(header);
|
||||
m_ERRLoadingTerrainHeader = header;
|
||||
m_ERRLoadingTerrainTitle = ""; m_ERRLoadingTerrainWait = 0;
|
||||
m_ERRLoadingTerrainTitle = ""; m_ERRLoadingTerrainDescription = ""; m_ERRLoadingTerrainWait = 0;
|
||||
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);
|
||||
if (title == m_ERRLoadingTerrainTitle) return;
|
||||
m_ERRLoadingTerrainTitle = title;
|
||||
m_Widgets.m_wTitle.SetText(title);
|
||||
if (m_Widgets.m_wTitle && title != m_ERRLoadingTerrainTitle)
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -35,11 +35,10 @@ modded class SCR_PlayerController
|
||||
RplComponent component = RplComponent.Cast(target.FindComponent(RplComponent));
|
||||
if (!component) return false;
|
||||
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);
|
||||
vector local[4], world[4];
|
||||
if (!m_ERRProbeViewBuffer.Read(component.Id(), now, seconds, local, fov, nearPlane)) return false;
|
||||
target.GetWorldTransform(world); Math3D.MatrixMultiply4(world, local, transform);
|
||||
return true;
|
||||
// Feed complete sampled world poses, as the real receiver does. The target
|
||||
// remains a fixture stand-in; this does not qualify live multiplayer relay.
|
||||
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);
|
||||
return m_ERRProbeViewBuffer.Read(component.Id(), now, seconds, transform, fov, nearPlane);
|
||||
}
|
||||
|
||||
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])
|
||||
{
|
||||
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();
|
||||
InputManager input = GetGame().GetInputManager();
|
||||
if (!follow || !input) return false;
|
||||
@@ -163,17 +162,30 @@ modded class ERR_GameMode
|
||||
foreach (SCR_PersistentDamageEffect effect : effects) m_ERRCameraDamage.TerminateDamageEffect(effect);
|
||||
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();
|
||||
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];
|
||||
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(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(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;
|
||||
vector aim = pc.ERR_ObserverAnchor() + "0 1.5 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(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(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(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, "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, "own-body pitch action changes production camera elevation")) return false;
|
||||
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, "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 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() == 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);
|
||||
Print("ERR CAMERA ORBIT: native action/frame regression; physical mouse delivery and multiplayer are not tested");
|
||||
return true;
|
||||
@@ -193,6 +205,9 @@ modded class ERR_GameMode
|
||||
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);
|
||||
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;
|
||||
}
|
||||
SCR_CharacterControllerComponent character = SCR_CharacterControllerComponent.Cast(m_ERRCameraBody.FindComponent(SCR_CharacterControllerComponent));
|
||||
@@ -209,22 +224,24 @@ modded class ERR_GameMode
|
||||
m_ERRCameraBody = pc.GetControlledEntity();
|
||||
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;
|
||||
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 (!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.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(!RecoveryStatus().IsEmpty() && RecoveryStatus().Contains("Treatment or natural recovery") && pc.ERR_ProbeAcceptedRecoveryStatus() == RecoveryStatus(), "accepted observer hint contains the current ongoing recovery status")) return;
|
||||
if (!ERR_CameraNoTargetOrbit(pc)) return;
|
||||
string expectedRecovery = RecoveryStatus() + "\n" + pc.ERR_SelfEpinephrineHint();
|
||||
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;
|
||||
}
|
||||
if (m_ERRCameraStage == 11)
|
||||
{
|
||||
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;
|
||||
m_ERRCameraStage = 111; m_ERRCameraWait = now + 500; return;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
protected ref ERR_AirReconSystem m_Recon;
|
||||
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_AirReconSystem Recon() { return m_Recon; }
|
||||
ERR_ScoutSystem Scouts() { return m_Scouts; }
|
||||
@@ -180,6 +182,7 @@ class ERR_HQ : ERR_CommandService
|
||||
m_Scouts = new ERR_ScoutSystem(mode);
|
||||
m_Recon = new ERR_AirReconSystem(mode);
|
||||
m_PlaneRecon = new ERR_PlaneRecon(mode);
|
||||
m_DroneRecon = new ERR_DroneRecon(mode);
|
||||
m_AirAssault = new ERR_AirAssaultSystem(this, mode);
|
||||
m_Airfield = new ERR_AirfieldSupport(this, mode); m_Recovery = new ERR_BodyRecovery(this, mode);
|
||||
bool legacyFixture;
|
||||
@@ -504,6 +507,24 @@ class ERR_HQ : ERR_CommandService
|
||||
}
|
||||
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)
|
||||
{
|
||||
contacts.Clear();
|
||||
@@ -537,10 +558,11 @@ class ERR_HQ : ERR_CommandService
|
||||
ERR_HQJournal Journal() { return m_Journal; }
|
||||
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())
|
||||
+ "\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();
|
||||
return status + "\n" + m_DroneRecon.Status();
|
||||
}
|
||||
|
||||
protected void BindPhysical(ERR_HQUnit unit)
|
||||
@@ -1746,6 +1768,7 @@ class ERR_HQ : ERR_CommandService
|
||||
// Aircraft already airborne keep flying even when the server is empty.
|
||||
m_Recon.Tick(now);
|
||||
m_PlaneRecon.Tick(now);
|
||||
m_DroneRecon.Tick(now);
|
||||
m_AirAssault.Maintain(now);
|
||||
m_Airfield.Maintain(now);
|
||||
}
|
||||
@@ -1880,6 +1903,7 @@ class ERR_HQ : ERR_CommandService
|
||||
m_AirAssault.Stop();
|
||||
m_Recon.Stop();
|
||||
m_PlaneRecon.Stop();
|
||||
m_DroneRecon.Stop();
|
||||
m_Scouts.Stop();
|
||||
m_ConvoyStartup.Stop();
|
||||
if (m_SupplyConvoys) m_SupplyConvoys.Stop();
|
||||
|
||||
@@ -51,6 +51,7 @@ class ERR_HQReport
|
||||
bool HasVelocity;
|
||||
bool Engaged;
|
||||
bool Distress;
|
||||
bool DroneObservation;
|
||||
int SeenEscapists = -1;
|
||||
string TravelMode = "unknown";
|
||||
string RoadStatus = "road unknown";
|
||||
@@ -72,6 +73,7 @@ class ERR_HQReport
|
||||
copy.ObservedAt = ObservedAt;
|
||||
copy.Engaged = Engaged;
|
||||
copy.Distress = Distress;
|
||||
copy.DroneObservation = DroneObservation;
|
||||
copy.HasVelocity = HasVelocity;
|
||||
copy.SeenEscapists = SeenEscapists;
|
||||
copy.TravelMode = TravelMode;
|
||||
|
||||
@@ -14,6 +14,20 @@ class ERR_VehicleBehaviorCleanup
|
||||
|
||||
// Preserve original hunter actors. Reboarding uses native AI tasks, never new
|
||||
// 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
|
||||
{
|
||||
protected ERR_HunterVehicle m_Unit;
|
||||
@@ -22,6 +36,7 @@ class ERR_HunterCrewRecovery
|
||||
protected float m_NextAttempt;
|
||||
protected string m_LastReason;
|
||||
protected bool m_Stopped;
|
||||
ref ERR_HunterRecoveryWait Wait = new ERR_HunterRecoveryWait();
|
||||
string Reason;
|
||||
void ERR_HunterCrewRecovery(ERR_HunterVehicle unit) { m_Unit = unit; }
|
||||
void SuspendBoarding()
|
||||
|
||||
@@ -81,6 +81,7 @@ class ERR_HunterVehicle : ERR_ConvoyVehicle
|
||||
bool DeliveredContact;
|
||||
ref ERR_HunterCrewRecovery Recovery;
|
||||
ref ERR_HunterTactics Tactics;
|
||||
ref ERR_HunterApproachMemory FailedApproaches = new ERR_HunterApproachMemory();
|
||||
ref ERR_HunterCloseContact CloseContact;
|
||||
bool CloseContactHeld;
|
||||
bool CombatHeld;
|
||||
@@ -770,10 +771,22 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
// A delivered manoeuvre owns dismount/reboarding. The ordinary recovery
|
||||
// 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.Tick(m_Mode, now)) continue;
|
||||
if (unit.Tactics && unit.Tactics.Tick(hq, now)) continue;
|
||||
if (unit.CloseContact.Tick(m_Mode, now))
|
||||
{ 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);
|
||||
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.Registered)
|
||||
{
|
||||
@@ -860,7 +873,7 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
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());
|
||||
}
|
||||
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;
|
||||
// Stop reboarding/tactics before detaching the vehicle. Preserve the original
|
||||
@@ -878,7 +891,7 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
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));
|
||||
}
|
||||
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()
|
||||
{
|
||||
@@ -902,6 +915,7 @@ modded class SCR_CompartmentAccessComponent
|
||||
if (mode) hq = ERR_HQ.Cast(mode.Command());
|
||||
if (hq && hq.Hunters()) hq.Hunters().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);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
// A bounded response by original hunter crews. Enemy positions are immutable
|
||||
// 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
|
||||
{
|
||||
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)
|
||||
{
|
||||
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();
|
||||
if (!utility) return false;
|
||||
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||
float currentRange = vector.DistanceXZ(Unit.Entity.GetOrigin(), Attack.ReportPosition);
|
||||
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
|
||||
// to send the opposite-side truck around the entire fight before dismount.
|
||||
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;
|
||||
vector seed = desired - approachForward * (attempt * 15) + approachLeft * lateral;
|
||||
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;
|
||||
Stage = point; found = true; break;
|
||||
}
|
||||
@@ -359,7 +397,7 @@ class ERR_HunterTactics
|
||||
vector point = desired;
|
||||
if (offroad > 0) point += Vector(Math.RandomFloat(-90, 90), 0, Math.RandomFloat(-90, 90));
|
||||
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 (Role == 0 && !ERR_MountedCover.ClearLane(Unit.Entity, point, Attack.ReportPosition)) continue;
|
||||
Stage = point; found = true;
|
||||
@@ -371,7 +409,7 @@ class ERR_HunterTactics
|
||||
vector parked = Unit.Entity.GetOrigin();
|
||||
float range = vector.DistanceXZ(parked, Attack.ReportPosition);
|
||||
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 (!found) { Note("no separated safe approach; keeping current orders"); return false; }
|
||||
@@ -410,6 +448,7 @@ class ERR_HunterTactics
|
||||
}
|
||||
protected bool AlternateStage(float now)
|
||||
{
|
||||
RejectStage(now);
|
||||
// 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.
|
||||
array<vector> occupied = {};
|
||||
@@ -426,9 +465,13 @@ class ERR_HunterTactics
|
||||
Note("alternate approach assigned=" + Stage.ToString());
|
||||
return true;
|
||||
}
|
||||
protected void RejectStage(float now)
|
||||
{
|
||||
Unit.FailedApproaches.Reject(Stage, 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++;
|
||||
bool assigned;
|
||||
if (m_ApproachRetries == 1) assigned = Move(Stage, 35);
|
||||
@@ -444,7 +487,7 @@ class ERR_HunterTactics
|
||||
if (VisualCover()) { m_BlindSince = -1; return true; }
|
||||
if (m_BlindSince < 0) m_BlindSince = now;
|
||||
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;
|
||||
Note("cover position has no visual contact; looking for another firing angle");
|
||||
AlternateStage(now);
|
||||
@@ -500,7 +543,7 @@ class ERR_HunterTactics
|
||||
{
|
||||
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"); }
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class ERR_MortarSystem
|
||||
protected bool CreateBattery(vector authoredPosition, int id)
|
||||
{
|
||||
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);
|
||||
SCR_AIGroup group = SCR_AIGroup.Cast(ERR_Actors.Spawn(MortarGroup(), ERR_Actors.Ground(position + "0 0 4")));
|
||||
if (!gun || !group)
|
||||
|
||||
@@ -62,10 +62,24 @@ class ERR_RecommendedAddons
|
||||
static void Snapshot(ERR_GameMode mode, array<string> values)
|
||||
{
|
||||
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, "weapons", loaded, {"BFC890A6F1603EC0"}, {"Phaseline Marksman Rifles"}, mode.Settings().Get("automaticEquipmentChancePercent") != 0);
|
||||
ERR_HQ hq = ERR_HQ.Cast(mode.Command());
|
||||
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
|
||||
{
|
||||
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 vector m_Pickup;
|
||||
protected float m_RescueDeadline, m_NextThreat;
|
||||
@@ -168,6 +209,7 @@ class ERR_RescueFlightComponent : ERR_ReconFlightComponent
|
||||
}
|
||||
override void EOnSimulate(IEntity owner, float timeSlice)
|
||||
{
|
||||
if (m_ResultsHeld) { ApplyResultsHold(); return; }
|
||||
// Initial airborne crew allocation has a fixed 45-second deadline.
|
||||
if (Replication.IsServer() && m_Initialized && m_Phase == ERR_ReconPhase.CREW)
|
||||
{
|
||||
|
||||
@@ -4,7 +4,22 @@ class ERR_RoadblockRules
|
||||
{
|
||||
// Admission never waits longer for a high count. Remaining posts are built
|
||||
// by the same safe placement path in background, including after prison unlock.
|
||||
static int PropCount() { return 18; }
|
||||
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 SearchAttempts() { return 300; }
|
||||
static bool Road(float width, float length, float heightDifference)
|
||||
@@ -51,6 +66,8 @@ class ERR_Roadblocks
|
||||
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 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 AIWaypoint m_BuildHold;
|
||||
protected ref array<IEntity> m_BuildSpawned = {};
|
||||
@@ -61,10 +78,7 @@ class ERR_Roadblocks
|
||||
protected ref array<ref Resource> m_Loaded = {};
|
||||
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 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 SHELTER = "{01AE5FD77A9A4C21}Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et";
|
||||
static const ResourceName TABLE = "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et";
|
||||
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); }
|
||||
bool Contains(SCR_AIGroup group) { return group && m_Groups.Contains(group); }
|
||||
@@ -75,7 +89,7 @@ class ERR_Roadblocks
|
||||
if (m_Ready && m_Positions.Count() < m_Target) progress = "placement stopped; see server log";
|
||||
if (m_Background) progress = "background placement pending";
|
||||
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)
|
||||
{
|
||||
@@ -93,7 +107,7 @@ class ERR_Roadblocks
|
||||
if (profile && profile.IsValid()) source = profile.GetResource().ToEntitySource();
|
||||
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.
|
||||
array<ResourceName> resources = {COVER, BARRIER, OBSTACLE, SIGN, SHELTER, TABLE, "{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 prefab : resources)
|
||||
{
|
||||
@@ -122,11 +136,15 @@ class ERR_Roadblocks
|
||||
if (i > 0) length += vector.DistanceXZ(points[i - 1], points[i]);
|
||||
}
|
||||
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;
|
||||
if (!ERR_ConvoyRoadPlan.Sample(points, distance, centre, m_Forward)
|
||||
|| !ERR_ConvoyRoadPlan.Sample(points, distance - 15, before, beforeDirection)
|
||||
|| !ERR_ConvoyRoadPlan.Sample(points, distance + 15, after, afterDirection)) return false;
|
||||
|| !ERR_ConvoyRoadPlan.Sample(points, distance - margin, before, beforeDirection)
|
||||
|| !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;
|
||||
m_Centre = ERR_Actors.Ground(centre);
|
||||
if (!ERR_RoadblockRules.Road(road.GetWidth(), length, m_Centre[1] - centre[1])) return false;
|
||||
@@ -142,7 +160,7 @@ class ERR_Roadblocks
|
||||
m_Right = Vector(m_Forward[2], 0, -m_Forward[0]);
|
||||
// Protected guard positions sit on the shoulders; approach barriers narrow
|
||||
// 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();
|
||||
return true;
|
||||
}
|
||||
@@ -187,24 +205,30 @@ class ERR_Roadblocks
|
||||
protected void Layout(out array<ref ERR_RoadblockProp> covers)
|
||||
{
|
||||
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.
|
||||
for (int side = -1; side <= 1; side += 2)
|
||||
{
|
||||
// Two front walls, an approach wall and an outer return form
|
||||
// a sheltered bay for each pair. The rear stays open for guard access.
|
||||
// A short sandbag screen protects each pair without a wide outer bay.
|
||||
AddProp(covers, COVER, side * m_Shoulder, -2, 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 * 4.8, m_Forward, 1.9, 0.7, 1);
|
||||
AddProp(covers, COVER, side * (m_Shoulder + 5.6), side * 2, m_Right, 1.9, 0.7, 1);
|
||||
// Paired stop-line barriers funnel traffic into a >=4m centre passage.
|
||||
AddProp(covers, BARRIER, side * 3.3, side * 8, m_Forward, 1.2, 0.4, 1.3);
|
||||
AddProp(covers, BARRIER, side * 5.9, side * 8, m_Forward, 1.2, 0.4, 1.3);
|
||||
AddProp(covers, OBSTACLE, side * (m_Shoulder + 4.5), side * 8, m_Forward, 1, 1, 1.6);
|
||||
AddProp(covers, SIGN, side * 3, side * 12, m_Forward * (side * 1.0), 0.45, 0.4, 0.8);
|
||||
// One barrier and sign per approach preserve a >=4m centre passage.
|
||||
AddProp(covers, BARRIER, side * 3.3, side * 6, m_Forward, 1.2, 0.4, 1.3);
|
||||
AddProp(covers, SIGN, side * 3, side * 8.5, m_Forward * (side * 1.0), 0.45, 0.4, 0.8);
|
||||
}
|
||||
// A small inspection/rest area behind one bay, away from the vehicle lane.
|
||||
AddProp(covers, TABLE, m_Shoulder + 3, -4.8, m_Forward, 0.9, 0.7, 1.1);
|
||||
AddProp(covers, SHELTER, m_Shoulder + 3, -8.5, m_Forward, 1.2, 2.4, 1.4);
|
||||
}
|
||||
|
||||
protected void AddProp(array<ref ERR_RoadblockProp> covers, ResourceName prefab, float across, float along, vector forward, float halfWidth, float halfDepth, float height)
|
||||
@@ -220,7 +244,7 @@ class ERR_Roadblocks
|
||||
// Start a transaction; physical pieces are created separately in BuildPost.
|
||||
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() != ERR_RoadblockRules.PropCount()) 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));
|
||||
if (!m_BuildGroup) return false;
|
||||
m_BuildGroup.SetFaction(m_Mode.EnemyFaction()); m_BuildGroup.SetSpawnImmediately(false); m_BuildGroup.SetDeleteWhenEmpty(false); m_BuildGroup.SetPermanentLOD(0);
|
||||
@@ -236,12 +260,12 @@ class ERR_Roadblocks
|
||||
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 (<=540 props) on
|
||||
// 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_BuildSpawned.Count() != m_BuildPositions.Count() + m_BuildCovers.Count() + 1) return false;
|
||||
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];
|
||||
@@ -294,18 +318,25 @@ class ERR_Roadblocks
|
||||
CancelPost(); return;
|
||||
}
|
||||
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; }
|
||||
foreach (IEntity ownedEntity : m_BuildSpawned) m_Mode.Own(ownedEntity);
|
||||
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_Mode.RegisterGroup(m_BuildGroup); m_BuildGroup.ActivateAI();
|
||||
m_Mining.Register(m_BuildGroup, m_BuildHold, m_Right);
|
||||
PrintFormat("Reintroduced [roadblocks]: %1 at %2 guards=4 props=%3 networkedProps=%3 propStreaming=always roadPassage=open HQ=registered", id, m_Centre, m_BuildCovers.Count());
|
||||
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_BuildPositions.Clear(); m_BuildCovers.Clear();
|
||||
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.
|
||||
protected int Plan(out array<vector> positions, out array<ref ERR_RoadblockProp> covers)
|
||||
{
|
||||
@@ -318,9 +349,9 @@ class ERR_Roadblocks
|
||||
map<int, vector> reserved = new map<int, vector>();
|
||||
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; }
|
||||
vector reachable;
|
||||
if (!mesh.GetReachablePoint(point, 1, reachable) || vector.DistanceXZ(point, reachable) > 1 || !m_Placement.Clear(point, reserved)) return 0;
|
||||
@@ -335,7 +366,10 @@ class ERR_Roadblocks
|
||||
if (m_Ready) return ERR_PrepareResult.READY;
|
||||
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");
|
||||
#ifdef WORKBENCH
|
||||
// The legacy convoy/hq-memory fixtures use the original 60-body allowance and six-squad layout.
|
||||
|
||||
@@ -13,6 +13,9 @@ class ERR_IntelAdminProvider : ERR_AdminProvider
|
||||
if (arguments == "intel show")
|
||||
{
|
||||
replies.Insert("aiIntelChancePercent=" + mode.Settings().Get("aiIntelChancePercent").ToString());
|
||||
replies.Insert("radioIntelSiteCount=" + mode.Settings().Get("radioIntelSiteCount").ToString() + " (next round)");
|
||||
if (mode.Discovery()) replies.Insert(mode.Discovery().RadioIntelStatus());
|
||||
replies.Insert("showAllSites=" + mode.Settings().Get("showAllSites").ToString());
|
||||
replies.Insert("intelRevealChancePercent=" + mode.Settings().Get("intelRevealChancePercent").ToString());
|
||||
return true;
|
||||
}
|
||||
@@ -30,6 +33,8 @@ class ERR_IntelAdminProvider : ERR_AdminProvider
|
||||
override bool Help(string topic, array<string> replies)
|
||||
{
|
||||
if (topic != "intel") return false;
|
||||
replies.Insert("/esc intel radiosites show|set N|help (0-20; 0 disables); non-radio sites required before radio intel, next round.");
|
||||
replies.Insert("/esc intel sites show|set on|off; show anonymous site positions and let intel identify their types.");
|
||||
replies.Insert("/esc intel show | intel carrying show|set N (0-100; alias: intel chance)");
|
||||
replies.Insert("Carrying changes affect newly registered AI. /esc intel reroll applies to eligible living AI; intel reroll show reports progress.");
|
||||
replies.Insert("/esc intel reveal show|set N|help (0-100); newly recovered documents change, known locations and spent documents remain unchanged.");
|
||||
|
||||
@@ -7,6 +7,7 @@ modded class ERR_GameMode
|
||||
protected float m_ERRIntelMessageAt;
|
||||
protected float m_ERRIntelExhaustedAt;
|
||||
protected bool m_ERRIntelDuplicateReported, m_ERRIntelExhaustionReported;
|
||||
protected bool m_ERRRadioIntelWaitingReported;
|
||||
protected bool m_ERRIntelFeedbackStopped;
|
||||
protected int m_ERRIntelMessageSequence;
|
||||
protected int m_ERRIntelReceivedSequence;
|
||||
@@ -62,6 +63,15 @@ modded class ERR_GameMode
|
||||
m_ERRIntelReceivedSequence = sequence;
|
||||
SCR_ChatComponent.RadioProtocolMessage(message);
|
||||
}
|
||||
void ERR_RadioIntelWaiting()
|
||||
{
|
||||
if (!Replication.IsServer() || m_ERRIntelFeedbackStopped || !Active() || m_ERRRadioIntelWaitingReported) return;
|
||||
m_ERRRadioIntelWaitingReported = true;
|
||||
string message = "Keep this radio document. Identify more non-radio sites to decipher it; it will be read automatically when ready.";
|
||||
m_ERRIntelMessageSequence++;
|
||||
ERR_ReceiveDiscoveryFeedback(m_ERRIntelMessageSequence, message);
|
||||
Rpc(ERR_ReceiveDiscoveryFeedback, m_ERRIntelMessageSequence, message);
|
||||
}
|
||||
void ERR_IntelExhausted(bool duplicate = false)
|
||||
{
|
||||
if (!Replication.IsServer() || m_ERRIntelFeedbackStopped || !Active()) return;
|
||||
|
||||
@@ -48,16 +48,18 @@ class ERR_IntelEvidence
|
||||
}
|
||||
// Spread new folders over unfinished sites. Counts persist after recovery or
|
||||
// retirement so extra camps/AI do not repeatedly roll the same early targets.
|
||||
// Existing sealed contents are never rewritten when team knowledge changes.
|
||||
// Only recovery of a progression-locked radio folder can redirect its contents.
|
||||
protected string NextTarget()
|
||||
{
|
||||
array<ERR_LocationComponent> locations = {};
|
||||
ERR_GameMode.Get().Locations(locations);
|
||||
array<ERR_LocationComponent> candidates = {};
|
||||
int least = -1;
|
||||
bool radioAllowed = ERR_GameMode.Get().Discovery() && ERR_GameMode.Get().Discovery().RadioIntelAllowed();
|
||||
foreach (ERR_LocationComponent location : locations)
|
||||
{
|
||||
if (!location || location.Knowledge() >= 2 || location.Kind() == ERR_LocationKind.PRISON || location.Kind() == ERR_LocationKind.EXTRACTION) continue;
|
||||
if (!radioAllowed && location.Kind() == ERR_LocationKind.RADIO) continue;
|
||||
int assigned = 0;
|
||||
m_TargetAssignments.Find(location.Id(), assigned);
|
||||
if (least < 0 || assigned < least) { least = assigned; candidates.Clear(); }
|
||||
@@ -99,6 +101,20 @@ class ERR_IntelEvidence
|
||||
foreach (ERR_IssuedIntel issued : record.Issued) if (issued.Folder == folder) return issued.SiteId;
|
||||
return "";
|
||||
}
|
||||
// Update physical document bookkeeping only. Inspect() has already copied
|
||||
// any queued report into its own Missing list; delivered HQ reports stay intact.
|
||||
void Retarget(IEntity folder, string target)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped || !folder || target.IsEmpty()) return;
|
||||
bool changed;
|
||||
foreach (ERR_BodyEvidence record : m_Roster)
|
||||
foreach (ERR_IssuedIntel issued : record.Issued)
|
||||
if (issued.Folder == folder && issued.SiteId != target) { issued.SiteId = target; changed = true; }
|
||||
if (!changed) return;
|
||||
int assigned;
|
||||
m_TargetAssignments.Find(target, assigned);
|
||||
m_TargetAssignments.Set(target, assigned + 1);
|
||||
}
|
||||
void Tick(float now)
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
|
||||
@@ -14,24 +14,30 @@ ImageWidgetClass { "Is Visible" 1 Name "Selected_Main_Mission" Slot FrameWidgetS
|
||||
FrameWidgetClass { "Is Visible" 1 Name "Sidebar_Mission" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 166 OffsetTop 166 SizeX 180 OffsetRight -208 SizeY 480 OffsetBottom -646 } {
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenSettings" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 0 OffsetTop 0 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -42 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "General" } } }
|
||||
ImageWidgetClass { "Is Visible" 1 Name "Selected_OpenSettings" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 44 OffsetTop 44 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -47 } Color 0.9 0.66 0.25 1 }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenWorld" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 54 OffsetTop 54 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -96 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "World" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenWorld" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 98 OffsetTop 98 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -101 } Color 0.9 0.66 0.25 1 }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenTerrains" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 108 OffsetTop 108 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -150 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Terrains" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenTerrains" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 152 OffsetTop 152 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -155 } Color 0.9 0.66 0.25 1 }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenVehicles" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 54 OffsetTop 54 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -96 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Civilian vehicles" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenVehicles" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 98 OffsetTop 98 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -101 } Color 0.9 0.66 0.25 1 }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenWorld" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 108 OffsetTop 108 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -150 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "World" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenWorld" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 152 OffsetTop 152 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -155 } Color 0.9 0.66 0.25 1 }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenTerrains" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 162 OffsetTop 162 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -204 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Terrains" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenTerrains" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 206 OffsetTop 206 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -209 } Color 0.9 0.66 0.25 1 }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenTelemetry" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 216 OffsetTop 216 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -258 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Statistics" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenTelemetry" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 260 OffsetTop 260 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -263 } Color 0.9 0.66 0.25 1 }
|
||||
} }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Main_Forces" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 174 OffsetLeft 174 PositionY 106 OffsetTop 106 SizeX 138 OffsetRight -312 SizeY 42 OffsetBottom -148 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Forces" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_Main_Forces" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 174 OffsetLeft 174 PositionY 150 OffsetTop 150 SizeX 138 OffsetRight -312 SizeY 3 OffsetBottom -153 } Color 0.9 0.66 0.25 1 }
|
||||
FrameWidgetClass { "Is Visible" 0 Name "Sidebar_Forces" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 166 OffsetTop 166 SizeX 180 OffsetRight -208 SizeY 480 OffsetBottom -646 } {
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenForces" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 0 OffsetTop 0 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -42 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Patrols" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenForces" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 44 OffsetTop 44 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -47 } Color 0.9 0.66 0.25 1 }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenFactions" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 54 OffsetTop 54 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -96 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Factions" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenFactions" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 98 OffsetTop 98 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -101 } Color 0.9 0.66 0.25 1 }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenGuards" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 108 OffsetTop 108 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -150 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Site guards" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenGuards" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 152 OffsetTop 152 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -155 } Color 0.9 0.66 0.25 1 }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenSites" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 162 OffsetTop 162 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -204 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Field sites" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenSites" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 206 OffsetTop 206 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -209 } Color 0.9 0.66 0.25 1 }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenSarka" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 216 OffsetTop 216 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -258 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Šárka hunters" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenSarka" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 260 OffsetTop 260 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -263 } Color 0.9 0.66 0.25 1 }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenCheckpoints" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 54 OffsetTop 54 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -96 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Checkpoints" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenCheckpoints" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 98 OffsetTop 98 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -101 } Color 0.9 0.66 0.25 1 }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenFactions" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 108 OffsetTop 108 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -150 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Factions" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenFactions" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 152 OffsetTop 152 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -155 } Color 0.9 0.66 0.25 1 }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenGuards" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 162 OffsetTop 162 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -204 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Site guards" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenGuards" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 206 OffsetTop 206 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -209 } Color 0.9 0.66 0.25 1 }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenSites" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 216 OffsetTop 216 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -258 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Field sites" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenSites" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 260 OffsetTop 260 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -263 } Color 0.9 0.66 0.25 1 }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenSarka" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 270 OffsetTop 270 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -312 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Šárka hunters" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenSarka" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 314 OffsetTop 314 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -317 } Color 0.9 0.66 0.25 1 }
|
||||
} }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Main_HQ" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 320 OffsetLeft 320 PositionY 106 OffsetTop 106 SizeX 138 OffsetRight -458 SizeY 42 OffsetBottom -148 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "HQ" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_Main_HQ" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 320 OffsetLeft 320 PositionY 150 OffsetTop 150 SizeX 138 OffsetRight -458 SizeY 3 OffsetBottom -153 } Color 0.9 0.66 0.25 1 }
|
||||
@@ -42,12 +48,16 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenAI" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 98 OffsetTop 98 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -101 } Color 0.9 0.66 0.25 1 }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenPlane" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 108 OffsetTop 108 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -150 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Recon plane" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenPlane" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 152 OffsetTop 152 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -155 } Color 0.9 0.66 0.25 1 }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenDrones" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 162 OffsetTop 162 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -204 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Recon drones" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenDrones" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 206 OffsetTop 206 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -209 } Color 0.9 0.66 0.25 1 }
|
||||
} }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Main_Intel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 466 OffsetLeft 466 PositionY 106 OffsetTop 106 SizeX 138 OffsetRight -604 SizeY 42 OffsetBottom -148 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Intel" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_Main_Intel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 466 OffsetLeft 466 PositionY 150 OffsetTop 150 SizeX 138 OffsetRight -604 SizeY 3 OffsetBottom -153 } Color 0.9 0.66 0.25 1 }
|
||||
FrameWidgetClass { "Is Visible" 0 Name "Sidebar_Intel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 166 OffsetTop 166 SizeX 180 OffsetRight -208 SizeY 480 OffsetBottom -646 } {
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenIntel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 0 OffsetTop 0 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -42 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Intelligence" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenIntel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 44 OffsetTop 44 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -47 } Color 0.9 0.66 0.25 1 }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenRadioIntel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 54 OffsetTop 54 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -96 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Radio intel" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenRadioIntel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 98 OffsetTop 98 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -101 } Color 0.9 0.66 0.25 1 }
|
||||
} }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Main_Items" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 612 OffsetLeft 612 PositionY 106 OffsetTop 106 SizeX 138 OffsetRight -750 SizeY 42 OffsetBottom -148 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Items" } } }
|
||||
ImageWidgetClass { "Is Visible" 0 Name "Selected_Main_Items" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 612 OffsetLeft 612 PositionY 150 OffsetTop 150 SizeX 138 OffsetRight -750 SizeY 3 OffsetBottom -153 } Color 0.9 0.66 0.25 1 }
|
||||
@@ -115,11 +125,6 @@ TextWidgetClass { Name "Saved_keepWeaponOnUnconscious" Slot FrameWidgetSlot { An
|
||||
TextWidgetClass { Name "SelfEpiLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 332 OffsetTop 332 SizeX 240 OffsetRight -268 SizeY 28 OffsetBottom -360 } Color 0.9 0.92 0.94 1 Text "Self epinephrine (Space)" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "SelfEpiOn" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 280 OffsetLeft 280 PositionY 332 OffsetTop 332 SizeX 90 OffsetRight -370 SizeY 28 OffsetBottom -360 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Enabled" } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "SelfEpiOff" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 382 OffsetLeft 382 PositionY 332 OffsetTop 332 SizeX 90 OffsetRight -472 SizeY 28 OffsetBottom -360 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Disabled" } } }
|
||||
TextWidgetClass { Name "Label_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 244 OffsetTop 244 SizeX 456 OffsetRight -964 SizeY 22 OffsetBottom -266 } Color 0.9 0.92 0.94 1 Text "Civilian traffic (0-6 cars; 0 off)" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 268 OffsetTop 268 SizeX 180 OffsetRight -688 SizeY 32 OffsetBottom -300 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 700 OffsetLeft 700 PositionY 268 OffsetTop 268 SizeX 100 OffsetRight -800 SizeY 32 OffsetBottom -300 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
|
||||
TextWidgetClass { Name "Saved_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 304 OffsetTop 304 SizeX 456 OffsetRight -964 SizeY 28 OffsetBottom -332 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Help_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 332 OffsetTop 332 SizeX 456 OffsetRight -964 SizeY 28 OffsetBottom -360 } Color 0.9 0.92 0.94 1 Text "0 disables new traffic. Player-used cars are kept." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Label_restartOnEmpty" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 364 OffsetTop 364 SizeX 456 OffsetRight -484 SizeY 22 OffsetBottom -386 } Color 0.9 0.92 0.94 1 Text "Restart after last disconnect" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "RestartEmptyOn" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 388 OffsetTop 388 SizeX 140 OffsetRight -168 SizeY 32 OffsetBottom -420 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Enabled" } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "RestartEmptyOff" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 180 OffsetLeft 180 PositionY 388 OffsetTop 388 SizeX 152 OffsetRight -332 SizeY 32 OffsetBottom -420 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Disabled" } } }
|
||||
@@ -131,6 +136,44 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
||||
TextWidgetClass { Name "Saved_emptyRestartHours" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 424 OffsetTop 424 SizeX 456 OffsetRight -964 SizeY 24 OffsetBottom -448 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Help_emptyRestartHours" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 452 OffsetTop 452 SizeX 456 OffsetRight -964 SizeY 28 OffsetBottom -480 } Color 0.9 0.92 0.94 1 Text "0 off; 1-168 real hours empty. Default 5. Applies now." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
} }
|
||||
FrameWidgetClass { "Is Visible" 0 Name "VehiclesPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1220 SizeY 486 OffsetBottom -646 } {
|
||||
TextWidgetClass { Name "Label_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 4 OffsetTop 4 SizeX 456 OffsetRight -484 SizeY 22 OffsetBottom -26 } Color 0.9 0.92 0.94 1 Text "Civilian traffic (0-6 cars; 0 off)" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 28 OffsetTop 28 SizeX 180 OffsetRight -208 SizeY 32 OffsetBottom -60 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 28 OffsetTop 28 SizeX 100 OffsetRight -320 SizeY 32 OffsetBottom -60 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
|
||||
TextWidgetClass { Name "Saved_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 64 OffsetTop 64 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -92 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Help_civilianTrafficCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 92 OffsetTop 92 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -120 } Color 0.9 0.92 0.94 1 Text "0 disables new traffic. Player-used cars are kept." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Label_garageVehicleCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 4 OffsetTop 4 SizeX 456 OffsetRight -964 SizeY 22 OffsetBottom -26 } Color 0.9 0.92 0.94 1 Text "Nearby garage vehicles (0-24; 0 off)" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "garageVehicleCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 28 OffsetTop 28 SizeX 180 OffsetRight -688 SizeY 32 OffsetBottom -60 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_garageVehicleCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 700 OffsetLeft 700 PositionY 28 OffsetTop 28 SizeX 100 OffsetRight -800 SizeY 32 OffsetBottom -60 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
|
||||
TextWidgetClass { Name "Saved_garageVehicleCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 64 OffsetTop 64 SizeX 456 OffsetRight -964 SizeY 28 OffsetBottom -92 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Help_garageVehicleCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 92 OffsetTop 92 SizeX 456 OffsetRight -964 SizeY 28 OffsetBottom -120 } Color 0.9 0.92 0.94 1 Text "Default 16 unclaimed vehicles. Set 0 to disable; next round." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Label_garageChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 144 OffsetTop 144 SizeX 456 OffsetRight -484 SizeY 22 OffsetBottom -166 } Color 0.9 0.92 0.94 1 Text "Garage selection chance (0-100%)" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "garageChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 168 OffsetTop 168 SizeX 180 OffsetRight -208 SizeY 32 OffsetBottom -200 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_garageChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 168 OffsetTop 168 SizeX 100 OffsetRight -320 SizeY 32 OffsetBottom -200 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
|
||||
TextWidgetClass { Name "Saved_garageChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 204 OffsetTop 204 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -232 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Help_garageChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 232 OffsetTop 232 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -260 } Color 0.9 0.92 0.94 1 Text "Default 70%; 100 selects every suitable garage. Next round." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Label_garageRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 144 OffsetTop 144 SizeX 456 OffsetRight -964 SizeY 22 OffsetBottom -166 } Color 0.9 0.92 0.94 1 Text "Spawn radius (250-1500 metres)" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "garageRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 168 OffsetTop 168 SizeX 180 OffsetRight -688 SizeY 32 OffsetBottom -200 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_garageRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 700 OffsetLeft 700 PositionY 168 OffsetTop 168 SizeX 100 OffsetRight -800 SizeY 32 OffsetBottom -200 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
|
||||
TextWidgetClass { Name "Saved_garageRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 204 OffsetTop 204 SizeX 456 OffsetRight -964 SizeY 28 OffsetBottom -232 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Help_garageRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 232 OffsetTop 232 SizeX 456 OffsetRight -964 SizeY 28 OffsetBottom -260 } Color 0.9 0.92 0.94 1 Text "Distance around players; default 1000 m. Next round." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Label_garageSpawnIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 284 OffsetTop 284 SizeX 456 OffsetRight -484 SizeY 22 OffsetBottom -306 } Color 0.9 0.92 0.94 1 Text "Time between spawns (5-60 seconds)" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "garageSpawnIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 308 OffsetTop 308 SizeX 180 OffsetRight -208 SizeY 32 OffsetBottom -340 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_garageSpawnIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 308 OffsetTop 308 SizeX 100 OffsetRight -320 SizeY 32 OffsetBottom -340 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
|
||||
TextWidgetClass { Name "Saved_garageSpawnIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 344 OffsetTop 344 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -372 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Help_garageSpawnIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 372 OffsetTop 372 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -400 } Color 0.9 0.92 0.94 1 Text "Default 5 s. Increase to spread out spawning. Next round." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "GarageSettingsNote" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 284 OffsetTop 284 SizeX 456 OffsetRight -964 SizeY 110 OffsetBottom -394 } Color 0.9 0.92 0.94 1 Text "Garage settings apply next round. Used vehicles stay for the round. Empty garages are filled gradually when hidden from players." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
} }
|
||||
FrameWidgetClass { "Is Visible" 0 Name "TelemetryPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1220 SizeY 486 OffsetBottom -646 } {
|
||||
TextWidgetClass { Name "TelemetryHeading" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 10 OffsetTop 10 SizeX 936 OffsetRight -964 SizeY 32 OffsetBottom -42 } Color 0.9 0.92 0.94 1 Text "Mission statistics" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "TelemetryDescription" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 55 OffsetTop 55 SizeX 936 OffsetRight -964 SizeY 80 OffsetBottom -135 } Color 0.9 0.92 0.94 1 Text "Share mission starts, progress, results, terrain, loaded mods and player counts with the configured statistics service. Enabled by default." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "TelemetryOn" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 150 OffsetTop 150 SizeX 140 OffsetRight -168 SizeY 32 OffsetBottom -182 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Enabled" } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "TelemetryOff" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 180 OffsetLeft 180 PositionY 150 OffsetTop 150 SizeX 152 OffsetRight -332 SizeY 32 OffsetBottom -182 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Disabled" } } }
|
||||
TextWidgetClass { Name "Saved_telemetryEnabled" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 200 OffsetTop 200 SizeX 936 OffsetRight -964 SizeY 32 OffsetBottom -232 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "TelemetryStatus" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 232 OffsetTop 232 SizeX 936 OffsetRight -964 SizeY 26 OffsetBottom -258 } Color 0.9 0.92 0.94 1 Text "Delivery status unavailable" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "TelemetryPrivacy" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 278 OffsetTop 278 SizeX 936 OffsetRight -964 SizeY 78 OffsetBottom -356 } Color 0.9 0.92 0.94 1 Text "Player names and stable game account IDs are included to count participation and escapes across missions. Connected spectators are distinguished from participants and escapees." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "TelemetryDisableHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 360 OffsetTop 360 SizeX 936 OffsetRight -964 SizeY 80 OffsetBottom -440 } Color 0.9 0.92 0.94 1 Text "Disable to stop collection and sending, and discard unsent records. Already delivered statistics remain with the statistics service." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
} }
|
||||
FrameWidgetClass { "Is Visible" 0 Name "HQPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1220 SizeY 486 OffsetBottom -646 } {
|
||||
TextWidgetClass {
|
||||
Name "HQHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 510 OffsetRight -538 SizeY 50 OffsetBottom -56 }
|
||||
@@ -630,30 +673,30 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
||||
} }
|
||||
FrameWidgetClass { "Is Visible" 0 Name "IntelPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1220 SizeY 486 OffsetBottom -646 } {
|
||||
TextWidgetClass {
|
||||
Name "IntelHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
|
||||
Color 0.9 0.92 0.94 1 Text "Control how often soldiers carry intel and whether the first clue reveals an exact location." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
||||
Name "IntelHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 40 OffsetBottom -46 }
|
||||
Color 0.9 0.92 0.94 1 Text "Choose how sites appear on the map and how often soldiers carry intel." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||
}
|
||||
TextWidgetClass {
|
||||
Name "Label_aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 80 OffsetTop 80 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -108 }
|
||||
Name "Label_aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 150 OffsetTop 150 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -178 }
|
||||
Color 0.9 0.92 0.94 1 Text "AI carrying intel (0-100%)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
|
||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||
}
|
||||
}
|
||||
TextWidgetClass {
|
||||
Name "Saved_aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 112 OffsetTop 112 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -136 }
|
||||
Name "Saved_aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 194 OffsetTop 194 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -214 }
|
||||
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
|
||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||
}
|
||||
}
|
||||
TextWidgetClass {
|
||||
Name "Help_aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 136 OffsetTop 136 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -156 }
|
||||
Name "Help_aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 218 OffsetTop 218 SizeX 936 OffsetRight -964 SizeY 32 OffsetBottom -250 }
|
||||
Color 0.72 0.81 0.87 1 Text "Chance for each new enemy soldier to carry intel. Reassign carried intel updates existing soldiers." "Font Size" 16 "Min Font Size" 16 Wrap 1
|
||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||
}
|
||||
|
||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
||||
Name "aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 80 OffsetTop 80 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -122 }
|
||||
Name "aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 150 OffsetTop 150 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -192 }
|
||||
components {
|
||||
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
|
||||
m_sLabel ""
|
||||
@@ -661,7 +704,7 @@ ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
||||
}
|
||||
}
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||
Name "Apply_aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 80 OffsetTop 80 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -122 }
|
||||
Name "Apply_aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 150 OffsetTop 150 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -192 }
|
||||
components {
|
||||
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
||||
m_sText "Apply"
|
||||
@@ -669,7 +712,7 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
||||
}
|
||||
}
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||
Name "Random_aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 80 OffsetTop 80 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -122 }
|
||||
Name "Random_aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 150 OffsetTop 150 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -192 }
|
||||
components {
|
||||
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
||||
m_sText "Random"
|
||||
@@ -677,25 +720,25 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
||||
}
|
||||
}
|
||||
TextWidgetClass {
|
||||
Name "Label_intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 168 OffsetTop 168 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -196 }
|
||||
Name "Label_intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 252 OffsetTop 252 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -280 }
|
||||
Color 0.9 0.92 0.94 1 Text "First-document exact reveal (0-100%)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
|
||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||
}
|
||||
}
|
||||
TextWidgetClass {
|
||||
Name "Saved_intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 200 OffsetTop 200 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -224 }
|
||||
Name "Saved_intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 296 OffsetTop 296 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -316 }
|
||||
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
|
||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||
}
|
||||
}
|
||||
TextWidgetClass {
|
||||
Name "Help_intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 224 OffsetTop 224 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -244 }
|
||||
Color 0.72 0.81 0.87 1 Text "First clue: chance to reveal the exact site. Otherwise a question mark shows an approximate area." "Font Size" 16 "Min Font Size" 16 Wrap 1
|
||||
Name "Help_intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 320 OffsetTop 320 SizeX 936 OffsetRight -964 SizeY 36 OffsetBottom -356 }
|
||||
Color 0.72 0.81 0.87 1 Text "With all sites shown, intel always identifies the type. Otherwise this chance controls exact versus approximate clues." "Font Size" 16 "Min Font Size" 16 Wrap 1
|
||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||
}
|
||||
|
||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
||||
Name "intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 168 OffsetTop 168 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -210 }
|
||||
Name "intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 252 OffsetTop 252 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -294 }
|
||||
components {
|
||||
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
|
||||
m_sLabel ""
|
||||
@@ -703,7 +746,7 @@ ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
||||
}
|
||||
}
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||
Name "Apply_intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 168 OffsetTop 168 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -210 }
|
||||
Name "Apply_intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 252 OffsetTop 252 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -294 }
|
||||
components {
|
||||
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
||||
m_sText "Apply"
|
||||
@@ -711,7 +754,7 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
||||
}
|
||||
}
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||
Name "Random_intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 168 OffsetTop 168 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -210 }
|
||||
Name "Random_intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 252 OffsetTop 252 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -294 }
|
||||
components {
|
||||
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
||||
m_sText "Random"
|
||||
@@ -719,7 +762,7 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
||||
}
|
||||
}
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||
Name "IntelSummary" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 276 OffsetTop 276 SizeX 260 OffsetRight -288 SizeY 42 OffsetBottom -318 }
|
||||
Name "IntelSummary" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 364 OffsetTop 364 SizeX 260 OffsetRight -288 SizeY 42 OffsetBottom -406 }
|
||||
components {
|
||||
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
||||
m_sText "Intel summary"
|
||||
@@ -727,7 +770,7 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
||||
}
|
||||
}
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||
Name "RerollIntel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 306 OffsetLeft 306 PositionY 276 OffsetTop 276 SizeX 260 OffsetRight -566 SizeY 42 OffsetBottom -318 }
|
||||
Name "RerollIntel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 306 OffsetLeft 306 PositionY 364 OffsetTop 364 SizeX 260 OffsetRight -566 SizeY 42 OffsetBottom -406 }
|
||||
components {
|
||||
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
||||
m_sText "Reassign carried intel"
|
||||
@@ -735,15 +778,44 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
||||
}
|
||||
}
|
||||
TextWidgetClass {
|
||||
Name "IntelRerollHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 336 OffsetTop 336 SizeX 936 OffsetRight -964 SizeY 52 OffsetBottom -388 }
|
||||
Color 0.9 0.92 0.94 1 Text "Reassign carried intel uses the saved chance on existing AI. Intel already collected stays revealed." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
||||
Name "IntelRerollHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 420 OffsetTop 420 SizeX 936 OffsetRight -964 SizeY 26 OffsetBottom -446 }
|
||||
Color 0.9 0.92 0.94 1 Text "Reassign carried intel updates existing AI. Collected intel stays revealed." "Font Size" 16 "Min Font Size" 16 Wrap 1
|
||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||
}
|
||||
TextWidgetClass {
|
||||
Name "EvidenceHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 402 OffsetTop 402 SizeX 936 OffsetRight -964 SizeY 60 OffsetBottom -462 }
|
||||
Color 0.9 0.92 0.94 1 Text "Enemy bodies stay until a recovery team searches them and reports to HQ. Cleanup is then enabled with a five-minute delay." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
||||
Name "EvidenceHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 454 OffsetTop 454 SizeX 936 OffsetRight -964 SizeY 28 OffsetBottom -482 }
|
||||
Color 0.9 0.92 0.94 1 Text "Enemy bodies remain until searched and reported, then become eligible for cleanup after five minutes." "Font Size" 16 "Min Font Size" 16 Wrap 1
|
||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||
}
|
||||
TextWidgetClass {
|
||||
Name "Label_showAllSites" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 58 OffsetTop 58 SizeX 550 OffsetRight -578 SizeY 32 OffsetBottom -90 }
|
||||
Color 0.9 0.92 0.94 1 Text "Show all sites as question marks" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||
}
|
||||
TextWidgetClass {
|
||||
Name "Help_showAllSites" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 102 OffsetTop 102 SizeX 936 OffsetRight -964 SizeY 40 OffsetBottom -142 }
|
||||
Color 0.72 0.81 0.87 1 Text "Show prepared site positions without names or types. Intel identifies each site. Applies now for everyone; disabled by default." "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||
}
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||
Name "SitesOn" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 54 OffsetTop 54 SizeX 146 OffsetRight -806 SizeY 42 OffsetBottom -96 }
|
||||
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Enabled" } }
|
||||
}
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||
Name "SitesOff" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 818 OffsetLeft 818 PositionY 54 OffsetTop 54 SizeX 146 OffsetRight -964 SizeY 42 OffsetBottom -96 }
|
||||
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Disabled" } }
|
||||
}
|
||||
} }
|
||||
FrameWidgetClass { "Is Visible" 0 Name "RadioIntelPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1220 SizeY 486 OffsetBottom -646 } {
|
||||
TextWidgetClass { Name "RadioIntelHeading" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 710 OffsetRight -738 SizeY 32 OffsetBottom -38 } Color 0.9 0.92 0.94 1 Text "Radio intel progression" "Font Size" 22 "Min Font Size" 22 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
|
||||
TextWidgetClass { Name "RadioIntelHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 50 OffsetTop 50 SizeX 936 OffsetRight -964 SizeY 44 OffsetBottom -94 } Color 0.9 0.92 0.94 1 Text "Identify other sites before documents reveal radio stations. Finding and using a radio in person is always allowed." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Label_radioIntelSiteCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 112 OffsetTop 112 SizeX 590 OffsetRight -618 SizeY 32 OffsetBottom -144 } Color 0.9 0.92 0.94 1 Text "Sites required before radio intel (0-20)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "radioIntelSiteCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 640 OffsetLeft 640 PositionY 108 OffsetTop 108 SizeX 112 OffsetRight -752 SizeY 38 OffsetBottom -146 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_radioIntelSiteCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 764 OffsetLeft 764 PositionY 108 OffsetTop 108 SizeX 200 OffsetRight -964 SizeY 38 OffsetBottom -146 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
|
||||
TextWidgetClass { Name "Saved_radioIntelSiteCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 640 OffsetLeft 640 PositionY 148 OffsetTop 148 SizeX 324 OffsetRight -964 SizeY 24 OffsetBottom -172 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Help_radioIntelSiteCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 176 OffsetTop 176 SizeX 936 OffsetRight -964 SizeY 26 OffsetBottom -202 } Color 0.9 0.92 0.94 1 Text "Default 5. Set 0 to disable. Apply saves the requirement for the next round." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "RadioIntelSettingsHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 424 OffsetTop 424 SizeX 936 OffsetRight -964 SizeY 52 OffsetBottom -476 } Color 0.9 0.92 0.94 1 Text "Only fully identified non-radio sites count. Prison, extraction and automatic starting reveals are excluded. The requirement adjusts to available sites after preparation finishes." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "RadioIntelProgress" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 232 OffsetTop 232 SizeX 936 OffsetRight -964 SizeY 64 OffsetBottom -296 } Color 0.9 0.92 0.94 1 Text "Waiting for current-round progress" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "RadioIntelDocumentsHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 316 OffsetTop 316 SizeX 936 OffsetRight -964 SizeY 76 OffsetBottom -392 } Color 0.9 0.92 0.94 1 Text "Ordinary folders reveal other sites while radio intel is locked. Keep fixed radio documents: they will be read automatically after unlocking. Question marks and rough clues do not count as identified sites." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
} }
|
||||
FrameWidgetClass { "Is Visible" 0 Name "StatusPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1220 SizeY 486 OffsetBottom -646 } {
|
||||
TextWidgetClass {
|
||||
@@ -865,11 +937,6 @@ TextWidgetClass {
|
||||
Color 0.72 0.81 0.87 1 Text "0 disables convoys. Lost convoys stay lost. Next round." "Font Size" 14 "Min Font Size" 14 Wrap 1
|
||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||
}
|
||||
TextWidgetClass { Name "Label_roadblockCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 66 OffsetTop 66 SizeX 456 OffsetRight -964 SizeY 26 OffsetBottom -92 } Color 0.9 0.92 0.94 1 Text "Roadblocks (0-30; 0 off)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "roadblockCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 96 OffsetTop 96 SizeX 130 OffsetRight -638 SizeY 38 OffsetBottom -134 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_roadblockCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 650 OffsetLeft 650 PositionY 96 OffsetTop 96 SizeX 110 OffsetRight -760 SizeY 38 OffsetBottom -134 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
|
||||
TextWidgetClass { Name "Saved_roadblockCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 136 OffsetTop 136 SizeX 456 OffsetRight -964 SizeY 22 OffsetBottom -158 } Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Help_roadblockCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 160 OffsetTop 160 SizeX 456 OffsetRight -964 SizeY 32 OffsetBottom -192 } Color 0.9 0.92 0.94 1 Text "Four guards per post. Applies next round." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
|
||||
|
||||
TextWidgetClass { Name "GuardLinkLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 202 OffsetTop 202 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -230 } Color 0.9 0.92 0.94 1 Text "Building guards, camps and hospitals" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
@@ -965,6 +1032,24 @@ TextWidgetClass {
|
||||
}
|
||||
|
||||
|
||||
} }
|
||||
FrameWidgetClass { "Is Visible" 0 Name "CheckpointsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1220 SizeY 486 OffsetBottom -646 } {
|
||||
TextWidgetClass { Name "CheckpointsHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 } Color 0.9 0.92 0.94 1 Text "Set counts separately for small and main-road checkpoints. Changes apply next round. Road width determines the type; unsuitable locations are skipped." "Font Size" 19 "Min Font Size" 19 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Label_roadblockCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 66 OffsetTop 66 SizeX 456 OffsetRight -484 SizeY 26 OffsetBottom -92 } Color 0.9 0.92 0.94 1 Text "Small checkpoints (0-30; 0 off)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "roadblockCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 96 OffsetTop 96 SizeX 130 OffsetRight -158 SizeY 38 OffsetBottom -134 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_roadblockCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 170 OffsetLeft 170 PositionY 96 OffsetTop 96 SizeX 110 OffsetRight -280 SizeY 38 OffsetBottom -134 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
|
||||
TextWidgetClass { Name "Saved_roadblockCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 136 OffsetTop 136 SizeX 456 OffsetRight -484 SizeY 22 OffsetBottom -158 } Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Help_roadblockCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 160 OffsetTop 160 SizeX 456 OffsetRight -484 SizeY 32 OffsetBottom -192 } Color 0.9 0.92 0.94 1 Text "Four guards, compact cover. Below the main-road width." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Label_largeRoadblockCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 66 OffsetTop 66 SizeX 456 OffsetRight -964 SizeY 26 OffsetBottom -92 } Color 0.9 0.92 0.94 1 Text "Large checkpoints (0-30; 0 off)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "largeRoadblockCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 96 OffsetTop 96 SizeX 130 OffsetRight -638 SizeY 38 OffsetBottom -134 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_largeRoadblockCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 650 OffsetLeft 650 PositionY 96 OffsetTop 96 SizeX 110 OffsetRight -760 SizeY 38 OffsetBottom -134 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
|
||||
TextWidgetClass { Name "Saved_largeRoadblockCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 136 OffsetTop 136 SizeX 456 OffsetRight -964 SizeY 22 OffsetBottom -158 } Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Help_largeRoadblockCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 160 OffsetTop 160 SizeX 456 OffsetRight -964 SizeY 32 OffsetBottom -192 } Color 0.9 0.92 0.94 1 Text "Eight guards, extended cover and approach barriers." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Label_mainRoadWidthMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 202 OffsetTop 202 SizeX 456 OffsetRight -484 SizeY 26 OffsetBottom -228 } Color 0.9 0.92 0.94 1 Text "Main-road minimum width (5-12 m)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "mainRoadWidthMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 232 OffsetTop 232 SizeX 130 OffsetRight -158 SizeY 38 OffsetBottom -270 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_mainRoadWidthMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 170 OffsetLeft 170 PositionY 232 OffsetTop 232 SizeX 110 OffsetRight -280 SizeY 38 OffsetBottom -270 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
|
||||
TextWidgetClass { Name "Saved_mainRoadWidthMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 272 OffsetTop 272 SizeX 456 OffsetRight -484 SizeY 22 OffsetBottom -294 } Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Help_mainRoadWidthMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 296 OffsetTop 296 SizeX 456 OffsetRight -484 SizeY 32 OffsetBottom -328 } Color 0.9 0.92 0.94 1 Text "Default 6 m. Roads at or above this width use large posts." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
} }
|
||||
FrameWidgetClass { "Is Visible" 0 Name "GuardsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1220 SizeY 486 OffsetBottom -646 } {
|
||||
TextWidgetClass { Name "GuardsHeading" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 710 OffsetRight -738 SizeY 32 OffsetBottom -38 } Color 0.9 0.92 0.94 1 Text "Building guards - changes apply next round" "Font Size" 22 "Min Font Size" 22 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
@@ -1032,8 +1117,26 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
||||
TextWidgetClass { Name "Saved_planeSectorSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 480 OffsetLeft 480 PositionY 410 OffsetTop 410 SizeX 484 OffsetRight -964 SizeY 22 OffsetBottom -432 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Help_planeSectorSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 436 OffsetTop 436 SizeX 936 OffsetRight -964 SizeY 28 OffsetBottom -464 } Color 0.9 0.92 0.94 1 Text "Chooses another area at this interval, or sooner when it reaches its destination." "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
} }
|
||||
FrameWidgetClass { "Is Visible" 0 Name "DronesPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1220 SizeY 486 OffsetBottom -646 } {
|
||||
TextWidgetClass { Name "DronesHeading" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 710 OffsetRight -738 SizeY 32 OffsetBottom -38 } Color 0.9 0.92 0.94 1 Text "Enemy recon drones - changes apply next round" "Font Size" 22 "Min Font Size" 22 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
|
||||
TextWidgetClass { Name "DronesHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 50 OffsetTop 50 SizeX 936 OffsetRight -964 SizeY 48 OffsetBottom -98 } Color 0.9 0.92 0.94 1 Text "Realistic Combat Drones | Workshop: 65AD60E204191D37. Optional; install separately to use reconnaissance drones." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Label_droneReconCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 170 OffsetTop 170 SizeX 440 OffsetRight -468 SizeY 30 OffsetBottom -200 } Color 0.9 0.92 0.94 1 Text "Maximum active drones (0-3; 0 disabled)" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "droneReconCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 480 OffsetLeft 480 PositionY 170 OffsetTop 170 SizeX 160 OffsetRight -640 SizeY 38 OffsetBottom -208 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_droneReconCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 652 OffsetLeft 652 PositionY 170 OffsetTop 170 SizeX 112 OffsetRight -764 SizeY 38 OffsetBottom -208 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
|
||||
TextWidgetClass { Name "Saved_droneReconCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 480 OffsetLeft 480 PositionY 210 OffsetTop 210 SizeX 484 OffsetRight -964 SizeY 22 OffsetBottom -232 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Help_droneReconCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 236 OffsetTop 236 SizeX 936 OffsetRight -964 SizeY 28 OffsetBottom -264 } Color 0.9 0.92 0.94 1 Text "Searches recent HQ sightings after escape. Reports visual contact back to HQ." "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Label_droneCooldownSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 270 OffsetTop 270 SizeX 440 OffsetRight -468 SizeY 30 OffsetBottom -300 } Color 0.9 0.92 0.94 1 Text "Launch cooldown (60-1800 s; default 300)" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "droneCooldownSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 480 OffsetLeft 480 PositionY 270 OffsetTop 270 SizeX 160 OffsetRight -640 SizeY 38 OffsetBottom -308 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_droneCooldownSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 652 OffsetLeft 652 PositionY 270 OffsetTop 270 SizeX 112 OffsetRight -764 SizeY 38 OffsetBottom -308 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
|
||||
TextWidgetClass { Name "Saved_droneCooldownSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 480 OffsetLeft 480 PositionY 310 OffsetTop 310 SizeX 484 OffsetRight -964 SizeY 22 OffsetBottom -332 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Help_droneCooldownSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 336 OffsetTop 336 SizeX 936 OffsetRight -964 SizeY 28 OffsetBottom -364 } Color 0.9 0.92 0.94 1 Text "Minimum delay between launches, including replacements. Drones carry no explosives." "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "recommended_drones" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 112 OffsetTop 112 SizeX 936 OffsetRight -964 SizeY 44 OffsetBottom -156 } Color 0.9 0.92 0.94 1 Text "Waiting for server addon list" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "DroneRuntime" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 385 OffsetTop 385 SizeX 936 OffsetRight -964 SizeY 72 OffsetBottom -457 } Color 0.9 0.92 0.94 1 Text "Current round: waiting for server" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
} }
|
||||
FrameWidgetClass { "Is Visible" 0 Name "RecommendationsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1220 SizeY 486 OffsetBottom -646 } {
|
||||
TextWidgetClass { Name "RecommendedHeading" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 710 OffsetRight -738 SizeY 32 OffsetBottom -38 } Color 0.9 0.92 0.94 1 Text "Recommended optional addons" "Font Size" 22 "Min Font Size" 22 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "RecommendedDrones" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 750 OffsetLeft 750 PositionY 4 OffsetTop 4 SizeX 214 OffsetRight -964 SizeY 38 OffsetBottom -42 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Recon drones" } } }
|
||||
|
||||
TextWidgetClass { Name "RecommendedHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 47 OffsetTop 47 SizeX 936 OffsetRight -964 SizeY 28 OffsetBottom -75 } Color 0.9 0.92 0.94 1 Text "Optional extras. Add them to the server mod list and restart. Escape also works without them." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "Label_motorbikeCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 84 OffsetTop 84 SizeX 590 OffsetRight -618 SizeY 30 OffsetBottom -114 } Color 0.9 0.92 0.94 1 Text "WCS Motorbikes - patrol count (0-6; 0 off)" "Font Size" 21 "Min Font Size" 21 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
@@ -1126,7 +1229,7 @@ TextWidgetClass { Name "Help_aiShadow" Slot FrameWidgetSlot { Anchor 0 0 0 0 Pos
|
||||
TextWidgetClass { Name "AIChangesHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 350 OffsetTop 350 SizeX 936 OffsetRight -964 SizeY 72 OffsetBottom -422 } Color 0.9 0.92 0.94 1 Text "Current is the default. Proposal logging is for comparing plans before enabling coordinated orders. Neither setting changes the running round." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
} }
|
||||
FrameWidgetClass { "Is Visible" 0 Name "StarterPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1220 SizeY 486 OffsetBottom -646 } {
|
||||
TextWidgetClass { Name "StarterHeading" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 5 OffsetTop 5 SizeX 350 OffsetRight -378 SizeY 30 OffsetBottom -35 } Color 0.9 0.92 0.94 1 Text "Shared starting backpack" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "StarterHeading" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 5 OffsetTop 5 SizeX 350 OffsetRight -378 SizeY 30 OffsetBottom -35 } Color 0.9 0.92 0.94 1 Text "Shared starting supplies" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "StarterDefault" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 410 OffsetLeft 410 PositionY 0 OffsetTop 0 SizeX 260 OffsetRight -670 SizeY 40 OffsetBottom -40 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Default loadout" } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "StarterCustom" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 690 OffsetLeft 690 PositionY 0 OffsetTop 0 SizeX 274 OffsetRight -964 SizeY 40 OffsetBottom -40 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Custom loadout" } } }
|
||||
TextWidgetClass { Name "StarterState" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 46 OffsetTop 46 SizeX 936 OffsetRight -964 SizeY 44 OffsetBottom -90 } Color 0.9 0.92 0.94 1 Text "Waiting for saved loadout..." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
@@ -1139,11 +1242,12 @@ TextWidgetClass { Name "StarterQuantityLabel" Slot FrameWidgetSlot { Anchor 0 0
|
||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "StarterQuantity" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 676 OffsetLeft 676 PositionY 144 OffsetTop 144 SizeX 88 OffsetRight -764 SizeY 40 OffsetBottom -184 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "StarterAdd" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 776 OffsetLeft 776 PositionY 144 OffsetTop 144 SizeX 188 OffsetRight -964 SizeY 40 OffsetBottom -184 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Save quantity" } } }
|
||||
TextWidgetClass { Name "StarterCatalogState" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 192 OffsetTop 192 SizeX 936 OffsetRight -964 SizeY 28 OffsetBottom -220 } Color 0.9 0.92 0.94 1 Text "Choose a mod, then an item. Quantity 0 removes an item." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "StarterSavedHeading" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 230 OffsetTop 230 SizeX 936 OffsetRight -964 SizeY 28 OffsetBottom -258 } Color 0.9 0.92 0.94 1 Text "Saved custom contents (select an entry to remove)" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{4B5AE6E64037FFB4}UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout" { Name "StarterSaved" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 266 OffsetTop 266 SizeX 728 OffsetRight -756 SizeY 40 OffsetBottom -306 } components { SCR_ComboBoxComponent "{546B1D464BC32853}" { m_bUseLabel 0 m_sLabel "" m_fSizeWithoutLabel 40 } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "StarterRemove" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 776 OffsetLeft 776 PositionY 266 OffsetTop 266 SizeX 188 OffsetRight -964 SizeY 40 OffsetBottom -306 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Remove item" } } }
|
||||
TextWidgetClass { Name "StarterSummary" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 314 OffsetTop 314 SizeX 936 OffsetRight -964 SizeY 30 OffsetBottom -344 } Color 0.9 0.92 0.94 1 Text "Custom list is empty." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "StarterHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 352 OffsetTop 352 SizeX 936 OffsetRight -964 SizeY 72 OffsetBottom -424 } Color 0.9 0.92 0.94 1 Text "Up to 32 item types / 64 items. Changes apply next round, at prison or crash. The backpack has limited space; items that do not fit are omitted. Enemy spawn weights do not affect this loadout." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{4B5AE6E64037FFB4}UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout" { Name "StarterDestination" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 226 OffsetTop 226 SizeX 540 OffsetRight -568 SizeY 40 OffsetBottom -266 } components { SCR_ComboBoxComponent "{546B1D464BC32853}" { m_bUseLabel 0 m_sLabel "" m_fSizeWithoutLabel 40 } } }
|
||||
TextWidgetClass { Name "StarterSavedHeading" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 278 OffsetTop 278 SizeX 936 OffsetRight -964 SizeY 28 OffsetBottom -306 } Color 0.9 0.92 0.94 1 Text "Saved custom contents (select an entry to remove)" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
ButtonWidgetClass : "{4B5AE6E64037FFB4}UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout" { Name "StarterSaved" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 314 OffsetTop 314 SizeX 728 OffsetRight -756 SizeY 40 OffsetBottom -354 } components { SCR_ComboBoxComponent "{546B1D464BC32853}" { m_bUseLabel 0 m_sLabel "" m_fSizeWithoutLabel 40 } } }
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "StarterRemove" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 776 OffsetLeft 776 PositionY 314 OffsetTop 314 SizeX 188 OffsetRight -964 SizeY 40 OffsetBottom -354 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Remove item" } } }
|
||||
TextWidgetClass { Name "StarterSummary" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 362 OffsetTop 362 SizeX 936 OffsetRight -964 SizeY 30 OffsetBottom -392 } Color 0.9 0.92 0.94 1 Text "Custom list is empty." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
TextWidgetClass { Name "StarterHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 400 OffsetTop 400 SizeX 936 OffsetRight -964 SizeY 72 OffsetBottom -472 } Color 0.9 0.92 0.94 1 Text "Choose where to spawn each quantity before saving: inside the backpack or on the ground beside it. Up to 32 entries / 64 items combined. Applies next round, at prison or crash. Items that do not fit inside are also left nearby." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
} }
|
||||
FrameWidgetClass { "Is Visible" 0 Name "SarkaPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1220 SizeY 486 OffsetBottom -646 } {
|
||||
TextWidgetClass { Name "SarkaHeading" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 710 OffsetRight -738 SizeY 32 OffsetBottom -38 } Color 0.9 0.92 0.94 1 Text "Šárka special-forces hunters" "Font Size" 22 "Min Font Size" 22 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
FrameWidgetClass {
|
||||
Name "ERR_PreparationFailure"
|
||||
Slot FrameWidgetSlot { Anchor 0 0 1 1 OffsetLeft 0 OffsetTop 0 OffsetRight 0 OffsetBottom 0 }
|
||||
{
|
||||
ImageWidgetClass {
|
||||
Name "Background"
|
||||
Slot FrameWidgetSlot { Anchor 0 0 1 1 OffsetLeft 0 OffsetTop 0 OffsetRight 0 OffsetBottom 0 }
|
||||
Color 0.025 0.035 0.04 1
|
||||
}
|
||||
FrameWidgetClass {
|
||||
Name "FailurePanel"
|
||||
Slot FrameWidgetSlot { Anchor 0.5 0.5 0.5 0.5 OffsetLeft 0 OffsetTop 0 SizeX 960 OffsetRight -960 SizeY 620 OffsetBottom -620 Alignment 0.5 0.5 }
|
||||
{
|
||||
TextWidgetClass {
|
||||
Name "FailureTitle"
|
||||
Slot FrameWidgetSlot { Anchor 0 0 1 0 OffsetLeft 0 OffsetTop 0 SizeY 58 OffsetBottom -58 }
|
||||
Color 1 1 1 1
|
||||
Text "Mission could not start"
|
||||
"Font Size" 32
|
||||
"Min Font Size" 32
|
||||
}
|
||||
TextWidgetClass {
|
||||
Name "FailureDescription"
|
||||
Slot FrameWidgetSlot { Anchor 0 0 1 0 OffsetLeft 0 OffsetTop 80 SizeY 400 OffsetBottom -480 }
|
||||
Color 1 1 1 1
|
||||
"Font Size" 22
|
||||
"Min Font Size" 22
|
||||
Wrap 1
|
||||
}
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||
Name "ReturnToMainMenu"
|
||||
Slot FrameWidgetSlot { Anchor 0 0 0 0 OffsetLeft 0 OffsetTop 520 SizeX 400 OffsetRight -400 SizeY 56 OffsetBottom -576 }
|
||||
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Return to main menu" } }
|
||||
}
|
||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||
Name "OpenPauseMenu"
|
||||
Slot FrameWidgetSlot { Anchor 0 0 0 0 OffsetLeft 430 OffsetTop 520 SizeX 400 OffsetRight -830 SizeY 56 OffsetBottom -576 }
|
||||
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Pause menu (Esc / Back)" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
MetaFileClass {
|
||||
Name "{28DD8E27B1CF40C5}UI/layouts/ERR/PreparationFailure.layout"
|
||||
Configurations {
|
||||
LayoutResourceClass PC { }
|
||||
LayoutResourceClass HEADLESS : PC { }
|
||||
LayoutResourceClass XBOX_ONE : PC { }
|
||||
LayoutResourceClass XBOX_SERIES : PC { }
|
||||
LayoutResourceClass PS4 : PC { }
|
||||
LayoutResourceClass PS5 : PC { }
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ FrameWidgetClass {
|
||||
OffsetTop 0
|
||||
SizeX 900
|
||||
OffsetRight -900
|
||||
SizeY 240
|
||||
OffsetBottom -240
|
||||
SizeY 520
|
||||
OffsetBottom -520
|
||||
Alignment 0.5 0.5
|
||||
}
|
||||
"Z Order" 10
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
SizeLayoutWidgetClass {
|
||||
Name "ERR_RoundResults"
|
||||
AllowWidthOverride 1
|
||||
WidthOverride 1100
|
||||
AllowHeightOverride 1
|
||||
HeightOverride 620
|
||||
Slot OverlayWidgetSlot { HorizontalAlign 1 VerticalAlign 1 }
|
||||
components { SCR_GameOverScreenContentUIComponent "{6E81CD1BC83BE245}" { } }
|
||||
{
|
||||
FrameWidgetClass {
|
||||
Name "ResultFrame"
|
||||
Slot OverlayWidgetSlot { HorizontalAlign 3 VerticalAlign 3 }
|
||||
{
|
||||
TextWidgetClass {
|
||||
Name "GameOver_State"
|
||||
Slot FrameWidgetSlot { Anchor 0 0 1 0 OffsetLeft 20 OffsetTop 0 OffsetRight 20 SizeY 52 OffsetBottom -52 }
|
||||
Color 1 1 1 1 "Font Size" 36 "Min Font Size" 36
|
||||
Text "Round ended"
|
||||
FontProperties FontProperties { Font "{EABA4FE9D014CCEF}UI/Fonts/RobotoCondensed/RobotoCondensed_Bold.fnt" }
|
||||
}
|
||||
TextWidgetClass {
|
||||
Name "GameOver_Description"
|
||||
Slot FrameWidgetSlot { Anchor 0 0 1 0 OffsetLeft 20 OffsetTop 58 OffsetRight 20 SizeY 64 OffsetBottom -122 }
|
||||
Color 0.9 0.92 0.94 1 "Font Size" 22 "Min Font Size" 22 Wrap 1
|
||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||
}
|
||||
TextWidgetClass {
|
||||
Name "StatisticsHeading"
|
||||
Slot FrameWidgetSlot { Anchor 0 0 1 0 OffsetLeft 20 OffsetTop 132 OffsetRight 20 SizeY 36 OffsetBottom -168 }
|
||||
Color 0.9 0.8 0.5 1 "Font Size" 26 "Min Font Size" 26 Text "Mission statistics"
|
||||
FontProperties FontProperties { Font "{EABA4FE9D014CCEF}UI/Fonts/RobotoCondensed/RobotoCondensed_Bold.fnt" }
|
||||
}
|
||||
ScrollLayoutWidgetClass {
|
||||
Name "StatisticsScroll"
|
||||
Slot FrameWidgetSlot { Anchor 0 0 1 1 OffsetLeft 20 OffsetTop 178 OffsetRight 20 OffsetBottom 12 }
|
||||
components { SCR_GamepadScrollComponent "{5707100AC31E657E}" { } }
|
||||
{
|
||||
VerticalLayoutWidgetClass {
|
||||
Name "StatisticsContent"
|
||||
Slot AlignableSlot { HorizontalAlign 3 VerticalAlign 0 Padding 0 0 18 0 }
|
||||
{
|
||||
TextWidgetClass {
|
||||
Name "RoundStatistics"
|
||||
Slot LayoutSlot { HorizontalAlign 3 Padding 0 0 0 8 SizeMode Auto }
|
||||
Clipping Ancestor Color 0.9 0.92 0.94 1
|
||||
Text "Waiting for mission statistics..."
|
||||
"Font Size" 20 "Min Font Size" 20 Wrap 1
|
||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
MetaFileClass {
|
||||
Name "{C63B4D1E09A572F8}UI/layouts/ERR/RoundResults.layout"
|
||||
Configurations {
|
||||
LayoutResourceClass PC { }
|
||||
LayoutResourceClass HEADLESS : PC { }
|
||||
LayoutResourceClass XBOX_ONE : PC { }
|
||||
LayoutResourceClass XBOX_SERIES : PC { }
|
||||
LayoutResourceClass PS4 : PC { }
|
||||
LayoutResourceClass PS5 : PC { }
|
||||
}
|
||||
}
|
||||
@@ -21,3 +21,32 @@ Chat delegates the reveal alias to Core instead of defining a duplicate. Its `in
|
||||
`ERR_NativeAdminSettingsProbe.Run(ERR_GameMode mode)` is an opt-in WORKBENCH test awaiting central native execution. It checks actual authorized read/help dispatch and unauthorized/invalid mutation rejection. Valid changes exercise the same production setting-command handler with a separate `ERR_Settings` instance at `$profile:erri-native-admin-settings-probe.json`; they never mutate the active mission's settings. The fixture rejects already-existing fixture files, verifies native save/reload and unrelated values, then removes only its own fixture primary, backup and temporary files.
|
||||
|
||||
A policy pass does not prove native script compilation, remote RPC ownership or readable chat presentation. Run the native probe inside Core-only and companion regression fixtures, then retain real-client administration in multiplayer acceptance.
|
||||
|
||||
## Radio intel progression
|
||||
|
||||
**Intel → Radio intel** configures `radioIntelSiteCount` (0–20, default 5).
|
||||
Use `/esc intel radiosites show|set N|help` from the same authenticated command
|
||||
path. Zero disables the requirement. The discovery service snapshots the saved
|
||||
value at round initialization; edits affect the next round. Older settings files
|
||||
receive the default without changing their other values.
|
||||
|
||||
Only distinct, prepared, fully identified (`Knowledge() == 2`) non-radio sites
|
||||
count. Prison, extraction and sites marked `RevealOnEscape` are excluded. Rough
|
||||
clues and anonymous question marks do not contribute. After all background site
|
||||
preparation finishes, the effective requirement is capped at the available
|
||||
eligible site count. Once unlocked, radio intel stays unlocked for that round.
|
||||
The submenu shows this round's progress separately from the saved requirement.
|
||||
|
||||
Ordinary document assignment and recovery exclude radios while locked. An issued
|
||||
ordinary folder already targeting a radio redirects to another unfinished site;
|
||||
its issued evidence identity is updated, but already queued or delivered enemy
|
||||
reports keep their copied contents. With no alternative, the folder remains
|
||||
unspent. Fixed objective documents keep their radio targets and are automatically
|
||||
read from player inventory after unlocking. A team notice explains the wait once
|
||||
per round. Normal exhaustion/cleanup behavior resumes after unlock.
|
||||
|
||||
The gate applies only to document targeting. Physical observation, map question
|
||||
marks, radio interaction and radio defence retain their existing rules.
|
||||
|
||||
Validation: `tests/test_radio_intel.py`, settings/menu/feedback regressions and
|
||||
[the isolated native mission evidence](evidence/radio-intel-2026-10-21/README.md).
|
||||
|
||||
@@ -5,12 +5,17 @@ crate. The remaining supply cache and medical stash provide the two reachable
|
||||
radio-intel sources. Mortar reserve/courier preparation uses the existing eastern
|
||||
outpost, preserving its finite logistics without creating a replacement site.
|
||||
|
||||
Extraction is authored at 4800.953 / 11487.412 on Everon's airfield. Before
|
||||
spawning the helicopter, `ERR_AirfieldPlacement` checks nearby physical airfield
|
||||
anchors, dry terrain, slope and the aircraft/overhead clearance volume. A failed
|
||||
check fails preparation; there is no fallback into the woods. New islands need
|
||||
their own authored pad and recognizable airfield anchors. Aircraft damage and
|
||||
physics remain normal after spawning.
|
||||
As of 28 October 2026, extraction can use suitable clear ground anywhere on the
|
||||
terrain. Original Everon randomizes its pickup with `RandomClear 1` and
|
||||
`RequireAirfield 0`; its previous airport coordinate remains only a fallback
|
||||
subject to the same landing-space and escape-boundary checks. Terrain-independent
|
||||
placement also samples the entire terrain immediately, without an airport-first
|
||||
pass or a dedicated airport-discovery wait.
|
||||
|
||||
Primary ground/overhead clearance and secondary transport space are checked
|
||||
before accepting a candidate. Site creation rechecks the generated primary pad.
|
||||
Aircraft damage and physics remain normal. Explicit authored airport-only pads
|
||||
remain supported through `RequireAirfield 1`.
|
||||
|
||||
Exact discoveries use native wrapper images and matching glow images:
|
||||
helicopter for extraction, radio for radio camps, artillery for mortar sites,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Unarmed civilians drive ordinary S105 cars and S1203 vans along the terrain's roads. This is ambient life, separate from enemy convoys and VirtualHQ: drivers receive no enemy loadouts, intelligence or pursuit orders.
|
||||
|
||||
The admin menu's **Factions** page has **Civilian traffic (0–6 cars)**. The default is 3. Set 0 to stop new traffic; existing unclaimed cars retire only when the normal distance and visibility checks allow it. The same setting is available as `/esc mission traffic set N` and `mission.civilianTrafficCount` in the unified server `Escapists.json`. Old configurations receive the default automatically.
|
||||
The admin menu's **Mission → Civilian vehicles** page has **Civilian traffic (0–6 cars)**. The default is 3. Set 0 to stop new traffic; existing unclaimed cars retire only when the normal distance and visibility checks allow it. The same setting is available as `/esc mission traffic set N` and `mission.civilianTrafficCount` in the unified server `Escapists.json`. Old configurations receive the default automatically.
|
||||
|
||||
After the prison phase, the server tries one new car every 30 seconds, with at most eight route candidates per attempt. Each fixed route starts on a clear road at least 1,000 m from every controlled player body and known spectator target, passes a road near the selected player area, then leaves along another road. Exit candidates include both sides of the area so a coastal road does not require an endpoint across water. It is not redirected to follow moving players. Positions come from the current terrain's road network; terrain-specific adapters are unnecessary. Roads and usable native vehicle navigation are still required.
|
||||
|
||||
|
||||
+92
-20
@@ -39,17 +39,21 @@ and `/esc mission startammo set N` (also `show` and `help`).
|
||||
|
||||
## Roadblocks
|
||||
|
||||
The admin menu **Settings → Roadblocks next round** sets `mission.roadblockCount`
|
||||
in `Escapists.json`: **0–30**, default **4**; **0 disables** them. The chat equivalent
|
||||
is `/esc mission roadblocks set N` (also `show` and `help`). Changes apply on the
|
||||
next round/restart, leaving current units and cover in place.
|
||||
Under **Forces → Checkpoints**, configure two independent next-round quotas:
|
||||
|
||||
Each checkpoint has two defensive bays built from eight sandbag walls, four
|
||||
approach barriers, two anti-vehicle obstacles, two stop signs, a small rest tent
|
||||
and an inspection table. Four soldiers come from the selected enemy faction. They use normal equipment rules and physical
|
||||
radio reporting to VirtualHQ; HQ can send their squad to respond to contacts.
|
||||
They do not reveal player locations to HQ merely by being created. Soldiers and
|
||||
cover are owned by the round; killed guards are not replaced.
|
||||
| Setting | Range | Default | Chat command |
|
||||
| --- | --- | --- | --- |
|
||||
| Small checkpoints (`mission.roadblockCount`) | 0–30 | 4 | `/esc mission roadblocks set N` |
|
||||
| Large checkpoints (`mission.largeRoadblockCount`) | 0–30 | 4 | `/esc mission largeroadblocks set N` |
|
||||
| Main-road minimum width (`mission.mainRoadWidthMetres`) | 5–12 m | 6 m | `/esc mission mainroadwidth set N` |
|
||||
|
||||
Both counts can be disabled separately with zero; set both to zero to disable all checkpoints. The existing `roadblockCount` value now controls only small roads. Old configurations retain that value and receive the new defaults. Changes save in `Escapists.json` and apply next round; existing units and cover stay in place. `show` and `help` also work for each chat command.
|
||||
|
||||
Roads at or above the width cutoff get large checkpoints; roads below it get compact posts. This is a width classification from the terrain's road network, not a guaranteed asphalt/dirt or forest distinction. An exhausted or disabled quota cannot consume the other type's allowance. Both types share the existing minimum spacing, so they cannot overlap each other.
|
||||
|
||||
Small checkpoints retain four guards, four sandbag walls, two approach barriers and two signs (about 17.8 m long). Large checkpoints have eight guards, eight sandbag walls, four approach barriers and two signs (about 42.8 m long), with four protected guard bays on each shoulder. Large candidates require at least 60 m of road and a straight clear approach. Both preserve a straight vehicle passage at least four metres wide.
|
||||
|
||||
Soldiers come from the selected enemy faction, use normal equipment and physical radio reporting, and register with VirtualHQ at their actual four- or eight-soldier strength. They do not reveal player locations simply by spawning. Guards are not replaced after death. Every post is constructed one actor, prop or waypoint per callback; a failed or interrupted build rolls back without consuming either quota. At maximum settings the two types can create up to 360 guards and 660 replicated props; their default combined count is eight posts and 48 guards. The Status page reports placed/requested counts separately.
|
||||
|
||||
Placement uses the loaded terrain's road network and Soldiers navmesh after
|
||||
terrain/site preparation. The initial search yields after 12 seconds; unfinished
|
||||
@@ -63,6 +67,10 @@ requested totals instead of preventing mission startup. The admin Status page
|
||||
also shows these totals. This is an interim checkpoint system, independent of the
|
||||
existing HQ-directed road interceptions.
|
||||
|
||||
The [1 November native checkpoint test](evidence/large-checkpoints-2026-11-01/native-checks.log) placed one small and one large post on Everon: 12 living guards, 22 replicated props, correct HQ strength, model footprints outside the centre passage, and separate completed quotas. The [menu/settings fixture](evidence/large-checkpoints-2026-11-01/menu/native-checks.log) saved and reloaded distinct counts and width, and measured the new controls’ native text bounds. It instantiates the generated layout directly; it does not claim human input, remote RPC delivery or screenshot acceptance. Both completed with unchanged inputs and cleanup; strict runners retain 13 existing resource/world errors and no script errors. Earlier failed iterations are recorded alongside the evidence. Full-load performance and multiplayer acceptance remain unmeasured.
|
||||
|
||||
Fresh settings now initialize both drone keys, including the valid 300-second cooldown; the missing cooldown previously prevented a fresh settings document from passing save/readback validation.
|
||||
|
||||
## Šárka special-forces hunters
|
||||
|
||||
Under **Forces → Šárka hunters**, set **Anger points per Šárka** from 1 to 30.
|
||||
@@ -123,9 +131,12 @@ for at least 15 minutes after a disconnect, including a client crash. A longer
|
||||
native `operating.slotReservationTimeout` is respected, as is its unlimited
|
||||
setting. Rejoining with the same authenticated account returns control of that
|
||||
body, keeping its inventory, injuries and vehicle seat. It stays vulnerable while
|
||||
the player is away. Expiry, death or a different round does not grant a replacement
|
||||
life or duplicate equipment. This is recovery within the running mission, not
|
||||
saved characters across server restarts. Reconnect reservation, rejection and
|
||||
the player is away. If the reservation is missing, expired or its body has died,
|
||||
reconnecting grants a fresh soldier through normal admission before extraction
|
||||
begins. The new soldier uses the normal loadout, without copying old equipment.
|
||||
A valid reservation whose handoff fails remains reserved instead of granting a
|
||||
second soldier. New rounds start with fresh admission records; characters are
|
||||
not saved across server restarts. Reconnect reservation, rejection and
|
||||
successful handoff are logged without account identifiers.
|
||||
|
||||
The root has `"configVersion": 1`. Missing legacy files get defaults. Existing
|
||||
@@ -324,12 +335,16 @@ authoring fields, failure behavior and validation.
|
||||
|
||||
The initial prison patrol contains 3–5 soldiers, chosen once during preparation from the selected enemy faction roster. An existing RTO is retained. If that roster has no large radio, one soldier is equipped with a physical manpack (US/USAF: AN/PRC-77; otherwise R-107M). Preparation waits for that radio to be present. The guards start at separate random clear positions 12–24 m around the prison and remain frozen until it is unlocked; other site squads keep their configured size.
|
||||
|
||||
## Airfield extraction and site icons
|
||||
## Extraction landing zones and site icons
|
||||
|
||||
Everon extraction uses the airfield pad at 4800.953 / 11487.412. Site authors set
|
||||
`RequireAirfield 1` and `AircraftOffset 0 0 0` on that extraction definition.
|
||||
Preparation checks airfield anchors, terrain and clearance before creating the
|
||||
aircraft, and refuses an invalid pad without relocating it elsewhere.
|
||||
Extraction searches for suitable clear ground across the terrain from the first
|
||||
attempt, on original Everon and on other terrains. It does not prioritize airports.
|
||||
Dry ground, slope, obstacle/overhead clearance, nearby rescue-fleet space and the
|
||||
prison's escape boundary still constrain placement. Terrain-generated scenarios
|
||||
also require reachable navigation at the landing zone and guard positions.
|
||||
Original Everon uses `RandomClear 1` and `RequireAirfield 0`; its old airport
|
||||
coordinate is only a validated fallback if the random search is exhausted.
|
||||
Scenario authors can still explicitly require an airfield for their own fixed pads.
|
||||
|
||||
Identified sites use helicopter, radio, artillery, building, medical and supplies
|
||||
icons. Partial intel shows a question mark at an approximate position; the prison
|
||||
@@ -372,8 +387,65 @@ See [reconnaissance plane details](cessna-reconnaissance.md).
|
||||
|
||||
## Starting backpack contents
|
||||
|
||||
Open **Items → Starting backpack** in the admin menu. Select a loaded mod, choose **Show items**, browse its catalog pages, choose an item and save its total quantity. Saving the same item again replaces its quantity; zero or **Remove item** removes it. Weapons, matching magazines and medical supplies can be selected explicitly. The saved-content dropdown lists every selected item and its quantity.
|
||||
Open **Items → Starting backpack** in the admin menu. Select a loaded mod, choose **Show items**, browse its catalog pages, choose an item, quantity and destination (**Inside backpack** or **On ground beside backpack**), then choose **Save quantity**. Saving the same item and destination again replaces that quantity; zero removes it from that destination. The same item can have separate backpack and ground quantities. **Remove item** removes the selected saved entry. Weapons, matching magazines and medical supplies can be selected explicitly. The saved-content dropdown lists every selected item, quantity and destination.
|
||||
|
||||
Select **Custom loadout** to use that list for the next prison or crash start. The shared field map is always included. **Default loadout** restores the existing pistol and magazine count settings without erasing your custom list. Existing backpacks are never refilled or changed by these edits. Enemy item permissions and probability weights do not change this explicit starting loadout.
|
||||
|
||||
The list permits up to 32 different items, 48 of one item and 64 total items, plus the map. The physical backpack still has limited capacity: unavailable items and items that cannot be inserted are omitted and logged. Add matching magazines explicitly; selecting a weapon does not automatically add spare ammunition. Settings persist in the `startBackpack` section of `Escapists.json`. Older configurations keep the default loadout.
|
||||
The list permits up to 32 item/destination entries, 48 per entry and 64 total items across both destinations, plus the map. Ground items bypass inventory insertion and remain available nearby for pickup. Items that do not fit inside the backpack also stay nearby instead of blocking startup or being deleted. Unavailable custom items are skipped and logged. Add matching magazines explicitly; selecting a weapon does not automatically add spare ammunition. Settings persist in the `startBackpack` section of `Escapists.json`. Existing version 1 loadouts remain valid and their items keep the backpack destination. Saving writes version 2 records as `prefab|quantity|backpack` or `prefab|quantity|ground`; the enclosing configuration version does not change.
|
||||
|
||||
## After-mission statistics
|
||||
|
||||
The results screen remains open for 120 seconds before automatic restart. A
|
||||
server's explicit `-autoReload` command-line setting still takes precedence.
|
||||
|
||||
Victory and defeat screens include a scrollable mission summary: duration,
|
||||
participant and escape counts, squad totals, and each participant's kills,
|
||||
friendly kills, deaths, times downed, recoveries and final escape status.
|
||||
Disconnected participants retain their records. Only participants who actually
|
||||
escaped according to the extraction service are marked **Escaped**.
|
||||
|
||||
Kills count non-friendly characters credited by native damage events to a
|
||||
participant's body; friendly kills are separate. Vehicles, self-kills, editor
|
||||
kills and unattributed damage do not add to the kill score. A fatal loss counts
|
||||
as a death. Because the mission normally keeps wounded players alive, unconscious
|
||||
episodes are counted separately as **Downed**. **Recoveries** include both medical
|
||||
treatment and natural/self-recovery; they are not a medic's revive score.
|
||||
|
||||
The report freezes when the round ends. Counts belong to the current round and
|
||||
start fresh with the next mission. They work with statistics telemetry disabled
|
||||
and require no web service. At most 128 distinct participants are retained; the
|
||||
report explicitly marks truncation if that limit is reached. No account IDs are
|
||||
included in the displayed or replicated summary.
|
||||
|
||||
After a victory or defeat is accepted by the community statistics receiver, the
|
||||
screen also shows the current map's wins, failures and win rate on this server
|
||||
and across all reporting servers. Totals include this round and update if they
|
||||
arrive while the screen is open. Different maps are counted separately using
|
||||
their loaded world resources. Startup failures, empty restarts and interrupted
|
||||
sessions are excluded. Community totals show unavailable if reporting is disabled
|
||||
or delivery fails; local round statistics still work.
|
||||
|
||||
### Parked civilian vehicles
|
||||
|
||||
Open **Admin menu → Mission → Civilian vehicles**. Each garage control has an **Apply** button and saved-value display; changes apply next round.
|
||||
|
||||
| Control | Range | Default |
|
||||
| --- | --- | --- |
|
||||
| Nearby garage vehicles | 0–24; 0 disables | 16 |
|
||||
| Garage selection chance | 0–100%; 0 disables | 70% |
|
||||
| Spawn radius | 250–1500 metres | 1000 m |
|
||||
| Time between spawns | 5–60 seconds | 5 s |
|
||||
|
||||
The additional saved keys are `mission.garageChancePercent`, `mission.garageRadiusMetres` and `mission.garageSpawnIntervalSeconds`. Chat equivalents are `/esc mission garagechance set N`, `/esc mission garageradius set N`, and `/esc mission garageinterval set N`.
|
||||
|
||||
In **Mission → Civilian vehicles**, set **Nearby garage vehicles** (or `/esc mission garagevehicles set N`). `mission.garageVehicleCount` defaults to 16, accepts 0–24, and applies next round. Zero disables placement. This is a shared limit on **unclaimed nearby vehicles**, not a lifetime limit of 16 across the island. Every fourth vehicle attempts an optional unarmed WCS motorbike when its dependencies are loaded; otherwise cars fill the slots.
|
||||
|
||||
As players explore, the server discovers native `Garage_E_01` and `HouseAddon_Garage_E_01` buildings within the configured radius. Each garage uses the configured selection chance, rolled once on discovery. A bay must be at least 35 metres from every player body/spectator target, hidden by scenery from all of them, dry and collision-free. Open editor cameras or unknown observer targets suspend spawning and retirement. Adjacent suitable garages can both contain vehicles; the old 150-metre spacing and whole-island survey are gone. Custom garage models are not currently supported.
|
||||
|
||||
Discovery, candidate checks, prefab loading and creation run as separate updates, every 250 ms. Each update scans at most one 500-metre cell, maintains one vehicle, checks one physical bay, loads one prefab, or creates one vehicle. Native creations respect the configured delay, with a minimum of five seconds. Prefabs stay cached for later spawns. Native resource loading and entity creation are indivisible calls, so this limits bursts rather than guaranteeing zero frame spikes.
|
||||
|
||||
Vehicles have 30–80% fuel and no AI crew. Entering a vehicle permanently preserves it, including after exiting. Occupants or movement beyond three metres also protect it as a fallback for missed entry callbacks or towing. Its original garage is then consumed for the round, preventing repeated vehicle farming. Used vehicles do not occupy the nearby unclaimed allowance. A separate limit of 128 retained-plus-current vehicles bounds accumulation during unusually long rounds; it never deletes used vehicles.
|
||||
|
||||
An untouched vehicle can be retired only when it is at least 1,400 metres from every known observer (or the configured spawn radius plus 400 metres, whichever is larger) and hidden for a sampled 60-second grace period. Returning nearby, gaining visibility or losing observer information cancels that grace period. Retirement frees a nearby slot for the players' new area; an unused garage can become eligible again on a later visit. Unclaimed cars are maintained one at a time. The discovery cache is capped at 16,384 cells and 2,048 garages per round. Vehicles still incur ordinary physics and replication costs.
|
||||
|
||||
Validation: [menu settings and round persistence](evidence/garage-vehicles-2026-10-30/menu/report.json), [nearby garage spawning and retention](evidence/garage-vehicles-2026-10-30/nearby/report.json), plus production-method observer, lifecycle, settings and menu tests.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# Optional enemy reconnaissance drones
|
||||
|
||||
The supported package is [**Realistic Combat Drones**](https://reforger.armaplatform.com/workshop/65AD60E204191D37-RealisticCombatDrones) by SalamiBoiDev,
|
||||
Workshop ID `65AD60E204191D37`. Integration targets its **2.3.7** unarmed
|
||||
`Prefabs/Drones/DJIBase/DJIBase.et` airframe. Install the official package
|
||||
separately; Escape does not require it or include its assets. AI Use Drones,
|
||||
Vampire, the MANW variant and the RHS arsenal extension are not required.
|
||||
|
||||
Open **Mods → Recommended mods → Recon drones**, or **HQ → Recon drones**.
|
||||
|
||||
| Setting | Default | Range | Command |
|
||||
| --- | --- | --- | --- |
|
||||
| Maximum active drones (`droneReconCount`) | 0 (disabled) | 0–3 | `/esc mission drones set N` |
|
||||
| Launch cooldown (`droneCooldownSeconds`) | 300 seconds | 60–1800 | `/esc mission dronecooldown set N` |
|
||||
|
||||
Settings use the existing authenticated administration and saved mission
|
||||
configuration, and apply next round. Older profiles receive the disabled default.
|
||||
The page distinguishes installed/enabled settings from current-round readiness,
|
||||
active count and failure reasons. Enabling a setting does not install an addon.
|
||||
|
||||
## Behaviour
|
||||
|
||||
After escape, HQ may dispatch an unarmed quadcopter from the squad responsible
|
||||
for a delivered sighting no older than 60 seconds. The living squad leader must
|
||||
carry a radio and be within 1.2 km of the reported location. The initial pad must
|
||||
have overhead clearance, be on land and be at least 100 m from players. Launch
|
||||
selection uses captured report coordinates, never the reported target's live
|
||||
position. Replayed reports and the drone's own reports cannot trigger launches.
|
||||
Nearby searches and squads already operating a drone are not duplicated.
|
||||
|
||||
A server autopilot drives the addon's stabilized flight controls. The addon
|
||||
supplies the actual airframe, physics forces, damage, rotor animation/audio and
|
||||
network updates. Flights climb before departure, travel towards the report and
|
||||
search for up to two minutes after reaching its area. The drone returns after
|
||||
searching or losing its operator/link. Flight time has a ten-minute upper bound;
|
||||
obstructed takeoff, destruction and player takeover release automatic control.
|
||||
Takeover preserves the player's aircraft. A returned drone stays at the launch
|
||||
site and resumes the addon's normal cleanup behaviour.
|
||||
|
||||
The reconnaissance sensor samples at most one living player per drone per
|
||||
second. It uses a 60-degree cone aimed at the reported search location, a 350 m
|
||||
range and a physical world/entity trace. The first implementation reports
|
||||
visible players on foot; it does not identify concealed vehicle occupants,
|
||||
inventory or weapons. This is an explicit geometric sensor, not native infantry
|
||||
perception or a thermal/smoke/weather simulation.
|
||||
|
||||
Observations require the original operator to remain conscious, within 1.8 km,
|
||||
with a clear physical link. Registered RCD jammers in range deny that link.
|
||||
The operator's squad must then have a usable HQ radio route: reports enter the
|
||||
existing delayed radio queue and only normal delivery updates HQ memory.
|
||||
Existing HQ response and mortar rules remain responsible for any response.
|
||||
Drones themselves have no weapons or explosive payload.
|
||||
|
||||
## Bounds and optional API
|
||||
|
||||
- Zero to three concurrent flights; a shared minimum launch interval.
|
||||
- Sixty-four allocations per round and a pause after three consecutive launch
|
||||
or pre-flight failures. Successful takeoff clears the failure streak.
|
||||
- Controls update at 20 Hz, add-on position packets at up to 10 Hz, and link/
|
||||
camera checks at most once per second per drone. Jammer lookup is capped at
|
||||
64 entries. No island-wide entity scan runs per frame.
|
||||
- All flight callbacks stop with the mission. Launches stop on an empty server;
|
||||
existing aircraft can finish their bounded flights.
|
||||
- Foreign types occur only inside a fixed dynamically compiled adapter after
|
||||
detecting the addon. Missing/incompatible APIs disable drones for that round.
|
||||
- The dynamic compiler is supported on dedicated servers and Workbench. Normal
|
||||
listen-server hosts skip this integration, matching the existing Cessna bridge.
|
||||
- Only the mission-created drone's simulation/battery callbacks are temporarily
|
||||
replaced; release restores them. Player drones and third-party files are not
|
||||
modified.
|
||||
|
||||
## Verification
|
||||
|
||||
`python3 tests/test_drone_recon.py` executes the production scheduling, camera
|
||||
cone and control rules with native boundaries substituted. It also checks the
|
||||
optional dependency and HQ radio integration boundaries. Menu, settings migration
|
||||
and resource checks run through `python3 tools/check.py`.
|
||||
|
||||
The [21 October native evidence](evidence/drone-recon-2026-10-21/README.md)
|
||||
records powered takeoff/translation, injected takeover, unowned disarm and startup
|
||||
without the optional addon: seven assertions with no script/runtime failures.
|
||||
Engine resource/world/platform errors remain in both complete reports. Dedicated
|
||||
multiplayer, automatic sighting-to-launch-to-report flow, and a full search/return
|
||||
cycle remain mission-playtest requirements.
|
||||
@@ -0,0 +1 @@
|
||||
15:30:50.026 SCRIPT (E): ERR SMOKE FAIL: horn driver seat
|
||||
@@ -0,0 +1,5 @@
|
||||
15:34:03.323 SCRIPT : ERR HORN RESULT: prefab={AD80FB47D868C775}Prefabs/Vehicles/Wheeled/S105/S105_green.et allowedFrames=31 blockedFrames=878 violations=0
|
||||
15:34:03.323 SCRIPT : ERR SMOKE PASS: native AI driver continuous horn demand bounded with cooldown
|
||||
15:34:03.324 SCRIPT : ERR HORN RESULT: prefab={0B4DEA8078B78A9B}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469_PKM.et allowedFrames=31 blockedFrames=878 violations=0
|
||||
15:34:03.324 SCRIPT : ERR SMOKE PASS: native AI driver continuous horn demand bounded with cooldown
|
||||
15:34:03.324 SCRIPT : ERR SMOKE COMPLETE: native UAZ and civilian AI horn input limits
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Frozen local Workbench ai-driver-horn fixture; no live server or installed asset writes."""
|
||||
from pathlib import Path
|
||||
import hashlib,json,shutil,subprocess,sys,os,time,re
|
||||
ROOT=Path(__file__).resolve().parents[3]
|
||||
sys.path.insert(0,str(ROOT/'tools'))
|
||||
from source_manifest import addon_manifest
|
||||
from headless_game import assert_prefix_idle
|
||||
name=sys.argv[1]
|
||||
if not name.isalnum(): raise ValueError('Use an alphanumeric run suffix')
|
||||
source=Path('/tmp/erri-ai-driver-horn-source-'+name)
|
||||
profile=Path('/tmp/erri-ai-driver-horn-run-'+name)
|
||||
compat=ROOT/'.local/playtest-compat-20260920'
|
||||
assert_prefix_idle(compat)
|
||||
source.mkdir();profile.mkdir()
|
||||
for rel in addon_manifest(ROOT):
|
||||
target=source/rel;target.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(ROOT/rel,target)
|
||||
shutil.copytree(ROOT/'tools',source/'tools',ignore=shutil.ignore_patterns('__pycache__'))
|
||||
(source/'docs').mkdir();shutil.copy2(ROOT/'docs/external-addons.json',source/'docs/external-addons.json')
|
||||
fixture=Path(__file__).with_name('native-fixture.c')
|
||||
target=source/'addons/core/Scripts/Game/ERR/10_AI/ZZZ_HornFixture.c'
|
||||
target.write_text(fixture.read_text())
|
||||
# Opt-in fixture is independent of the historical generic Core smoke.
|
||||
p=source/'addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c'
|
||||
p.write_text(p.read_text().replace('array<string> independentFixtures = {','array<string> independentFixtures = {"ai-driver-horn", '))
|
||||
p=source/'tools/headless_workbench.py'
|
||||
p.write_text(p.read_text().replace("choices=['coordinated-ai',", "choices=['ai-driver-horn', 'coordinated-ai',").replace("a.smoke not in {'coordinated-ai',", "a.smoke not in {'ai-driver-horn', 'coordinated-ai',"))
|
||||
# The existing fatal-loss diagnostic is guarded by errSmoke; this Core-only
|
||||
# runner uses errBaseSmoke. Adapt only that test-helper flag in the frozen copy.
|
||||
p=source/'addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerMedical.c'
|
||||
p.write_text(p.read_text().replace('System.GetCLIParam("errSmoke", enabled)', 'System.GetCLIParam("errBaseSmoke", enabled)'))
|
||||
installed=Path.home()/'.local/share/Steam/steamapps/compatdata/1874880/pfx/drive_c/users/steamuser/Documents/My Games/ArmaReforger/addons'
|
||||
(profile/'profile').mkdir()
|
||||
(profile/'profile/EscapistsFactions.json').write_text(json.dumps({'escapists':'ussr','enemy':'us','independent':'fia'})+'\n')
|
||||
(profiles:=profile/'fixture-inputs.json').write_text(json.dumps({'fixture_sha256':hashlib.sha256(fixture.read_bytes()).hexdigest(),'production_sources':addon_manifest(ROOT),'source':str(source),'profile':str(profile)},indent=2)+'\n')
|
||||
command=[sys.executable,str(source/'tools/headless_workbench.py'),'--compat',str(compat),'--profile',str(profile),'--external-addons',str(installed),'--project','core','--smoke','ai-driver-horn','--rounds','1','--timeout','300']
|
||||
result=subprocess.run(command)
|
||||
raise SystemExit(result.returncode)
|
||||
@@ -0,0 +1,101 @@
|
||||
#ifdef WORKBENCH
|
||||
// Inject continuous horn demand into two fixture-owned AI drivers, then observe
|
||||
// the actual production OnApplyControls chain. No human input is generated.
|
||||
class ERR_HornFixtureDriver
|
||||
{
|
||||
IEntity Driver;
|
||||
Vehicle Car;
|
||||
SCR_AIGroup Group;
|
||||
int Started = -1;
|
||||
int Allowed;
|
||||
int Blocked;
|
||||
int Violations;
|
||||
}
|
||||
class ERR_HornFixture
|
||||
{
|
||||
static ref array<ref ERR_HornFixtureDriver> Drivers = {};
|
||||
static ERR_HornFixtureDriver Find(IEntity actor)
|
||||
{
|
||||
foreach (ERR_HornFixtureDriver driver : Drivers) if (driver.Driver == actor) return driver;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
modded class SCR_CharacterControllerComponent
|
||||
{
|
||||
protected override void OnApplyControls(IEntity owner, float timeSlice)
|
||||
{
|
||||
ERR_HornFixtureDriver fixture = ERR_HornFixture.Find(owner);
|
||||
CharacterInputContext input = GetInputContext();
|
||||
bool seated = fixture && CompartmentAccessComponent.GetVehicleIn(owner) == fixture.Car;
|
||||
if (seated && input) input.SetVehicleHorn(1);
|
||||
super.OnApplyControls(owner, timeSlice);
|
||||
if (!seated || !input) return;
|
||||
int now = System.GetTickCount();
|
||||
if (fixture.Started < 0) fixture.Started = now;
|
||||
int elapsed = now - fixture.Started;
|
||||
bool sounding = input.GetVehicleHorn() != 0;
|
||||
if (sounding) fixture.Allowed++; else fixture.Blocked++;
|
||||
if (sounding && elapsed >= 600 && elapsed < 29900) fixture.Violations++;
|
||||
}
|
||||
}
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected bool m_ERRHornSpawned;
|
||||
protected int m_ERRHornDeadline;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
if (Replication.IsServer() && !SCR_Global.IsEditMode()) GetGame().GetCallqueue().CallLater(ERR_HornTest, 500, true);
|
||||
}
|
||||
protected void ERR_HornTest()
|
||||
{
|
||||
if (!Active()) return;
|
||||
SCR_PlayerController player = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
if (!player || !player.GetControlledEntity()) return;
|
||||
if (!m_ERRHornSpawned)
|
||||
{
|
||||
m_ERRHornSpawned = true; m_ERRHornDeadline = System.GetTickCount() + 60000;
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
ERR_HornFixtureDriver driver = new ERR_HornFixtureDriver();
|
||||
vector position = ERR_Actors.Ground(player.GetControlledEntity().GetOrigin() + Vector(30 + i * 15, 0, 0));
|
||||
ResourceName prefab = ERR_CivilianTraffic.CarPrefab(0);
|
||||
if (i == 1) prefab = ERR_ConvoyResources.VehiclePrefab(0);
|
||||
driver.Car = Vehicle.Cast(ERR_Actors.Spawn(prefab, position));
|
||||
driver.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), position));
|
||||
if (!driver.Car || !driver.Group) { Print("ERR SMOKE FAIL: horn vehicle/group creation", LogLevel.ERROR); return; }
|
||||
driver.Group.SetPermanentLOD(0); driver.Group.ActivateAI();
|
||||
ERR_HornFixture.Drivers.Insert(driver);
|
||||
}
|
||||
return;
|
||||
}
|
||||
bool ready = true;
|
||||
foreach (ERR_HornFixtureDriver driver : ERR_HornFixture.Drivers)
|
||||
{
|
||||
// Vehicle compartments are registered asynchronously after spawning.
|
||||
if (!driver.Driver)
|
||||
{
|
||||
BaseVehicleControllerComponent controls = BaseVehicleControllerComponent.Cast(driver.Car.FindComponent(BaseVehicleControllerComponent));
|
||||
if (controls && controls.GetPilotCompartmentSlot())
|
||||
{
|
||||
AIGroup group = driver.Group;
|
||||
driver.Driver = controls.GetPilotCompartmentSlot().SpawnCharacterInCompartment(ERR_CivilianTraffic.DriverPrefab(), group, ERR_ConvoyResources.GroupPrefab());
|
||||
}
|
||||
}
|
||||
array<AIAgent> agents = {}; driver.Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents) { agent.SetPermanentLOD(0); agent.ActivateAI(); }
|
||||
if (driver.Started < 0 || System.GetTickCount() - driver.Started < 32000) ready = false;
|
||||
}
|
||||
if (!ready && System.GetTickCount() < m_ERRHornDeadline) return;
|
||||
GetGame().GetCallqueue().Remove(ERR_HornTest);
|
||||
foreach (ERR_HornFixtureDriver driver : ERR_HornFixture.Drivers)
|
||||
{
|
||||
PrintFormat("ERR HORN RESULT: prefab=%1 allowedFrames=%2 blockedFrames=%3 violations=%4", driver.Car.GetPrefabData().GetPrefabName(), driver.Allowed, driver.Blocked, driver.Violations);
|
||||
if (!ready || driver.Allowed == 0 || driver.Blocked == 0 || driver.Violations != 0) { Print("ERR SMOKE FAIL: native AI horn limit", LogLevel.ERROR); return; }
|
||||
Print("ERR SMOKE PASS: native AI driver continuous horn demand bounded with cooldown");
|
||||
}
|
||||
ERR_HornFixture.Drivers.Clear();
|
||||
Print("ERR SMOKE COMPLETE: native UAZ and civilian AI horn input limits");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"change": "Half-second horn pulses with 30 seconds between starts, scoped to authoritative AI road drivers in Escape. Player controls and the separate breach alarm are unchanged.",
|
||||
"policy_tests": [
|
||||
"test_ai_driver_horn.py",
|
||||
"test_vehicle_alarm.py",
|
||||
"test_civilian_traffic.py"
|
||||
],
|
||||
"native": {
|
||||
"horn01": {
|
||||
"complete": false,
|
||||
"passes": 0,
|
||||
"failures": [
|
||||
"15:30:50.026 SCRIPT (E): ERR SMOKE FAIL: horn driver seat"
|
||||
]
|
||||
},
|
||||
"horn02": {
|
||||
"complete": true,
|
||||
"passes": 2,
|
||||
"failures": []
|
||||
}
|
||||
},
|
||||
"fixture_correction": "First fixture queried the pilot compartment immediately after vehicle spawning. The final fixture waits for asynchronous seat registration and runs its demand injector outside the production control override.",
|
||||
"limits": "Injected continuous demand in actual UAZ and S105 driver control chains. No reproduction of the reported traffic scene, audible waveform measurement or remote-client qualification. Native engine asset errors are reported by the runner separately.",
|
||||
"published": true,
|
||||
"publication": "User confirmed successful Workshop upload; local publishing manifest confirms 0.0.129."
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
# Shovel body burial — native checks, 21 October 2026
|
||||
|
||||
Both isolated Workbench fixtures completed **11 assertions with no script/runtime
|
||||
failures**, first with the normal dependency closure and then with Realistic
|
||||
Combat Drones 2.3.7 also loaded. The latter also extends the character base prefab;
|
||||
the burial action remained available. Both runners verified unchanged source and
|
||||
external inputs and cleanup of their private prefix.
|
||||
|
||||
The native reporter still fails its strict whole-engine verdict: 13 retained
|
||||
resource/world errors in the standard run and 20 with drones. Full console logs,
|
||||
reports, input hashes and runner records are retained in [standard](standard/)
|
||||
and [with-drones](with-drones/). Tested production bytes are recorded in
|
||||
[production-sha256.json](production-sha256.json).
|
||||
|
||||
Observed in both runs:
|
||||
|
||||
- A character inherited the new action through its native actions manager.
|
||||
- A living character was ineligible, as was a corpse with an empty-handed caller.
|
||||
- A shovel placed in inventory did not enable burial; equipping the native
|
||||
entrenching-tool gadget did.
|
||||
- The nearby corpse passed real range/visibility checks. Moving it beyond reach
|
||||
rejected burial, as did invoking the action with a different owner.
|
||||
- The real action callback and deferred completion removed the corpse through
|
||||
the replicated entity deletion API, preserving the player and held shovel.
|
||||
- A repeated completion after deletion was harmless.
|
||||
|
||||
The fixture invokes the real action callback directly. It does not prove eight
|
||||
seconds of ordinary held input, interruption UI, remote-client replication or
|
||||
visual digging animation. The action uses native `Duration 8` progress and
|
||||
cancellation. Burial removes the body and remaining attached equipment; it does
|
||||
not create a grave prop or terrain deformation.
|
||||
|
||||
`tests/test_body_burial.py` executes production eligibility/completion against
|
||||
substituted native boundaries, covering unconscious/dead callers, living and
|
||||
unconscious targets, admission, mission state, held-item changes, vehicles,
|
||||
obstructions, distance and repeated deletion. Existing HQ tests retain delivered
|
||||
casualty attribution when a corpse disappears during radio transit.
|
||||
|
||||
Reproduce with the installed dependencies and existing private Workbench prefix:
|
||||
|
||||
```
|
||||
python3 docs/evidence/body-burial-2026-10-21/isolated-runner.py standard02
|
||||
python3 docs/evidence/body-burial-2026-10-21/isolated-runner.py compat02 --with-drones
|
||||
```
|
||||
|
||||
Use unused alphanumeric suffixes. The runner freezes source into `/tmp` and uses a
|
||||
disposable profile. No installed addon or live server is modified.
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Frozen local Workbench body burial fixture; no live server or installed asset writes."""
|
||||
from pathlib import Path
|
||||
import hashlib,json,shutil,subprocess,sys
|
||||
ROOT=Path(__file__).resolve().parents[3]
|
||||
sys.path.insert(0,str(ROOT/'tools'))
|
||||
from source_manifest import addon_manifest
|
||||
from headless_game import assert_prefix_idle
|
||||
from external_addons import parse_project
|
||||
name=sys.argv[1]
|
||||
absent="--with-drones" not in sys.argv[2:]
|
||||
if not name.isalnum(): raise ValueError('Use an alphanumeric run suffix')
|
||||
source=Path('/tmp/erri-body-burial-source-'+name)
|
||||
profile=Path('/tmp/erri-body-burial-run-'+name)
|
||||
compat=ROOT/'.local/playtest-compat-20260920'
|
||||
assert_prefix_idle(compat)
|
||||
source.mkdir();profile.mkdir()
|
||||
for rel in addon_manifest(ROOT):
|
||||
target=source/rel;target.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(ROOT/rel,target)
|
||||
shutil.copytree(ROOT/'tools',source/'tools',ignore=shutil.ignore_patterns('__pycache__'))
|
||||
(source/'docs').mkdir();shutil.copy2(ROOT/'docs/external-addons.json',source/'docs/external-addons.json')
|
||||
fixture=Path(__file__).with_name('native-fixture.c')
|
||||
shutil.copy2(fixture,source/'addons/core/Scripts/Game/ERR/00_Core/ERR_ZBurialFixture.c')
|
||||
# Opt-in fixture is independent of the historical generic Core smoke.
|
||||
p=source/'addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c'
|
||||
p.write_text(p.read_text().replace('array<string> independentFixtures = {','array<string> independentFixtures = {"body-burial", '))
|
||||
p=source/'tools/headless_workbench.py'
|
||||
p.write_text(p.read_text().replace("choices=['coordinated-ai',", "choices=['body-burial', 'coordinated-ai',").replace("a.smoke not in {'coordinated-ai',", "a.smoke not in {'body-burial', 'coordinated-ai',"))
|
||||
# The optional drone addon is added only for the compatibility variant to this isolated source's
|
||||
# exact dependency policy only. Runtime files are hashed and checked by the
|
||||
# normal runner, and all original dependency checks remain enabled.
|
||||
installed=Path.home()/'.local/share/Steam/steamapps/compatdata/1874880/pfx/drive_c/users/steamuser/Documents/My Games/ArmaReforger/addons'
|
||||
optional={'65AD60E204191D37':('Realistic Combat Drones','RealisticCombatDrones_65AD60E204191D37')}
|
||||
if absent: optional={}
|
||||
policy_file=source/'docs/external-addons.json';policy=json.loads(policy_file.read_text());allowed={}
|
||||
for guid,(title,folder) in optional.items():
|
||||
project=installed/folder/'addon.gproj';actual,deps=parse_project(project.read_text());assert actual==guid
|
||||
allowed[guid]=deps
|
||||
policy['addons'][guid]={'title':title,'version':'2.3.7; installed 2026-10-21','dependencies':sorted(deps),'project_sha256':hashlib.sha256(project.read_bytes()).hexdigest()}
|
||||
policy['direct_ai_dependencies'].append(guid)
|
||||
policy_file.write_text(json.dumps(policy,indent=2)+'\n')
|
||||
p=source/'tools/external_addons.py'
|
||||
s=p.read_text();i=s.index('\ndef require(')
|
||||
s=s[:i]+'\nALLOWED.update('+repr(allowed)+')\nDIRECT += '+repr(tuple(optional))+'\n'+s[i:];p.write_text(s)
|
||||
if not absent:
|
||||
p=source/'addons/core/addon.gproj';s=p.read_text().replace(' Dependencies {',' Dependencies {\n "65AD60E204191D37"');p.write_text(s)
|
||||
(profile/'profile').mkdir()
|
||||
(profile/'profile/EscapistsFactions.json').write_text(json.dumps({'escapists':'ussr','enemy':'us','independent':'fia'})+'\n')
|
||||
(profiles:=profile/'fixture-inputs.json').write_text(json.dumps({'fixture_sha256':hashlib.sha256(fixture.read_bytes()).hexdigest(),'production_sources':addon_manifest(ROOT),'source':str(source),'profile':str(profile)},indent=2)+'\n')
|
||||
command=[sys.executable,str(source/'tools/headless_workbench.py'),'--compat',str(compat),'--profile',str(profile),'--external-addons',str(installed),'--project','core','--smoke','body-burial','--rounds','1','--timeout','180']
|
||||
with (profile/'runner-output.log').open('w') as output:
|
||||
result=subprocess.run(command,stdout=output,stderr=subprocess.STDOUT)
|
||||
print(json.dumps({'exit':result.returncode,'source':str(source),'profile':str(profile)}))
|
||||
print('\n'.join((profile/'runner-output.log').read_text().splitlines()[-45:]))
|
||||
raise SystemExit(result.returncode)
|
||||
@@ -0,0 +1,85 @@
|
||||
#ifdef WORKBENCH
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
IEntity m_BurialPlayer, m_BurialBody, m_BurialShovel;
|
||||
ERR_BuryBodyAction m_BurialAction;
|
||||
float m_BurialDeadline;
|
||||
int m_BurialStage;
|
||||
bool m_BurialStopped;
|
||||
override protected void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
string kind;
|
||||
if (SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", kind) || kind != "body-burial") return;
|
||||
GetGame().GetCallqueue().CallLater(ERR_TestBurialTick, 100, true);
|
||||
}
|
||||
bool ERR_BurialCheck(bool condition, string label)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: burial " + label); return true; }
|
||||
Print("ERR SMOKE FAIL: burial " + label, LogLevel.ERROR);
|
||||
m_BurialStopped = true; GetGame().GetCallqueue().Remove(ERR_TestBurialTick);
|
||||
return false;
|
||||
}
|
||||
void ERR_TestBurialTick()
|
||||
{
|
||||
if (m_BurialStopped || !Active() || Phase() != ERR_Phase.PRISON) return;
|
||||
float now = GetWorld().GetWorldTime();
|
||||
if (m_BurialDeadline > 0 && now > m_BurialDeadline) { ERR_BurialCheck(false, "fixture stage deadline " + m_BurialStage); return; }
|
||||
if (m_BurialStage == 0)
|
||||
{
|
||||
m_BurialPlayer = GetGame().GetPlayerController().GetControlledEntity();
|
||||
if (!m_BurialPlayer || !Players().CurrentActor(m_BurialPlayer)) return;
|
||||
m_BurialBody = ERR_Actors.Spawn("{C31F210ABE078199}Prefabs/ERR/ReconPilotWest.et", ERR_Actors.Ground(m_BurialPlayer.GetOrigin() + Vector(1.2,0,0)));
|
||||
Own(m_BurialBody);
|
||||
if (!ERR_BurialCheck(m_BurialBody != null, "native character spawned")) return;
|
||||
BaseActionsManagerComponent actions = BaseActionsManagerComponent.Cast(m_BurialBody.FindComponent(BaseActionsManagerComponent));
|
||||
array<BaseUserAction> list = {}; actions.GetActionsList(list);
|
||||
foreach (BaseUserAction action : list) if (ERR_BuryBodyAction.Cast(action)) m_BurialAction = ERR_BuryBodyAction.Cast(action);
|
||||
if (!ERR_BurialCheck(m_BurialAction != null, "inherited body action exists in native manager")) return;
|
||||
if (!ERR_BurialCheck(!m_BurialAction.CanBeShownScript(m_BurialPlayer) && !ERR_Burial.Dead(m_BurialBody), "living character cannot be buried")) return;
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(m_BurialBody.FindComponent(DamageManagerComponent));
|
||||
damage.SetHealthScaled(0);
|
||||
m_BurialStage = 1; m_BurialDeadline = now + 10000; return;
|
||||
}
|
||||
if (m_BurialStage == 1)
|
||||
{
|
||||
if (!ERR_Burial.Dead(m_BurialBody)) return;
|
||||
if (!ERR_BurialCheck(!m_BurialAction.CanBeShownScript(m_BurialPlayer) && !ERR_Burial.Permitted(m_BurialPlayer, m_BurialBody), "empty hands reject corpse burial")) return;
|
||||
m_BurialShovel = ERR_Actors.Spawn("{6E35D94130954509}Prefabs/Items/Equipment/Accessories/ETool_ALICE/ETool_ALICE_FreeRoamBuilding_Gadget.et", m_BurialPlayer.GetOrigin());
|
||||
SCR_InventoryStorageManagerComponent inventory = SCR_InventoryStorageManagerComponent.Cast(m_BurialPlayer.FindComponent(SCR_InventoryStorageManagerComponent));
|
||||
if (!ERR_BurialCheck(m_BurialShovel && inventory.TryInsertItem(m_BurialShovel), "native shovel inserted into inventory")) return;
|
||||
m_BurialStage = 2; m_BurialDeadline = now + 10000; return;
|
||||
}
|
||||
if (m_BurialStage == 2)
|
||||
{
|
||||
if (!ERR_BurialCheck(!ERR_Burial.ShovelHeld(m_BurialPlayer) && !m_BurialAction.CanBeShownScript(m_BurialPlayer), "carried shovel alone is insufficient")) return;
|
||||
SCR_GadgetManagerComponent.GetGadgetManager(m_BurialPlayer).SetGadgetMode(m_BurialShovel, EGadgetMode.IN_HAND);
|
||||
m_BurialStage = 3; m_BurialDeadline = now + 15000; return;
|
||||
}
|
||||
if (m_BurialStage == 3)
|
||||
{
|
||||
if (!ERR_Burial.ShovelHeld(m_BurialPlayer)) return;
|
||||
PrintFormat("ERR BURIAL DIAG user=%1 corpse=%2 reason=%3", m_BurialPlayer.GetOrigin(), m_BurialBody.GetOrigin(), ERR_Burial.Rejection(m_BurialPlayer,m_BurialBody));
|
||||
if (!ERR_BurialCheck(m_BurialAction.CanBeShownScript(m_BurialPlayer) && m_BurialAction.CanBePerformedScript(m_BurialPlayer) && ERR_Burial.Permitted(m_BurialPlayer,m_BurialBody), "equipped shovel permits nearby native corpse")) return;
|
||||
vector near = m_BurialBody.GetOrigin();
|
||||
m_BurialBody.SetOrigin(near + Vector(10,0,0));
|
||||
if (!ERR_BurialCheck(!ERR_Burial.Permitted(m_BurialPlayer,m_BurialBody), "distant corpse is rejected")) return;
|
||||
m_BurialBody.SetOrigin(near);
|
||||
m_BurialAction.PerformAction(m_BurialPlayer, m_BurialPlayer);
|
||||
if (!ERR_BurialCheck(m_BurialBody != null, "wrong action owner cannot remove corpse")) return;
|
||||
// Real action callback; the duration is native UI configuration, not forged here.
|
||||
m_BurialAction.PerformAction(m_BurialBody, m_BurialPlayer);
|
||||
m_BurialStage = 4; m_BurialDeadline = now + 3000; return;
|
||||
}
|
||||
if (m_BurialStage == 4)
|
||||
{
|
||||
if (m_BurialBody) return;
|
||||
if (!ERR_BurialCheck(ERR_Actors.CanFight(m_BurialPlayer) && m_BurialShovel && ERR_Burial.ShovelHeld(m_BurialPlayer), "replicated corpse removal preserves player and held shovel")) return;
|
||||
ERR_Burial.Complete(m_BurialPlayer, m_BurialBody);
|
||||
Print("ERR SMOKE PASS: burial duplicate completion is harmless");
|
||||
Print("ERR SMOKE COMPLETE: body-burial");
|
||||
m_BurialStopped = true; GetGame().GetCallqueue().Remove(ERR_TestBurialTick);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"addons/core/Scripts/Game/ERR/00_Core/ERR_BuryBodyAction.c": "adbf4cee5b5f5317b185f17738205f28b701a15364e643cc9fb443866a121109",
|
||||
"addons/core/Prefabs/Characters/Core/Character_Base.et": "74a6f03a1e76173a0cfa20b23c711d02e70bac810d6743e16c60476bfe5be457",
|
||||
"addons/core/Prefabs/Characters/Core/Character_Base.et.meta": "c6b142d1985719236268cdf12ac522fee4b1206871a6f22b568d9198d1fcc077"
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
---------------------------------------------
|
||||
Log Z:\tmp\erri-body-burial-run-a01\logs\logs_2026-10-21_15-15-06\console.log started at 2026-10-21 15:15:06 (2026-10-21 13:15:06 UTC)
|
||||
|
||||
15:15:06.967 ENGINE : FileSystem: Adding relative directory 'Z:\tmp\erri-body-burial-run-a01\logs\logs_2026-10-21_15-15-06' to filesystem under name logs
|
||||
15:15:06.967 ENGINE : FileSystem: Adding relative directory 'Z:\tmp\erri-body-burial-run-a01\profile' to filesystem under name profile
|
||||
15:15:06.967 ENGINE : FileSystem: Current working directory: 'Z:\home\jakub\.local\share\Steam\steamapps\common\Arma Reforger Tools\Workbench\'
|
||||
15:15:06.969 PROFILING : Workbench Create Engine took: 9.383300 ms
|
||||
15:15:07.995 INIT : INIT : Workbench Init Engine
|
||||
15:15:07.995 ENGINE : Initializing engine, version 192142
|
||||
15:15:07.995 ENGINE : CLI Params: -gproj Z:/tmp/erri-body-burial-source-a01/addons/core/addon.gproj -addonsDir Z:/tmp/erri-body-burial-source-a01/addons -profile Z:/tmp/erri-body-burial-run-a01 -logconsole -wbModule WorldEditor -load Worlds/ERR_Everon.ent -plugin ERR_ValidateBaseWorldPlugin -errPlay 1 -errBaseSmoke 1 -errSmokeOutcome body-burial -noThrow -disableCrashReporter
|
||||
15:15:08.008 ENGINE : Addon dirs:
|
||||
15:15:08.008 ENGINE : dir: 'Z:/tmp/erri-body-burial-source-a01/addons/core/'
|
||||
15:15:08.008 ENGINE : dir: 'Z:/tmp/erri-body-burial-source-a01/addons'
|
||||
15:15:08.008 ENGINE : dir: './addons'
|
||||
15:15:08.008 ENGINE : dir: 'Z:/tmp/erri-body-burial-run-a01/addons'
|
||||
15:15:08.008 ENGINE : Available addons:
|
||||
15:15:08.008 ENGINE : gproj: 'Z:/tmp/erri-body-burial-source-a01/addons/core/addon.gproj' guid: '2528466DE220D91C'
|
||||
15:15:08.008 ENGINE : gproj: './addons/core/core.gproj' guid: '5614BBCCBB55ED1C' (packed)
|
||||
15:15:08.008 ENGINE : gproj: 'Z:/tmp/erri-body-burial-run-a01/addons/ArmaReforger/ArmaReforger.gproj' guid: '58D0FB3206B6F859' (packed)
|
||||
15:15:08.008 ENGINE : gproj: 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_1337C0DE5DABBEEF/addon.gproj' guid: '1337C0DE5DABBEEF' (packed)
|
||||
15:15:08.008 ENGINE : gproj: 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_595F2BF2F44836FB/addon.gproj' guid: '595F2BF2F44836FB' (packed)
|
||||
15:15:08.008 ENGINE : gproj: 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_5E389BB9F58B79A6/addon.gproj' guid: '5E389BB9F58B79A6' (packed)
|
||||
15:15:08.008 ENGINE : gproj: 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_5E524E4FEECCA92B/addon.gproj' guid: '5E524E4FEECCA92B' (packed)
|
||||
15:15:08.008 ENGINE : gproj: 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_629B2BA37EFFD577/addon.gproj' guid: '629B2BA37EFFD577' (packed)
|
||||
15:15:08.008 ENGINE : gproj: 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_65930CB4CD0237B2/addon.gproj' guid: '65930CB4CD0237B2' (packed)
|
||||
15:15:08.008 ENGINE : gproj: 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_BADC0DEDABBEDA5E/addon.gproj' guid: 'BADC0DEDABBEDA5E' (packed)
|
||||
15:15:08.008 ENGINE : FileSystem: Adding relative directory 'Z:\tmp\erri-body-burial-source-a01\addons\core' to filesystem under name ERR_Core
|
||||
15:15:08.009 ENGINE : FileSystem: Adding package 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_65930CB4CD0237B2/' (pak count: 1) to filesystem under name Parachutes
|
||||
15:15:08.010 ENGINE : FileSystem: Adding package 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_5E524E4FEECCA92B/' (pak count: 1) to filesystem under name WCS_BMP-1
|
||||
15:15:08.012 ENGINE : FileSystem: Adding package 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_5E389BB9F58B79A6/' (pak count: 1) to filesystem under name WCS_SpaceCore
|
||||
15:15:08.014 ENGINE : FileSystem: Adding package 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_629B2BA37EFFD577/' (pak count: 1) to filesystem under name WCS_Armaments
|
||||
15:15:08.018 ENGINE : FileSystem: Adding package 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_595F2BF2F44836FB/' (pak count: 1) to filesystem under name RHS_Core
|
||||
15:15:08.020 ENGINE : FileSystem: Adding package 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_BADC0DEDABBEDA5E/' (pak count: 2) to filesystem under name RHS_Content_02
|
||||
15:15:08.025 ENGINE : FileSystem: Adding package 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_1337C0DE5DABBEEF/' (pak count: 4) to filesystem under name RHS_Content_01
|
||||
15:15:08.129 ENGINE : FileSystem: Adding package 'Z:/tmp/erri-body-burial-run-a01/addons/ArmaReforger/' (pak count: 14) to filesystem under name ArmaReforger
|
||||
15:15:08.132 ENGINE : FileSystem: Adding package './addons/core/' (pak count: 2) to filesystem under name core
|
||||
15:15:08.132 ENGINE : CPU vendor : AuthenticAMD
|
||||
15:15:08.132 ENGINE : CPU type : 2
|
||||
15:15:08.132 ENGINE : CPU name : AMD Ryzen 7 3800XT 8-Core Processor
|
||||
15:15:08.132 ENGINE : CPU cache info : L1 [Inst 32KB, Data 32KB], L2 [Unif 512KB], L3 [Unif 16384KB]
|
||||
15:15:08.132 ENGINE : CPU count logical : 16
|
||||
15:15:08.132 ENGINE : CPU count physical : 8
|
||||
15:15:08.132 ENGINE : CPU features : mmx sse sse2 sse3 ssse3 sse4_1 sse4_2 popcnt aesni pclmulqdq osxsave avx sha avx2
|
||||
15:15:08.132 RESOURCES : ResourceDB: loading cache (id=0 name=ERR_Core path=Z:\tmp\erri-body-burial-source-a01\addons\core/resourceDatabase.rdb)
|
||||
15:15:08.132 RESOURCES : ResourceDB: loading cache (id=1 name=Parachutes path=Z:/tmp/erri-body-burial-run-a01/addons/ERRI_65930CB4CD0237B2/resourceDatabase.rdb)
|
||||
15:15:08.133 RESOURCES : ResourceDB: loading cache (id=2 name=WCS_BMP-1 path=Z:/tmp/erri-body-burial-run-a01/addons/ERRI_5E524E4FEECCA92B/resourceDatabase.rdb)
|
||||
15:15:08.133 RESOURCES : ResourceDB: loading cache (id=3 name=WCS_SpaceCore path=Z:/tmp/erri-body-burial-run-a01/addons/ERRI_5E389BB9F58B79A6/resourceDatabase.rdb)
|
||||
15:15:08.134 RESOURCES : ResourceDB: loading cache (id=4 name=WCS_Armaments path=Z:/tmp/erri-body-burial-run-a01/addons/ERRI_629B2BA37EFFD577/resourceDatabase.rdb)
|
||||
15:15:08.136 RESOURCES : ResourceDB: loading cache (id=5 name=RHS_Core path=Z:/tmp/erri-body-burial-run-a01/addons/ERRI_595F2BF2F44836FB/resourceDatabase.rdb)
|
||||
15:15:08.145 RESOURCES : ResourceDB: loading cache (id=6 name=RHS_Content_02 path=Z:/tmp/erri-body-burial-run-a01/addons/ERRI_BADC0DEDABBEDA5E/resourceDatabase.rdb)
|
||||
15:15:08.147 RESOURCES : ResourceDB: loading cache (id=7 name=RHS_Content_01 path=Z:/tmp/erri-body-burial-run-a01/addons/ERRI_1337C0DE5DABBEEF/resourceDatabase.rdb)
|
||||
15:15:08.154 RESOURCES : ResourceDB: loading cache (id=8 name=ArmaReforger path=Z:/tmp/erri-body-burial-run-a01/addons/ArmaReforger/resourceDatabase.rdb)
|
||||
15:15:08.287 RESOURCES : ResourceDB: loading cache (id=9 name=core path=./addons/core/resourceDatabase.rdb)
|
||||
15:15:08.288 RESOURCES : ResourceDB: loading cache (id=10 name=profile path=Z:\tmp\erri-body-burial-run-a01\profile/resourceDatabase.rdb)
|
||||
15:15:08.288 RESOURCES : ResourceDB: loading cache (id=11 name=logs path=Z:\tmp\erri-body-burial-run-a01\logs\logs_2026-10-21_15-15-06/resourceDatabase.rdb)
|
||||
15:15:08.442 RESOURCES : RESOURCES : ResourceDB: full scan
|
||||
15:15:08.448 RESOURCES : RESOURCES : scanning...
|
||||
15:15:08.448 RESOURCES : RESOURCES : scanning @"ERR_Core"
|
||||
15:15:08.470 RESOURCES : RESOURCES : scanning @"Parachutes"
|
||||
15:15:08.473 RESOURCES : RESOURCES : scanning @"WCS_BMP-1"
|
||||
15:15:08.476 RESOURCES : RESOURCES : scanning @"WCS_SpaceCore"
|
||||
15:15:08.478 RESOURCES : RESOURCES : scanning @"WCS_Armaments"
|
||||
15:15:08.484 RESOURCES : RESOURCES : scanning @"RHS_Core"
|
||||
15:15:08.495 RESOURCES : RESOURCES : scanning @"RHS_Content_02"
|
||||
15:15:08.499 RESOURCES : RESOURCES : scanning @"RHS_Content_01"
|
||||
15:15:08.507 RESOURCES : RESOURCES : scanning @"ArmaReforger"
|
||||
15:15:08.726 RESOURCES : RESOURCES : scanning @"core"
|
||||
15:15:08.731 RESOURCES : RESOURCES : scanning @"profile"
|
||||
15:15:08.731 RESOURCES : RESOURCES : scanning @"logs"
|
||||
15:15:08.731 RESOURCES : RESOURCES : processing meta-files...
|
||||
15:15:08.839 RESOURCES : RESOURCES : processing files...
|
||||
15:15:08.840 INIT : Workbench startup
|
||||
15:15:08.840 INIT : Workbench Init Engine
|
||||
15:15:08.840 RESOURCES : ResourceDB: full scan
|
||||
15:15:08.840 RESOURCES : processing files...
|
||||
15:15:08.840 RESOURCES (W): resource not registered: @"$RHS_Core:system/thermal/thermalProfileTreeCrownDefault.conf". Setting null GUID
|
||||
15:15:08.852 RESOURCES (W): resource not registered: @"$profile:EscapistsFactions.json". Setting null GUID
|
||||
15:15:08.852 RESOURCES (W): resource not registered: @"$logs:external-addons-inputs.json". Setting null GUID
|
||||
15:15:08.852 RESOURCES (W): resource not registered: @"$logs:launch-source-sha256.json". Setting null GUID
|
||||
15:15:08.852 RESOURCES (W): resource not registered: @"$logs:launch-source.json". Setting null GUID
|
||||
15:15:08.852 RESOURCES : RESOURCES : processing folders...
|
||||
15:15:08.860 RESOURCES : RESOURCES : creating GUID-item mapping...
|
||||
15:15:08.877 RESOURCES : RESOURCES : building tree...
|
||||
15:15:08.932 PROFILING : ResourceDB: full scan took: 489.685900 ms
|
||||
15:15:08.933 RESOURCES : ResourceDB: saving cache (id=0 name=ERR_Core path=Z:\tmp\erri-body-burial-source-a01\addons\core/resourceDatabase.rdb)
|
||||
15:15:08.935 RESOURCES : ResourceDB: written 36628/36628 bytes
|
||||
15:15:08.935 RESOURCES : ResourceDB: cache successfully saved
|
||||
15:15:08.935 RESOURCES : ResourceDB: saving cache (id=10 name=profile path=Z:\tmp\erri-body-burial-run-a01\profile/resourceDatabase.rdb)
|
||||
15:15:08.936 RESOURCES : ResourceDB: written 92/92 bytes
|
||||
15:15:08.936 RESOURCES : ResourceDB: cache successfully saved
|
||||
15:15:08.936 RESOURCES : ResourceDB: saving cache (id=11 name=logs path=Z:\tmp\erri-body-burial-run-a01\logs\logs_2026-10-21_15-15-06/resourceDatabase.rdb)
|
||||
15:15:08.938 RESOURCES : ResourceDB: written 208/208 bytes
|
||||
15:15:08.938 RESOURCES : ResourceDB: cache successfully saved
|
||||
15:15:08.938 ENGINE : GameProject load
|
||||
15:15:08.938 ENGINE : Loaded addons:
|
||||
15:15:08.938 ENGINE : gproj: './addons/core/core.gproj' guid: '5614BBCCBB55ED1C'
|
||||
15:15:08.938 ENGINE : gproj: 'Z:/tmp/erri-body-burial-run-a01/addons/ArmaReforger/ArmaReforger.gproj' guid: '58D0FB3206B6F859'
|
||||
15:15:08.938 ENGINE : gproj: 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_1337C0DE5DABBEEF/addon.gproj' guid: '1337C0DE5DABBEEF'
|
||||
15:15:08.938 ENGINE : gproj: 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_BADC0DEDABBEDA5E/addon.gproj' guid: 'BADC0DEDABBEDA5E'
|
||||
15:15:08.938 ENGINE : gproj: 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_595F2BF2F44836FB/addon.gproj' guid: '595F2BF2F44836FB'
|
||||
15:15:08.938 ENGINE : gproj: 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_629B2BA37EFFD577/addon.gproj' guid: '629B2BA37EFFD577'
|
||||
15:15:08.938 ENGINE : gproj: 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_5E389BB9F58B79A6/addon.gproj' guid: '5E389BB9F58B79A6'
|
||||
15:15:08.938 ENGINE : gproj: 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_5E524E4FEECCA92B/addon.gproj' guid: '5E524E4FEECCA92B'
|
||||
15:15:08.938 ENGINE : gproj: 'Z:/tmp/erri-body-burial-run-a01/addons/ERRI_65930CB4CD0237B2/addon.gproj' guid: '65930CB4CD0237B2'
|
||||
15:15:08.938 ENGINE : gproj: 'Z:/tmp/erri-body-burial-source-a01/addons/core/addon.gproj' guid: '2528466DE220D91C'
|
||||
15:15:08.940 ENGINE : GameProject load
|
||||
15:15:08.940 INIT : GameProject load project data @"Z:/tmp/erri-body-burial-run-a01/addons/ERRI_595F2BF2F44836FB/addon.gproj"
|
||||
15:15:08.940 RESOURCES (E): Wrong GUID/name for resource @"{0AFDD005CD669892}Language/rhs_localization.st" in property "StringTableSource"
|
||||
15:15:08.940 INIT : GameProject load project data @"Z:/tmp/erri-body-burial-run-a01/addons/ERRI_629B2BA37EFFD577/addon.gproj"
|
||||
15:15:08.940 RESOURCES (E): Wrong GUID/name for resource @"{41FA2A08B6971D4C}Language/wcs_armament_localization.st" in property "StringTableSource"
|
||||
15:15:08.940 INIT : GameProject load project data @"Z:/tmp/erri-body-burial-run-a01/addons/ERRI_5E389BB9F58B79A6/addon.gproj"
|
||||
15:15:08.940 RESOURCES (E): Wrong GUID/name for resource @"{84270E09F7A4A7A0}Language/wcs_spacecore_localization.st" in property "StringTableSource"
|
||||
15:15:08.941 INIT : GameProject load project data @"Z:/tmp/erri-body-burial-run-a01/addons/ERRI_5E524E4FEECCA92B/addon.gproj"
|
||||
15:15:08.941 RESOURCES (E): Wrong GUID/name for resource @"{E50E557B468C7073}Language/wcs_bmp1_localization.st" in property "StringTableSource"
|
||||
15:15:08.941 ENGINE : Loading custom engine user config '$profile:.save/app1874910_user76561198020202999/settings/ReforgerEngineSettings.conf'
|
||||
15:15:08.941 ENGINE : No Enfusion settings file loaded. Continuing with defaults
|
||||
15:15:09.041 RENDER : (OK) Adapter 'AMD Radeon RX 6950 XT (RADV NAVI21)' supports feature level 12_0
|
||||
15:15:09.043 RENDER : Basic screen setup:
|
||||
15:15:09.043 RENDER : Back buffer format : TEXFMT_R8G8B8A8_SRGB (29)
|
||||
15:15:09.043 RENDER : MSAA : none
|
||||
15:15:09.055 RENDER : AMD AGS library initialized, version 6.1.0 x DLL 6.1.0
|
||||
15:15:09.055 RENDER : Extendeded AMD info for AMD Radeon RX 6950 XT (RADV NAVI21) (primary device)
|
||||
15:15:09.055 RENDER : * architecture RDNA2, 80 CUs, 40 WGPs (for RDNA), 64 ROPs
|
||||
15:15:09.055 RENDER : * core clocks 2720 MHz, memory 1124 MHz
|
||||
15:15:09.055 RENDER : * TFlops 27.9, memory bandwidth (562.0 GB/s)
|
||||
15:15:09.055 RENDER : AMD AGS correctly initialized
|
||||
15:15:09.055 RENDER : GPU_ATI (0x1002) (AMD Radeon RX 6950 XT (RADV NAVI21)) with 1 GPU detected, VRAM 16128MB (fast 0MB), shared 64362MB, driver version 99.19.02-230831a-396538C-AMD-Software-Adrenalin-Edition
|
||||
15:15:09.055 RENDER : Detected output devices:
|
||||
15:15:09.055 RENDER : * 1 device/s connected to AMD Radeon RX 6950 XT (RADV NAVI21):
|
||||
15:15:09.055 RENDER : Generic Non-PnP Monitor (\\.\DISPLAY1), 1280x720 (offset 0x0), 30Hz
|
||||
15:15:09.055 RENDER : Concurrent frames limit 2
|
||||
15:15:09.128 RENDER : DX12 device successfully created by AMD AGS library
|
||||
15:15:09.128 RENDER : * user markers not supported in this version AMD SW
|
||||
15:15:09.128 RENDER : Graphics device features
|
||||
15:15:09.128 RENDER : Needed:
|
||||
15:15:09.128 RENDER : * Highest supported shader model: 6.8 (6.0 needed)
|
||||
15:15:09.128 RENDER : Optional:
|
||||
15:15:09.128 RENDER : * Allow tearing (VSYNC) : 1
|
||||
15:15:09.128 RENDER : * 16 bits : 1
|
||||
15:15:09.128 RENDER : * Resource heap tier : 3
|
||||
15:15:09.128 RENDER : * Persistent heaps (DRED) : 1
|
||||
15:15:09.128 RENDER : * DRED 1.2 is supported by compiler
|
||||
15:15:09.148 RENDER : Main RT format setup.
|
||||
15:15:09.148 RENDER : Main RT format for quality 0 : TEXFMT_R11G11B10_FLOAT (26)
|
||||
15:15:09.148 RENDER : Main RT format for quality 1 : TEXFMT_R16G16B16A16_FLOAT (10)
|
||||
15:15:09.150 ENGINE : Job system settings: coreCount=16, shortJobContextCount=16, shortJobThreadCount=15, longJobThreadCount=16
|
||||
15:15:09.177 PROFILING : Settings changed took: 0.016000 ms
|
||||
15:15:10.276 AUDIO : Endpoint: Speakers (PulseAudio Output)
|
||||
15:15:10.276 AUDIO : Format: Channels: 2, Sample rate: 48kHz, Avg. bytes: 384kB, Sample bits: 32, Block align: 8B
|
||||
15:15:10.276 AUDIO : Audio client:
|
||||
15:15:10.276 AUDIO : Format: Channels: 2, Sample rate: 48kHz, Buffer size: 1536B, Frame: 512
|
||||
15:15:10.279 RENDER : RENDER : Reload shaders
|
||||
15:15:10.279 RENDER : Enfusion shader version 13
|
||||
15:15:10.279 INIT : INIT : ShaderListReload
|
||||
15:15:10.279 PROFILING : ShaderListReload took: 0.000400 ms
|
||||
15:15:10.422 AUDIO : Endpoint: Speakers (PulseAudio Output)
|
||||
15:15:10.422 AUDIO : Format: Channels: 2, Sample rate: 48kHz, Avg. bytes: 384kB, Sample bits: 32, Block align: 8B
|
||||
15:15:10.422 AUDIO : Audio client:
|
||||
15:15:10.422 AUDIO : Format: Channels: 2, Sample rate: 48kHz, Buffer size: 1536B, Frame: 512
|
||||
15:15:10.474 RENDER : Loaded 3752 newer shader binaries
|
||||
15:15:10.474 PROFILING : Reload shaders took: 195.895400 ms
|
||||
15:15:10.474 ENGINE : Initializing inputs.
|
||||
15:15:10.485 NETWORK : Initializing networking.
|
||||
15:15:10.485 NETWORK : Using new GameRplScheduler.
|
||||
15:15:10.486 SCRIPT : SCRIPT : Initializing scripts
|
||||
15:15:10.486 SCRIPT : SCRIPT : ScriptProjectManager init
|
||||
15:15:12.010 PROFILING : ScriptProjectManager init took: 1524.112800 ms
|
||||
15:15:12.043 PROFILING : Initializing scripts took: 1556.934900 ms
|
||||
15:15:12.061 ENGINE : Enfusion engine successfully created.
|
||||
15:15:12.061 PROFILING : Workbench Init Engine took: 4066.896300 ms
|
||||
15:15:12.061 INIT : INIT : Workbench Create Game
|
||||
15:15:12.071 GUI : Using default language (en_us)
|
||||
15:15:12.071 GUI : Loading 'en_us' localization file.
|
||||
15:15:12.071 INIT : INIT : Loading StringTable
|
||||
15:15:12.098 PROFILING : Loading StringTable took: 26.499300 ms
|
||||
15:15:12.098 SCRIPT : SCRIPT : Compiling GameLib scripts
|
||||
15:15:12.143 SCRIPT : Module: Workbench; loaded 18x files; 36x classes; used 125K of static memory; defines: "ENF_WB,WORKBENCH,PLATFORM_WINDOWS,ENABLE_DIAG,ENF_ENABLE_VHC_OVERRIDE,ENF_ENABLE_RPLCOMPONENT_IN_GAMECODE"; CRC32: 90ce0460
|
||||
15:15:12.143 SCRIPT : Module: GameLib; loaded 466x files; 648x classes; used 828K of static memory; defines: "ENF_WB,WORKBENCH,PLATFORM_WINDOWS,ENABLE_DIAG,ENF_ENABLE_VHC_OVERRIDE,ENF_ENABLE_RPLCOMPONENT_IN_GAMECODE"; CRC32: d66caacb
|
||||
15:15:12.144 PROFILING : Compiling GameLib scripts took: 45.868300 ms
|
||||
15:15:12.144 SCRIPT : SCRIPT : Compiling Game scripts
|
||||
15:15:12.937 INIT : Workbench Create Game
|
||||
15:15:12.937 SCRIPT : Compiling Game scripts
|
||||
15:15:12.937 SCRIPT (W): @"Scripts/Game/Parachutes/ParachuteDeployed.c,7": Pointer type 'Physics' can only be used with local variables
|
||||
15:15:13.457 SCRIPT (W): @"Scripts/Game/Editor/Components/EditableEntity/SCR_EditableEntityComponent.c,785": 'GetPlayerPlatformKind' is obsolete
|
||||
15:15:13.457 SCRIPT (W): @"Scripts/Game/Editor/Components/EditableEntity/SCR_EditableEntityComponent.c,786": 'GetPlayerPlatformId' is obsolete
|
||||
15:15:13.833 SCRIPT (W): @"Scripts/Game/ERR/00_Core/ERR_SiteService.c,362": 'GetCompartmentType' is obsolete: Use GetType() on the compartment instance directly.
|
||||
15:15:13.847 SCRIPT (W): @"Scripts/Game/Workshop/SCR_AddonManager.c,876": 'GetRefCount' is obsolete: Will be removed
|
||||
15:15:13.890 SCRIPT (W): @"Scripts/Game/Campaign/SCR_PlayerProfileManagerComponent.c,142": 'PlayerData' is obsolete
|
||||
15:15:14.019 SCRIPT (W): @"Scripts/Game/UI/Backend/CareerBackend.c,159": 'GetDSSession' is obsolete
|
||||
15:15:14.019 SCRIPT (W): @"Scripts/Game/UI/Backend/CareerBackend.c,162": 'RoomID' is obsolete
|
||||
15:15:14.019 SCRIPT (W): @"Scripts/Game/UI/Backend/CareerBackend.c,197": 'GetDSSession' is obsolete
|
||||
15:15:14.019 SCRIPT (W): @"Scripts/Game/UI/Backend/CareerBackend.c,201": 'RoomID' is obsolete
|
||||
15:15:14.043 SCRIPT (W): @"Scripts/Game/ERR/00_Core/ERR_NativeVehicleSecurityProbe.c,77": 'GetCompartmentType' is obsolete: Use GetType() on the compartment instance directly.
|
||||
15:15:14.053 SCRIPT (W): @"Scripts/Game/ERR/00_Core/ERR_CasualtyGroundSupport.c,21": 'GetBoneIndex' is obsolete: Use GetAnimation().GetBoneIndex() instead
|
||||
15:15:14.122 SCRIPT (W): @"Scripts/Game/DataCollection/SCR_DataCollectorComponent.c,481": 'GetStorage' is obsolete
|
||||
15:15:14.122 SCRIPT (W): @"Scripts/Game/DataCollection/SCR_DataCollectorComponent.c,483": 'GetOnlineWritePrivilege' is obsolete
|
||||
15:15:14.124 SCRIPT (W): @"Scripts/Game/Utilities/SCR_PlayerIdentityUtils.c,20": 'GetPlayerIdentityId' is obsolete
|
||||
15:15:14.151 SCRIPT (W): @"Scripts/Game/ERR/00_Core/ERR_NativeVehicleSecurityProbe.c,186": 'GetCompartmentType' is obsolete: Use GetType() on the compartment instance directly.
|
||||
15:15:14.174 SCRIPT (W): @"Scripts/Game/ERR/00_Core/ERR_ExtractionService.c,300": 'GetCompartmentType' is obsolete: Use GetType() on the compartment instance directly.
|
||||
15:15:14.174 SCRIPT (W): @"Scripts/Game/ERR/00_Core/ERR_ExtractionService.c,312": 'GetCompartmentType' is obsolete: Use GetType() on the compartment instance directly.
|
||||
15:15:14.176 SCRIPT (W): @"Scripts/Game/UI/Menu/MainMenuUI.c,643": 'GetNotifyCount' is obsolete: Will be substitued by Notification Api.
|
||||
15:15:14.176 SCRIPT (W): @"Scripts/Game/UI/Menu/MainMenuUI.c,645": 'GetNotifyItem' is obsolete: Will be substitued by Notification Api.
|
||||
15:15:14.207 SCRIPT (W): @"Scripts/Game/ERR/00_Core/ERR_NativeTransportProbe.c,138": 'GetCompartmentType' is obsolete: Use GetType() on the compartment instance directly.
|
||||
15:15:14.242 SCRIPT (W): @"Scripts/Game/DataCollection/PlayerData/SCR_PlayerData.c,210": 'PlayerData' is obsolete
|
||||
15:15:14.611 SCRIPT : Module: WorkbenchGame; loaded 187x files; 658x classes; used 2755K of static memory; defines: "ENF_WB,WORKBENCH,PLATFORM_WINDOWS,ENABLE_DIAG,ENF_ENABLE_VHC_OVERRIDE,ENF_ENABLE_RPLCOMPONENT_IN_GAMECODE"; CRC32: b5662d84
|
||||
15:15:14.619 SCRIPT : Module: Game; loaded 6786x files; 14048x classes; used 43566K of static memory; defines: "ENF_WB,WORKBENCH,PLATFORM_WINDOWS,ENABLE_DIAG,ENF_ENABLE_VHC_OVERRIDE,ENF_ENABLE_RPLCOMPONENT_IN_GAMECODE,ENABLE_BASE_DESTRUCTION,BUDGET_OPTIMIZATION_CHECKS,SHOW_DISCLAIMER,DEBUG,RESPAWN_TIMER_COMPONENT_DEBUG,GAME_MODE_DEBUG,DEBUG_CONFIGURABLE_DIALOGS,DISABLE_WEAPON_SWITCHING,SCRIPTED_AIM_MODIFIER_DEBUG,WCS_DEFINES_ARMAMENTS,WCS_DEFINES_SPACECORE"; CRC32: a5ff8b81
|
||||
15:15:14.669 PROFILING : Compiling Game scripts took: 2525.325600 ms
|
||||
15:15:14.674 INIT : Creating game instance(ArmaReforgerScripted), version 1.8.0.13 built 2026-08-21 14:30:18 UTC.
|
||||
15:15:14.722 ENGINE : Game successfully created.
|
||||
15:15:14.723 PROFILING : Workbench Create Game took: 2662.208700 ms
|
||||
15:15:14.731 DEFAULT : Using thumbnail cache directory: C:/users/steamuser/AppData/Local/Temp/Arma Reforger Workbench/Projects/ERR_Core/thumbnails
|
||||
15:15:15.598 INIT : INIT : Workbench Reload Game
|
||||
15:15:15.599 ENGINE : Game destroyed.
|
||||
15:15:15.605 GUI : Using default language (en_us)
|
||||
15:15:15.605 GUI : Loading 'en_us' localization file.
|
||||
15:15:15.605 INIT : INIT : Loading StringTable
|
||||
15:15:15.615 PROFILING : Loading StringTable took: 9.685800 ms
|
||||
15:15:15.615 INIT : Creating game instance(ArmaReforgerScripted), version 1.8.0.13 built 2026-08-21 14:30:18 UTC.
|
||||
15:15:15.623 SCRIPT : Unable to register a script command due keyword duplicity: ban
|
||||
15:15:15.623 SCRIPT : Unable to register a script command due keyword duplicity: kick
|
||||
15:15:15.623 ENGINE : Game successfully created.
|
||||
15:15:15.623 ENGINE : Game successfully initialized.
|
||||
15:15:15.623 PROFILING : Workbench Reload Game took: 25.231900 ms
|
||||
15:15:16.292 WORLD : WORLD : Entities load '$ERR_Core:Worlds/ERR_Everon.ent'
|
||||
15:15:17.284 PROFILING : Entities load took: 991.532500 ms
|
||||
15:15:17.294 WORLD : WORLD : CreateEntities
|
||||
15:15:17.300 WORLD : WORLD : NotifyBeforeEntitiesCreated
|
||||
15:15:17.300 PROFILING : NotifyBeforeEntitiesCreated took: 0.003300 ms
|
||||
15:15:17.301 WORLD : WORLD : Wait for preloading data
|
||||
15:15:17.301 PROFILING : Wait for preloading data took: 0.003400 ms
|
||||
15:15:17.425 ENTITY : Create entity @"ENTITY:2" ('Eden', GenericTerrainEntity) at <0.000000 0.000000 0.000000> @"{5AD924374E8C7AF6}Prefabs/World/Game/Eden/GenericTerrain_Eden.et"
|
||||
15:15:17.425 PROFILING : PROFILING : Terrain load
|
||||
15:15:18.950 PROFILING : Terrain load took: 1524.010300 ms
|
||||
15:15:22.751 WORLD : CreateEntities
|
||||
15:15:22.752 ENTITY : Create entity @"ENTITY:610406" ('StaticModelEntity','Assets/Structures/Infrastructure/Towers/TransmitterTower_01/TransmitterTower_01_small.xob') at <1931.796021 81.777000 5138.725098> @"{6A004A8F0571D456}Prefabs/Structures/Infrastructure/Towers/TransmitterTower_01/TransmitterTower_01_small.et"
|
||||
15:15:22.752 ENTITY : CreateComponent 'ScriptedRadioComponent'
|
||||
15:15:22.752 DEFAULT (W): World doesn't contain RadioManagerEntity to support any BaseRadioComponent.
|
||||
15:15:32.063 WORLD : WORLD : NotifyBeforeEntitiesInitialized
|
||||
15:15:32.063 PROFILING : NotifyBeforeEntitiesInitialized took: 0.006100 ms
|
||||
15:15:32.479 WORLD : InitEntities 'world'
|
||||
15:15:32.479 ENTITY : Init entity @"ENTITY:2305843009213693953" ('Reintroduced', ERR_GameMode) at <60.452000 1.000000 213.136002> @"{978E045E53EF36C8}Prefabs/ERR/GameMode.et"
|
||||
15:15:32.479 SCRIPT : Reintroduced [config]: using $profile:Escapists.json; legacy files retained but no longer used.
|
||||
15:15:32.481 WORLD : WORLD : NotifyEntitiesInitialized
|
||||
15:15:32.481 TERRAIN : TERRAIN : Initializing road networks...
|
||||
15:15:32.483 PROFILING : Initializing road networks... took: 2.119700 ms
|
||||
15:15:32.484 PROFILING : NotifyEntitiesInitialized took: 2.394700 ms
|
||||
15:15:32.484 PROFILING : CreateEntities took: 15188.859300 ms
|
||||
15:15:32.642 PROFILING : * WorldEditor: LoadWorld: 16354.865800 ms
|
||||
15:15:34.045 TERRAIN : Loading tile map config (worlds/Eden/Eden/tileMap.conf)
|
||||
15:15:34.045 TERRAIN : Loading minimap config (worlds/Eden/Eden/miniMap.conf)
|
||||
15:15:34.046 TERRAIN : The default minimap texture ('worlds/Eden/EdenRasterized.edds') not found. Trying 'worlds/Eden/Eden.edds'...
|
||||
15:15:34.046 TERRAIN : The default minimap texture ('worlds/Eden/Eden.edds') not found. Skipping...
|
||||
15:15:34.137 SCRIPT : ERR SITE SURVEY: supply=6150/7050 elevationRange=0.625 min=202.875 max=203.5
|
||||
15:15:34.162 BACKEND : Attempting online Game Config instead.
|
||||
15:15:37.072 INIT : INIT : Workbench Reload Game
|
||||
15:15:37.074 ENGINE : Game destroyed.
|
||||
15:15:37.080 GUI : Using default language (en_us)
|
||||
15:15:37.080 GUI : Loading 'en_us' localization file.
|
||||
15:15:37.080 INIT : INIT : Loading StringTable
|
||||
15:15:37.104 PROFILING : Loading StringTable took: 23.502800 ms
|
||||
15:15:37.104 INIT : Creating game instance(ArmaReforgerScripted), version 1.8.0.13 built 2026-08-21 14:30:18 UTC.
|
||||
15:15:37.113 SCRIPT : Unable to register a script command due keyword duplicity: ban
|
||||
15:15:37.113 SCRIPT : Unable to register a script command due keyword duplicity: kick
|
||||
15:15:37.113 ENGINE : Game successfully created.
|
||||
15:15:37.113 ENGINE : Game successfully initialized.
|
||||
15:15:37.113 PROFILING : Workbench Reload Game took: 41.695100 ms
|
||||
15:15:37.116 DEFAULT (W): LogiLED: Cannot initialize! (service not running or no compatible device detected)
|
||||
15:15:37.232 PLATFORM : Save data from container 'settings' have been loaded
|
||||
15:15:37.322 PLATFORM : Save data from container 'sessions' have been loaded
|
||||
15:15:37.419 DEFAULT : [SaveGameManager] Starting new playthrough nr.0 '' for mission '{B6C516F86302C679}Worlds/ERR_Everon.ent'.
|
||||
15:15:37.427 WORLD : WORLD : Game::LoadEntities
|
||||
15:15:37.427 WORLD : WORLD : Initializing world systems @"{1C60D2EDA2B468B8}Configs/Systems/BaseGameModeSystems.conf"
|
||||
15:15:37.429 NETWORK : Enabled lag compensation, 29 frames with 100 FPS and 280 ms rewind limits.
|
||||
15:15:37.429 WORLD : WORLD : CreateEntities
|
||||
15:15:37.429 WORLD : WORLD : CreateEntities
|
||||
15:15:37.429 WORLD : WORLD : NotifyBeforeEntitiesCreated
|
||||
15:15:37.434 RESOURCES : Loaded mission headers from 10 addon(s)!
|
||||
15:15:37.434 PROFILING : NotifyBeforeEntitiesCreated took: 5.399000 ms
|
||||
15:15:37.434 WORLD : WORLD : Wait for preloading data
|
||||
15:15:37.434 PROFILING : Wait for preloading data took: 0.001000 ms
|
||||
15:15:41.822 WORLD : Game::LoadEntities
|
||||
15:15:41.822 WORLD : CreateEntities
|
||||
15:15:41.822 WORLD : CreateEntities
|
||||
15:15:41.822 ENTITY : Create entity @"ENTITY:610406" ('StaticModelEntity','Assets/Structures/Infrastructure/Towers/TransmitterTower_01/TransmitterTower_01_small.xob') at <1931.796021 81.777000 5138.725098> @"{6A004A8F0571D456}Prefabs/Structures/Infrastructure/Towers/TransmitterTower_01/TransmitterTower_01_small.et"
|
||||
15:15:41.822 ENTITY : CreateComponent 'ScriptedRadioComponent'
|
||||
15:15:41.822 DEFAULT (W): World doesn't contain RadioManagerEntity to support any BaseRadioComponent.
|
||||
15:15:43.281 ENTITY : Create entity @"ENTITY:842066" ('GameEntity','Assets/Structures/BuildingsParts/Lights/LampTower_01.xob') at <1080.213013 10.776751 6018.347168>
|
||||
15:15:43.281 ENTITY : CreateComponent 'Hierarchy'
|
||||
15:15:43.281 ENTITY (W): Invalid PivotID 'socket_light_04' in Hierarchy component ('@"ENTITY:842066" ('GameEntity','Assets/Structures/BuildingsParts/Lights/LampTower_01.xob') at <1080.213013 10.776751 6018.347168>')
|
||||
15:15:43.281 ENTITY : Create entity @"ENTITY:842068" ('GameEntity','Assets/Structures/BuildingsParts/Lights/LampTower_01.xob') at <1082.185913 10.778749 6018.331055>
|
||||
15:15:43.281 ENTITY : CreateComponent 'Hierarchy'
|
||||
15:15:43.281 ENTITY (W): Invalid PivotID 'socket_light_04' in Hierarchy component ('@"ENTITY:842068" ('GameEntity','Assets/Structures/BuildingsParts/Lights/LampTower_01.xob') at <1082.185913 10.778749 6018.331055>')
|
||||
15:15:43.281 ENTITY : Create entity @"ENTITY:842070" ('GameEntity','Assets/Structures/BuildingsParts/Lights/LampTower_01.xob') at <1084.190918 10.790751 6018.334961>
|
||||
15:15:43.281 ENTITY : CreateComponent 'Hierarchy'
|
||||
15:15:43.281 ENTITY (W): Invalid PivotID 'socket_light_04' in Hierarchy component ('@"ENTITY:842070" ('GameEntity','Assets/Structures/BuildingsParts/Lights/LampTower_01.xob') at <1084.190918 10.790751 6018.334961>')
|
||||
15:15:43.286 ENTITY : Create entity @"ENTITY:842333" ('GameEntity','Assets/Structures/BuildingsParts/Lights/LampTower_01.xob') at <4425.594727 9.981750 11029.402344>
|
||||
15:15:43.286 ENTITY : CreateComponent 'Hierarchy'
|
||||
15:15:43.286 ENTITY (W): Invalid PivotID 'socket_light_04' in Hierarchy component ('@"ENTITY:842333" ('GameEntity','Assets/Structures/BuildingsParts/Lights/LampTower_01.xob') at <4425.594727 9.981750 11029.402344>')
|
||||
15:15:43.286 ENTITY : Create entity @"ENTITY:842335" ('GameEntity','Assets/Structures/BuildingsParts/Lights/LampTower_01.xob') at <4426.584473 9.983749 11031.109375>
|
||||
15:15:43.286 ENTITY : CreateComponent 'Hierarchy'
|
||||
15:15:43.286 ENTITY (W): Invalid PivotID 'socket_light_04' in Hierarchy component ('@"ENTITY:842335" ('GameEntity','Assets/Structures/BuildingsParts/Lights/LampTower_01.xob') at <4426.584473 9.983749 11031.109375>')
|
||||
15:15:43.287 ENTITY : Create entity @"ENTITY:842337" ('GameEntity','Assets/Structures/BuildingsParts/Lights/LampTower_01.xob') at <4427.572266 9.995750 11032.853516>
|
||||
15:15:43.287 ENTITY : CreateComponent 'Hierarchy'
|
||||
15:15:43.287 ENTITY (W): Invalid PivotID 'socket_light_04' in Hierarchy component ('@"ENTITY:842337" ('GameEntity','Assets/Structures/BuildingsParts/Lights/LampTower_01.xob') at <4427.572266 9.995750 11032.853516>')
|
||||
15:15:43.290 ENTITY : Create entity @"ENTITY:842591" ('GameEntity','Assets/Structures/BuildingsParts/Lights/LampTower_01.xob') at <10009.946289 10.470250 1551.986084>
|
||||
15:15:43.290 ENTITY : CreateComponent 'Hierarchy'
|
||||
15:15:43.290 ENTITY (W): Invalid PivotID 'socket_light_04' in Hierarchy component ('@"ENTITY:842591" ('GameEntity','Assets/Structures/BuildingsParts/Lights/LampTower_01.xob') at <10009.946289 10.470250 1551.986084>')
|
||||
15:15:43.291 ENTITY : Create entity @"ENTITY:842593" ('GameEntity','Assets/Structures/BuildingsParts/Lights/LampTower_01.xob') at <10009.430664 10.472250 1550.081421>
|
||||
15:15:43.291 ENTITY : CreateComponent 'Hierarchy'
|
||||
15:15:43.291 ENTITY (W): Invalid PivotID 'socket_light_04' in Hierarchy component ('@"ENTITY:842593" ('GameEntity','Assets/Structures/BuildingsParts/Lights/LampTower_01.xob') at <10009.430664 10.472250 1550.081421>')
|
||||
15:15:43.291 ENTITY : Create entity @"ENTITY:842595" ('GameEntity','Assets/Structures/BuildingsParts/Lights/LampTower_01.xob') at <10008.926758 10.484250 1548.140869>
|
||||
15:15:43.291 ENTITY : CreateComponent 'Hierarchy'
|
||||
15:15:43.291 ENTITY (W): Invalid PivotID 'socket_light_04' in Hierarchy component ('@"ENTITY:842595" ('GameEntity','Assets/Structures/BuildingsParts/Lights/LampTower_01.xob') at <10008.926758 10.484250 1548.140869>')
|
||||
15:15:48.042 ENTITY : Create entity @"ENTITY:2305843009213693956" ('Factions', SCR_FactionManager) at <0.000000 0.000000 0.000000> @"{5414041A15DC4B54}Prefabs/ERR/FactionManager.et"
|
||||
15:15:48.042 RESOURCES : GetResourceObject @"{442939C9617DF228}Prefabs/Vehicles/Wheeled/BRDM2/BRDM2_FIA.et"
|
||||
15:15:48.042 WORLD : Entity prefab load @"{442939C9617DF228}Prefabs/Vehicles/Wheeled/BRDM2/BRDM2_FIA.et"
|
||||
15:15:48.042 RESOURCES : GetResourceObject @"{532B5F3CF8496E59}Prefabs/Vehicles/Wheeled/BRDM2/BRDM2_base.et"
|
||||
15:15:48.042 WORLD : Entity prefab load @"{532B5F3CF8496E59}Prefabs/Vehicles/Wheeled/BRDM2/BRDM2_base.et"
|
||||
15:15:48.042 RESOURCES : GetResourceObject @"{532B5F3CF8496E59}Prefabs/Vehicles/Wheeled/BRDM2/BRDM2_base.et"
|
||||
15:15:48.042 WORLD : Entity prefab load @"{532B5F3CF8496E59}Prefabs/Vehicles/Wheeled/BRDM2/BRDM2_base.et"
|
||||
15:15:48.042 WORLD (E): Unknown keyword/data 'Parent' at offset 35955(0x8c73)
|
||||
15:15:48.042 WORLD (E): Unknown keyword/data 'Parent' at offset 36915(0x9033)
|
||||
15:15:48.043 RESOURCES : GetResourceObject @"{B47110AA1A806556}Prefabs/Vehicles/Wheeled/BTR70/BTR70_FIA.et"
|
||||
15:15:48.043 WORLD : Entity prefab load @"{B47110AA1A806556}Prefabs/Vehicles/Wheeled/BTR70/BTR70_FIA.et"
|
||||
15:15:48.043 RESOURCES : GetResourceObject @"{B47110AA1A806556}Prefabs/Vehicles/Wheeled/BTR70/BTR70_FIA.et"
|
||||
15:15:48.043 WORLD : Entity prefab load @"{B47110AA1A806556}Prefabs/Vehicles/Wheeled/BTR70/BTR70_FIA.et"
|
||||
15:15:48.043 RESOURCES : GetResourceObject @"{1C5FE7B7FF49BB8D}Prefabs/Vehicles/Wheeled/BTR70/BTR70_Base.et"
|
||||
15:15:48.043 WORLD : Entity prefab load @"{1C5FE7B7FF49BB8D}Prefabs/Vehicles/Wheeled/BTR70/BTR70_Base.et"
|
||||
15:15:48.043 RESOURCES : GetResourceObject @"{1C5FE7B7FF49BB8D}Prefabs/Vehicles/Wheeled/BTR70/BTR70_Base.et"
|
||||
15:15:48.043 WORLD : Entity prefab load @"{1C5FE7B7FF49BB8D}Prefabs/Vehicles/Wheeled/BTR70/BTR70_Base.et"
|
||||
15:15:48.043 WORLD (E): Unknown keyword/data 'Parent' at offset 42646(0xa696)
|
||||
15:15:48.043 WORLD (E): Unknown keyword/data 'Parent' at offset 43645(0xaa7d)
|
||||
15:15:48.043 WORLD (E): Unknown keyword/data 'Parent' at offset 44617(0xae49)
|
||||
15:15:48.043 WORLD (E): Unknown keyword/data 'Parent' at offset 45590(0xb216)
|
||||
15:15:48.163 RESOURCES : GetResourceObject @"{F6B23D17D5067C11}Prefabs/Vehicles/Wheeled/M151A2/M151A2_M2HB.et"
|
||||
15:15:48.163 WORLD : Entity prefab load @"{F6B23D17D5067C11}Prefabs/Vehicles/Wheeled/M151A2/M151A2_M2HB.et"
|
||||
15:15:48.163 RESOURCES : GetResourceObject @"{F649585ABB3706C4}Prefabs/Vehicles/Wheeled/M151A2/M151A2.et"
|
||||
15:15:48.163 WORLD : Entity prefab load @"{F649585ABB3706C4}Prefabs/Vehicles/Wheeled/M151A2/M151A2.et"
|
||||
15:15:48.163 RESOURCES : GetResourceObject @"{F649585ABB3706C4}Prefabs/Vehicles/Wheeled/M151A2/M151A2.et"
|
||||
15:15:48.163 WORLD : Entity prefab load @"{F649585ABB3706C4}Prefabs/Vehicles/Wheeled/M151A2/M151A2.et"
|
||||
15:15:48.163 RESOURCES : GetResourceObject @"{F649585ABB3706C4}Prefabs/Vehicles/Wheeled/M151A2/M151A2.et"
|
||||
15:15:48.163 WORLD : Entity prefab load @"{F649585ABB3706C4}Prefabs/Vehicles/Wheeled/M151A2/M151A2.et"
|
||||
15:15:48.163 WORLD (E): Unknown keyword/data 'SlidingTrackMaterial' at offset 19441(0x4bf1)
|
||||
15:15:48.788 WORLD : WORLD : NotifyBeforeEntitiesInitialized
|
||||
15:15:48.788 PROFILING : NotifyBeforeEntitiesInitialized took: 0.031100 ms
|
||||
15:15:50.918 WORLD : InitEntities 'world'
|
||||
15:15:50.918 ENTITY : Init entity @"ENTITY:2305843009213693953" ('Reintroduced', ERR_GameMode) at <60.452000 1.000000 213.136002> @"{978E045E53EF36C8}Prefabs/ERR/GameMode.et"
|
||||
15:15:50.918 SCRIPT : Reintroduced [loading]: respawn animation and preparation display attached to Escape
|
||||
15:15:50.934 WORLD : WORLD : NotifyEntitiesInitialized
|
||||
15:15:50.934 TERRAIN : TERRAIN : Initializing road networks...
|
||||
15:15:50.940 PROFILING : Initializing road networks... took: 6.146400 ms
|
||||
15:15:50.940 DEFAULT : DEFAULT : WorldVolumesCollection
|
||||
15:15:50.997 PROFILING : WorldVolumesCollection took: 56.683800 ms
|
||||
15:15:50.997 INIT : INIT : SoundWorldInit
|
||||
15:15:51.010 PROFILING : SoundWorldInit took: 13.307400 ms
|
||||
15:15:51.011 PROFILING : NotifyEntitiesInitialized took: 76.405100 ms
|
||||
15:15:51.011 PROFILING : CreateEntities took: 13581.363100 ms
|
||||
15:15:51.011 PROFILING : CreateEntities took: 13581.506500 ms
|
||||
15:15:51.011 WORLD : WORLD : Wait for preloading data around camera
|
||||
15:15:51.011 PROFILING : Wait for preloading data around camera took: 0.000900 ms
|
||||
15:15:51.011 WORLD : WORLD : EOnActivate
|
||||
15:15:51.043 PROFILING : EOnActivate took: 31.667400 ms
|
||||
15:15:51.043 PROFILING : Game::LoadEntities took: 13615.408100 ms
|
||||
15:15:51.326 DEFAULT : Entered offline game state.
|
||||
15:15:55.616 SCRIPT : Map load time: 2495ms
|
||||
15:15:55.617 NETWORK : Player connected: connectionID=2147483646
|
||||
15:15:55.617 NETWORK : ### Connecting player: connectionID=2147483646, Name=
|
||||
15:15:55.618 NETWORK : ### Creating player: PlayerId=1, Name=jakub
|
||||
15:15:55.621 RESOURCES : GetResourceObject @"{225E51284CC95CFA}Prefabs/Characters/Core/DefaultPlayerControllerMP.et"
|
||||
15:15:55.621 WORLD : Entity prefab load @"{225E51284CC95CFA}Prefabs/Characters/Core/DefaultPlayerControllerMP.et"
|
||||
15:15:55.621 RESOURCES : GetResourceObject @"{6E2BB64764E3BE9B}Prefabs/Characters/Core/DefaultPlayerController.et"
|
||||
15:15:55.621 WORLD : Entity prefab load @"{6E2BB64764E3BE9B}Prefabs/Characters/Core/DefaultPlayerController.et"
|
||||
15:15:55.621 WORLD (E): Unknown keyword/data 'm_HardLandingVelocity' at offset 133(0x85)
|
||||
15:15:55.622 WORLD (E): Unknown keyword/data 'm_DeathLandingVelocity' at offset 163(0xa3)
|
||||
15:15:55.625 ENTITY : SpawnEntityPrefab @"{225E51284CC95CFA}Prefabs/Characters/Core/DefaultPlayerControllerMP.et"
|
||||
15:15:55.626 ENTITY : Create entity @"ENTITY:4611686018427388386" ('SCR_PlayerController') at <0.000000 0.000000 0.000000>
|
||||
15:15:55.626 ENTITY : EntityPrefabUse @"{225E51284CC95CFA}Prefabs/Characters/Core/DefaultPlayerControllerMP.et"
|
||||
15:15:55.626 SCRIPT (W): Notification data in 'SCR_NotificationsLogDisplay' has duplicate notification info key: 'EDITOR_PERCEIVED_FACTION_PUNISHMENT_KILLING_SET'. There should only be one of each key!
|
||||
15:15:55.626 SCRIPT (W): Notification data in 'SCR_NotificationsLogDisplay' has duplicate notification info key: 'EDITOR_PERCEIVED_FACTION_TYPE_DISABLED'. There should only be one of each key!
|
||||
15:15:55.628 SCRIPT (W): Can't open config file ""
|
||||
15:15:55.634 WORLD : InitEntities 'world'
|
||||
15:15:55.634 ENTITY : Init entity @"ENTITY:4611686018427388386" ('SCR_PlayerController') at <0.000000 0.000000 0.000000> @"{225E51284CC95CFA}Prefabs/Characters/Core/DefaultPlayerControllerMP.et"
|
||||
15:15:55.634 GUI : WidgetManager: CrateWidgets @"{3C9DCDD76AB83157}UI/layouts/HUD/HUDLayouts/HUDManager_Root.layout"
|
||||
15:15:55.634 SCRIPT (W): Slot_WeaponInfo Has no Content!
|
||||
15:15:55.634 SCRIPT (W): Slot_Chat Has no Content!
|
||||
15:15:55.634 SCRIPT (W): Slot_VON Has no Content!
|
||||
15:15:55.635 SCRIPT (W): Slot_AvailableActions Has no Content!
|
||||
15:15:55.635 SCRIPT (W): Slot_Vehicle Has no Content!
|
||||
15:15:55.635 SCRIPT (W): Slot_GameVersion Has no Content!
|
||||
15:15:55.635 SCRIPT (W): Slot_Notifications Has no Content!
|
||||
15:15:55.635 SCRIPT (W): Slot_Hints Has no Content!
|
||||
15:15:55.649 ENTITY : SpawnEntityPrefab @"{59EF8ECAE1DCD417}Prefabs/Editor/Modes/EditorModeEdit.et"
|
||||
15:15:55.649 ENTITY : Create entity @"ENTITY:4611686018427388389" ('SCR_EditorModeEntity') at <0.000000 0.000000 0.000000>
|
||||
15:15:55.650 ENTITY : EntityPrefabUse @"{59EF8ECAE1DCD417}Prefabs/Editor/Modes/EditorModeEdit.et"
|
||||
15:15:55.650 SCRIPT (W): SCR_EntityCatalogManagerComponent.GetFilteredEditorPrefabsOfAllFactions: Detected that {259EE7B78C51B624}UAZ469.et is present in at least two different faction entity catalogs! Since this may cause issues, such as dupliacted entries in build mode, one of them will be discarded! This is only supported for items. Other things require creation of faction specific variants.
|
||||
15:15:55.651 SCRIPT (W): SCR_EntityCatalogManagerComponent.GetFilteredEditorPrefabsOfAllFactions: Detected that {DF5CCB7C0FF049F4}Mi8MT_unarmed_transport.et is present in at least two different faction entity catalogs! Since this may cause issues, such as dupliacted entries in build mode, one of them will be discarded! This is only supported for items. Other things require creation of faction specific variants.
|
||||
15:15:55.652 SCRIPT (W): SCR_EntityCatalogManagerComponent.GetFilteredEditorPrefabsOfAllFactions: Detected that {DF5CCB7C0FF049F4}Mi8MT_unarmed_transport.et is present in at least two different faction entity catalogs! Since this may cause issues, such as dupliacted entries in build mode, one of them will be discarded! This is only supported for items. Other things require creation of faction specific variants.
|
||||
15:15:55.656 NETWORK : ### Updating player: PlayerId=1, Name=jakub, rplIdentity=0xFFFFFFFE, IdentityId=00000000-0000-0000-0000-000000000000
|
||||
15:15:55.657 SCRIPT (W): Ban Settings could not be loaded from BE Storage: Relying on default config
|
||||
15:15:55.657 NETWORK : Players connecting: 0
|
||||
15:15:55.658 NETWORK : Players connected: 1 / 1
|
||||
15:15:55.687 DEFAULT : Using autosave directory: C:/users/steamuser/AppData/Local/Temp/Arma Reforger Workbench/Projects/ERR_Core/autosaves
|
||||
15:15:55.688 PROFILING : Workbench startup took: 48776.488600 ms
|
||||
15:15:55.806 WORLD : Frame start
|
||||
15:15:55.807 SCRIPT : ScenarioFramework: Available tasks are empty, no new tasks will be generated.
|
||||
15:15:55.820 WORLD : UpdateEntities
|
||||
15:15:55.820 WORLD : Frame
|
||||
15:15:55.820 SCRIPT : SCR_BaseGameMode::OnGameStateChanged = GAME
|
||||
15:15:56.002 WORLD : UpdateEntities
|
||||
15:15:56.002 WORLD : PostFrame
|
||||
15:15:56.002 GUI : WidgetManager: CrateWidgets @"{D89BBA18F99053D6}UI/layouts/HUD/Chat/ChatHud.layout"
|
||||
15:15:56.002 SCRIPT (W): EditBoxFilterComponent used on invalid widget type.
|
||||
15:15:56.047 RESOURCES : GetResourceObject @"{20A6917D2114E29A}UI/layouts/HUD/Hint/HintInfoDisplay.layout"
|
||||
15:15:56.047 GUI : Layout load @"{20A6917D2114E29A}UI/layouts/HUD/Hint/HintInfoDisplay.layout"
|
||||
15:15:56.047 GUI : Widget prefab @"{EA1CC57D868E94F9}UI/layouts/HUD/Hint/Hint.layout"
|
||||
15:15:56.048 GUI (W): Overridden prefab member (GUID: '{661AB14E0F471C66}' type: ButtonWidgetClass) no longer exists in prefab
|
||||
15:15:56.193 RENDER : RENDER : ObjectsHeightMap::Build
|
||||
15:15:56.193 RENDER : RENDER : MeshOccupancyGridManager::GenerateAllForWorld
|
||||
15:15:56.366 PROFILING : MeshOccupancyGridManager::GenerateAllForWorld took: 172.298500 ms
|
||||
15:15:56.375 PROFILING : ObjectsHeightMap::Build took: 180.962400 ms
|
||||
15:15:57.507 SCRIPT : Co10 Escape Reintroduced factions: players=USSR enemy=US independent=FIA
|
||||
15:15:57.545 SCRIPT : Reintroduced [sites]: authored everon-prison-1 at <6090,211.444,7160>
|
||||
15:15:57.546 SCRIPT : Reintroduced [sites]: cache clearance position=<6150,203.288,7050> clear=1
|
||||
15:15:57.590 SCRIPT : Reintroduced [sites]: authored everon-supply-1 at <6150,203.288,7050>
|
||||
15:15:57.592 ENTITY : SpawnEntityPrefab @"{B3949F6D215A9F78}Prefabs/Props/Military/Antennas/Antenna_FIA_02/Antenna_FIA_02_base.et"
|
||||
15:15:57.592 ENTITY : Create entity @"ENTITY:4611686018427388514" ('GenericEntity','Assets/Props/Military/Antennas/Antenna_FIA_02/Antenna_FIA_02.xob') at <6496.000000 160.006256 7202.000000> @"{B3949F6D215A9F78}Prefabs/Props/Military/Antennas/Antenna_FIA_02/Antenna_FIA_02_base.et"
|
||||
15:15:57.592 ENTITY : CreateComponent 'ScriptedRadioComponent'
|
||||
15:15:57.593 DEFAULT (W): BaseRadioComponent does not have any transceiver. (Entity: @"ENTITY:4611686018427388514" ('GenericEntity','Assets/Props/Military/Antennas/Antenna_FIA_02/Antenna_FIA_02.xob') at <6496.000000 160.006256 7202.000000> @"{B3949F6D215A9F78}Prefabs/Props/Military/Antennas/Antenna_FIA_02/Antenna_FIA_02_base.et"). Only printing this message once. There're possibly more nodes with this requirement.
|
||||
15:15:57.595 SCRIPT : Reintroduced [sites]: authored everon-radio-1 at <6500,159.756,7200>
|
||||
15:15:57.597 SCRIPT : Reintroduced [sites]: authored everon-extraction-1 at <7000,65.2562,7500>
|
||||
15:15:57.597 SCRIPT : Reintroduced [sites]: cache clearance position=<5960,220.256,7220> clear=1
|
||||
15:15:57.611 SCRIPT : Reintroduced [sites]: transport parking=<5960,220.256,7212> radius=4 terrainRange=0.8125 obstructed=0 clear=1
|
||||
15:15:57.685 SCRIPT : Reintroduced [sites]: authored everon-supply-2 at <5960,220.256,7220>
|
||||
15:15:57.686 SCRIPT : Reintroduced [sites]: cache clearance position=<6340,175.788,7400> clear=1
|
||||
15:15:57.688 SCRIPT : Reintroduced [sites]: authored everon-medical-1 at <6340,175.788,7400>
|
||||
15:15:57.688 SCRIPT : Reintroduced [sites]: cache clearance position=<6740,140.6,7300> clear=1
|
||||
15:15:57.694 SCRIPT : Reintroduced [sites]: cache clearance position=<6740,140.913,7296> clear=1
|
||||
15:15:57.695 SCRIPT : Reintroduced [sites]: cache clearance position=<6736,140.975,7300> clear=1
|
||||
15:15:57.695 SCRIPT : Reintroduced [sites]: cache clearance position=<6744,140.288,7300> clear=1
|
||||
15:15:57.697 SCRIPT : Reintroduced [sites]: authored everon-outpost-1 at <6740,140.6,7300>
|
||||
15:15:57.697 SCRIPT : Reintroduced [guards]: initial site=everon-prison-1 requested=4 physical=0 conscious=0 nativeComplete=0
|
||||
15:15:57.697 SCRIPT : Reintroduced [guards]: initial site=everon-supply-1 requested=5 physical=0 conscious=0 nativeComplete=0
|
||||
15:15:57.697 SCRIPT : Reintroduced [guards]: initial site=everon-radio-1 requested=4 physical=0 conscious=0 nativeComplete=0
|
||||
15:15:57.697 SCRIPT : Reintroduced [guards]: initial site=everon-extraction-1 requested=4 physical=0 conscious=0 nativeComplete=0
|
||||
15:15:57.697 SCRIPT : Reintroduced [guards]: initial site=everon-supply-2 requested=4 physical=0 conscious=0 nativeComplete=0
|
||||
15:15:57.698 SCRIPT : Reintroduced [guards]: initial site=everon-outpost-1 requested=4 physical=0 conscious=0 nativeComplete=0
|
||||
15:15:58.567 SCRIPT : Reintroduced [guards]: initial site=everon-prison-1 requested=4 physical=1 conscious=1 nativeComplete=0
|
||||
15:15:58.567 SCRIPT : Reintroduced [guards]: initial site=everon-supply-1 requested=5 physical=1 conscious=1 nativeComplete=0
|
||||
15:15:58.567 SCRIPT : Reintroduced [guards]: initial site=everon-radio-1 requested=4 physical=1 conscious=1 nativeComplete=0
|
||||
15:15:58.568 SCRIPT : Reintroduced [guards]: initial site=everon-extraction-1 requested=4 physical=1 conscious=1 nativeComplete=0
|
||||
15:15:59.091 SCRIPT : Reintroduced [guards]: initial site=everon-prison-1 requested=4 physical=2 conscious=2 nativeComplete=0
|
||||
15:15:59.092 SCRIPT : Reintroduced [guards]: initial site=everon-supply-1 requested=5 physical=2 conscious=2 nativeComplete=0
|
||||
15:15:59.092 SCRIPT : Reintroduced [guards]: initial site=everon-supply-2 requested=4 physical=1 conscious=1 nativeComplete=0
|
||||
15:15:59.092 SCRIPT : Reintroduced [guards]: initial site=everon-outpost-1 requested=4 physical=1 conscious=1 nativeComplete=0
|
||||
15:15:59.586 SCRIPT : Reintroduced [guards]: initial site=everon-prison-1 requested=4 physical=3 conscious=3 nativeComplete=0
|
||||
15:15:59.586 SCRIPT : Reintroduced [guards]: initial site=everon-radio-1 requested=4 physical=2 conscious=2 nativeComplete=0
|
||||
15:15:59.587 SCRIPT : Reintroduced [guards]: initial site=everon-supply-2 requested=4 physical=2 conscious=2 nativeComplete=0
|
||||
15:15:59.587 SCRIPT : Reintroduced [guards]: initial site=everon-outpost-1 requested=4 physical=2 conscious=2 nativeComplete=0
|
||||
15:16:00.104 SCRIPT : Reintroduced [guards]: initial site=everon-radio-1 requested=4 physical=3 conscious=3 nativeComplete=0
|
||||
15:16:00.104 SCRIPT : Reintroduced [guards]: initial site=everon-extraction-1 requested=4 physical=2 conscious=2 nativeComplete=0
|
||||
15:16:00.105 SCRIPT : Reintroduced [guards]: initial site=everon-supply-2 requested=4 physical=3 conscious=3 nativeComplete=0
|
||||
15:16:00.105 SCRIPT : Reintroduced [guards]: initial site=everon-outpost-1 requested=4 physical=3 conscious=3 nativeComplete=0
|
||||
15:16:00.612 SCRIPT : Reintroduced [guards]: initial site=everon-prison-1 requested=4 physical=4 conscious=4 nativeComplete=1
|
||||
15:16:00.620 SCRIPT : Reintroduced [guards]: initial site=everon-supply-1 requested=5 physical=3 conscious=3 nativeComplete=0
|
||||
15:16:00.620 SCRIPT : Reintroduced [guards]: initial site=everon-radio-1 requested=4 physical=4 conscious=4 nativeComplete=1
|
||||
15:16:00.620 SCRIPT : Reintroduced [guards]: initial site=everon-extraction-1 requested=4 physical=3 conscious=3 nativeComplete=0
|
||||
15:16:01.118 SCRIPT : Reintroduced [guards]: initial site=everon-supply-1 requested=5 physical=4 conscious=4 nativeComplete=0
|
||||
15:16:01.118 SCRIPT : Reintroduced [guards]: initial site=everon-extraction-1 requested=4 physical=4 conscious=4 nativeComplete=1
|
||||
15:16:01.118 SCRIPT : Reintroduced [guards]: initial site=everon-supply-2 requested=4 physical=4 conscious=4 nativeComplete=1
|
||||
15:16:01.118 SCRIPT : Reintroduced [guards]: initial site=everon-outpost-1 requested=4 physical=4 conscious=4 nativeComplete=1
|
||||
15:16:01.623 SCRIPT : Reintroduced [guards]: initial site=everon-supply-1 requested=5 physical=5 conscious=5 nativeComplete=1
|
||||
15:16:01.623 SCRIPT : Reintroduced [sites]: 7 sites and 122 physical stock items ready
|
||||
15:16:13.747 SCRIPT : Reintroduced [environment]: date=authored weather=authored
|
||||
15:16:13.748 SCRIPT : Reintroduced [time]: started at 14:00 with 1x progression
|
||||
15:16:13.748 SCRIPT : Reintroduced [mission]: phase 0 -> 1
|
||||
15:16:13.748 SCRIPT : Reintroduced [prison]: owned composition raw={BE6B8D56349038A6}Prefabs/ERR/Prison.et resolved={BE6B8D56349038A6}Prefabs/ERR/Prison.et position=<6090,211.444,7160>
|
||||
15:16:13.748 SCRIPT : Reintroduced [prison]: gate candidate raw= resolved={67F17696FF278AE0}Prefabs/Structures/BuildingParts/Doors/Door_Metal_A_RIGHT_EXT_COV_B.et lock=1 door=1 position=<6089.34,211.444,7154.05>
|
||||
15:16:13.749 SCRIPT : Reintroduced [prison]: one intact successor lock in owned prison
|
||||
15:16:14.123 SCRIPT : INFO: Faction: player jakub (playerID = 1 | UUID = 00000000-0000-0000-0000-000000000000) has joined faction #AR-Faction_USSR (USSR)
|
||||
15:16:14.136 SCRIPT : Reintroduced [spawn scene]: player=1 token=1 waiting for 67 replicated objects and 28 inventory items
|
||||
15:16:14.136 SCRIPT : Reintroduced [spawn scene]: client token=1 waiting for 67 replicated objects and 28 inventory items
|
||||
15:16:14.227 SCRIPT : Reintroduced [spawn scene]: client token=1 final=0 all 67 objects and 28 storage items ready
|
||||
15:16:14.228 SCRIPT : Reintroduced [spawn scene]: player=1 token=1 final=0 storage acknowledged after 106.041ms
|
||||
15:16:14.228 SCRIPT : Reintroduced [spawn scene]: player=1 token=1 scene ready after 106.041ms; starting asset preload
|
||||
15:16:15.227 SCRIPT : Reintroduced [spawn scene]: player=1 token=1 preload finished; verifying held storage
|
||||
15:16:15.734 SCRIPT : Reintroduced [SPAWN GROUP]: source=everon-prison-1 faction=US group=<0x00000001D85698B0> SCR_AIGroup position=<6125,209.938,7160> living=4
|
||||
15:16:15.734 SCRIPT : Reintroduced [SPAWN GROUP]: source=everon-supply-1 faction=FIA group=<0x00000001D856E0F0> SCR_AIGroup position=<6168,202.217,7050> living=5
|
||||
15:16:15.735 SCRIPT : Reintroduced [SPAWN GROUP]: source=everon-radio-1 faction=US group=<0x00000001D856E970> SCR_AIGroup position=<6530,157.768,7200> living=4
|
||||
15:16:15.736 SCRIPT : Reintroduced [SPAWN GROUP]: source=everon-extraction-1 faction=US group=<0x00000001C1202660> SCR_AIGroup position=<7030,62.1142,7500> living=4
|
||||
15:16:15.737 SCRIPT : Reintroduced [SPAWN GROUP]: source=everon-supply-2 faction=US group=<0x00000001D88B2780> SCR_AIGroup position=<5976,218.937,7220> living=4
|
||||
15:16:15.738 SCRIPT : Reintroduced [SPAWN GROUP]: source=everon-outpost-1 faction=US group=<0x00000001D88B3CC0> SCR_AIGroup position=<6756,139.371,7300> living=4
|
||||
15:16:15.964 SCRIPT : Reintroduced [SPAWN GROUP]: source=roadblock-0 faction=US group=<0x00000001E360B750> SCR_AIGroup position=<3966.51,17.6497,8405.46> living=4
|
||||
15:16:15.965 SCRIPT : Reintroduced [roadblocks]: roadblock-0 at <3960.9,17.4676,8409.58> guards=4 props=18 networkedProps=18 propStreaming=always roadPassage=open HQ=registered
|
||||
15:16:16.360 SCRIPT : Reintroduced [spawn scene]: client token=1 final=1 all 67 objects and 28 storage items ready
|
||||
15:16:16.360 SCRIPT : Reintroduced [spawn scene]: player=1 token=1 final=1 storage acknowledged after 2238.14ms
|
||||
15:16:16.369 SCRIPT : Reintroduced [spawn]: player 1 at <6088.5,211.389,7158.5>
|
||||
15:16:16.369 SCRIPT : Reintroduced [spawn scene]: player=1 retaining 67 objects until leaving the spawn area
|
||||
15:16:16.548 SCRIPT : ERR SMOKE PASS: burial native character spawned
|
||||
15:16:16.549 SCRIPT : ERR SMOKE PASS: burial inherited body action exists in native manager
|
||||
15:16:16.549 SCRIPT : ERR SMOKE PASS: burial living character cannot be buried
|
||||
15:16:16.549 SCRIPT (W): SCR_InstigatorContextData: No instigator type is set on Character death. It is likely that the KillerVictimRelation will be set to OTHER_DEATH which will not punish the victim for suicide.
|
||||
15:16:16.735 SCRIPT : ERR SMOKE PASS: burial empty hands reject corpse burial
|
||||
15:16:16.735 SCRIPT : ERR SMOKE PASS: burial native shovel inserted into inventory
|
||||
15:16:16.868 SCRIPT : ERR SMOKE PASS: burial carried shovel alone is insufficient
|
||||
15:16:17.062 SCRIPT : ERR BURIAL DIAG user=<6088.5,211.389,7158.5> corpse=<6089.62,211.579,7158.38> reason=
|
||||
15:16:17.062 SCRIPT : ERR SMOKE PASS: burial equipped shovel permits nearby native corpse
|
||||
15:16:17.062 SCRIPT : ERR SMOKE PASS: burial distant corpse is rejected
|
||||
15:16:17.062 SCRIPT : ERR SMOKE PASS: burial wrong action owner cannot remove corpse
|
||||
15:16:17.170 SCRIPT : ERR SMOKE PASS: burial replicated corpse removal preserves player and held shovel
|
||||
15:16:17.170 SCRIPT : ERR SMOKE PASS: burial duplicate completion is harmless
|
||||
15:16:17.170 SCRIPT : ERR SMOKE COMPLETE: body-burial
|
||||
15:16:19.491 SCRIPT : Reintroduced [welcome voice]: playing startup clip
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user