Harden participant and map ownership and validate bounded physical flight

This commit is contained in:
2026-09-19 12:17:01 +02:00
parent dfd7cc49e2
commit 4125018d04
120 changed files with 20043 additions and 47 deletions
@@ -166,6 +166,9 @@ class ERR_MortarReadiness
}
if (m_Battery.CountShells() != 27) return ERR_MortarSupplyState.PENDING;
m_Ready = true;
// One physical-ready handoff: HQ already owns/reserves this group, while
// Intel must see its actual late-spawned members after finite stock settles.
mode.RegisterGroup(group);
group.ActivateAI();
foreach (AIAgent readyAgent : agents)
{
@@ -55,7 +55,10 @@ class ERR_MortarSystem
m_Batteries.Insert(battery);
ERR_GameMode mode = ERR_GameMode.Get();
mode.Own(gun);
mode.RegisterGroup(group);
// HQ owns/reserves the finite asset immediately. Publish its physical
// membership to companion consumers only after readiness has completed.
mode.Own(group);
m_HQ.RegisterGroup(group);
m_HQ.ReserveGroup(group);
battery.Audit("DEPLOY", "authored gun and four-person crew; finite ammunition initialization pending");
return true;
@@ -18,6 +18,8 @@ modded class ERR_GameMode
protected bool m_ERRTestHQResponse;
protected bool m_ERRTestMortar;
protected bool m_ERRTestMortarFire;
protected bool m_ERRTestMapOwnership;
protected ref ERRM_NativeMapOwnershipProbe m_ERRMapOwnership;
protected bool m_ERRTestMortarSalvo;
protected ref ERR_NativeMortarSalvoProbe m_ERRMortarSalvo;
protected bool m_ERRTestMortarCourier;
@@ -40,6 +42,7 @@ modded class ERR_GameMode
m_ERRTestRadioNetwork = outcome == "radio-network";
m_ERRTestHQResponse = outcome == "hq-response";
m_ERRTestMortar = outcome == "mortar-inventory";
m_ERRTestMapOwnership = outcome == "map-ownership";
m_ERRTestMortarFire = outcome == "mortar-fire" || outcome == "mortar-expiry";
m_ERRTestMortarCourier = outcome == "mortar-courier";
m_ERRTestMortarSalvo = outcome == "mortar-salvo" || outcome == "mortar-refresh";
@@ -69,6 +72,7 @@ modded class ERR_GameMode
}
protected void ERR_StopHQPatrol()
{
if (m_ERRMapOwnership) m_ERRMapOwnership.Stop();
if (m_ERRHQResponseProof) m_ERRHQResponseProof.Stop();
if (m_ERRMortarSalvo) m_ERRMortarSalvo.Stop();
if (m_ERRMortarCourier) m_ERRMortarCourier.Stop();
@@ -109,6 +113,15 @@ modded class ERR_GameMode
protected void ERR_SmokePhase(ERR_Phase previous, ERR_Phase current)
{
if (m_ERRSmokeDone || !ERR_MissionRules.Terminal(current)) return;
if (m_ERRTestMapOwnership)
{
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 201, "native map ownership probe cleaned up before explicit reset")) return;
m_ERRSmokeDone = true;
GetGame().GetCallqueue().Remove(ERR_SmokeTick);
ERR_StopHQPatrol();
Print("ERR SMOKE COMPLETE: native replacement map/config/root ownership and spare deletion; camera authorization observed, render flag not measured");
return;
}
if (m_ERRTestMortarSalvo)
{
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 191, "native configured salvo and unchanged-report replay observation complete before reset")) return;
@@ -207,6 +220,13 @@ modded class ERR_GameMode
if (m_ERRSmokeDone) return;
if (now > m_ERRSmokeDeadline) { ERR_Check(false, "timeout at step " + m_ERRSmokeStep.ToString()); return; }
if (now < m_ERRSmokeWait) return;
if (m_ERRSmokeStep == 200)
{
int result = m_ERRMapOwnership.Tick(now);
if (result < 0) { ERR_Check(false, "native map ownership fixture"); return; }
if (result == 1) { m_ERRSmokeStep = 201; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRSmokeStep == 190)
{
int result = m_ERRMortarSalvo.Tick(now);
@@ -267,6 +287,13 @@ modded class ERR_GameMode
m_ERRSmokeStep = 130;
return;
}
if (m_ERRTestMapOwnership)
{
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRMapOwnership = new ERRM_NativeMapOwnershipProbe();
m_ERRSmokeStep = 200;
return;
}
if (m_ERRTestMortarSalvo)
{
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
@@ -425,6 +452,16 @@ modded class ERR_GameMode
if (!ERR_Check(battery.CountShells() == 27, "mortar starts with exactly 27 physical HE shells")) return;
array<AIAgent> agents = {};
battery.Group.GetAgents(agents);
array<IEntity> intelCrew = {};
foreach (AIAgent intelAgent : agents)
{
IEntity intelActor;
if (intelAgent) intelActor = intelAgent.GetControlledEntity();
if (!ERR_Check(intelActor && !intelCrew.Contains(intelActor), "mortar Intel observation uses distinct actual crew actors")) return;
if (!ERR_Check(m_Intel && m_Intel.ERR_ProbeWasRolled(intelActor), "ready mortar crew actor reached its one optional Intel carrying roll")) return;
intelCrew.Insert(intelActor);
}
if (!ERR_Check(intelCrew.Count() == 4, "all four original ready physical crew reached Intel without forcing documents or changing carrying chance")) return;
foreach (AIAgent agent : agents)
{
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(agent.GetControlledEntity().FindComponent(InventoryStorageManagerComponent));
@@ -182,11 +182,13 @@ modded class ERR_GameMode
Print("ERR SMOKE COMPLETE: actual gate destruction and sampled prison crossing; repositioned fixture, not walking or multiplayer");
return;
}
if (m_ERRExtractionCase == "powered-flight")
if (m_ERRExtractionCase == "powered-flight" || m_ERRExtractionCase == "powered-translate")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1802, "native powered-flight fixture finished before explicit test reset")) return;
ERR_StopHealthProbes();
Print("ERR SMOKE COMPLETE: native pilot-input powered takeoff, hover, contact landing and shutdown; isolated mechanism fixture reset");
if (m_ERRExtractionCase == "powered-translate")
Print("ERR SMOKE COMPLETE: native powered takeoff, bounded four-metre translation, target hold, contact landing and engine-off; isolated mechanism fixture reset");
else Print("ERR SMOKE COMPLETE: native pilot-input powered takeoff, hover, contact landing and shutdown; isolated mechanism fixture reset");
return;
}
if (m_ERRExtractionCase == "local-admission")
@@ -515,7 +517,7 @@ modded class ERR_GameMode
m_ERRBaseStep = 1900;
return;
}
if (m_ERRExtractionCase == "powered-flight")
if (m_ERRExtractionCase == "powered-flight" || m_ERRExtractionCase == "powered-translate")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRPoweredFlightProbe = new ERR_NativePoweredFlightProbe(this);
@@ -115,6 +115,42 @@ class ERR_PoweredFlightRules
{
return contact && height < 1 && height > -1 && speed < 0.5 && angular < 0.1 && upright > 0.98;
}
static bool TranslationInputs(bool selected, bool actions, bool autohover)
{
return !selected || (actions && autohover);
}
static bool TranslationActive(bool selected, bool frozen, bool actions, bool autohover, int stage)
{
return selected && frozen && actions && autohover && stage == 5;
}
static bool TranslationTarget(vector home, vector start, vector fallback, out vector target)
{
target = vector.Zero;
if (!(vector.DistanceXZ(home, start) < 8)) return false;
vector direction = home - start; direction[1] = 0;
if (direction.Length() < 0.5) { direction = fallback; direction[1] = 0; }
float length = direction.Length();
if (!(length >= 0.5)) return false;
target = start + direction * (4 / length);
return vector.DistanceXZ(start, target) >= 3.99 && vector.DistanceXZ(start, target) <= 4.01 && vector.DistanceXZ(home, target) <= 5;
}
static vector TranslationDemand(vector target, vector position, vector velocity)
{
vector error = target - position; error[1] = 0;
vector desired = error * 0.25;
float speed = desired.Length();
if (speed > 0.5) desired = desired * (0.5 / speed);
velocity[1] = 0;
// Experimental outer velocity request only; native autohover stabilizes attitude.
vector demand = (desired - velocity) * 0.05;
float magnitude = demand.Length();
if (magnitude > 0.03) demand = demand * (0.03 / magnitude);
return demand;
}
static bool TranslationHeld(bool contact, float height, float speed, float vertical, float upright, float drift, float horizontalSpeed, float targetError, float displacement)
{
return !contact && Hover(height, speed, vertical, upright, drift) && horizontalSpeed >= 0 && horizontalSpeed < 0.25 && targetError >= 0 && targetError <= 0.75 && displacement >= 3.25;
}
}
class ERR_NativePoweredFlightProbe
{
@@ -167,6 +203,12 @@ class ERR_NativePoweredFlightProbe
protected bool m_ActionInputs;
protected bool m_Started;
protected bool m_AutohoverMode;
protected bool m_TranslationMode;
protected bool m_TranslationFrozen;
protected bool m_TranslationProven;
protected vector m_TranslationOrigin;
protected vector m_TranslationTarget;
protected int m_TranslationUpdates;
protected bool m_AutohoverSaved;
protected bool m_OriginalAutohover;
protected HelicopterControllerComponent m_AutohoverController;
@@ -178,13 +220,15 @@ class ERR_NativePoweredFlightProbe
static bool Enabled()
{
string enabled, outcome;
return Replication.IsServer() && System.GetCLIParam("errBaseSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "powered-flight";
return Replication.IsServer() && System.GetCLIParam("errBaseSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && (outcome == "powered-flight" || outcome == "powered-translate");
}
static ERR_NativePoweredFlightProbe Current() { return s_Current; }
void ERR_NativePoweredFlightProbe(ERR_GameMode mode)
{
string actionInputs;
m_ActionInputs = System.GetCLIParam("errFlightActionInputs", actionInputs) && actionInputs == "1";
string outcome;
m_TranslationMode = System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "powered-translate";
m_Mode = mode; m_Deadline = GetGame().GetWorld().GetWorldTime() + 240000;
if (Enabled() && !s_Current) s_Current = this;
else m_Stopped = true;
@@ -299,6 +343,13 @@ class ERR_NativePoweredFlightProbe
// Give its lateral controller zero custom cyclic in this experiment.
// Preserve collective/yaw and bounds; never combine two cyclic controllers.
if (m_AutohoverMode) { forward = 0; aside = 0; }
if (ERR_PoweredFlightRules.TranslationActive(m_TranslationMode, m_TranslationFrozen, m_ActionInputs, m_AutohoverMode, m_Stage))
{
vector demand = ERR_PoweredFlightRules.TranslationDemand(m_TranslationTarget, transform[3], velocity);
forward = ERR_PoweredFlightRules.ClampInput(vector.Dot(demand, transform[2]), 0.03);
aside = ERR_PoweredFlightRules.ClampInput(vector.Dot(demand, transform[0]), 0.03);
m_TranslationUpdates++;
}
float headingError = m_Heading - m_Aircraft.GetYawPitchRoll()[0];
while (headingError > 180) headingError -= 360;
while (headingError < -180) headingError += 360;
@@ -415,6 +466,7 @@ class ERR_NativePoweredFlightProbe
if (!m_Simulation || !m_Aircraft || now < m_NextLog) return;
m_NextLog = now + 2000;
if (m_Stage == 4 && now < m_StageDeadline - 57000) m_NextLog = now + 250;
if (m_TranslationMode && m_Stage == 5) m_NextLog = now + 500;
if (m_ActionInputs)
{
bool ownedActions = OwnActionManager(m_OwnedActions);
@@ -429,6 +481,22 @@ class ERR_NativePoweredFlightProbe
PrintFormat("ERR POWERED ROTOR AXIS: local=%1 acceleration=%2", rotorAxis, m_Simulation.GetAcceleration());
PrintFormat("ERR POWERED MOTION: offset=%1 velocity=%2 angularLocal=%3 forward=%4 right=%5", m_Home - transform[3], m_Simulation.GetVelocity(), m_Simulation.GetAngularVelocityLocal(), transform[2], transform[0]);
PrintFormat("ERR POWERED INPUT: pilotHooks=%1 vehicleHooks=%2 readback=%3 collective=%4 cyclicF=%5 cyclicR=%6 antiTorque=%7 rotorRPM=%8 targetRPM=%9", m_ControlUpdates, m_HelicopterUpdates, m_ReadbackOK, m_Input.GetVehicleCollective(), m_Input.GetVehicleCyclicForward(), m_Input.GetVehicleCyclicAside(), m_Input.GetVehicleAntiTorque(), m_Simulation.RotorGetRPM(0), m_Simulation.RotorGetRPMTarget(0));
if (m_TranslationMode && m_TranslationFrozen)
{
vector direction = m_TranslationTarget - m_TranslationOrigin; direction[1] = 0; direction = direction * 0.25;
vector travel = transform[3] - m_TranslationOrigin; travel[1] = 0;
float along = vector.Dot(travel, direction);
vector cross = travel - direction * along;
vector horizontal = m_Simulation.GetVelocity(); horizontal[1] = 0;
PrintFormat("ERR POWERED TRANSLATION: stage=%1 origin=%2 target=%3 position=%4 along=%5 cross=%6 error=%7 horizontalSpeed=%8 updates=%9", m_Stage, m_TranslationOrigin, m_TranslationTarget, transform[3], along, cross.Length(), vector.DistanceXZ(transform[3], m_TranslationTarget), horizontal.Length(), m_TranslationUpdates);
SignalsManagerComponent signals = m_Controller.GetSignalsManager();
if (signals)
{
int cyclicForward = signals.FindSignal("MainRotorCyclicForward");
int cyclicAside = signals.FindSignal("MainRotorCyclicAside");
if (cyclicForward >= 0 && cyclicAside >= 0) PrintFormat("ERR POWERED TRANSLATION SIGNALS: pilotForward=%1 pilotAside=%2 rotorForward=%3 rotorAside=%4 (native autohover may modify rotor commands)", m_Input.GetVehicleCyclicForward(), m_Input.GetVehicleCyclicAside(), signals.GetSignalValue(cyclicForward), signals.GetSignalValue(cyclicAside));
}
}
}
protected bool PollNativeFlightComponents(float now)
{
@@ -533,12 +601,42 @@ class ERR_NativePoweredFlightProbe
// Keep real airborne/failed test actors for diagnosis and mode-owned cleanup.
// Stop never repairs, stabilizes, teleports, refuels or replaces them.
}
protected bool BeginTranslation(float now, vector position, vector fallback)
{
if (!Check(m_TranslationMode && !m_TranslationFrozen && m_Stage == 4 && m_ActionInputs && m_AutohoverMode && AutohoverReady(), "translation starts once after the original native hover stage")) return false;
vector target;
if (!Check(ERR_PoweredFlightRules.TranslationTarget(m_Home, position, fallback, target), "frozen four-metre horizontal target remains within five metres of original home")) return false;
SignalsManagerComponent signals = m_Controller.GetSignalsManager();
if (!Check(signals && signals.FindSignal("MainRotorCyclicForward") >= 0 && signals.FindSignal("MainRotorCyclicAside") >= 0, "existing native cyclic signals available for translation observation")) return false;
m_TranslationOrigin = position;
m_TranslationTarget = target;
m_TranslationFrozen = true;
m_Stage = 5; m_StageDeadline = now + 45000; m_Stable = -1; m_NextLog = 0;
PrintFormat("ERR POWERED TRANSLATION BEGIN: origin=%1 target=%2 originalHome=%3 deadline=%4 overallDeadline=%5", m_TranslationOrigin, m_TranslationTarget, m_Home, m_StageDeadline, m_Deadline);
return true;
}
protected bool CheckTranslation(float now, vector position, float height, float speed, float upright, float drift)
{
if (!ERR_PoweredFlightRules.TranslationActive(m_TranslationMode, m_TranslationFrozen, m_ActionInputs, m_AutohoverMode, m_Stage)) return Check(false, "translation retains its selected inputs and frozen stage ownership");
if (now >= m_StageDeadline) return Check(false, "four-metre translation and five-second target hold finish within 45 seconds");
vector velocity = m_Simulation.GetVelocity();
vector horizontal = velocity; horizontal[1] = 0;
bool held = m_TranslationUpdates >= 3 && ERR_PoweredFlightRules.TranslationHeld(m_Simulation.HasAnyGroundContact(), height, speed, velocity[1], upright, drift, horizontal.Length(), vector.DistanceXZ(position, m_TranslationTarget), vector.DistanceXZ(position, m_TranslationOrigin));
if (!held) m_Stable = -1;
if (held && m_Stable < 0) m_Stable = now;
if (m_Stable < 0 || now - m_Stable < 5000) return true;
if (!Check(true, "actual horizontal translation reaches frozen target and holds five seconds at 7–9m with horizontal speed below 0.25m/s")) return false;
m_TranslationProven = true;
m_Stage = 6; m_StageDeadline = now + 65000; m_Stable = -1;
return true;
}
int Tick(float now)
{
if (m_Complete) return 1;
if (m_Stopped) return -1;
m_Started = true;
if (!Enabled() || !m_Mode || now >= m_Deadline || ERR_MissionRules.Terminal(m_Mode.Phase())) { Check(false, "server fixture active within 240-second bound"); return -1; }
if (!ERR_PoweredFlightRules.TranslationInputs(m_TranslationMode, m_ActionInputs, m_AutohoverMode)) { Check(false, "translation requires actor-local actions and selected native autohover before allocation"); return -1; }
if (m_PreviousTick >= 0 && (now < m_PreviousTick || now - m_PreviousTick > 1500)) { m_Stable = -1; m_PositiveSignalSince = -1; m_NegativeSignalSince = -1; }
m_PreviousTick = now;
if (m_Stage == 0)
@@ -672,6 +770,11 @@ class ERR_NativePoweredFlightProbe
if (!Check(BeginAutohover(), "selected native autohover enabled only after both direct collective consumption proofs")) return -1;
m_Collective = -0.8; m_Stage = 4; m_StageDeadline = now + 65000; m_Stable = -1; return 0;
}
if (m_TranslationMode && m_Stage == 5)
{
if (!CheckTranslation(now, transform[3], height, speed, transform[1][1], drift)) return -1;
return 0;
}
if (m_Stage == 4 || m_Stage == 5)
{
bool hover = !m_Simulation.HasAnyGroundContact() && ERR_PoweredFlightRules.Hover(height, speed, m_Simulation.GetVelocity()[1], transform[1][1], drift);
@@ -681,12 +784,18 @@ class ERR_NativePoweredFlightProbe
if (m_Stable >= 0 && now - m_Stable >= 5000)
{
if (!Check(m_ControlUpdates > 100 && m_Fuel.GetTotalFuel() < m_InitialFuel - 0.05, "actual 7–9 metre powered hover stable five seconds with native fuel consumption")) return -1;
if (m_TranslationMode)
{
if (!BeginTranslation(now, transform[3], transform[0])) return -1;
return 0;
}
m_Stage = 6; m_StageDeadline = now + 65000; m_Stable = -1;
}
return 0;
}
if (m_Stage == 6)
{
if (m_TranslationMode && !m_TranslationProven) { Check(false, "translation reaches its frozen target before landing"); return -1; }
bool landed = ERR_PoweredFlightRules.Landed(m_Simulation.HasAnyGroundContact(), height, speed, m_Simulation.GetAngularVelocity().Length(), transform[1][1]);
if (!landed) m_Stable = -1;
if (landed && m_Stable < 0) m_Stable = now;
+1 -1
View File
@@ -145,7 +145,7 @@ modded class SCR_PlayerController
ERR_Actors.Players(survivors);
Faction friendly = mode.PlayerFaction();
for (int i = survivors.Count() - 1; i >= 0; i--)
if (SCR_FactionManager.SGetFaction(survivors[i]) != friendly) survivors.RemoveOrdered(i);
if (!friendly || !mode.Players() || !mode.Players().CurrentActor(survivors[i]) || SCR_FactionManager.SGetFaction(survivors[i]) != friendly) survivors.RemoveOrdered(i);
int index = survivors.Find(m_ERRServerObserved);
if (index < 0) index = 0;
else index += direction;
@@ -142,7 +142,8 @@ class ERR_SpawnLogic : SCR_SpawnLogic
ERR_Actors.Players(survivors, true);
foreach (IEntity survivor : survivors)
{
if (SCR_FactionManager.SGetFaction(survivor) != mode.PlayerFaction()) continue;
// A controlled replacement or unfinished preload is not a round survivor.
if (!mode.Players() || !mode.Players().CurrentActor(survivor) || !mode.PlayerFaction() || SCR_FactionManager.SGetFaction(survivor) != mode.PlayerFaction()) continue;
if (m_Placement.Near(survivor, m_AuthorizedPositions, position)) return true;
}
return m_Placement.Prison(mode.PrisonPosition(), m_AuthorizedPositions, position);
@@ -58,6 +58,14 @@ class ERR_IntelService
}
#ifdef WORKBENCH
// Observation only: a negative carrying decision still counts as its one roll.
bool ERR_ProbeWasRolled(IEntity actor)
{
return Replication.IsServer() && actor && m_Rolled.Contains(actor);
}
#endif
void Stop() { m_Requests.Clear(); m_Rolled.Clear(); }
}
@@ -0,0 +1,328 @@
#ifdef WORKBENCH
// Opt-in local UI evidence. These adapters execute production methods and always
// delegate their overrides; no native map/camera operation is intercepted.
modded class ERRM_SharedMapComponent
{
protected ERRM_NativeMapOwnershipProbe m_ERRMOwnershipProbe;
protected int m_ERRMOwnershipSlot;
void ERRM_ObserveOwnership(ERRM_NativeMapOwnershipProbe probe, int slot) { m_ERRMOwnershipProbe = probe; m_ERRMOwnershipSlot = slot; }
Widget ERRM_OwnershipRoot() { return m_ESMRootWidget; }
bool ERRM_OwnershipReady() { return m_ESMRootWidget && m_ESMRenderTarget; }
bool ERRM_BeginOwnershipCapture(IEntity holder)
{
m_bESMInHand = true;
m_ESMCharacterOwner = holder;
TryCaptureMapSurface();
// Deliberately delay only these fixture-owned callbacks until the replacement
// map has completed native initialization. The real method bodies run below.
GetGame().GetCallqueue().Remove(CenterMapOnHolder);
GetGame().GetCallqueue().Remove(FinishMapCapture);
return OwnsMapCapture();
}
void ERRM_StaleOwnershipCenter() { CenterMapOnHolder(); }
void ERRM_QueueStaleOwnershipCallbacks()
{
GetGame().GetCallqueue().CallLater(CenterMapOnHolder, 100, false);
GetGame().GetCallqueue().CallLater(FinishMapCapture, 200, false);
}
void ERRM_StaleOwnershipCamera() { RestoreCharacterCamera(); }
void ERRM_StopOwnershipCapture()
{
StopMapCapture();
// Include the real successor + vanilla ModeClear chain, even for a spare map.
ModeClear(EGadgetMode.IN_HAND);
}
protected override void OnDelete(IEntity owner)
{
ERRM_NativeMapOwnershipProbe probe = m_ERRMOwnershipProbe;
int slot = m_ERRMOwnershipSlot;
super.OnDelete(owner);
// Observe after the complete production/vanilla chain, without clearing any
// evidence references or overriding the native deletion operation.
if (probe) probe.ObserveDeletion(slot, !m_ESMRootWidget && !m_ESMRenderTarget && !m_ESMMapEntity && !m_ESMMapConfig && !m_ESMCharacterOwner && !m_bESMOwnsMapRenderer && !m_bESMInHand);
}
protected override void CenterMapOnHolder()
{
if (m_ERRMOwnershipProbe) m_ERRMOwnershipProbe.ObserveCenter();
super.CenterMapOnHolder();
}
protected override void FinishMapCapture()
{
if (m_ERRMOwnershipProbe) m_ERRMOwnershipProbe.ObserveFinish();
super.FinishMapCapture();
}
protected override void RestoreCharacterCamera()
{
if (m_ERRMOwnershipProbe) m_ERRMOwnershipProbe.ObserveRestore(OwnsMapCapture());
super.RestoreCharacterCamera();
}
protected override void CompleteMapModeSwitch()
{
if (m_ERRMOwnershipProbe) m_ERRMOwnershipProbe.ObserveCompletion();
super.CompleteMapModeSwitch();
}
}
class ERRM_NativeMapOwnershipProbe
{
protected static const ResourceName MAP_PREFAB = "{A46500E4A30B653B}Prefabs/Items/Equipment/ERRM_SharedFieldMap.et";
protected static const ResourceName MAP_LAYOUT = "{255E1265F702B4AE}UI/layouts/ERRMap/SharedMapRenderTarget.layout";
protected static const ResourceName MAP_CONFIG = "{A786DD4868598F15}Configs/Map/MapPlain.conf";
protected SCR_MapEntity m_Map;
protected IEntity m_CaptureEntity;
protected IEntity m_SpareEntity;
protected ERRM_SharedMapComponent m_Capture;
protected ERRM_SharedMapComponent m_Spare;
protected IEntity m_Holder;
protected Widget m_ReplacementRoot;
protected ref MapConfiguration m_ReplacementConfig;
protected MapConfiguration m_ReadyConfig;
protected int m_Step;
protected int m_CloseEvents;
protected int m_RestoreCalls;
protected int m_RestoreAuthorized;
protected int m_Completions;
protected int m_CenterCalls;
protected int m_FinishCalls;
protected int m_CenterBaseline;
protected int m_FinishBaseline;
protected int m_CloseBaseline;
protected int m_RestoreBaseline;
protected int m_AuthorizedBaseline;
protected int m_CompletionBaseline;
protected vector m_PanBaseline;
protected float m_ZoomBaseline;
protected bool m_DeleteRequested;
protected bool m_DeleteStateFailed;
protected int m_CaptureDeleted;
protected int m_SpareDeleted;
protected float m_DeleteDeadline;
protected float m_Deadline;
protected float m_WaitUntil;
protected bool m_Stopped;
protected bool m_Done;
static bool Enabled()
{
string enabled, outcome;
return System.GetCLIParam("errSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "map-ownership";
}
void ObserveRestore(bool authorized)
{
m_RestoreCalls++;
if (authorized) m_RestoreAuthorized++;
}
void ObserveDeletion(int slot, bool released)
{
if (!m_DeleteRequested || !released) m_DeleteStateFailed = true;
if (slot == 1) m_CaptureDeleted++;
else if (slot == 2) m_SpareDeleted++;
else m_DeleteStateFailed = true;
Print(string.Format("ERR MAP DELETE: afterSuper slot=%1 released=%2 captureCallbacks=%3 spareCallbacks=%4", slot, released, m_CaptureDeleted, m_SpareDeleted));
}
protected bool DeletionComplete()
{
return m_DeleteRequested && !m_SpareEntity && !m_CaptureEntity && !m_Spare && !m_Capture && m_CaptureDeleted == 1 && m_SpareDeleted == 1 && !m_DeleteStateFailed;
}
protected void RequestOwnedDeletion()
{
if (m_DeleteRequested) return;
m_DeleteRequested = true;
if (m_SpareEntity) SCR_EntityHelper.DeleteEntityAndChildren(m_SpareEntity);
if (m_CaptureEntity) SCR_EntityHelper.DeleteEntityAndChildren(m_CaptureEntity);
}
protected void LogDeletionState(string when)
{
Print(string.Format("ERR MAP DELETE: %1 captureEntity=%2 spareEntity=%3 captureComponent=%4 spareComponent=%5 captureCallbacks=%6 spareCallbacks=%7 cleanupFailed=%8", when, m_CaptureEntity != null, m_SpareEntity != null, m_Capture != null, m_Spare != null, m_CaptureDeleted, m_SpareDeleted, m_DeleteStateFailed));
}
void ObserveCompletion() { m_Completions++; }
void ObserveCenter() { m_CenterCalls++; }
void ObserveFinish() { m_FinishCalls++; }
protected void OnMapClosed(MapConfiguration config) { m_CloseEvents++; }
protected void OnMapReady(MapConfiguration config) { m_ReadyConfig = config; }
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: shared map ownership " + detail); return true; }
Print("ERR SMOKE FAIL: shared map ownership " + detail, LogLevel.ERROR);
Stop(); return false;
}
protected IEntity SpawnMap(vector origin)
{
EntitySpawnParams params = new EntitySpawnParams();
params.TransformMode = ETransformMode.WORLD;
params.Transform[3] = origin;
return GetGame().SpawnEntityPrefab(Resource.Load(MAP_PREFAB), GetGame().GetWorld(), params);
}
// A distinct native configuration with the same vanilla plain-map settings.
// It intentionally shares immutable settings/module arrays with the closed
// capture configuration; neither configuration is serialized or resource-edited.
protected MapConfiguration CopyPlainConfig(MapConfiguration source, Widget root)
{
MapConfiguration copy = new MapConfiguration();
copy.MapEntityMode = source.MapEntityMode;
copy.RootWidgetRef = root;
copy.LayerCount = source.LayerCount;
copy.MapPropsConfig = source.MapPropsConfig;
copy.LayerConfig = source.LayerConfig;
copy.DescriptorVisibilityConfig = source.DescriptorVisibilityConfig;
copy.DescriptorDefsConfig = source.DescriptorDefsConfig;
copy.Modules = source.Modules;
copy.Components = source.Components;
copy.OtherComponents = source.OtherComponents;
return copy;
}
protected bool ReplacementIsCurrent()
{
return m_Map && SCR_MapEntity.GetMapInstance() == m_Map && m_Map.IsOpen() && m_ReplacementConfig && m_Map.GetMapConfig() == m_ReplacementConfig && m_ReplacementConfig.RootWidgetRef == m_ReplacementRoot && m_Map.GetMapMenuRoot() == m_ReplacementRoot;
}
protected void SaveCounters()
{
m_CloseBaseline = m_CloseEvents;
m_RestoreBaseline = m_RestoreCalls;
m_AuthorizedBaseline = m_RestoreAuthorized;
m_CompletionBaseline = m_Completions;
m_CenterBaseline = m_CenterCalls; m_FinishBaseline = m_FinishCalls;
}
protected bool BeginReplacement(bool reuseConfig)
{
if (!Check(m_Map && !m_Map.IsOpen() && m_Capture.ERRM_BeginOwnershipCapture(m_Holder), "actual owned prefab captures its native plain-map root")) return false;
MapConfiguration captured = m_Map.GetMapConfig();
if (!Check(captured && captured.RootWidgetRef == m_Capture.ERRM_OwnershipRoot(), "capture config belongs to exact fixture component")) return false;
// Close only the just-verified fixture view before replacing it; this also
// avoids the native double-open warning. Delayed callbacks remain stale.
m_Map.CloseMap();
if (reuseConfig) m_ReplacementConfig = m_Map.SetupMapConfig(EMapEntityMode.PLAIN, MAP_CONFIG, m_ReplacementRoot);
else m_ReplacementConfig = CopyPlainConfig(captured, m_ReplacementRoot);
if (!Check(m_ReplacementConfig && ((reuseConfig && m_ReplacementConfig == captured) || (!reuseConfig && m_ReplacementConfig != captured)), "requested distinct/reused native configuration identity established")) return false;
m_ReadyConfig = null;
m_Map.OpenMap(m_ReplacementConfig);
if (!Check(ReplacementIsCurrent(), "actual replacement root is current native map")) return false;
SaveCounters();
return true;
}
protected bool ExerciseStaleCallbacks()
{
if (!Check(ReplacementIsCurrent() && m_ReadyConfig == m_ReplacementConfig && !m_Map.IsZooming(), "replacement remains open after native open-complete event")) return false;
vector pan = m_Map.GetCurrentPan();
float zoom = m_Map.GetTargetZoomPPU();
m_Capture.ERRM_StaleOwnershipCenter();
if (!Check(m_Map.GetCurrentPan() == pan && m_Map.GetTargetZoomPPU() == zoom && !m_Map.IsZooming(), "actual stale center callback cannot pan or retarget replacement zoom")) return false;
m_Capture.ERRM_StaleOwnershipCamera();
if (!Check(m_RestoreCalls == m_RestoreBaseline + 1 && m_RestoreAuthorized == m_AuthorizedBaseline, "stale camera-restore method executes but native-write authorization is denied")) return false;
m_PanBaseline = pan; m_ZoomBaseline = zoom;
m_Capture.ERRM_QueueStaleOwnershipCallbacks();
return true;
}
protected bool VerifyStaleAndStop()
{
if (!Check(m_CenterCalls == m_CenterBaseline + 2 && m_FinishCalls == m_FinishBaseline + 1, "both real delayed production callbacks ran exactly once")) return false;
if (!Check(ReplacementIsCurrent() && m_Map.GetCurrentPan() == m_PanBaseline && m_Map.GetTargetZoomPPU() == m_ZoomBaseline, "real delayed center and finish leave replacement view unchanged")) return false;
m_Capture.ERRM_StopOwnershipCapture();
if (!Check(ReplacementIsCurrent() && m_CloseEvents == m_CloseBaseline && m_Completions == m_CompletionBaseline && m_RestoreCalls == m_RestoreBaseline + 1, "actual finish, stop and vanilla mode-clear preserve replacement and do not request camera restore")) return false;
return true;
}
// Returns 0 while waiting, 1 only after cleanup, -1 on failure. now is native
// world time in milliseconds, matching existing Chat smoke probes.
int Tick(float now)
{
if (m_Done) return 1;
if (m_Stopped) return -1;
if (!Enabled()) { Stop(); return -1; }
if (m_Step == 0)
{
m_Map = SCR_MapEntity.GetMapInstance();
PlayerController controller = GetGame().GetPlayerController();
if (!Check(m_Map && !m_Map.IsOpen() && controller && controller.GetControlledEntity() && GetGame().GetCameraManager().CurrentCamera() == controller.GetPlayerCamera(), "fixture starts only with closed map and actual local character camera")) return -1;
m_Holder = controller.GetControlledEntity();
m_Deadline = now + 15000;
m_ReplacementRoot = GetGame().GetWorkspace().CreateWidgets(MAP_LAYOUT);
if (!Check(m_ReplacementRoot != null, "owned replacement native map layout created")) return -1;
m_ReplacementRoot.SetVisible(false);
RTTextureWidget target = RTTextureWidget.Cast(m_ReplacementRoot.FindAnyWidget("RTTexture0"));
if (target) target.SetEnabled(false);
SCR_MapEntity.GetOnMapClose().Insert(OnMapClosed);
SCR_MapEntity.GetOnMapOpenComplete().Insert(OnMapReady);
m_CaptureEntity = SpawnMap(m_Holder.GetOrigin() + Vector(0, 0.2, 0));
m_SpareEntity = SpawnMap(m_Holder.GetOrigin() + Vector(0.3, 0.2, 0));
if (!Check(m_CaptureEntity && m_SpareEntity && m_CaptureEntity != m_SpareEntity, "two separate real successor map prefabs spawned without inventory changes")) return -1;
m_WaitUntil = now + 1000; m_Step = 1; return 0;
}
if (now > m_Deadline) { Check(false, "bounded fixture deadline"); return -1; }
if (now < m_WaitUntil) return 0;
if (m_Step == 1)
{
if (!Check(m_CaptureEntity && m_SpareEntity, "both fixture entities survive until native component initialization")) return -1;
m_Capture = ERRM_SharedMapComponent.Cast(m_CaptureEntity.FindComponent(ERRM_SharedMapComponent));
m_Spare = ERRM_SharedMapComponent.Cast(m_SpareEntity.FindComponent(ERRM_SharedMapComponent));
if (!Check(m_Capture && m_Spare && m_Capture.ERRM_OwnershipReady() && m_Spare.ERRM_OwnershipReady(), "both native map components initialize actual render roots")) return -1;
m_Capture.ERRM_ObserveOwnership(this, 1); m_Spare.ERRM_ObserveOwnership(this, 2);
if (!BeginReplacement(false)) return -1;
m_WaitUntil = now + 1500; m_Step = 2; return 0;
}
if (m_Step == 2)
{
if (!ExerciseStaleCallbacks()) return -1;
m_WaitUntil = now + 750; m_Step = 3; return 0;
}
if (m_Step == 3)
{
if (!VerifyStaleAndStop()) return -1;
m_Map.CloseMap(); // exact replacement was checked immediately above
if (!BeginReplacement(true)) return -1;
m_WaitUntil = now + 1500; m_Step = 4; return 0;
}
if (m_Step == 4)
{
if (!ExerciseStaleCallbacks()) return -1;
m_WaitUntil = now + 750; m_Step = 5; return 0;
}
if (m_Step == 5)
{
if (!VerifyStaleAndStop()) return -1;
SaveCounters();
m_Spare.ERRM_StopOwnershipCapture();
RequestOwnedDeletion();
LogDeletionState("request returned");
m_DeleteDeadline = now + 3000;
m_WaitUntil = now + 100; m_Step = 6; return 0;
}
if (m_Step == 6)
{
if (!ReplacementIsCurrent() || m_CloseEvents != m_CloseBaseline || m_RestoreCalls != m_RestoreBaseline)
{
Check(false, "replacement remains unchanged while native deletion finishes"); return -1;
}
if (!DeletionComplete())
{
if (now >= m_DeleteDeadline)
{
LogDeletionState("bounded deletion timeout");
Check(false, "both actual entities/components delete and both superclass cleanup callbacks complete within three seconds"); return -1;
}
return 0;
}
LogDeletionState("completed on later tick");
if (!Check(DeletionComplete(), "actual prefab deletion runs successor and vanilla superclass cleanup")) return -1;
if (!Check(ReplacementIsCurrent() && m_CloseEvents == m_CloseBaseline && m_RestoreCalls == m_RestoreBaseline, "spare and interrupted map deletion preserve replacement without restore calls")) return -1;
m_WaitUntil = now + 1500; m_Step = 7; return 0;
}
if (!Check(ReplacementIsCurrent() && m_CloseEvents == m_CloseBaseline && m_RestoreCalls == m_RestoreBaseline && m_Completions == m_CompletionBaseline && m_CenterCalls == m_CenterBaseline && m_FinishCalls == m_FinishBaseline, "replacement survives delayed native callback window after both deletions")) return -1;
Stop(); m_Done = true;
Print("ERR SMOKE PASS: shared map ownership owned UI removed; camera render flag has no native getter and is not claimed as measured");
return 1;
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
if (m_Capture) m_Capture.ERRM_StopOwnershipCapture();
if (m_Spare) m_Spare.ERRM_StopOwnershipCapture();
if (ReplacementIsCurrent()) m_Map.CloseMap();
SCR_MapEntity.GetOnMapClose().Remove(OnMapClosed);
SCR_MapEntity.GetOnMapOpenComplete().Remove(OnMapReady);
RequestOwnedDeletion();
if (m_ReplacementRoot) m_ReplacementRoot.RemoveFromHierarchy();
m_ReplacementRoot = null; m_ReplacementConfig = null; m_ReadyConfig = null; m_Map = null;
m_Capture = null; m_Spare = null; m_Holder = null;
}
}
#endif
@@ -31,6 +31,7 @@ class ERRM_SharedMapComponent : SCR_MapGadgetComponent
protected Widget m_ESMRootWidget;
protected RTTextureWidget m_ESMRenderTarget;
protected SCR_MapEntity m_ESMMapEntity;
protected ref MapConfiguration m_ESMMapConfig;
protected IEntity m_ESMCharacterOwner;
protected bool m_bESMInHand;
protected bool m_bESMOwnsMapRenderer;
@@ -107,12 +108,21 @@ class ERRM_SharedMapComponent : SCR_MapGadgetComponent
MapConfiguration mapConfig = m_ESMMapEntity.SetupMapConfig(EMapEntityMode.PLAIN, MAP_CONFIG, m_ESMRootWidget);
if (!mapConfig)
{
ReleaseMapCapture();
CompleteMapModeSwitch();
return;
}
m_ESMMapEntity.OpenMap(mapConfig);
m_ESMMapConfig = mapConfig;
m_bESMOwnsMapRenderer = true;
m_ESMMapEntity.OpenMap(mapConfig);
if (!OwnsMapCapture())
{
ReleaseMapCapture();
if (m_bESMInHand && GetGame())
GetGame().GetCallqueue().CallLater(TryCaptureMapSurface, MAP_RETRY_DELAY_MS, false);
return;
}
RestoreCharacterCamera();
GetGame().GetCallqueue().CallLater(CenterMapOnHolder, MAP_SETUP_DELAY_MS, false);
@@ -121,7 +131,7 @@ class ERRM_SharedMapComponent : SCR_MapGadgetComponent
protected void CenterMapOnHolder()
{
if (!m_bESMOwnsMapRenderer || !m_ESMMapEntity || !m_ESMCharacterOwner)
if (!OwnsMapCapture() || !m_ESMCharacterOwner)
return;
ERRM_SharedMapComponentClass data = ERRM_SharedMapComponentClass.Cast(GetComponentData(GetOwner()));
@@ -136,19 +146,41 @@ class ERRM_SharedMapComponent : SCR_MapGadgetComponent
{
if (!m_bESMOwnsMapRenderer)
return;
if (!OwnsMapCapture())
{
ReleaseMapCapture();
// A native UI replaced this capture. Do not close, pan, fade or
// focus that UI; use the existing retry path after it closes.
if (m_bESMInHand && GetGame())
GetGame().GetCallqueue().CallLater(TryCaptureMapSurface, MAP_RETRY_DELAY_MS, false);
return;
}
// Freeze the last rendered frame before closing the auxiliary map.
// Leaving the widget enabled would redraw the closed map as an empty
// texture and make the physical map appear to vanish.
ReleaseMapCapture();
CompleteMapModeSwitch();
}
protected bool OwnsMapCapture()
{
if (!GetGame() || !m_bESMOwnsMapRenderer || !m_ESMMapEntity || !m_ESMMapConfig || !m_ESMRootWidget)
return false;
if (SCR_MapEntity.GetMapInstance() != m_ESMMapEntity || !m_ESMMapEntity.IsOpen())
return false;
// Vanilla reuses the same config object for equal map modes and changes
// RootWidgetRef, so pointer identity alone cannot establish ownership.
return m_ESMMapEntity.GetMapConfig() == m_ESMMapConfig && m_ESMMapConfig.RootWidgetRef == m_ESMRootWidget;
}
protected void ReleaseMapCapture()
{
bool ownsRenderer = OwnsMapCapture();
// Freeze our own target even on interruption or foreign replacement.
if (m_ESMRenderTarget)
m_ESMRenderTarget.SetEnabled(false);
if (m_ESMMapEntity && m_ESMMapEntity.IsOpen())
m_ESMMapEntity.CloseMap();
m_bESMOwnsMapRenderer = false;
RestoreCharacterCamera();
CompleteMapModeSwitch();
m_ESMMapConfig = null;
if (ownsRenderer)
m_ESMMapEntity.CloseMap(); // vanilla also restores its character camera
}
protected void CompleteMapModeSwitch()
@@ -163,21 +195,20 @@ class ERRM_SharedMapComponent : SCR_MapGadgetComponent
protected void StopMapCapture()
{
m_bESMInHand = false;
GetGame().GetCallqueue().Remove(TryCaptureMapSurface);
GetGame().GetCallqueue().Remove(CenterMapOnHolder);
GetGame().GetCallqueue().Remove(FinishMapCapture);
if (m_bESMOwnsMapRenderer && m_ESMMapEntity && m_ESMMapEntity.IsOpen())
m_ESMMapEntity.CloseMap();
m_bESMOwnsMapRenderer = false;
if (GetGame())
{
GetGame().GetCallqueue().Remove(TryCaptureMapSurface);
GetGame().GetCallqueue().Remove(CenterMapOnHolder);
GetGame().GetCallqueue().Remove(FinishMapCapture);
}
ReleaseMapCapture();
m_ESMCharacterOwner = null;
RestoreCharacterCamera();
m_ESMMapEntity = null;
}
protected void RestoreCharacterCamera()
{
if (!GetGame()) return;
if (!OwnsMapCapture()) return;
PlayerController playerController = GetGame().GetPlayerController();
if (playerController)
playerController.SetCharacterCameraRenderActive(true);