Stop successor services on native game-over and preserve escape results
This commit is contained in:
@@ -9,8 +9,10 @@ class ERR_EscapeGameOverScreenInfo : SCR_BaseGameOverScreenInfo
|
||||
}
|
||||
override string GetTitle(int playerId, array<int> otherPlayerIds)
|
||||
{
|
||||
string title = "The helicopter escaped";
|
||||
if (PersonalResult(playerId) == ERR_PersonalEscapeResult.ESCAPED) title = "You escaped Everon";
|
||||
string title = "Round ended";
|
||||
ERR_PersonalEscapeResult result = PersonalResult(playerId);
|
||||
if (result == ERR_PersonalEscapeResult.ESCAPED) title = "You escaped Everon";
|
||||
else if (result == ERR_PersonalEscapeResult.MISSED) title = "The helicopter escaped";
|
||||
#ifdef WORKBENCH
|
||||
PrintFormat("ERR RESULT UI: player=%1 title=%2", playerId, title);
|
||||
#endif
|
||||
|
||||
@@ -36,6 +36,7 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
protected string m_RecoveryStatus;
|
||||
protected bool m_StartedPlayers;
|
||||
protected bool m_Stopped;
|
||||
protected bool m_NativeEndHandled;
|
||||
protected float m_NoSurvivorsSince = -1;
|
||||
protected ref ScriptInvoker m_OnPhaseChanged = new ScriptInvoker();
|
||||
protected ref ScriptInvoker m_OnTick = new ScriptInvoker();
|
||||
@@ -149,7 +150,7 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
m_OwnedEntities.Clear();
|
||||
}
|
||||
vector PrisonPosition() { return ERR_Actors.Ground(m_PrisonPosition); }
|
||||
bool Active() { return m_Phase >= ERR_Phase.PRISON && !ERR_MissionRules.Terminal(m_Phase); }
|
||||
bool Active() { return !m_Stopped && !m_NativeEndHandled && m_Phase >= ERR_Phase.PRISON && !ERR_MissionRules.Terminal(m_Phase); }
|
||||
protected ERR_CommandService CreateCommandService() { return null; }
|
||||
|
||||
void ConfigureStart(vector position)
|
||||
@@ -224,7 +225,7 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
if (phase == ERR_Phase.VICTORY && m_Extraction) m_Extraction.ConnectedWinners(winners);
|
||||
EGameOverTypes reason = EGameOverTypes.LOOSE;
|
||||
if (phase == ERR_Phase.VICTORY) reason = EGameOverTypes.VICTORY;
|
||||
EndGameMode(SCR_GameModeEndData.Create(reason, winners));
|
||||
if (!m_NativeEndHandled) EndGameMode(SCR_GameModeEndData.Create(reason, winners));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -304,6 +305,11 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
|
||||
protected void BindLocalObserver()
|
||||
{
|
||||
if (m_Stopped || m_NativeEndHandled)
|
||||
{
|
||||
GetGame().GetCallqueue().Remove(BindLocalObserver);
|
||||
return;
|
||||
}
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
if (!controller) return;
|
||||
controller.ERR_StartObserverMonitor();
|
||||
@@ -312,7 +318,7 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
|
||||
protected void StartScenario()
|
||||
{
|
||||
if (m_Phase != ERR_Phase.PREPARING || !m_PreparationError.IsEmpty()) return;
|
||||
if (m_Stopped || m_NativeEndHandled || m_Phase != ERR_Phase.PREPARING || !m_PreparationError.IsEmpty()) return;
|
||||
if (!m_Factions || !m_Factions.WorldReady()) { FailPreparation("The configured player and enemy factions or required assets are missing, invalid or not hostile."); return; }
|
||||
// Scenario content is created through a separate authoring component.
|
||||
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(FindComponent(ERR_ScenarioComponent));
|
||||
@@ -392,11 +398,39 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
m_OnTick.Invoke(now);
|
||||
}
|
||||
|
||||
// Vanilla editor/session termination enters POSTGAME directly, without our
|
||||
// Transition(). Close the successor before vanilla publishes/restarts the round.
|
||||
protected override void OnGameModeEnd(SCR_GameModeEndData endData)
|
||||
{
|
||||
if (m_NativeEndHandled) return;
|
||||
m_NativeEndHandled = true;
|
||||
GetGame().GetCallqueue().Remove(BindLocalObserver);
|
||||
if (Replication.IsServer())
|
||||
{
|
||||
if (!ERR_MissionRules.Terminal(m_Phase))
|
||||
{
|
||||
ERR_Phase previous = m_Phase;
|
||||
// A native end is not proof that anyone crossed the escape boundary.
|
||||
m_Phase = ERR_Phase.DEFEAT;
|
||||
if (m_EndReason.IsEmpty()) m_EndReason = "The game session ended this round.";
|
||||
RefreshRadioFeedback();
|
||||
PublishEscapeResults();
|
||||
Replication.BumpMe();
|
||||
m_OnPhaseChanged.Invoke(previous, m_Phase);
|
||||
}
|
||||
StopServices();
|
||||
}
|
||||
// Preserve native end data and restart ownership. Never recurse via EndGameMode.
|
||||
super.OnGameModeEnd(endData);
|
||||
}
|
||||
|
||||
protected void StopServices()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
GetGame().GetCallqueue().Remove(StartScenario);
|
||||
GetGame().GetCallqueue().Remove(ServerTick);
|
||||
GetGame().GetCallqueue().Remove(BindLocalObserver);
|
||||
if (m_Command) m_Command.Stop();
|
||||
m_OnStop.Invoke();
|
||||
if (Replication.IsServer())
|
||||
|
||||
@@ -53,6 +53,8 @@ modded class ERR_GameMode
|
||||
super.EOnInit(owner);
|
||||
string enabled;
|
||||
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
|
||||
string nativeEndCase;
|
||||
if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && (nativeEndCase == "native-end-active" || nativeEndCase == "native-end-preparing")) return;
|
||||
ERR_NativeObjectiveProbe.BeginRound();
|
||||
ERR_NativeProgressionIdentityProbe.BeginRound();
|
||||
string outcome;
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
#ifdef WORKBENCH
|
||||
// Uses vanilla EndGameMode, deliberately bypassing successor Transition.
|
||||
// No lifecycle override or synthetic phase change in this fixture.
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected static const string ERR_NATIVE_END_MARKER = "$profile:erri-native-end-round.json";
|
||||
protected bool m_ERREndProbe;
|
||||
protected bool m_ERREndPreparing;
|
||||
protected bool m_ERREndRequested;
|
||||
protected bool m_ERREndDone;
|
||||
protected int m_ERREndRound = 1;
|
||||
protected int m_ERREndStops;
|
||||
protected int m_ERREndPhases;
|
||||
protected int m_ERREndTicks;
|
||||
protected int m_ERREndPreviousPhases;
|
||||
protected int m_ERREndPreviousTicks;
|
||||
protected float m_ERREndDeadline;
|
||||
protected float m_ERREndObserveAt;
|
||||
protected ref SCR_GameModeEndData m_ERREndData;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
string enabled, outcome;
|
||||
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1" || !System.GetCLIParam("errSmokeOutcome", outcome)) return;
|
||||
if (outcome != "native-end-active" && outcome != "native-end-preparing") return;
|
||||
m_ERREndProbe = true; m_ERREndPreparing = outcome == "native-end-preparing";
|
||||
if (FileIO.FileExists(ERR_NATIVE_END_MARKER))
|
||||
{
|
||||
JsonLoadContext marker = new JsonLoadContext(); int completed;
|
||||
if (!ERR_EndCheck(marker.LoadFromFile(ERR_NATIVE_END_MARKER) && marker.ReadValue("completedRound", completed) && completed == 1, "second instance reads only first completed round marker")) return;
|
||||
m_ERREndRound = 2;
|
||||
}
|
||||
if (!ERR_EndCheck(!m_Stopped && !m_NativeEndHandled && m_Phase == ERR_Phase.PREPARING && m_Extraction && m_Extraction.State() == ERR_ExtractionState.WAITING, "fresh mode resets native-end guard, phase, service ownership and extraction result")) return;
|
||||
OnStop().Insert(ERR_EndStopped); OnPhaseChanged().Insert(ERR_EndPhase); OnTick().Insert(ERR_EndTicked);
|
||||
m_ERREndDeadline = GetWorld().GetWorldTime() + 45000;
|
||||
GetGame().GetCallqueue().CallLater(ERR_EndProbeTick, 100, true);
|
||||
PrintFormat("ERR NATIVE END INSTANCE: round=%1 preparing=%2", m_ERREndRound, m_ERREndPreparing);
|
||||
}
|
||||
protected bool ERR_EndCheck(bool passed, string detail)
|
||||
{
|
||||
if (passed) { Print("ERR SMOKE PASS: native-end " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: native-end " + detail, LogLevel.ERROR);
|
||||
m_ERREndDone = true;
|
||||
GetGame().GetCallqueue().Remove(ERR_EndProbeTick);
|
||||
return false;
|
||||
}
|
||||
protected void ERR_EndStopped() { m_ERREndStops++; }
|
||||
protected void ERR_EndPhase(ERR_Phase previous, ERR_Phase current) { m_ERREndPhases++; }
|
||||
protected void ERR_EndTicked(float now) { m_ERREndTicks++; }
|
||||
protected void ERR_EndProbeTick()
|
||||
{
|
||||
if (!m_ERREndProbe || m_ERREndDone) return;
|
||||
float now = GetWorld().GetWorldTime();
|
||||
if (now >= m_ERREndDeadline) { ERR_EndCheck(false, "bounded native-end observation"); return; }
|
||||
if (!m_ERREndRequested)
|
||||
{
|
||||
if (!IsRunning()) return;
|
||||
if (m_ERREndPreparing)
|
||||
{
|
||||
if (!ERR_EndCheck(Phase() == ERR_Phase.PREPARING && GetGame().GetCallqueue().GetRemainingTime(StartScenario) >= 0, "native end precedes the actual queued scenario preparation")) return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Phase() == ERR_Phase.PREPARING) return;
|
||||
if (!ERR_EndCheck(Phase() == ERR_Phase.PRISON && Active() && Sites() && Sites().Count() > 0, "native end starts from actual prepared active mission")) return;
|
||||
}
|
||||
if (!ERR_EndCheck(GetAutoReloadDelay() >= 2, "vanilla reload leaves time to observe stopped callbacks")) return;
|
||||
m_ERREndRequested = true; m_ERREndPreviousPhases = m_ERREndPhases; m_ERREndPreviousTicks = m_ERREndTicks;
|
||||
m_ERREndData = SCR_GameModeEndData.CreateSimple(EGameOverTypes.LOOSE);
|
||||
EndGameMode(m_ERREndData);
|
||||
if (!ERR_EndCheck(GetState() == SCR_EGameModeState.POSTGAME && GetEndGameData() == m_ERREndData, "actual native end retains supplied data and enters POSTGAME")) return;
|
||||
if (!ERR_EndCheck(m_NativeEndHandled && m_Stopped && !Active() && Phase() == ERR_Phase.DEFEAT && m_ERREndStops == 1 && m_ERREndPhases == m_ERREndPreviousPhases + 1, "successor terminal phase and services close exactly once")) return;
|
||||
if (!ERR_EndCheck(m_Extraction.State() == ERR_ExtractionState.WAITING && m_Extraction.EscapedCount() == 0, "external native end creates no escaped identities or extraction success")) return;
|
||||
m_ERREndObserveAt = now + 1250;
|
||||
return;
|
||||
}
|
||||
if (now < m_ERREndObserveAt) return;
|
||||
if (!ERR_EndCheck(GetGame().GetCallqueue().GetRemainingTime(ServerTick) < 0 && GetGame().GetCallqueue().GetRemainingTime(StartScenario) < 0 && GetGame().GetCallqueue().GetRemainingTime(BindLocalObserver) < 0, "production tick, local observer binding and queued preparation callbacks are removed before reload")) return;
|
||||
int ownedCount = m_OwnedEntities.Count(); int locations = m_Locations.Count();
|
||||
// Exercise actual callback guards without recreating their work or invoking End twice.
|
||||
StartScenario(); ServerTick(); StopServices();
|
||||
if (!ERR_EndCheck(m_OwnedEntities.Count() == ownedCount && m_Locations.Count() == locations && m_ERREndTicks == m_ERREndPreviousTicks && m_ERREndStops == 1 && m_ERREndPhases == m_ERREndPreviousPhases + 1, "stale callback bodies and repeated shutdown perform no new work")) return;
|
||||
if (!ERR_EndCheck(GetEndGameData() == m_ERREndData && Phase() == ERR_Phase.DEFEAT && !Active(), "native result and successor terminal state persist until vanilla reload")) return;
|
||||
if (m_ERREndRound == 1)
|
||||
{
|
||||
JsonSaveContext marker = new JsonSaveContext(); marker.EnableTypeDiscriminator(false); marker.WriteValue("completedRound", 1);
|
||||
if (!ERR_EndCheck(marker.SaveToFile(ERR_NATIVE_END_MARKER), "first instance writes completed observation marker for vanilla restart")) return;
|
||||
}
|
||||
else
|
||||
{
|
||||
FileIO.DeleteFile(ERR_NATIVE_END_MARKER);
|
||||
if (!ERR_EndCheck(!FileIO.FileExists(ERR_NATIVE_END_MARKER), "second independently initialized instance removes its own marker")) return;
|
||||
}
|
||||
m_ERREndDone = true;
|
||||
GetGame().GetCallqueue().Remove(ERR_EndProbeTick);
|
||||
PrintFormat("ERR SMOKE COMPLETE: native-end round=%1 preparing=%2 actual native termination, stopped services and unchanged end data; vanilla owns restart", m_ERREndRound, m_ERREndPreparing);
|
||||
}
|
||||
void ~ERR_GameMode()
|
||||
{
|
||||
if (!m_ERREndProbe || !GetGame()) return;
|
||||
GetGame().GetCallqueue().Remove(ERR_EndProbeTick);
|
||||
OnStop().Remove(ERR_EndStopped); OnPhaseChanged().Remove(ERR_EndPhase); OnTick().Remove(ERR_EndTicked);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -30,7 +30,7 @@ class ERR_NativeResultProbe
|
||||
array<int> copiedIds = {};
|
||||
staleData.GetWinnerIds(copiedIds);
|
||||
controller.ERR_ResetEscapeResult();
|
||||
if (!Check(controller.ERR_EscapeResult() == ERR_PersonalEscapeResult.UNKNOWN && info.GetTitle(playerId, copiedIds) == "The helicopter escaped", "unknown controller ignores stale native end-data winner ID")) return false;
|
||||
if (!Check(controller.ERR_EscapeResult() == ERR_PersonalEscapeResult.UNKNOWN && info.GetTitle(playerId, copiedIds) == "Round ended", "unknown controller ignores stale native end-data winner ID")) return false;
|
||||
ResourceName layout = "{C4507D07F3C54831}UI/layouts/HUD/GameOver/EndScreen/EndScreenContent_NoImage.layout";
|
||||
s_Content = GetGame().GetWorkspace().CreateWidgets(layout);
|
||||
if (!Check(s_Content != null, "native result content widgets created")) return false;
|
||||
@@ -39,7 +39,7 @@ class ERR_NativeResultProbe
|
||||
if (!Check(contentHandler != null, "native result content handler attached")) return false;
|
||||
SCR_GameOverScreenUIContentData content = new SCR_GameOverScreenUIContentData(layout, info, staleData, "STALE WINNER", string.Empty, "STALE DEBRIEF", ResourceName.Empty, ResourceName.Empty, null, string.Empty, string.Empty, string.Empty);
|
||||
contentHandler.InitContent(content);
|
||||
if (!Check(NativeText("The helicopter escaped", "Your personal escape result is not available."), "opened native content ignores a stale winner while personal result is unknown")) return false;
|
||||
if (!Check(NativeText("Round ended", "Your personal escape result is not available."), "opened native content ignores a stale winner while personal result is unknown")) return false;
|
||||
ERR_PlayerRoster roster = new ERR_PlayerRoster();
|
||||
if (!Check(roster.Bind(playerId, escapedAccount), "isolated presentation account binds to local controller ID")) return false;
|
||||
roster.Spawned(playerId, controller.GetControlledEntity());
|
||||
@@ -56,8 +56,8 @@ class ERR_NativeResultProbe
|
||||
controller.ERR_PublishEscapeResult(roster, extraction);
|
||||
if (!Check(info.GetTitle(playerId, noWinners) == "You escaped Everon", "presentation follows returning account rather than copied end data")) return false;
|
||||
controller.ERR_ResetEscapeResult();
|
||||
if (!Check(controller.ERR_EscapeResult() == ERR_PersonalEscapeResult.UNKNOWN && info.GetTitle(playerId, copiedIds) == "The helicopter escaped", "round reset clears personal result despite stale winner ID")) return false;
|
||||
if (!Check(NativeText("The helicopter escaped", "Your personal escape result is not available."), "round reset clears the open native result text")) return false;
|
||||
if (!Check(controller.ERR_EscapeResult() == ERR_PersonalEscapeResult.UNKNOWN && info.GetTitle(playerId, copiedIds) == "Round ended", "round reset clears personal result despite stale winner ID")) return false;
|
||||
if (!Check(NativeText("Round ended", "Your personal escape result is not available."), "round reset clears the open native result text")) return false;
|
||||
s_Content.RemoveFromHierarchy();
|
||||
s_Content = null;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
|
||||
Reference in New Issue
Block a user