Build realistic roadblocks with staggered barriers and guarded positions

This commit is contained in:
2026-10-15 12:39:04 +02:00
parent 7b13231f29
commit 3b920e0715
6 changed files with 267 additions and 40 deletions
@@ -17,11 +17,79 @@ modded class ERR_Roadblocks
m_Mode.Settings().Set("roadblockCount", 30); m_Mode.Settings().Set("roadblockCount", 30);
return super.Prepare(definition); return super.Prepare(definition);
} }
protected int m_ERRLayoutPosts;
protected bool m_ERRLayoutFailed, m_ERRLayoutCaptured;
protected SCR_ManualCamera m_ERRLayoutCamera;
override protected void BuildPost()
{
string outcome;
if (!System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "roadblocks") { super.BuildPost(); return; }
// Inspect the actual spawned models before the transaction clears its refs.
if (m_BuildHold)
{
bool valid = m_BuildCovers.Count() == 10 && m_BuildSpawned.Count() == 15;
for (int i = 0; i < m_BuildCovers.Count() && valid; i++)
{
ERR_RoadblockProp prop = m_BuildCovers[i];
IEntity entity = m_BuildSpawned[i + 4];
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
|| Math.Max(Math.AbsFloat(low[2]), Math.AbsFloat(high[2])) > prop.HalfDepth || high[1] > prop.Height) valid = false;
// Every actual prop corner must stay outside the four-metre centre lane.
for (int x = 0; x < 2; x++)
for (int z = 0; z < 2; z++)
{
vector corner = low;
if (x) corner[0] = high[0];
if (z) corner[2] = high[2];
vector delta = entity.CoordToParent(corner) - m_Centre;
if (Math.AbsFloat(vector.Dot(delta, m_Right)) < 2) valid = false;
}
}
if (!valid) { m_ERRLayoutFailed = true; Print("ERR SMOKE FAIL: roadblock prop geometry or centre lane", LogLevel.ERROR); }
if (!m_ERRLayoutCaptured && m_Mode.Active())
{
m_ERRLayoutCaptured = true;
EntitySpawnParams params = new EntitySpawnParams();
m_ERRLayoutCamera = SCR_ManualCamera.Cast(GetGame().SpawnEntity(SCR_ManualCamera, GetGame().GetWorld(), params));
if (m_ERRLayoutCamera)
{
vector camera[4];
vector eye = m_Centre - m_Forward * 18 + m_Right * 12 + "0 13 0";
Math3D.DirectionAndUpMatrix(vector.Direction(eye, m_Centre), "0 1 0", camera);
camera[3] = eye; m_ERRLayoutCamera.SetWorldTransform(camera); m_ERRLayoutCamera.SetVerticalFOV(65);
GetGame().GetCameraManager().SetCamera(m_ERRLayoutCamera);
GetGame().GetCallqueue().CallLater(ERR_ProbeCaptureLayout, 3000, false);
}
}
m_ERRLayoutPosts++;
}
super.BuildPost();
}
protected void ERR_ProbeCaptureLayout()
{
System.MakeScreenshot("$logs:err-roadblock-layout");
GetGame().GetCallqueue().CallLater(ERR_ProbeCloseLayoutCamera, 1000, false);
}
protected void ERR_ProbeCloseLayoutCamera()
{
if (!m_ERRLayoutCamera) return;
GetGame().GetCameraManager().SetCamera(GetGame().GetPlayerController().GetPlayerCamera());
m_ERRLayoutCamera.Terminate(); delete m_ERRLayoutCamera; m_ERRLayoutCamera = null;
}
override void Stop()
{
GetGame().GetCallqueue().Remove(ERR_ProbeCaptureLayout);
GetGame().GetCallqueue().Remove(ERR_ProbeCloseLayoutCamera);
ERR_ProbeCloseLayoutCamera();
super.Stop();
}
int ERR_ProbePostCount() { return m_Positions.Count(); } int ERR_ProbePostCount() { return m_Positions.Count(); }
bool ERR_ProbeBackgroundPending() { return m_Background; } bool ERR_ProbeBackgroundPending() { return m_Background; }
bool ERR_ProbeCompleteRoadblocks() bool ERR_ProbeCompleteRoadblocks()
{ {
if (m_Target != 30 || m_Positions.Count() != 30 || m_Background || !m_Ready) return false; if (m_ERRLayoutFailed || m_ERRLayoutPosts < 30 || m_Target != 30 || m_Positions.Count() != 30 || m_Background || !m_Ready) return false;
foreach (SCR_AIGroup group : m_Groups) foreach (SCR_AIGroup group : m_Groups)
{ {
ERR_ForceRecord record = m_Mode.Forces().FindGroup(group); ERR_ForceRecord record = m_Mode.Forces().FindGroup(group);
@@ -58,7 +126,7 @@ modded class ERR_GameMode
super.EOnInit(owner); super.EOnInit(owner);
string outcome; string outcome;
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "roadblocks") return; if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "roadblocks") return;
m_ERRRoadblockDeadline = System.GetTickCount() + 600000; m_ERRRoadblockDeadline = System.GetTickCount() + 900000;
GetGame().GetCallqueue().CallLater(ERR_RoadblockProbeTick, 500, true); GetGame().GetCallqueue().CallLater(ERR_RoadblockProbeTick, 500, true);
OnStop().Insert(ERR_StopRoadblockProbe); OnStop().Insert(ERR_StopRoadblockProbe);
} }
@@ -85,7 +153,7 @@ modded class ERR_GameMode
} }
if (posts.ERR_ProbeBackgroundPending()) return; if (posts.ERR_ProbeBackgroundPending()) return;
if (!posts.ERR_ProbeCompleteRoadblocks() || posts.ERR_ProbePostCount() <= m_ERRRoadblockInitial) { ERR_RoadblockProbeFail("30 physical four-guard posts, ledger and HQ admission"); return; } if (!posts.ERR_ProbeCompleteRoadblocks() || 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"); Print("ERR SMOKE PASS: roadblocks 30 physical posts / 120 guards / 300 checkpoint props; verified model footprints and centre lane; background completed after prison unlock, no extra posts on repeat tick");
ERR_StopRoadblockProbe(); ERR_StopRoadblockProbe();
Transition(ERR_Phase.DEFEAT); Transition(ERR_Phase.DEFEAT);
Print("ERR SMOKE COMPLETE: roadblocks background completion on Everon; scripted local fixture"); Print("ERR SMOKE COMPLETE: roadblocks background completion on Everon; scripted local fixture");
@@ -22,6 +22,20 @@ class ERR_RoadblockRules
} }
} }
// A small, staged composition: each prop has its own heading and clearance box.
class ERR_RoadblockProp
{
ResourceName Prefab;
vector Position, Angles, Right, Forward;
float HalfWidth, HalfDepth, Height;
void ERR_RoadblockProp(ResourceName prefab, vector position, vector forward, float halfWidth, float halfDepth, float height)
{
Prefab = prefab; Position = position; Forward = forward;
Right = Vector(forward[2], 0, -forward[0]); Angles = forward.VectorToAngles();
HalfWidth = halfWidth; HalfDepth = halfDepth; Height = height;
}
}
class ERR_Roadblocks class ERR_Roadblocks
{ {
protected ERR_GameMode m_Mode; protected ERR_GameMode m_Mode;
@@ -39,11 +53,15 @@ class ERR_Roadblocks
protected SCR_AIGroup m_BuildGroup; protected SCR_AIGroup m_BuildGroup;
protected AIWaypoint m_BuildHold; protected AIWaypoint m_BuildHold;
protected ref array<IEntity> m_BuildSpawned = {}; protected ref array<IEntity> m_BuildSpawned = {};
protected ref array<vector> m_BuildPositions = {}, m_BuildCovers = {}; protected ref array<vector> m_BuildPositions = {};
protected ref array<ref ERR_RoadblockProp> m_BuildCovers = {};
protected int m_BuildActors, m_BuildCoverCount; protected int m_BuildActors, m_BuildCoverCount;
protected ref array<ResourceName> m_Roster = {}; protected ref array<ResourceName> m_Roster = {};
protected ref array<ref Resource> m_Loaded = {}; protected ref array<ref Resource> m_Loaded = {};
static const ResourceName COVER = "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et"; static const ResourceName COVER = "{21D83950DF36F250}Prefabs/Props/Military/Sandbags/Sandbag_01_long_burlap.et";
static const ResourceName BARRIER = "{64C9C1D1C2C1B38D}Prefabs/Props/Infrastructure/Roadblocks/RoadBlock_01.et";
static const ResourceName OBSTACLE = "{A46D45922F8CAD6C}Prefabs/Props/Military/Fortification/CzechHedgehog_01_painted.et";
static const ResourceName SIGN = "{C1977A326F2AA011}Prefabs/Structures/Signs/Military/SignCheckpoint_01_stop.et";
void ERR_Roadblocks(ERR_GameMode mode) { m_Mode = mode; m_Mining = new ERR_RoadblockMining(mode); } void ERR_Roadblocks(ERR_GameMode mode) { m_Mode = mode; m_Mining = new ERR_RoadblockMining(mode); }
void MineTick(array<ref ERR_HQUnit> units, array<IEntity> attempted, float now) { if (!m_Stopped) m_Mining.Tick(units, attempted, now); } void MineTick(array<ref ERR_HQUnit> units, array<IEntity> attempted, float now) { if (!m_Stopped) m_Mining.Tick(units, attempted, now); }
bool Contains(SCR_AIGroup group) { return group && m_Groups.Contains(group); } bool Contains(SCR_AIGroup group) { return group && m_Groups.Contains(group); }
@@ -72,7 +90,7 @@ class ERR_Roadblocks
if (profile && profile.IsValid()) source = profile.GetResource().ToEntitySource(); if (profile && profile.IsValid()) source = profile.GetResource().ToEntitySource();
if (!source || !source.Get("m_aUnitPrefabSlots", m_Roster) || m_Roster.IsEmpty()) return false; if (!source || !source.Get("m_aUnitPrefabSlots", m_Roster) || m_Roster.IsEmpty()) return false;
// Preload before physical creation. A failed prefab never leaves half a post. // Preload before physical creation. A failed prefab never leaves half a post.
array<ResourceName> resources = {COVER, "{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", ERR_Resources.DefendWaypoint()}; array<ResourceName> resources = {COVER, BARRIER, OBSTACLE, SIGN, "{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", ERR_Resources.DefendWaypoint()};
foreach (ResourceName actor : m_Roster) resources.Insert(actor); foreach (ResourceName actor : m_Roster) resources.Insert(actor);
foreach (ResourceName prefab : resources) foreach (ResourceName prefab : resources)
{ {
@@ -119,8 +137,8 @@ class ERR_Roadblocks
if (m_Forward.Length() < 0.1) return false; if (m_Forward.Length() < 0.1) return false;
m_Forward.Normalize(); m_Forward.Normalize();
m_Right = Vector(m_Forward[2], 0, -m_Forward[0]); m_Right = Vector(m_Forward[2], 0, -m_Forward[0]);
// Keep all physical cover outside the travelled road. Even a narrow road has // Protected guard positions sit on the shoulders; approach barriers narrow
// a >=6m centre gap between these compact sandbag positions. // 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 + 1.5, 4);
m_Candidates++; m_CandidateStarted = System.GetTickCount(); m_Candidates++; m_CandidateStarted = System.GetTickCount();
return true; return true;
@@ -135,32 +153,68 @@ class ERR_Roadblocks
} }
return true; return true;
} }
protected bool CoverClear(vector point) protected bool CoverClear(ERR_RoadblockProp prop)
{ {
vector point = prop.Position;
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point)) return false; if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point)) return false;
for (int x = -1; x <= 1; x++) for (int x = -1; x <= 1; x++)
for (int z = -1; z <= 1; z++) for (int z = -1; z <= 1; z++)
{ {
vector sample = ERR_Actors.Ground(point + Vector(x * 1.5, 0, z * 1.5)); vector sample = ERR_Actors.Ground(point + prop.Right * (x * prop.HalfWidth) + prop.Forward * (z * prop.HalfDepth));
if (Math.AbsFloat(sample[1] - point[1]) > 0.3) return false; if (Math.AbsFloat(sample[1] - point[1]) > 0.3) return false;
} }
// Planned props have separate local footprints. Exclude our own transaction
// from the enclosing world-axis trace, which otherwise falsely overlaps
// adjacent pieces at diagonal road headings. Still reject a moving guard.
foreach (IEntity builtEntity : m_BuildSpawned)
{
if (!ChimeraCharacter.Cast(builtEntity)) continue;
vector delta = builtEntity.GetOrigin() - point;
if (Math.AbsFloat(vector.Dot(delta, prop.Right)) < prop.HalfWidth + 0.6
&& Math.AbsFloat(vector.Dot(delta, prop.Forward)) < prop.HalfDepth + 0.6 && Math.AbsFloat(delta[1]) < 2) return false;
}
// Conservative world-axis enclosure of the rotated prop, including its feet.
float extentX = Math.AbsFloat(prop.Right[0]) * prop.HalfWidth + Math.AbsFloat(prop.Forward[0]) * prop.HalfDepth;
float extentZ = Math.AbsFloat(prop.Right[2]) * prop.HalfWidth + Math.AbsFloat(prop.Forward[2]) * prop.HalfDepth;
TraceBox trace = new TraceBox(); trace.Start = point + "0 0.15 0"; trace.End = trace.Start + "0 0.01 0"; 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; trace.Mins = Vector(-extentX, 0, -extentZ); trace.Maxs = Vector(extentX, prop.Height, extentZ); trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
trace.ExcludeArray = m_BuildSpawned;
return GetGame().GetWorld().TraceMove(trace, null) == 1; return GetGame().GetWorld().TraceMove(trace, null) == 1;
} }
protected void Layout(out array<ref ERR_RoadblockProp> covers)
{
covers.Clear();
// Footprints enclose the native model bounds, including small placement margins.
for (int side = -1; side <= 1; side += 2)
{
// Two long sandbag walls shelter each pair from the road and its approach.
AddProp(covers, COVER, side * m_Shoulder, 0, m_Right, 1.9, 0.7, 1);
AddProp(covers, COVER, side * (m_Shoulder + 2.8), side * 2.8, m_Forward, 1.9, 0.7, 1);
// Opposing, staggered stop lines: the inner ends leave a >=4m centre lane.
AddProp(covers, BARRIER, side * 3.3, side * 8, m_Forward, 1.2, 0.4, 1.3);
AddProp(covers, OBSTACLE, side * (m_Shoulder + 4), side * 8, m_Forward, 1, 1, 1.6);
AddProp(covers, SIGN, side * 3, side * 12, m_Forward * (side * 1.0), 0.45, 0.4, 0.8);
}
}
protected void AddProp(array<ref ERR_RoadblockProp> covers, ResourceName prefab, float across, float along, vector forward, float halfWidth, float halfDepth, float height)
{
vector point = ERR_Actors.Ground(m_Centre + m_Right * across + m_Forward * along);
covers.Insert(new ERR_RoadblockProp(prefab, point, forward, halfWidth, halfDepth, height));
}
protected void Rollback(array<IEntity> spawned, SCR_AIGroup group) protected void Rollback(array<IEntity> spawned, SCR_AIGroup group)
{ {
foreach (IEntity entity : spawned) ERR_Actors.DeleteSpawned(entity); foreach (IEntity entity : spawned) ERR_Actors.DeleteSpawned(entity);
ERR_Actors.DeleteSpawned(group); ERR_Actors.DeleteSpawned(group);
} }
// Start a transaction; physical pieces are created separately in BuildPost. // Start a transaction; physical pieces are created separately in BuildPost.
protected bool SpawnPost(array<vector> positions, array<vector> covers) protected bool SpawnPost(array<vector> positions, array<ref ERR_RoadblockProp> covers)
{ {
if (!PlayersClear() || m_Mode.Forces().Count() >= 250 || m_BuildGroup || positions.Count() != 4 || covers.Count() != 2) return false; if (!PlayersClear() || m_Mode.Forces().Count() >= 250 || m_BuildGroup || positions.Count() != 4 || covers.Count() != 10) return false;
m_BuildGroup = SCR_AIGroup.Cast(ERR_Actors.Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", m_Centre)); m_BuildGroup = SCR_AIGroup.Cast(ERR_Actors.Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", m_Centre));
if (!m_BuildGroup) return false; if (!m_BuildGroup) return false;
m_BuildGroup.SetFaction(m_Mode.EnemyFaction()); m_BuildGroup.SetSpawnImmediately(false); m_BuildGroup.SetDeleteWhenEmpty(false); m_BuildGroup.SetPermanentLOD(0); m_BuildGroup.SetFaction(m_Mode.EnemyFaction()); m_BuildGroup.SetSpawnImmediately(false); m_BuildGroup.SetDeleteWhenEmpty(false); m_BuildGroup.SetPermanentLOD(0);
m_BuildPositions.Copy(positions); m_BuildCovers.Copy(covers); m_BuildPositions.Copy(positions); m_BuildCovers.Clear();
foreach (ERR_RoadblockProp plannedProp : covers) m_BuildCovers.Insert(plannedProp);
m_BuildActors = 0; m_BuildCoverCount = 0; m_BuildActors = 0; m_BuildCoverCount = 0;
return true; return true;
} }
@@ -188,9 +242,9 @@ class ERR_Roadblocks
} }
if (m_BuildCoverCount < m_BuildCovers.Count()) if (m_BuildCoverCount < m_BuildCovers.Count())
{ {
vector coverPosition = m_BuildCovers[m_BuildCoverCount]; ERR_RoadblockProp prop = m_BuildCovers[m_BuildCoverCount];
if (!CoverClear(coverPosition)) { CancelPost(); return; } if (!CoverClear(prop)) { CancelPost(); return; }
IEntity cover = ERR_Actors.Spawn(COVER, coverPosition, m_Forward.VectorToAngles()); IEntity cover = ERR_Actors.Spawn(prop.Prefab, prop.Position, prop.Angles);
if (!cover) { CancelPost(); return; } if (!cover) { CancelPost(); return; }
m_BuildSpawned.Insert(cover); m_BuildCoverCount++; m_BuildSpawned.Insert(cover); m_BuildCoverCount++;
return; return;
@@ -210,26 +264,26 @@ class ERR_Roadblocks
m_BuildGroup.AddWaypoint(m_BuildHold); m_BuildGroup.AddWaypoint(m_BuildHold);
m_Mode.RegisterGroup(m_BuildGroup); m_BuildGroup.ActivateAI(); m_Mode.RegisterGroup(m_BuildGroup); m_BuildGroup.ActivateAI();
m_Mining.Register(m_BuildGroup, m_BuildHold, m_Right); m_Mining.Register(m_BuildGroup, m_BuildHold, m_Right);
PrintFormat("Reintroduced [roadblocks]: %1 at %2 guards=4 roadPassage=open HQ=registered", id, m_Centre); PrintFormat("Reintroduced [roadblocks]: %1 at %2 guards=4 props=10 roadPassage=open HQ=registered", id, m_Centre);
m_BuildSpawned.Clear(); m_BuildGroup = null; m_BuildHold = null; m_BuildSpawned.Clear(); m_BuildGroup = null; m_BuildHold = null;
m_BuildPositions.Clear(); m_BuildCovers.Clear(); m_BuildPositions.Clear(); m_BuildCovers.Clear();
m_NextSearch = System.GetTickCount() + 2000; m_NextSearch = System.GetTickCount() + 2000;
} }
// 0: reject, 1: awaiting native tile, 2: ready. // 0: reject, 1: awaiting native tile, 2: ready.
protected int Plan(out array<vector> positions, out array<vector> covers) protected int Plan(out array<vector> positions, out array<ref ERR_RoadblockProp> covers)
{ {
positions.Clear(); covers.Clear(); positions.Clear(); covers.Clear();
NavmeshWorldComponent mesh = GetGame().GetAIWorld().GetNavmeshWorldComponent("Soldiers"); NavmeshWorldComponent mesh = GetGame().GetAIWorld().GetNavmeshWorldComponent("Soldiers");
if (!mesh || !PlayersClear()) return 0; if (!mesh || !PlayersClear()) return 0;
Layout(covers);
foreach (ERR_RoadblockProp prop : covers)
if (!CoverClear(prop)) return 0;
map<int, vector> reserved = new map<int, vector>(); map<int, vector> reserved = new map<int, vector>();
for (int side = -1; side <= 1; side += 2) for (int side = -1; side <= 1; side += 2)
{ {
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) 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)); vector point = ERR_Actors.Ground(m_Centre + m_Right * (side * (m_Shoulder + 2.5)) + m_Forward * (side * (row - 1.5)));
if (!mesh.IsTileLoaded(point)) { mesh.LoadTileIn(point); return 1; } if (!mesh.IsTileLoaded(point)) { mesh.LoadTileIn(point); return 1; }
vector reachable; vector reachable;
if (!mesh.GetReachablePoint(point, 1, reachable) || vector.DistanceXZ(point, reachable) > 1 || !m_Placement.Clear(point, reserved)) return 0; if (!mesh.GetReachablePoint(point, 1, reachable) || vector.DistanceXZ(point, reachable) > 1 || !m_Placement.Clear(point, reserved)) return 0;
@@ -260,7 +314,7 @@ class ERR_Roadblocks
{ {
m_Ready = true; m_Background = true; m_Definition = definition; m_Ready = true; m_Background = true; m_Definition = definition;
m_NextReport = System.GetTickCount() + 60000; m_NextReport = System.GetTickCount() + 60000;
GetGame().GetCallqueue().CallLater(BackgroundStep, 500, true); GetGame().GetCallqueue().CallLater(BackgroundStep, 200, true);
ERR_HQ.Event("ROADBLOCKS", Status() + " Initial search yielded; remaining posts will continue in background."); ERR_HQ.Event("ROADBLOCKS", Status() + " Initial search yielded; remaining posts will continue in background.");
return ERR_PrepareResult.READY; return ERR_PrepareResult.READY;
} }
@@ -285,7 +339,8 @@ class ERR_Roadblocks
} }
return; return;
} }
array<vector> positions = {}, covers = {}; array<vector> positions = {};
array<ref ERR_RoadblockProp> covers = {};
int plan = Plan(positions, covers); int plan = Plan(positions, covers);
if (plan == 1 && System.GetTickCount() - m_CandidateStarted < 2000) return; if (plan == 1 && System.GetTickCount() - m_CandidateStarted < 2000) return;
if (plan == 2) if (plan == 2)
@@ -318,7 +373,7 @@ class ERR_Roadblocks
if (now >= m_NextReport) if (now >= m_NextReport)
{ {
m_NextReport = now + 60000; 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); PrintFormat("Reintroduced [roadblocks background]: placed=%1/%2 attempts=%3 steps=%4 workMs=%5 maxStepMs=%6 candidates=%7 plans=%8; still searching safe locations", m_Positions.Count(), m_Target, m_Attempts, m_BackgroundSteps, m_BackgroundWork, m_MaxBackgroundStep, m_Candidates, m_Planned);
} }
} }
void Stop() void Stop()
+4
View File
@@ -110,7 +110,10 @@
"Prefabs/MP/ScriptedChatEntity.et": "F69BC912AC8236F9", "Prefabs/MP/ScriptedChatEntity.et": "F69BC912AC8236F9",
"Prefabs/Props/EmptyStorages/USSR_AmmoBox_empty.et": "EBBC859CCD94D96D", "Prefabs/Props/EmptyStorages/USSR_AmmoBox_empty.et": "EBBC859CCD94D96D",
"Prefabs/Props/Furniture/Table_01.et": "53506DCB398C7EBE", "Prefabs/Props/Furniture/Table_01.et": "53506DCB398C7EBE",
"Prefabs/Props/Infrastructure/Roadblocks/RoadBlock_01.et": "64C9C1D1C2C1B38D",
"Prefabs/Props/Military/Antennas/Antenna_FIA_02/Antenna_FIA_02_base.et": "B3949F6D215A9F78", "Prefabs/Props/Military/Antennas/Antenna_FIA_02/Antenna_FIA_02_base.et": "B3949F6D215A9F78",
"Prefabs/Props/Military/Fortification/CzechHedgehog_01_painted.et": "A46D45922F8CAD6C",
"Prefabs/Props/Military/Sandbags/Sandbag_01_long_burlap.et": "21D83950DF36F250",
"Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et": "CFFEB23FBEA00322", "Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et": "CFFEB23FBEA00322",
"Prefabs/Props/Wrecks/UH1H_wreck/UH1H_wreck_static.et": "74DCE1AEC6832E18", "Prefabs/Props/Wrecks/UH1H_wreck/UH1H_wreck_static.et": "74DCE1AEC6832E18",
"Prefabs/Structures/BuildingParts/Doors/DoorSet_Base.et": "87CADB5ADC989993", "Prefabs/Structures/BuildingParts/Doors/DoorSet_Base.et": "87CADB5ADC989993",
@@ -119,6 +122,7 @@
"Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et": "01AE5FD77A9A4C21", "Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et": "01AE5FD77A9A4C21",
"Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et": "BE5D5CFA90224646", "Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et": "BE5D5CFA90224646",
"Prefabs/Structures/Military/Camps/TentUS_01/TentUSMedical_01.et": "F8427C3EFC52644E", "Prefabs/Structures/Military/Camps/TentUS_01/TentUSMedical_01.et": "F8427C3EFC52644E",
"Prefabs/Structures/Signs/Military/SignCheckpoint_01_stop.et": "C1977A326F2AA011",
"Prefabs/Structures/Walls/BarbedWire/BarbedTape_01/BarbedTape_01_Long_noStake.et": "46904AF5D473FB75", "Prefabs/Structures/Walls/BarbedWire/BarbedTape_01/BarbedTape_01_Long_noStake.et": "46904AF5D473FB75",
"Prefabs/Structures/Walls/BarbedWire/BarbedTape_01/BarbedTape_01_Short_noStake.et": "CAE1588B6265A2AB", "Prefabs/Structures/Walls/BarbedWire/BarbedTape_01/BarbedTape_01_Short_noStake.et": "CAE1588B6265A2AB",
"Prefabs/Structures/Walls/Concrete/ConcreteWall_01/ConcreteWall_01_3m.et": "60E5729BEA18F875", "Prefabs/Structures/Walls/Concrete/ConcreteWall_01/ConcreteWall_01_3m.et": "60E5729BEA18F875",
+15 -13
View File
@@ -5,8 +5,8 @@ p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c'
s='\n'.join(body(p,signature) for signature in ('protected ERR_PrepareResult Finish(', 'ERR_PrepareResult Prepare(', 'protected void SearchStep(', 'protected void BackgroundStep()', 'void Stop()')) s='\n'.join(body(p,signature) for signature in ('protected ERR_PrepareResult Finish(', 'ERR_PrepareResult Prepare(', 'protected void SearchStep(', 'protected void BackgroundStep()', 'void Stop()'))
s=re.sub(r'#ifdef WORKBENCH.*?#endif','',s,flags=re.S) s=re.sub(r'#ifdef WORKBENCH.*?#endif','',s,flags=re.S)
s=s.replace('protected ', '').replace('ERR_ScenarioDefinition definition', 'Definition* definition') s=s.replace('protected ', '').replace('ERR_ScenarioDefinition definition', 'Definition* definition')
s=s.replace('array<vector> positions = {}, covers = {};', 'Array<int> positions, covers;') s=s.replace('array<vector> positions = {};', 'Array<int> positions;').replace('array<ref ERR_RoadblockProp> covers = {};', 'Array<int> covers;')
s=s.replace('GetGame().GetCallqueue().CallLater(BackgroundStep, 500, true)', 'queue.Schedule(500)').replace('GetGame().GetCallqueue().Remove(BackgroundStep)', 'queue.Remove()') s=s.replace('GetGame().GetCallqueue().CallLater(BackgroundStep, 200, true)', 'queue.Schedule(200)').replace('GetGame().GetCallqueue().Remove(BackgroundStep)', 'queue.Remove()')
s=s.replace('GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High)', 'Bounds(m_Low, m_High)').replace('GetGame().GetPlayerManager().GetPlayerCount()', 'players') s=s.replace('GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High)', 'Bounds(m_Low, m_High)').replace('GetGame().GetPlayerManager().GetPlayerCount()', 'players')
s=s.replace('m_Mode.Settings().Get("roadblockCount")', 'm_Mode->target').replace('m_Mode.Forces().Count()', 'm_Mode->ledger') s=s.replace('m_Mode.Settings().Get("roadblockCount")', 'm_Mode->target').replace('m_Mode.Forces().Count()', 'm_Mode->ledger')
s=s.replace('m_Mode.', 'm_Mode->').replace('System.GetTickCount()', 'Now()').replace('Replication.IsServer()', 'server').replace('ERR_GameMode.Get()', 'current') s=s.replace('m_Mode.', 'm_Mode->').replace('System.GetTickCount()', 'Now()').replace('Replication.IsServer()', 'server').replace('ERR_GameMode.Get()', 'current')
@@ -27,7 +27,7 @@ int clockMs=0,players=1;bool server=true;
int Now(){return clockMs;}bool GetGame(){return true;}void Bounds(int&,int&){}bool ValidBounds(int,int){return true;} int Now(){return clockMs;}bool GetGame(){return true;}void Bounds(int&,int&){}bool ValidBounds(int,int){return true;}
template<class...T>void PrintFormat(T...){}void Event(string,string){} template<class...T>void PrintFormat(T...){}void Event(string,string){}
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}void Clear(){this->clear();}}; template<class T>struct Array:std::vector<T>{int Count(){return this->size();}void Clear(){this->clear();}};
struct Queue{int scheduled=0,removed=0;bool active=false;void Schedule(int ms){assert(ms==500);scheduled++;active=true;}void Remove(){removed++;active=false;}}; struct Queue{int scheduled=0,removed=0;bool active=false;void Schedule(int ms){assert(ms==200);scheduled++;active=true;}void Remove(){removed++;active=false;}};
struct Mode{int target=30,ledger=0,phase=Phase::PRISON;bool active=true;int Phase(){return phase;}bool Active(){return active;}};Mode* current=nullptr; struct Mode{int target=30,ledger=0,phase=Phase::PRISON;bool active=true;int Phase(){return phase;}bool Active(){return active;}};Mode* current=nullptr;
struct Definition{}; struct Definition{};
struct Mining{bool stopped=false;void Stop(){stopped=true;}}; struct Mining{bool stopped=false;void Stop(){stopped=true;}};
@@ -99,9 +99,10 @@ print('PASS: actual roadblock scheduler yields initial deadline/attempt cap to b
# Run actual staged physical creation and rollback, with native entities doubled. # Run actual staged physical creation and rollback, with native entities doubled.
s='\n'.join(body(p,sig) for sig in ('protected bool SpawnPost(', 'protected void CancelPost()', 'protected void BuildPost()')) s='\n'.join(body(p,sig) for sig in ('protected bool SpawnPost(', 'protected void CancelPost()', 'protected void BuildPost()'))
s=s.replace('protected ', '').replace('array<vector> positions','Array<int>& positions').replace('array<vector> covers','Array<int>& covers') s=s.replace('protected ', '').replace('array<vector> positions','Array<int>& positions').replace('array<ref ERR_RoadblockProp> covers','Array<Prop>& covers').replace('ERR_RoadblockProp prop', 'Prop prop')
s=s.replace('SCR_AIGroup.Cast','Node::Cast').replace('AIWaypoint.Cast','Node::Cast').replace('IEntity actor','Node* actor').replace('IEntity cover','Node* cover') s=s.replace('SCR_AIGroup.Cast','Node::Cast').replace('AIWaypoint.Cast','Node::Cast').replace('IEntity actor','Node* actor').replace('IEntity cover','Node* cover')
s=s.replace('foreach (IEntity ownedEntity : m_BuildSpawned)','for (Node* ownedEntity : m_BuildSpawned)').replace('vector point','int point').replace('vector coverPosition','int coverPosition') s=s.replace('foreach (IEntity ownedEntity : m_BuildSpawned)','for (Node* ownedEntity : m_BuildSpawned)').replace('vector point','int point').replace('vector coverPosition','int coverPosition')
s=s.replace('foreach (ERR_RoadblockProp plannedProp : covers)', 'for (Prop plannedProp : covers)')
s=s.replace('m_Forward.VectorToAngles()', '0').replace('m_Positions.Count().ToString()', 'std::to_string(m_Positions.Count())') s=s.replace('m_Forward.VectorToAngles()', '0').replace('m_Positions.Count().ToString()', 'std::to_string(m_Positions.Count())')
s=s.replace('GetGame().GetWorld().GetWorldTime()', 'Now()').replace('System.GetTickCount()', 'Now()').replace('null','nullptr') s=s.replace('GetGame().GetWorld().GetWorldTime()', 'Now()').replace('System.GetTickCount()', 'Now()').replace('null','nullptr')
s=s.replace('m_BuildGroup.', 'm_BuildGroup->').replace('m_BuildHold.', 'm_BuildHold->') s=s.replace('m_BuildGroup.', 'm_BuildGroup->').replace('m_BuildHold.', 'm_BuildHold->')
@@ -122,31 +123,32 @@ struct Ledger{int count=0,registered=0;bool accept=true;int Count(){return count
struct Mode{Ledger ledger;int owned=0,groups=0;Ledger& Forces(){return ledger;}int EnemyFaction(){return 1;}void Own(Node*n){assert(n);owned++;}void RegisterGroup(Node*){groups++;}}; struct Mode{Ledger ledger;int owned=0,groups=0;Ledger& Forces(){return ledger;}int EnemyFaction(){return 1;}void Own(Node*n){assert(n);owned++;}void RegisterGroup(Node*){groups++;}};
struct Placement{bool clear=true;bool Clear(int,void*){return clear;}}; struct Placement{bool clear=true;bool Clear(int,void*){return clear;}};
struct Mining{int registered=0;void Register(Node*,Node*,int){registered++;}}; struct Mining{int registered=0;void Register(Node*,Node*,int){registered++;}};
struct Prop{string Prefab="cover";int Position=5,Angles=0;};
struct Build{ struct Build{
Mode m_Mode;Placement m_Placement;Mining m_Mining;Node* m_BuildGroup=nullptr;Node* m_BuildHold=nullptr; Mode m_Mode;Placement m_Placement;Mining m_Mining;Node* m_BuildGroup=nullptr;Node* m_BuildHold=nullptr;
Array<int> m_BuildPositions,m_BuildCovers,m_Positions;Array<Node*>m_BuildSpawned,m_Groups;Array<string>m_Roster={"one","two"}; Array<int> m_BuildPositions,m_Positions;Array<Prop>m_BuildCovers;Array<Node*>m_BuildSpawned,m_Groups;Array<string>m_Roster={"one","two"};
int m_BuildActors=0,m_BuildCoverCount=0,m_Centre=10,m_Right=0,m_NextSearch=0,rolledBack=0;string COVER="cover";bool playersClear=true,coversClear=true; int m_BuildActors=0,m_BuildCoverCount=0,m_Centre=10,m_Right=0,m_NextSearch=0,rolledBack=0;string COVER="cover";bool playersClear=true,coversClear=true;
bool PlayersClear(){return playersClear;}bool CoverClear(int){return coversClear;} bool PlayersClear(){return playersClear;}bool CoverClear(Prop){return coversClear;}
void Rollback(Array<Node*>&entities,Node*group){rolledBack+=entities.Count()+(group?1:0);} void Rollback(Array<Node*>&entities,Node*group){rolledBack+=entities.Count()+(group?1:0);}
'''+s+r''' '''+s+r'''
}; };
int main(){Array<int> positions={1,2,3,4},covers={5,6}; int main(){Array<int> positions={1,2,3,4};Array<Prop>covers(10);
{Build b;int start=ERR_Actors::calls;assert(b.SpawnPost(positions,covers));assert(ERR_Actors::calls==start+1); {Build b;int start=ERR_Actors::calls;assert(b.SpawnPost(positions,covers));assert(ERR_Actors::calls==start+1);
for(int piece=0;piece<7;piece++){int calls=ERR_Actors::calls;b.BuildPost();assert(ERR_Actors::calls==calls+1&&b.m_Positions.Count()==0&&b.m_Mode.owned==0);} for(int piece=0;piece<15;piece++){int calls=ERR_Actors::calls;b.BuildPost();assert(ERR_Actors::calls==calls+1&&b.m_Positions.Count()==0&&b.m_Mode.owned==0);}
b.BuildPost();assert(!b.m_BuildGroup&&b.m_Positions.Count()==1&&b.m_Groups.Count()==1&&b.m_Mode.owned==8&&b.m_Mode.groups==1&&b.m_Mining.registered==1&&b.m_NextSearch==2100); b.BuildPost();assert(!b.m_BuildGroup&&b.m_Positions.Count()==1&&b.m_Groups.Count()==1&&b.m_Mode.owned==16&&b.m_Mode.groups==1&&b.m_Mining.registered==1&&b.m_NextSearch==2100);
} }
// Player approaches at any stage: no registered post and every owned piece removed. // Player approaches at any stage: no registered post and every owned piece removed.
for(int stage=0;stage<=7;stage++){Build b;assert(b.SpawnPost(positions,covers));for(int i=0;i<stage;i++)b.BuildPost();b.playersClear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.m_Positions.Count()==0&&b.m_Mode.owned==0&&b.rolledBack==stage+1);} for(int stage=0;stage<=15;stage++){Build b;assert(b.SpawnPost(positions,covers));for(int i=0;i<stage;i++)b.BuildPost();b.playersClear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.m_Positions.Count()==0&&b.m_Mode.owned==0&&b.rolledBack==stage+1);}
// Any native entity creation failure rolls back rather than consuming a post. // Any native entity creation failure rolls back rather than consuming a post.
for(int stage=0;stage<8;stage++){Build b;ERR_Actors::failAt=ERR_Actors::calls+stage+1;bool begun=b.SpawnPost(positions,covers); for(int stage=0;stage<16;stage++){Build b;ERR_Actors::failAt=ERR_Actors::calls+stage+1;bool begun=b.SpawnPost(positions,covers);
if(stage==0)assert(!begun);else{for(int i=0;i<stage;i++)b.BuildPost();assert(b.rolledBack==stage);} if(stage==0)assert(!begun);else{for(int i=0;i<stage;i++)b.BuildPost();assert(b.rolledBack==stage);}
assert(!b.m_BuildGroup&&b.m_Positions.Count()==0&&b.m_Mode.owned==0);ERR_Actors::failAt=0; assert(!b.m_BuildGroup&&b.m_Positions.Count()==0&&b.m_Mode.owned==0);ERR_Actors::failAt=0;
} }
{Build b;b.SpawnPost(positions,covers);b.m_Placement.clear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==1);} {Build b;b.SpawnPost(positions,covers);b.m_Placement.clear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==1);}
{Build b;b.SpawnPost(positions,covers);for(int i=0;i<4;i++)b.BuildPost();b.coversClear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==5);} {Build b;b.SpawnPost(positions,covers);for(int i=0;i<4;i++)b.BuildPost();b.coversClear=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==5);}
{Build b;b.SpawnPost(positions,covers);for(int i=0;i<7;i++)b.BuildPost();b.m_Mode.ledger.accept=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==8&&b.m_Positions.Count()==0);} {Build b;b.SpawnPost(positions,covers);for(int i=0;i<15;i++)b.BuildPost();b.m_Mode.ledger.accept=false;b.BuildPost();assert(!b.m_BuildGroup&&b.rolledBack==16&&b.m_Positions.Count()==0);}
{Build b;b.SpawnPost(positions,covers);b.BuildPost();b.CancelPost();assert(!b.m_BuildGroup&&b.rolledBack==2&&b.m_Mode.owned==0);} {Build b;b.SpawnPost(positions,covers);b.BuildPost();b.CancelPost();assert(!b.m_BuildGroup&&b.rolledBack==2&&b.m_Mode.owned==0);}
for(auto*n:ERR_Actors::all)delete n; for(auto*n:ERR_Actors::all)delete n;
} }
''') ''')
print('PASS: actual staged post creation spawns at most one entity per callback, commits only all four guards/two covers/waypoint, and rolls back player approach, obstructions, spawn/ledger failure and cancellation') print('PASS: actual staged post creation spawns at most one entity per callback, commits only all four guards/ten props/waypoint, and rolls back player approach, obstructions, spawn/ledger failure and cancellation')
+98
View File
@@ -0,0 +1,98 @@
"""Execute the shipped checkpoint layout across road widths and rotations."""
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_Roadblocks.c'
s='\n'.join(body(p,x) for x in ('protected void Layout(', 'protected void AddProp('))
s=s.replace('protected ', '').replace('out ', '').replace('array<ref ERR_RoadblockProp> covers', 'Array<Prop*>& covers')
s=s.replace('ResourceName ', 'string ').replace('vector ', 'Vec ').replace('ERR_Actors.Ground','Ground').replace('new ERR_RoadblockProp','new Prop')
run(r'''
#include <cassert>
#include <cmath>
#include <string>
#include <vector>
#include <map>
#include <algorithm>
using string=std::string;
struct Vec{double x=0,y=0,z=0;Vec operator+(Vec b)const{return{x+b.x,y+b.y,z+b.z};}Vec operator-(Vec b)const{return{x-b.x,y-b.y,z-b.z};}Vec operator*(double k)const{return{x*k,y*k,z*k};}};
double dot(Vec a,Vec b){return a.x*b.x+a.y*b.y+a.z*b.z;}
Vec Ground(Vec p){p.y=50;return p;}
template<class T>struct Array:std::vector<T>{void Clear(){for(auto*p:*this)delete p;this->clear();}void Insert(T p){this->push_back(p);}};
struct Prop{string Prefab;Vec Position,Forward,Right;double HalfWidth,HalfDepth,Height;
Prop(string p,Vec v,Vec f,double w,double d,double h):Prefab(p),Position(v),Forward(f),Right({f.z,0,-f.x}),HalfWidth(w),HalfDepth(d),Height(h){}
};
struct Checkpoint{Vec m_Centre={1000,50,2000},m_Forward,m_Right;double m_Shoulder;
string COVER="cover",BARRIER="barrier",OBSTACLE="obstacle",SIGN="sign";
'''+s+r'''
};
int main(){
for(double width:{4.,6.,10.,16.})for(int degrees=0;degrees<360;degrees+=15){
Checkpoint l;double angle=degrees*3.141592653589793/180;
l.m_Forward={std::sin(angle),0,std::cos(angle)};l.m_Right={std::cos(angle),0,-std::sin(angle)};
l.m_Shoulder=std::max(width*.5+1.5,4.);Array<Prop*>props;l.Layout(props);assert(props.size()==10);
std::map<string,int>counts;std::vector<double>barriers;
for(auto*p:props){counts[p->Prefab]++;
Vec delta=p->Position-l.m_Centre;double across=dot(delta,l.m_Right),along=dot(delta,l.m_Forward);
// Whole footprints (not just origins) preserve a four-metre straight passage.
double reach=std::abs(dot(p->Right,l.m_Right))*p->HalfWidth+std::abs(dot(p->Forward,l.m_Right))*p->HalfDepth;
assert(std::abs(across)-reach>=2.-1e-8);
if(p->Prefab=="barrier")barriers.push_back(along);
if(p->Prefab=="sign")assert(dot(p->Forward,l.m_Forward)*along>0); // both approaches face outward
// Guard starting volumes cannot intersect any prop in the planned post.
for(int side:{-1,1})for(int row:{-1,1}){
Vec guard=l.m_Centre+l.m_Right*(side*(l.m_Shoulder+2.5))+l.m_Forward*(side*(row-1.5));
Vec rel=guard-p->Position;
assert(std::abs(dot(rel,p->Right))>p->HalfWidth+.45 || std::abs(dot(rel,p->Forward))>p->HalfDepth+.45);
}
}
assert(counts["cover"]==4&&counts["barrier"]==2&&counts["obstacle"]==2&&counts["sign"]==2);
assert(barriers.size()==2&&std::abs(barriers[0]-barriers[1])>15.9);
// No physical prop footprint intersects another (separating axes).
for(size_t i=0;i<props.size();i++)for(size_t j=i+1;j<props.size();j++){
auto*a=props[i];auto*b=props[j];bool separate=false;
for(Vec axis:{a->Right,a->Forward,b->Right,b->Forward}){
double ar=std::abs(dot(a->Right,axis))*a->HalfWidth+std::abs(dot(a->Forward,axis))*a->HalfDepth;
double br=std::abs(dot(b->Right,axis))*b->HalfWidth+std::abs(dot(b->Forward,axis))*b->HalfDepth;
if(std::abs(dot(a->Position-b->Position,axis))>=ar+br)separate=true;
}
assert(separate);
}
props.Clear();
}
}
''')
print('PASS: shipped checkpoint layout preserves vehicle lane, opposing signs, staggered barriers, clear guard slots and separate props across 4–16m roads and all tested headings')
# Execute the actual clearance path: owned pieces are excluded only after
# checking guards; terrain and foreign obstructions remain authoritative.
s=body(p,'protected bool CoverClear(').replace('protected ', '').replace('ERR_RoadblockProp prop','Prop prop')
s=s.replace('vector ', 'Vec ').replace('foreach (IEntity builtEntity : m_BuildSpawned)', 'for (Node* builtEntity : m_BuildSpawned)')
s=s.replace('ChimeraCharacter.Cast(builtEntity)', 'builtEntity->character').replace('builtEntity.GetOrigin()', 'builtEntity->point')
s=s.replace('ERR_Actors.Ground','Ground').replace('Vector(', 'Vec(').replace('Math.AbsFloat', 'std::abs').replace('vector.Dot','dot')
s=s.replace('ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point)', 'water').replace('GetGame().GetWorld().TraceMove(trace, null)', 'TraceMove(trace)')
s=s.replace('TraceBox trace = new TraceBox();','TraceBox trace;').replace('"0 0.15 0"','Vec(0,.15,0)').replace('"0 0.01 0"','Vec(0,.01,0)').replace('TraceFlags.', 'TraceFlags::')
run(r'''
#include <cassert>
#include <cmath>
#include <vector>
struct Vec {double v[3];Vec(double x=0,double y=0,double z=0):v{x,y,z}{}double& operator[](int i){return v[i];}Vec operator+(Vec b){return {v[0]+b[0],v[1]+b[1],v[2]+b[2]};}Vec operator-(Vec b){return {v[0]-b[0],v[1]-b[1],v[2]-b[2]};}Vec operator*(double n){return {v[0]*n,v[1]*n,v[2]*n};}};
double dot(Vec a,Vec b){return a[0]*b[0]+a[1]*b[1]+a[2]*b[2];}
struct Node{bool character;Vec point;};
struct Prop{Vec Position,Right={1,0,0},Forward={0,0,1};double HalfWidth=1.9,HalfDepth=.7,Height=1;};
namespace TraceFlags{int WORLD=1,ENTS=2;}
struct TraceBox{Vec Start,End,Mins,Maxs;int Flags=0;std::vector<Node*>ExcludeArray;};
struct Clearance{
bool water=false,blocked=false;double slope=0;int traces=0;std::vector<Node*>m_BuildSpawned;
Vec Ground(Vec p){p[1]=slope*p[0];return p;}
double TraceMove(TraceBox trace){traces++;assert(trace.Flags==3&&trace.ExcludeArray==m_BuildSpawned);assert(std::abs(trace.Mins[0]+1.9)<1e-5&&std::abs(trace.Maxs[2]-.7)<1e-5);return blocked?0:1;}
'''+s+r'''
};
int main(){
Prop prop;Clearance c;assert(c.CoverClear(prop));
c.water=true;assert(!c.CoverClear(prop));c.water=false;
c.slope=.2;assert(!c.CoverClear(prop));c.slope=0;
c.blocked=true;assert(!c.CoverClear(prop));c.blocked=false;
Node ownProp{false,{0,0,0}},guard{true,{3,0,0}};c.m_BuildSpawned={&ownProp,&guard};assert(c.CoverClear(prop));
int traces=c.traces;guard.point={2,0,0};assert(!c.CoverClear(prop)&&c.traces==traces);
guard.point={0,0,1.4};assert(c.CoverClear(prop));guard.point={0,0,1};assert(!c.CoverClear(prop));
}
''')
print('PASS: actual prop clearance excludes owned geometry while retaining moved-guard, foreign-obstruction, water and terrain rejection')
+1 -1
View File
File diff suppressed because one or more lines are too long