Complete roadblocks in background and fix session regressions
Continue safe roadblock placement after admission and prison unlock, build posts incrementally, and clean up incomplete transactions. Add bounded stuck-patrol dismount recovery, defer deployment initialization during terrain takeover, reset deferred site search timing on first player arrival, and fix atlas page serialization and terrain resource lookup. Validation: 30 native Everon posts with 120 living guards after unlock; seven affected regression suites pass; two final restart smoke rounds complete with 21 assertions and no script/runtime failures. Existing dependency asset errors remain.
This commit is contained in:
@@ -55,7 +55,7 @@ modded class ERR_GameMode
|
||||
string enabled;
|
||||
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
|
||||
string nativeEndCase;
|
||||
array<string> independentFixtures = {"coordinated-ai", "optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "objective-map", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "native-end-active", "native-end-preparing", "generated-sites", "mortar-station", "opening-tools", "airfield-markers", "air-logistics", "air-assault", "air-recon"};
|
||||
array<string> independentFixtures = {"coordinated-ai", "optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "objective-map", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "native-end-active", "native-end-preparing", "generated-sites", "roadblocks", "mortar-station", "opening-tools", "airfield-markers", "air-logistics", "air-assault", "air-recon"};
|
||||
if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && independentFixtures.Contains(nativeEndCase)) return;
|
||||
ERR_NativeObjectiveProbe.BeginRound();
|
||||
ERR_NativeProgressionIdentityProbe.BeginRound();
|
||||
|
||||
@@ -10,6 +10,29 @@ modded class ERR_HQ
|
||||
}
|
||||
modded class ERR_Roadblocks
|
||||
{
|
||||
override ERR_PrepareResult Prepare(ERR_ScenarioDefinition definition)
|
||||
{
|
||||
string outcome;
|
||||
if (!m_Begun && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "roadblocks")
|
||||
m_Mode.Settings().Set("roadblockCount", 30);
|
||||
return super.Prepare(definition);
|
||||
}
|
||||
int ERR_ProbePostCount() { return m_Positions.Count(); }
|
||||
bool ERR_ProbeBackgroundPending() { return m_Background; }
|
||||
bool ERR_ProbeCompleteRoadblocks()
|
||||
{
|
||||
if (m_Target != 30 || m_Positions.Count() != 30 || m_Background || !m_Ready) return false;
|
||||
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.GetFaction() != m_Mode.EnemyFaction() || !ERR_HQ.Get().ERR_ProbeRoadblockGroup(group)) 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;
|
||||
}
|
||||
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;
|
||||
@@ -27,4 +50,45 @@ modded class ERR_Roadblocks
|
||||
return true;
|
||||
}
|
||||
}
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected int m_ERRRoadblockDeadline, m_ERRRoadblockInitial = -1;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
string outcome;
|
||||
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "roadblocks") return;
|
||||
m_ERRRoadblockDeadline = System.GetTickCount() + 600000;
|
||||
GetGame().GetCallqueue().CallLater(ERR_RoadblockProbeTick, 500, true);
|
||||
OnStop().Insert(ERR_StopRoadblockProbe);
|
||||
}
|
||||
protected void ERR_StopRoadblockProbe() { GetGame().GetCallqueue().Remove(ERR_RoadblockProbeTick); }
|
||||
protected void ERR_RoadblockProbeFail(string reason)
|
||||
{
|
||||
Print("ERR SMOKE FAIL: roadblocks " + reason, LogLevel.ERROR);
|
||||
ERR_StopRoadblockProbe();
|
||||
}
|
||||
protected void ERR_RoadblockProbeTick()
|
||||
{
|
||||
if (System.GetTickCount() > m_ERRRoadblockDeadline || !PreparationError().IsEmpty()) { ERR_RoadblockProbeFail("deadline or preparation error"); return; }
|
||||
if (!Active()) return;
|
||||
ERR_Roadblocks posts = ERR_HQ.Get().Roadblocks();
|
||||
if (m_ERRRoadblockInitial < 0)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
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() >= 30 || !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/30 posts; background still pending", m_ERRRoadblockInitial);
|
||||
}
|
||||
if (posts.ERR_ProbeBackgroundPending()) return;
|
||||
if (!posts.ERR_ProbeCompleteRoadblocks() || posts.ERR_ProbePostCount() <= m_ERRRoadblockInitial) { ERR_RoadblockProbeFail("30 physical four-guard posts, ledger and HQ admission"); return; }
|
||||
Print("ERR SMOKE PASS: roadblocks 30 physical posts / 120 guards; background completed after prison unlock, no extra posts on repeat tick");
|
||||
ERR_StopRoadblockProbe();
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
Print("ERR SMOKE COMPLETE: roadblocks background completion on Everon; scripted local fixture");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -63,7 +63,7 @@ class ERR_ScenarioComponent : ScriptComponent
|
||||
#endif
|
||||
#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 != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
|
||||
if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "normal-escape-route" && historicalFixture != "crash-start" && historicalFixture != "mounted-ammo" && historicalFixture != "alarm-hearing" && historicalFixture != "optional-site-skip" && historicalFixture != "infantry-combat" && historicalFixture != "site-garrisons" && historicalFixture != "generated-sites" && historicalFixture != "roadblocks" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
|
||||
m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
|
||||
#endif
|
||||
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
|
||||
@@ -71,7 +71,7 @@ class ERR_ScenarioComponent : ScriptComponent
|
||||
#ifdef WORKBENCH
|
||||
// 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 != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
|
||||
if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "normal-escape-route" && fixture != "crash-start" && fixture != "mounted-ammo" && fixture != "alarm-hearing" && fixture != "optional-site-skip" && fixture != "infantry-combat" && fixture != "site-garrisons" && fixture != "generated-sites" && fixture != "roadblocks" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
|
||||
foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
|
||||
if (!ERR_ConfigurePatrolHQFixture(fixture)) { m_Failed = true; return ERR_PrepareResult.FAILED; }
|
||||
#endif
|
||||
@@ -114,7 +114,6 @@ class ERR_ScenarioComponent : ScriptComponent
|
||||
m_Finished = true;
|
||||
if (m_Progressive)
|
||||
{
|
||||
m_Generated.StartupOnly(false);
|
||||
GetGame().GetCallqueue().CallLater(BackgroundStep, 250, true);
|
||||
PrintFormat("Reintroduced [generation]: spawn-ready in %1 ms; remaining sites continue in background", System.GetTickCount() - m_PreparationStarted);
|
||||
}
|
||||
@@ -132,6 +131,9 @@ class ERR_ScenarioComponent : ScriptComponent
|
||||
if (!m_BackgroundActivated)
|
||||
{
|
||||
if (GetGame().GetPlayerManager().GetPlayerCount() == 0) return;
|
||||
// Resume the placement clock only when work can actually start. An empty
|
||||
// server may have waited hours since the prison became spawn-ready.
|
||||
m_Generated.StartupOnly(false);
|
||||
m_BackgroundActivated = true;
|
||||
}
|
||||
if (!m_PlacementDone)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// A replicated player controller can arrive in the gap between native mode
|
||||
// removal and Escape's bootstrap. Vanilla deployment initialization dereferences
|
||||
// GetGameMode() and can also retain the outgoing respawn singleton in that gap.
|
||||
// Defer the original initialization intact; never initialize it a second time.
|
||||
modded class SCR_PlayerDeployMenuHandlerComponent
|
||||
{
|
||||
protected IEntity m_ERRDeferredDeployOwner;
|
||||
protected BaseWorld m_ERRDeferredDeployWorld;
|
||||
protected int m_ERRDeployInitAttempts;
|
||||
protected bool m_ERRDeployInitComplete;
|
||||
|
||||
protected override void OnPostInit(IEntity owner)
|
||||
{
|
||||
if (m_ERRDeployInitComplete || m_ERRDeferredDeployOwner) return;
|
||||
if (!GetGame().ERR_ClientTerrainModePending())
|
||||
{
|
||||
m_ERRDeployInitComplete = true;
|
||||
super.OnPostInit(owner);
|
||||
return;
|
||||
}
|
||||
m_ERRDeferredDeployOwner = owner;
|
||||
m_ERRDeferredDeployWorld = owner.GetWorld();
|
||||
GetGame().GetCallqueue().CallLater(ERR_RetryDeployInit, 50, false);
|
||||
}
|
||||
protected void ERR_RetryDeployInit()
|
||||
{
|
||||
IEntity owner = m_ERRDeferredDeployOwner;
|
||||
if (!owner || !GetGame() || GetGame().GetWorld() != m_ERRDeferredDeployWorld) { m_ERRDeferredDeployOwner = null; return; }
|
||||
if (!GetGame().GetGameMode())
|
||||
{
|
||||
if (++m_ERRDeployInitAttempts < 200) GetGame().GetCallqueue().CallLater(ERR_RetryDeployInit, 50, false);
|
||||
else { m_ERRDeferredDeployOwner = null; Print("Reintroduced [terrain client]: deployment initialization timed out waiting for game mode", LogLevel.ERROR); }
|
||||
return;
|
||||
}
|
||||
m_ERRDeferredDeployOwner = null;
|
||||
m_ERRDeployInitComplete = true;
|
||||
super.OnPostInit(owner);
|
||||
Print("Reintroduced [terrain client]: deployment initialized after game mode arrived");
|
||||
}
|
||||
override protected void OnDelete(IEntity owner)
|
||||
{
|
||||
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_RetryDeployInit);
|
||||
m_ERRDeferredDeployOwner = null;
|
||||
m_ERRDeferredDeployWorld = null;
|
||||
super.OnDelete(owner);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,10 @@ modded class ArmaReforgerScripted
|
||||
protected ref array<IEntity> m_ERRClientOldManagers = {};
|
||||
protected bool m_ERRClientTerrainTakeover;
|
||||
bool ERR_IsClientTerrainTakeover() { return m_ERRClientTerrainTakeover; }
|
||||
bool ERR_ClientTerrainModePending()
|
||||
{
|
||||
return !Replication.IsServer() && !GetGameMode() && !m_ERRClientOldManagers.IsEmpty();
|
||||
}
|
||||
protected SCR_EntityCatalogManagerComponent m_ERROldCatalog;
|
||||
protected EntityID m_ERROldLoadoutId;
|
||||
protected SCR_AdditionalGameModeSettingsComponent m_ERROldSettings;
|
||||
|
||||
@@ -46,6 +46,9 @@ class ERR_TerrainDisplayName
|
||||
{
|
||||
if (seen.Contains(world)) break;
|
||||
seen.Insert(world);
|
||||
// Native subscene names can be plain paths. They are already the loaded
|
||||
// terrain, not GUID resources that LoadContainer can traverse safely.
|
||||
if (!world.IsExternal() && !world.IsInternal()) break;
|
||||
Resource resource = BaseContainerTools.LoadContainer(world);
|
||||
if (!resource || !resource.IsValid()) break;
|
||||
BaseContainer container = resource.GetResource().ToBaseContainer();
|
||||
|
||||
@@ -682,6 +682,7 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
// throwing away the delivered destination or the original physical crew.
|
||||
if (!unit.CrewValid(m_Mode)) continue;
|
||||
ObserveMovement(unit, now);
|
||||
if (unit.Released) continue;
|
||||
if (unit.PendingSearch && unit.Downlink && !unit.Search.Active(now)) unit.CancelHQDownlink();
|
||||
QueueDestination(unit, hq, now);
|
||||
if (unit.Downlink)
|
||||
@@ -737,7 +738,16 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
if (unit.Waypoint) deliveredDestination = unit.Waypoint.GetOrigin();
|
||||
ERR_HQ.Event("HUNTER MOVEMENT", string.Format("%1 position=%2 speed=%3m/s delivered destination=%4 travelling=%5 combat=%6 no progress=%7s replans=%8", unit.Source, position, speed, deliveredDestination, travelling, combat, (now - unit.ProgressAt) / 1000, unit.Repaths));
|
||||
}
|
||||
if (!travelling || combat || now - unit.ProgressAt < 60000 || unit.Repaths >= 3) return;
|
||||
if (!travelling || combat || now - unit.ProgressAt < 60000) return;
|
||||
if (unit.Repaths >= 3)
|
||||
{
|
||||
// A fourth minute without progress must not leave a patrol seated forever.
|
||||
// Native exit actions are safe only once the physical vehicle is slow.
|
||||
Physics physics = unit.Entity.GetPhysics();
|
||||
if (!physics || physics.GetVelocity().Length() > 3) return;
|
||||
AbandonStuckVehicle(unit);
|
||||
return;
|
||||
}
|
||||
unit.ProgressAt = now; unit.Repaths++;
|
||||
// Destination can already contain a newer order awaiting radio delivery.
|
||||
// Retry only the waypoint the crew has actually received.
|
||||
@@ -747,6 +757,26 @@ 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)
|
||||
{
|
||||
if (unit.Released || unit.PlayerCaptureObserved || ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) return;
|
||||
// Stop reboarding/tactics before detaching the vehicle. Preserve the original
|
||||
// actors, their radios and HQ registration so survivors can receive foot orders.
|
||||
unit.Release(m_Mode);
|
||||
SCR_AIGroupUtilityComponent utility = unit.Group.GetGroupUtilityComponent();
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
if (utility && usage) utility.RemoveUsableVehicle(usage);
|
||||
foreach (IEntity member : unit.Members)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(member) || ERR_Actors.Player(member) || ERR_Actors.Group(member) != unit.Group) continue;
|
||||
ERR_ConvoyMountedCrew.Release(member);
|
||||
if (CompartmentAccessComponent.GetVehicleIn(member) != unit.Entity) continue;
|
||||
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(member));
|
||||
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");
|
||||
}
|
||||
override void Stop()
|
||||
{
|
||||
m_Stopped = true;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
// Optional finite roadside checkpoints. Planned before player admission, using
|
||||
// this terrain's road network and navmesh; never replenish casualties in-round.
|
||||
// Finite roadside checkpoints. A short initial pass hands unfinished placement
|
||||
// to a throttled background search; never replenish casualties in-round.
|
||||
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 SearchMilliseconds() { return 12000; }
|
||||
static int SearchAttempts() { return 300; }
|
||||
static bool Road(float width, float length, float heightDifference)
|
||||
{
|
||||
return ERR_ForceRules.Finite(width) && ERR_ForceRules.Finite(length) && ERR_ForceRules.Finite(heightDifference)
|
||||
@@ -26,10 +30,17 @@ class ERR_Roadblocks
|
||||
protected ref array<SCR_AIGroup> m_Groups = {};
|
||||
protected ref ERR_SpawnPlacement m_Placement = new ERR_SpawnPlacement();
|
||||
protected ref ERR_SettlementSafety m_Settlements = new ERR_SettlementSafety();
|
||||
protected bool m_Begun, m_Ready, m_Stopped, m_HaveCandidate;
|
||||
protected bool m_Begun, m_Ready, m_Stopped, m_HaveCandidate, m_Background;
|
||||
protected ref ERR_ScenarioDefinition m_Definition;
|
||||
protected int m_NextSearch, m_NextReport, m_BackgroundSteps, m_BackgroundWork, m_MaxBackgroundStep;
|
||||
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 SCR_AIGroup m_BuildGroup;
|
||||
protected AIWaypoint m_BuildHold;
|
||||
protected ref array<IEntity> m_BuildSpawned = {};
|
||||
protected ref array<vector> m_BuildPositions = {}, m_BuildCovers = {};
|
||||
protected int m_BuildActors, m_BuildCoverCount;
|
||||
protected ref array<ResourceName> m_Roster = {};
|
||||
protected ref array<ref Resource> m_Loaded = {};
|
||||
static const ResourceName COVER = "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et";
|
||||
@@ -39,11 +50,17 @@ class ERR_Roadblocks
|
||||
string Status()
|
||||
{
|
||||
if (!m_Begun) return "Roadblocks: awaiting terrain preparation.";
|
||||
return string.Format("Roadblocks: %1/%2 placed this round; next-round setting %3. Four guards per post, no replacements.", m_Groups.Count(), m_Target, m_Mode.Settings().Get("roadblockCount"));
|
||||
string progress = "placement complete";
|
||||
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);
|
||||
}
|
||||
protected ERR_PrepareResult Finish(string reason)
|
||||
{
|
||||
m_Ready = true; m_Loaded.Clear();
|
||||
m_Ready = true; m_Background = false; m_HaveCandidate = false;
|
||||
m_Loaded.Clear(); m_Definition = null;
|
||||
if (GetGame()) GetGame().GetCallqueue().Remove(BackgroundStep);
|
||||
PrintFormat("Reintroduced [roadblocks]: placed=%1 requested=%2 attempts=%3 roadCandidates=%4 clearPlans=%5; %6", m_Groups.Count(), m_Target, m_Attempts, m_Candidates, m_Planned, reason);
|
||||
ERR_HQ.Event("ROADBLOCKS", Status() + " " + reason);
|
||||
return ERR_PrepareResult.READY;
|
||||
@@ -136,40 +153,68 @@ class ERR_Roadblocks
|
||||
foreach (IEntity entity : spawned) ERR_Actors.DeleteSpawned(entity);
|
||||
ERR_Actors.DeleteSpawned(group);
|
||||
}
|
||||
// Start a transaction; physical pieces are created separately in BuildPost.
|
||||
protected bool SpawnPost(array<vector> positions, array<vector> covers)
|
||||
{
|
||||
if (!PlayersClear() || m_Mode.Forces().Count() >= 250) return false;
|
||||
SCR_AIGroup group = SCR_AIGroup.Cast(ERR_Actors.Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", m_Centre));
|
||||
if (!group) return false;
|
||||
group.SetFaction(m_Mode.EnemyFaction()); group.SetSpawnImmediately(false); group.SetDeleteWhenEmpty(false); group.SetPermanentLOD(0);
|
||||
array<IEntity> spawned = {};
|
||||
for (int i = 0; i < positions.Count(); i++)
|
||||
{
|
||||
if (!PlayersClear()) { Rollback(spawned, group); return false; }
|
||||
IEntity actor = ERR_Actors.Spawn(m_Roster[i % m_Roster.Count()], positions[i]);
|
||||
if (!actor) { Rollback(spawned, group); return false; }
|
||||
spawned.Insert(actor);
|
||||
if (!ERR_Actors.SetFaction(actor, m_Mode.EnemyFaction()) || !group.AddAIEntityToGroup(actor)) { Rollback(spawned, group); return false; }
|
||||
}
|
||||
foreach (vector coverPosition : covers)
|
||||
{
|
||||
IEntity cover = ERR_Actors.Spawn(COVER, coverPosition, m_Forward.VectorToAngles());
|
||||
if (!cover) { Rollback(spawned, group); return false; }
|
||||
spawned.Insert(cover);
|
||||
}
|
||||
AIWaypoint hold = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.DefendWaypoint(), positions[0]));
|
||||
if (!hold) { Rollback(spawned, group); return false; }
|
||||
spawned.Insert(hold); hold.SetCompletionRadius(25); group.AddWaypoint(hold);
|
||||
string id = "roadblock-" + m_Groups.Count().ToString();
|
||||
if (!m_Mode.Forces().Register(id, ERR_ForceKind.CHECKPOINT, m_Centre, 4, null, group, GetGame().GetWorld().GetWorldTime()))
|
||||
{ Rollback(spawned, group); return false; }
|
||||
foreach (IEntity ownedEntity : spawned) m_Mode.Own(ownedEntity);
|
||||
m_Mode.Own(group); m_Groups.Insert(group); m_Positions.Insert(m_Centre);
|
||||
m_Mode.RegisterGroup(group); group.ActivateAI();
|
||||
m_Mining.Register(group, hold, m_Right);
|
||||
PrintFormat("Reintroduced [roadblocks]: %1 at %2 guards=4 roadPassage=open HQ=registered", id, m_Centre);
|
||||
if (!PlayersClear() || m_Mode.Forces().Count() >= 250 || m_BuildGroup || positions.Count() != 4 || covers.Count() != 2) 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);
|
||||
m_BuildPositions.Copy(positions); m_BuildCovers.Copy(covers);
|
||||
m_BuildActors = 0; m_BuildCoverCount = 0;
|
||||
return true;
|
||||
}
|
||||
protected void CancelPost()
|
||||
{
|
||||
Rollback(m_BuildSpawned, m_BuildGroup);
|
||||
m_BuildSpawned.Clear(); m_BuildGroup = null; m_BuildHold = null;
|
||||
m_BuildPositions.Clear(); m_BuildCovers.Clear();
|
||||
}
|
||||
protected void BuildPost()
|
||||
{
|
||||
if (!PlayersClear() || m_Mode.Forces().Count() >= 250) { CancelPost(); return; }
|
||||
// One actor/prop/waypoint per callback. Recheck moving obstructions and
|
||||
// player distance while the transaction is in progress; roll back on failure.
|
||||
if (m_BuildActors < m_BuildPositions.Count())
|
||||
{
|
||||
vector point = m_BuildPositions[m_BuildActors];
|
||||
if (!m_Placement.Clear(point, null)) { CancelPost(); return; }
|
||||
IEntity actor = ERR_Actors.Spawn(m_Roster[m_BuildActors % m_Roster.Count()], point);
|
||||
if (!actor) { CancelPost(); return; }
|
||||
m_BuildSpawned.Insert(actor);
|
||||
if (!ERR_Actors.SetFaction(actor, m_Mode.EnemyFaction()) || !m_BuildGroup.AddAIEntityToGroup(actor)) { CancelPost(); return; }
|
||||
m_BuildActors++;
|
||||
return;
|
||||
}
|
||||
if (m_BuildCoverCount < m_BuildCovers.Count())
|
||||
{
|
||||
vector coverPosition = m_BuildCovers[m_BuildCoverCount];
|
||||
if (!CoverClear(coverPosition)) { CancelPost(); return; }
|
||||
IEntity cover = ERR_Actors.Spawn(COVER, coverPosition, m_Forward.VectorToAngles());
|
||||
if (!cover) { CancelPost(); return; }
|
||||
m_BuildSpawned.Insert(cover); m_BuildCoverCount++;
|
||||
return;
|
||||
}
|
||||
if (!m_BuildHold)
|
||||
{
|
||||
m_BuildHold = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.DefendWaypoint(), m_BuildPositions[0]));
|
||||
if (!m_BuildHold) { CancelPost(); return; }
|
||||
m_BuildSpawned.Insert(m_BuildHold); m_BuildHold.SetCompletionRadius(25);
|
||||
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()))
|
||||
{ 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);
|
||||
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 roadPassage=open HQ=registered", id, m_Centre);
|
||||
m_BuildSpawned.Clear(); m_BuildGroup = null; m_BuildHold = null;
|
||||
m_BuildPositions.Clear(); m_BuildCovers.Clear();
|
||||
m_NextSearch = System.GetTickCount() + 2000;
|
||||
}
|
||||
// 0: reject, 1: awaiting native tile, 2: ready.
|
||||
protected int Plan(out array<vector> positions, out array<vector> covers)
|
||||
{
|
||||
@@ -210,19 +255,77 @@ class ERR_Roadblocks
|
||||
GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High);
|
||||
if (!ERR_TerrainSites.Bounds(m_Low, m_High) || !LoadRoster()) return Finish("terrain or faction resources unavailable; optional roadblocks skipped");
|
||||
}
|
||||
if (m_Groups.Count() >= m_Target) return Finish("requested count reached");
|
||||
if (System.GetTickCount() - m_Started >= 12000 || m_Attempts >= 300) return Finish("bounded search complete; unsuitable locations skipped");
|
||||
// Amortize geometry work: at most eight cheap road samples and one detailed
|
||||
// placement per preparation call. Never depend on finding every requested post.
|
||||
for (int sample = 0; sample < 8 && !m_HaveCandidate && m_Attempts < 300; sample++)
|
||||
{ m_Attempts++; m_HaveCandidate = Candidate(definition); }
|
||||
if (!m_HaveCandidate) return ERR_PrepareResult.PENDING;
|
||||
array<vector> positions = {}, covers = {};
|
||||
int plan = Plan(positions, covers);
|
||||
if (plan == 1 && System.GetTickCount() - m_CandidateStarted < 1500) return ERR_PrepareResult.PENDING;
|
||||
if (plan == 2) { m_Planned++; SpawnPost(positions, covers); }
|
||||
m_HaveCandidate = false;
|
||||
if (m_Positions.Count() >= m_Target) return Finish("requested count reached");
|
||||
if (System.GetTickCount() - m_Started >= ERR_RoadblockRules.SearchMilliseconds() || m_Attempts >= ERR_RoadblockRules.SearchAttempts())
|
||||
{
|
||||
m_Ready = true; m_Background = true; m_Definition = definition;
|
||||
m_NextReport = System.GetTickCount() + 60000;
|
||||
GetGame().GetCallqueue().CallLater(BackgroundStep, 500, true);
|
||||
ERR_HQ.Event("ROADBLOCKS", Status() + " Initial search yielded; remaining posts will continue in background.");
|
||||
return ERR_PrepareResult.READY;
|
||||
}
|
||||
SearchStep(definition, 8);
|
||||
if (m_Positions.Count() >= m_Target) return Finish("requested count reached");
|
||||
return ERR_PrepareResult.PENDING;
|
||||
}
|
||||
void Stop() { m_Stopped = true; m_Mining.Stop(); m_Loaded.Clear(); }
|
||||
protected void SearchStep(ERR_ScenarioDefinition definition, int samples)
|
||||
{
|
||||
if (m_BuildGroup) { BuildPost(); return; }
|
||||
int started = System.GetTickCount();
|
||||
// A cooperative 2ms search budget, checked between native calls. A single
|
||||
// native road/trace/spawn call cannot be preempted. Limit background to eight
|
||||
// cheap candidates OR one detailed plan/build piece per callback, never a whole scan.
|
||||
if (!m_HaveCandidate)
|
||||
{
|
||||
for (int sample = 0; sample < samples; sample++)
|
||||
{
|
||||
if (sample > 0 && System.GetTickCount() - started >= 2) break;
|
||||
m_Attempts++;
|
||||
if (Candidate(definition)) { m_HaveCandidate = true; break; }
|
||||
}
|
||||
return;
|
||||
}
|
||||
array<vector> positions = {}, covers = {};
|
||||
int plan = Plan(positions, covers);
|
||||
if (plan == 1 && System.GetTickCount() - m_CandidateStarted < 2000) return;
|
||||
if (plan == 2)
|
||||
{
|
||||
m_Planned++;
|
||||
SpawnPost(positions, covers);
|
||||
}
|
||||
m_HaveCandidate = false;
|
||||
}
|
||||
protected void BackgroundStep()
|
||||
{
|
||||
if (m_Stopped || !m_Background) return;
|
||||
if (!Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get()) { Stop(); return; }
|
||||
if (m_Mode.Phase() == ERR_Phase.PREPARING) return;
|
||||
if (!m_Mode.Active()) { Stop(); return; }
|
||||
if (m_Positions.Count() >= m_Target) { Finish("requested count reached in background"); return; }
|
||||
int now = System.GetTickCount();
|
||||
// Empty servers do no terrain searches. No new actors can fit a full ledger.
|
||||
if (GetGame().GetPlayerManager().GetPlayerCount() == 0 || now < m_NextSearch) return;
|
||||
if (m_Mode.Forces().Count() >= 250)
|
||||
{
|
||||
if (m_BuildGroup) CancelPost();
|
||||
return;
|
||||
}
|
||||
SearchStep(m_Definition, 8);
|
||||
int elapsed = System.GetTickCount() - now;
|
||||
m_BackgroundSteps++; m_BackgroundWork += elapsed;
|
||||
m_MaxBackgroundStep = Math.Max(m_MaxBackgroundStep, elapsed);
|
||||
if (m_Positions.Count() >= m_Target) { Finish("requested count reached in background"); return; }
|
||||
if (now >= m_NextReport)
|
||||
{
|
||||
m_NextReport = now + 60000;
|
||||
PrintFormat("Reintroduced [roadblocks background]: placed=%1/%2 attempts=%3 steps=%4 workMs=%5 maxStepMs=%6; still searching safe locations", m_Positions.Count(), m_Target, m_Attempts, m_BackgroundSteps, m_BackgroundWork, m_MaxBackgroundStep);
|
||||
}
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
m_Stopped = true; m_Background = false; m_HaveCandidate = false;
|
||||
if (GetGame()) GetGame().GetCallqueue().Remove(BackgroundStep);
|
||||
CancelPost();
|
||||
m_Mining.Stop(); m_Loaded.Clear(); m_Definition = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,12 +739,13 @@ class ERRM_SharedMapComponent : SCR_MapGadgetComponent
|
||||
|
||||
override bool RplSave(ScriptBitWriter writer)
|
||||
{
|
||||
writer.WriteIntRange(m_ESMPage, 0, 16777215);
|
||||
// Regional sheets start at 16777216, beyond the old 24-bit detail range.
|
||||
writer.WriteInt(m_ESMPage);
|
||||
return super.RplSave(writer);
|
||||
}
|
||||
override bool RplLoad(ScriptBitReader reader)
|
||||
{
|
||||
reader.ReadIntRange(m_ESMPage, 0, 16777215);
|
||||
if (!reader.ReadInt(m_ESMPage)) return false;
|
||||
m_ESMLastSelectedPage = m_ESMPage;
|
||||
return super.RplLoad(reader);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user