Allow off-airport extraction across terrains; record Workshop 0.0.124
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-10-28 15:19:50 +01:00
parent 95d21b06ff
commit 970dd338f5
33 changed files with 5892 additions and 134 deletions
@@ -186,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;
}
@@ -211,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()
@@ -234,7 +239,7 @@ class ERR_GeneratedSites
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("secondary aircraft pad");
else if (!RescueFits(site, position, heading)) RecordRejection("aircraft landing clearance / fleet space");
else fits = true;
if (fits)
{
@@ -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;
@@ -61,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;
@@ -185,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;
@@ -398,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));
@@ -7,8 +7,6 @@ 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.
@@ -38,7 +36,7 @@ class ERR_TerrainSites : ERR_GeneratedSites
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)
@@ -47,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);
}
@@ -105,9 +103,7 @@ class ERR_TerrainSites : ERR_GeneratedSites
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);
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;
@@ -117,68 +113,19 @@ class ERR_TerrainSites : ERR_GeneratedSites
}
protected vector SamplePosition(ERR_SiteDefinition site)
{
if (site.Kind != ERR_LocationKind.EXTRACTION || !site.RequireAirfield || 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 void ExpandExtractionSearch(ERR_SiteDefinition site)
{
if (site.Kind != ERR_LocationKind.EXTRACTION || !site.RequireAirfield) return;
if (m_Attempts < 12000 && System.GetTickCount() - m_Began < 30000) return;
// An airport is a preferred starting area, not proof that a usable pad exists.
// Use the remaining budget to find a clear helicopter landing zone elsewhere.
// Do not reset the total deadline or carry an airport candidate into this pass.
site.RequireAirfield = false;
site.Label = "Extraction landing zone";
m_HaveCandidate = false;
ReportSearch(site, "airport search exhausted; expanding search");
PrintFormat("Reintroduced [terrain]: %1 airport search exhausted; surveying off-airport landing zones with full aircraft clearance", site.Id);
}
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];
ReportProgress(site);
ExpandExtractionSearch(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);