Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
229 lines
12 KiB
C
229 lines
12 KiB
C
// Optional finite roadside checkpoints. Planned before player admission, using
|
|
// this terrain's road network and navmesh; never replenish casualties in-round.
|
|
class ERR_RoadblockRules
|
|
{
|
|
static bool Road(float width, float length, float heightDifference)
|
|
{
|
|
return ERR_ForceRules.Finite(width) && ERR_ForceRules.Finite(length) && ERR_ForceRules.Finite(heightDifference)
|
|
&& width >= 4 && width <= 16 && length >= 40 && Math.AbsFloat(heightDifference) <= 2;
|
|
}
|
|
static bool Spaced(vector point, array<vector> used, vector prison, float escapeRadius)
|
|
{
|
|
if (!ERR_ForceRules.FiniteVector(point) || !ERR_ForceRules.FiniteVector(prison) || !used || !ERR_ForceRules.Finite(escapeRadius)) return false;
|
|
float distance = vector.DistanceXZ(point, prison);
|
|
if (distance < 700 || distance + 200 >= escapeRadius) return false;
|
|
foreach (vector other : used)
|
|
if (!ERR_ForceRules.FiniteVector(other) || vector.DistanceXZ(point, other) < 600) return false;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
class ERR_Roadblocks
|
|
{
|
|
protected ERR_GameMode m_Mode;
|
|
protected ref ERR_RoadblockMining m_Mining;
|
|
protected ref array<vector> m_Positions = {};
|
|
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 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 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";
|
|
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); }
|
|
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"));
|
|
}
|
|
protected ERR_PrepareResult Finish(string reason)
|
|
{
|
|
m_Ready = true; m_Loaded.Clear();
|
|
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;
|
|
}
|
|
protected bool LoadRoster()
|
|
{
|
|
Resource profile = Resource.Load(m_Mode.FactionSelection().Choice("enemy").Group);
|
|
IEntitySource source;
|
|
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, "{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", ERR_Resources.DefendWaypoint()};
|
|
foreach (ResourceName actor : m_Roster) resources.Insert(actor);
|
|
foreach (ResourceName prefab : resources)
|
|
{
|
|
Resource loaded = Resource.Load(prefab);
|
|
if (!loaded || !loaded.IsValid()) return false;
|
|
m_Loaded.Insert(loaded);
|
|
}
|
|
return true;
|
|
}
|
|
protected bool Candidate(ERR_ScenarioDefinition definition)
|
|
{
|
|
SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld());
|
|
if (!world || !world.GetRoadNetworkManager()) return false;
|
|
vector seed = Vector(Math.RandomFloat(m_Low[0], m_High[0]), 0, Math.RandomFloat(m_Low[2], m_High[2]));
|
|
array<BaseRoad> roads = {};
|
|
world.GetRoadNetworkManager().GetRoadsInAABB(seed - "600 5000 600", seed + "600 5000 600", roads);
|
|
if (roads.IsEmpty() || roads.Count() > 512) return false;
|
|
BaseRoad road = roads.GetRandomElement();
|
|
if (!road) return false;
|
|
array<vector> points = {}; road.GetPoints(points);
|
|
if (points.Count() < 2 || points.Count() > 2048) return false;
|
|
float length;
|
|
for (int i = 0; i < points.Count(); i++)
|
|
{
|
|
if (!ERR_ForceRules.FiniteVector(points[i])) return false;
|
|
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);
|
|
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;
|
|
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;
|
|
ERR_SiteDefinition prison = definition.Find("everon-prison-1");
|
|
if (!prison || !ERR_RoadblockRules.Spaced(m_Centre, m_Positions, prison.Position, m_Mode.EscapeRadius())) return false;
|
|
foreach (ERR_SiteDefinition site : definition.Sites)
|
|
if (vector.DistanceXZ(site.Position, m_Centre) < 150) return false;
|
|
if (!m_Settlements.OutsideSettlements(m_Centre) || !ERR_HQRoadIntercept.Standable(m_Centre)) return false;
|
|
// Flatten so cover/guard offsets keep their metre distances on sloped roads.
|
|
m_Forward[1] = 0;
|
|
if (m_Forward.Length() < 0.1) return false;
|
|
m_Forward.Normalize();
|
|
m_Right = Vector(m_Forward[2], 0, -m_Forward[0]);
|
|
// Keep all physical cover outside the travelled road. Even a narrow road has
|
|
// a >=6m centre gap between these compact sandbag positions.
|
|
m_Shoulder = Math.Max(road.GetWidth() * 0.5 + 1.5, 4);
|
|
m_Candidates++; m_CandidateStarted = System.GetTickCount();
|
|
return true;
|
|
}
|
|
protected bool PlayersClear()
|
|
{
|
|
array<int> ids = {}; GetGame().GetPlayerManager().GetPlayers(ids);
|
|
foreach (int id : ids)
|
|
{
|
|
IEntity actor = GetGame().GetPlayerManager().GetPlayerControlledEntity(id);
|
|
if (actor && vector.DistanceXZ(actor.GetOrigin(), m_Centre) < 700) return false;
|
|
}
|
|
return true;
|
|
}
|
|
protected bool CoverClear(vector point)
|
|
{
|
|
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point)) return false;
|
|
for (int x = -1; x <= 1; x++)
|
|
for (int z = -1; z <= 1; z++)
|
|
{
|
|
vector sample = ERR_Actors.Ground(point + Vector(x * 1.5, 0, z * 1.5));
|
|
if (Math.AbsFloat(sample[1] - point[1]) > 0.3) return false;
|
|
}
|
|
TraceBox trace = new TraceBox(); trace.Start = point + "0 0.15 0"; trace.End = trace.Start + "0 0.01 0";
|
|
trace.Mins = "-1.5 0 -1.5"; trace.Maxs = "1.5 2 1.5"; trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
|
|
return GetGame().GetWorld().TraceMove(trace, null) == 1;
|
|
}
|
|
protected void Rollback(array<IEntity> spawned, SCR_AIGroup group)
|
|
{
|
|
foreach (IEntity entity : spawned) ERR_Actors.DeleteSpawned(entity);
|
|
ERR_Actors.DeleteSpawned(group);
|
|
}
|
|
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);
|
|
return true;
|
|
}
|
|
// 0: reject, 1: awaiting native tile, 2: ready.
|
|
protected int Plan(out array<vector> positions, out array<vector> covers)
|
|
{
|
|
positions.Clear(); covers.Clear();
|
|
NavmeshWorldComponent mesh = GetGame().GetAIWorld().GetNavmeshWorldComponent("Soldiers");
|
|
if (!mesh || !PlayersClear()) return 0;
|
|
map<int, vector> reserved = new map<int, vector>();
|
|
for (int side = -1; side <= 1; side += 2)
|
|
{
|
|
vector cover = ERR_Actors.Ground(m_Centre + m_Right * (side * m_Shoulder));
|
|
if (!CoverClear(cover)) return 0;
|
|
covers.Insert(cover);
|
|
for (int row = -1; row <= 1; row += 2)
|
|
{
|
|
vector point = ERR_Actors.Ground(m_Centre + m_Right * (side * (m_Shoulder + 3)) + m_Forward * (row * 3));
|
|
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;
|
|
reserved.Set(positions.Count(), point); positions.Insert(point);
|
|
}
|
|
}
|
|
return 2;
|
|
}
|
|
ERR_PrepareResult Prepare(ERR_ScenarioDefinition definition)
|
|
{
|
|
if (!Replication.IsServer() || m_Stopped || !definition) return ERR_PrepareResult.FAILED;
|
|
if (m_Ready) return ERR_PrepareResult.READY;
|
|
if (!m_Begun)
|
|
{
|
|
m_Begun = true; m_Started = System.GetTickCount(); m_Target = m_Mode.Settings().Get("roadblockCount");
|
|
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.
|
|
string fixtureEnabled, fixtureOutcome;
|
|
if (System.GetCLIParam("errSmoke", fixtureEnabled) && fixtureEnabled == "1" && System.GetCLIParam("errSmokeOutcome", fixtureOutcome) && (fixtureOutcome == "convoy" || fixtureOutcome == "hq-memory"))
|
|
return Finish("disabled for the legacy body-budget fixture");
|
|
#endif
|
|
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;
|
|
return ERR_PrepareResult.PENDING;
|
|
}
|
|
void Stop() { m_Stopped = true; m_Mining.Stop(); m_Loaded.Clear(); }
|
|
}
|