Add independent extraction results and configurable mission clock
This commit is contained in:
@@ -8,7 +8,7 @@ SCR_GameOverScreenConfig : "{651583FFFB7FC765}Configs/GameOverScreen/BaseGameOve
|
||||
m_sAudioOneShot "SOUND_ONVICTORY"
|
||||
}
|
||||
}
|
||||
SCR_BaseGameOverScreenInfo "{E66F29A7A8AD7438}" {
|
||||
ERR_FailureGameOverScreenInfo "{E66F29A7A8AD7438}" {
|
||||
m_GameOverScreenId LOOSE
|
||||
m_GameOverContentLayout "{C4507D07F3C54831}UI/layouts/HUD/GameOver/EndScreen/EndScreenContent_NoImage.layout"
|
||||
m_OptionalParams SCR_BaseGameOverScreenInfoOptional "{3DBF0BDE715945FA}" {
|
||||
|
||||
@@ -19,9 +19,32 @@ class ERR_AdminService
|
||||
return "";
|
||||
}
|
||||
protected bool Extension(ERR_GameMode mode, string arguments, array<string> replies) { return false; }
|
||||
protected bool TimeCommand(ERR_GameMode mode, array<string> parts, array<string> replies)
|
||||
{
|
||||
if (parts.Count() < 2 || parts[0] != "mission" || (parts[1] != "time" && parts[1] != "speed")) return false;
|
||||
string key = "startTimeMinutes";
|
||||
if (parts[1] == "speed") key = "timeMultiplier";
|
||||
if (parts.Count() == 3 && parts[2] == "show")
|
||||
{
|
||||
if (mode.Time()) replies.Insert(mode.Time().Status());
|
||||
replies.Insert("Next round: " + ERR_TimeService.Clock(mode.Settings().Get("startTimeMinutes")) + ", " + mode.Settings().Get("timeMultiplier").ToString() + "x progression.");
|
||||
return true;
|
||||
}
|
||||
int value;
|
||||
bool valid;
|
||||
if (parts.Count() == 4 && parts[2] == "set")
|
||||
{
|
||||
if (key == "startTimeMinutes") valid = ERR_TimeService.ParseClock(parts[3], value);
|
||||
else valid = ERR_CommandArguments.Integer(parts[3], value);
|
||||
}
|
||||
if (valid && mode.Settings().Set(key, value))
|
||||
replies.Insert("Saved for next round: " + ERR_TimeService.Clock(mode.Settings().Get("startTimeMinutes")) + ", " + mode.Settings().Get("timeMultiplier").ToString() + "x progression. Current round clock unchanged.");
|
||||
else replies.Insert("Invalid time or settings could not be saved. Use mission time set HH:MM (00:00-23:59) or mission speed set N (1-24). Previous setting retained.");
|
||||
return true;
|
||||
}
|
||||
protected string Help()
|
||||
{
|
||||
return "/esc status | settings | mission radio show|set N | spectate next|previous";
|
||||
return "/esc status | settings | mission radio show|set N | mission time show|set HH:MM | mission speed show|set N | spectate next|previous";
|
||||
}
|
||||
void Execute(ERR_GameMode mode, bool authorized, string arguments, array<string> replies)
|
||||
{
|
||||
@@ -40,6 +63,7 @@ class ERR_AdminService
|
||||
if (Extension(mode, arguments, replies)) return;
|
||||
array<string> parts = {};
|
||||
arguments.Split(" ", parts, true);
|
||||
if (TimeCommand(mode, parts, replies)) return;
|
||||
if (parts.Count() == 3 || parts.Count() == 4)
|
||||
{
|
||||
string key = Setting(parts[0], parts[1]);
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
[BaseContainerProps()]
|
||||
class ERR_EscapeGameOverScreenInfo : SCR_BaseGameOverScreenInfo
|
||||
{
|
||||
protected ERR_PersonalEscapeResult PersonalResult(int playerId)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
if (!controller || controller.GetPlayerId() != playerId) return ERR_PersonalEscapeResult.UNKNOWN;
|
||||
return controller.ERR_EscapeResult();
|
||||
}
|
||||
override string GetTitle(int playerId, array<int> otherPlayerIds)
|
||||
{
|
||||
string title = "The helicopter escaped";
|
||||
if (otherPlayerIds && otherPlayerIds.Contains(playerId)) title = "You escaped Everon";
|
||||
if (PersonalResult(playerId) == ERR_PersonalEscapeResult.ESCAPED) title = "You escaped Everon";
|
||||
#ifdef WORKBENCH
|
||||
PrintFormat("ERR RESULT UI: player=%1 title=%2", playerId, title);
|
||||
#endif
|
||||
@@ -12,8 +18,55 @@ class ERR_EscapeGameOverScreenInfo : SCR_BaseGameOverScreenInfo
|
||||
}
|
||||
override string GetDebriefing(int playerId, array<int> otherPlayerIds)
|
||||
{
|
||||
if (otherPlayerIds && otherPlayerIds.Contains(playerId))
|
||||
ERR_PersonalEscapeResult result = PersonalResult(playerId);
|
||||
if (result == ERR_PersonalEscapeResult.ESCAPED)
|
||||
return "You crossed the extraction boundary aboard the helicopter.";
|
||||
return "You were not aboard when the helicopter crossed the extraction boundary.";
|
||||
if (result == ERR_PersonalEscapeResult.MISSED) return "You did not escape with the helicopter.";
|
||||
return "Your personal escape result is not available.";
|
||||
}
|
||||
}
|
||||
|
||||
// End data and the controller replicate independently. Refresh an already-open
|
||||
// native result screen when the identity-derived controller result arrives.
|
||||
modded class SCR_GameOverScreenContentUIComponent
|
||||
{
|
||||
protected bool m_ERREscapeContent;
|
||||
override void InitContent(SCR_GameOverScreenUIContentData endScreenUIContent)
|
||||
{
|
||||
SCR_PlayerController.ERR_OnEscapeResultChanged().Remove(ERR_RefreshEscapeContent);
|
||||
super.InitContent(endScreenUIContent);
|
||||
m_ERREscapeContent = ERR_EscapeGameOverScreenInfo.Cast(endScreenUIContent.m_GameOverInfo) != null;
|
||||
if (!m_ERREscapeContent) return;
|
||||
SCR_PlayerController.ERR_OnEscapeResultChanged().Insert(ERR_RefreshEscapeContent);
|
||||
ERR_RefreshEscapeContent();
|
||||
}
|
||||
protected void ERR_RefreshEscapeContent()
|
||||
{
|
||||
if (!m_ERREscapeContent || !m_wRoot) return;
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
int playerId = -1;
|
||||
if (controller) playerId = controller.GetPlayerId();
|
||||
ERR_EscapeGameOverScreenInfo info = new ERR_EscapeGameOverScreenInfo();
|
||||
TextWidget title = TextWidget.Cast(m_wRoot.FindAnyWidget(m_sTileName));
|
||||
TextWidget debrief = TextWidget.Cast(m_wRoot.FindAnyWidget(m_sDebriefingName));
|
||||
if (title) title.SetText(info.GetTitle(playerId, null));
|
||||
if (debrief) debrief.SetText(info.GetDebriefing(playerId, null));
|
||||
}
|
||||
override void HandlerDeattached(Widget w)
|
||||
{
|
||||
SCR_PlayerController.ERR_OnEscapeResultChanged().Remove(ERR_RefreshEscapeContent);
|
||||
m_ERREscapeContent = false;
|
||||
super.HandlerDeattached(w);
|
||||
}
|
||||
}
|
||||
|
||||
[BaseContainerProps()]
|
||||
class ERR_FailureGameOverScreenInfo : SCR_BaseGameOverScreenInfo
|
||||
{
|
||||
override string GetDebriefing(int playerId, array<int> otherPlayerIds)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && !mode.EndReason().IsEmpty()) return mode.EndReason();
|
||||
return "No surviving prisoners can complete the escape, or the extraction helicopter has been lost.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
enum ERR_ExtractionState { WAITING, ESCAPED, LOST }
|
||||
|
||||
// Mission progression consumes the result; this service never spawns a vehicle.
|
||||
class ERR_ExtractionService
|
||||
{
|
||||
static const int REQUIRED_SEATS = 12;
|
||||
protected ref ERR_PlayerRoster m_Roster;
|
||||
protected Vehicle m_Vehicle;
|
||||
protected bool m_Assigned;
|
||||
protected bool m_Begun;
|
||||
protected bool m_Stopped;
|
||||
protected vector m_Center;
|
||||
protected float m_Radius;
|
||||
protected ERR_ExtractionState m_State;
|
||||
protected ref set<UUID> m_Escapees = new set<UUID>();
|
||||
protected int m_ParticipantCount;
|
||||
protected ref ScriptInvoker m_OnResolved = new ScriptInvoker();
|
||||
|
||||
void ERR_ExtractionService(ERR_PlayerRoster roster) { m_Roster = roster; }
|
||||
Vehicle VehicleEntity() { return m_Vehicle; }
|
||||
ERR_ExtractionState State() { return m_State; }
|
||||
ScriptInvoker OnResolved() { return m_OnResolved; }
|
||||
int EscapedCount() { return m_Escapees.Count(); }
|
||||
int LeftBehindCount() { return m_ParticipantCount - m_Escapees.Count(); }
|
||||
bool DidEscape(UUID identity) { return m_Escapees.Contains(identity); }
|
||||
|
||||
bool Assign(Vehicle vehicle)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Assigned || m_Stopped || !ERR_Actors.Alive(vehicle)) return false;
|
||||
m_Vehicle = vehicle;
|
||||
m_Assigned = true;
|
||||
return true;
|
||||
}
|
||||
int Seats(out array<BaseCompartmentSlot> seats)
|
||||
{
|
||||
seats.Clear();
|
||||
if (!m_Vehicle) return 0;
|
||||
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(m_Vehicle.FindComponent(BaseCompartmentManagerComponent));
|
||||
if (!manager) return 0;
|
||||
array<BaseCompartmentSlot> all = {};
|
||||
manager.GetCompartments(all);
|
||||
foreach (BaseCompartmentSlot slot : all)
|
||||
{
|
||||
if (!slot) continue;
|
||||
ECompartmentType type = SCR_CompartmentAccessComponent.GetCompartmentType(slot);
|
||||
if (type == ECompartmentType.PILOT || type == ECompartmentType.CARGO) seats.Insert(slot);
|
||||
}
|
||||
return seats.Count();
|
||||
}
|
||||
bool Ready()
|
||||
{
|
||||
if (!m_Assigned || !ERR_Actors.Alive(m_Vehicle) || !m_Vehicle.FindComponent(HelicopterControllerComponent)) return false;
|
||||
array<BaseCompartmentSlot> seats = {};
|
||||
if (Seats(seats) < REQUIRED_SEATS) return false;
|
||||
foreach (BaseCompartmentSlot slot : seats)
|
||||
if (SCR_CompartmentAccessComponent.GetCompartmentType(slot) == ECompartmentType.PILOT) return true;
|
||||
return false;
|
||||
}
|
||||
bool Begin(vector center, float radius)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Begun || m_Stopped || !m_Roster || radius <= 0 || !Ready()) return false;
|
||||
m_Center = center;
|
||||
m_Radius = radius;
|
||||
m_Begun = true;
|
||||
return true;
|
||||
}
|
||||
void Tick(ERR_Phase phase)
|
||||
{
|
||||
if (!Replication.IsServer() || !m_Begun || m_Stopped || m_State != ERR_ExtractionState.WAITING) return;
|
||||
if (phase < ERR_Phase.PRISON || ERR_MissionRules.Terminal(phase)) return;
|
||||
if (!ERR_Actors.Alive(m_Vehicle)) { Resolve(ERR_ExtractionState.LOST); return; }
|
||||
if (phase != ERR_Phase.EXTRACTION || vector.DistanceXZ(m_Vehicle.GetOrigin(), m_Center) <= m_Radius) return;
|
||||
array<ref ERR_PlayerLife> lives = {};
|
||||
m_Roster.Participants(lives);
|
||||
foreach (ERR_PlayerLife life : lives)
|
||||
{
|
||||
if (!ERR_Actors.Alive(life.Actor) || CompartmentAccessComponent.GetVehicleIn(life.Actor) != m_Vehicle) continue;
|
||||
// Reserved disconnected characters and unconscious passengers remain alive.
|
||||
// A recycled connection ID is never an identity for the escape snapshot.
|
||||
m_Escapees.Insert(life.AccountId);
|
||||
}
|
||||
if (m_Escapees.IsEmpty()) return;
|
||||
m_ParticipantCount = lives.Count();
|
||||
Resolve(ERR_ExtractionState.ESCAPED);
|
||||
}
|
||||
protected void Resolve(ERR_ExtractionState result)
|
||||
{
|
||||
m_State = result;
|
||||
m_OnResolved.Invoke(result);
|
||||
}
|
||||
void ConnectedWinners(out array<int> winners)
|
||||
{
|
||||
winners.Clear();
|
||||
if (m_State != ERR_ExtractionState.ESCAPED || !m_Roster) return;
|
||||
array<ref ERR_PlayerLife> lives = {};
|
||||
m_Roster.Participants(lives);
|
||||
foreach (ERR_PlayerLife life : lives)
|
||||
if (life.Connected && m_Roster.Get(life.ConnectionId) == life && m_Escapees.Contains(life.AccountId)) winners.Insert(life.ConnectionId);
|
||||
}
|
||||
void Stop() { m_Stopped = true; }
|
||||
}
|
||||
@@ -17,6 +17,7 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
protected string m_PreparationError;
|
||||
protected ref ERR_FactionCatalog m_Factions;
|
||||
protected ref ERR_Settings m_Settings;
|
||||
protected ref ERR_TimeService m_Time = new ERR_TimeService();
|
||||
protected ref ERR_DiscoveryService m_Discovery;
|
||||
protected ref ERR_Objectives m_Objectives;
|
||||
protected ref ERR_PlayerRoster m_Roster = new ERR_PlayerRoster();
|
||||
@@ -24,10 +25,10 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
protected ref ERR_CommandService m_Command;
|
||||
protected ref array<ERR_LocationComponent> m_Locations = {};
|
||||
protected ref array<IEntity> m_OwnedEntities = {};
|
||||
protected ref set<int> m_Participants = new set<int>();
|
||||
protected ref set<int> m_Escaped = new set<int>();
|
||||
protected ref ERR_ExtractionService m_Extraction;
|
||||
[RplProp()]
|
||||
protected string m_EndReason;
|
||||
protected ERR_LocationComponent m_Radio;
|
||||
protected Vehicle m_ExtractionVehicle;
|
||||
protected bool m_StartedPlayers;
|
||||
protected bool m_Stopped;
|
||||
protected float m_NoSurvivorsSince = -1;
|
||||
@@ -46,8 +47,16 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
ERR_PlayerRoster Players() { return m_Roster; }
|
||||
ERR_AdminService Admin() { return m_Admin; }
|
||||
ERR_Settings Settings() { return m_Settings; }
|
||||
ERR_TimeService Time() { return m_Time; }
|
||||
ERR_CommandService Command() { return m_Command; }
|
||||
ERR_Objectives Objectives() { return m_Objectives; }
|
||||
ERR_ExtractionService Extraction() { return m_Extraction; }
|
||||
string EndReason() { return m_EndReason; }
|
||||
Vehicle ExtractionVehicle()
|
||||
{
|
||||
if (!m_Extraction) return null;
|
||||
return m_Extraction.VehicleEntity();
|
||||
}
|
||||
float EscapeRadius() { return m_EscapeRadius; }
|
||||
bool DefenceOccupied() { return m_DefenceSince >= 0; }
|
||||
int DefenceSecondsRemaining()
|
||||
@@ -82,6 +91,7 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
string Status()
|
||||
{
|
||||
if (!m_PreparationError.IsEmpty()) return "Mission startup failed: " + m_PreparationError;
|
||||
if (!m_EndReason.IsEmpty()) return m_EndReason;
|
||||
if (m_Phase == ERR_Phase.PREPARING) return "Preparing mission sites and physical supplies.";
|
||||
string phase;
|
||||
if (m_Phase == ERR_Phase.PRISON) phase = "Escape the prison";
|
||||
@@ -99,6 +109,10 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
Replication.BumpMe();
|
||||
Print("Rereintroduced: scenario preparation failed; spawning remains disabled: " + detail, LogLevel.ERROR);
|
||||
StopServices();
|
||||
ERR_SiteService sites = Sites();
|
||||
if (sites) sites.Rollback();
|
||||
for (int i = m_OwnedEntities.Count() - 1; i >= 0; i--) ERR_Actors.DeleteSpawned(m_OwnedEntities[i]);
|
||||
m_OwnedEntities.Clear();
|
||||
}
|
||||
vector PrisonPosition() { return ERR_Actors.Ground(m_PrisonPosition); }
|
||||
bool Active() { return m_Phase >= ERR_Phase.PRISON && !ERR_MissionRules.Terminal(m_Phase); }
|
||||
@@ -133,7 +147,23 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
void UnregisterLocation(ERR_LocationComponent location) { m_Locations.RemoveItem(location); }
|
||||
void Locations(out array<ERR_LocationComponent> locations) { locations.Copy(m_Locations); }
|
||||
void Own(IEntity entity) { if (entity && !m_OwnedEntities.Contains(entity)) m_OwnedEntities.Insert(entity); }
|
||||
void SetExtractionVehicle(Vehicle vehicle) { if (Replication.IsServer()) m_ExtractionVehicle = vehicle; }
|
||||
bool SetExtractionVehicle(Vehicle vehicle)
|
||||
{
|
||||
return Replication.IsServer() && m_Phase == ERR_Phase.PREPARING && m_Extraction && m_Extraction.Assign(vehicle);
|
||||
}
|
||||
void PublishEscapeResults(bool reset = false)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
array<int> connected = {};
|
||||
GetGame().GetPlayerManager().GetPlayers(connected);
|
||||
foreach (int id : connected)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
|
||||
if (!controller) continue;
|
||||
if (reset) controller.ERR_ResetEscapeResult();
|
||||
else controller.ERR_PublishEscapeResult(m_Roster, m_Extraction);
|
||||
}
|
||||
}
|
||||
void RegisterGroup(SCR_AIGroup group)
|
||||
{
|
||||
if (!Replication.IsServer() || !group) return;
|
||||
@@ -147,6 +177,7 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
if (!Replication.IsServer() || !m_PreparationError.IsEmpty() || !ERR_MissionRules.CanTransition(m_Phase, phase)) return false;
|
||||
ERR_Phase previous = m_Phase;
|
||||
m_Phase = phase;
|
||||
if (ERR_MissionRules.Terminal(phase)) PublishEscapeResults();
|
||||
Replication.BumpMe();
|
||||
PrintFormat("Rereintroduced [mission]: phase %1 -> %2", previous, phase);
|
||||
m_OnPhaseChanged.Invoke(previous, phase);
|
||||
@@ -154,7 +185,7 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
{
|
||||
StopServices();
|
||||
array<int> winners = {};
|
||||
foreach (int playerId : m_Escaped) winners.Insert(playerId);
|
||||
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));
|
||||
@@ -183,7 +214,6 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
{
|
||||
if (!Replication.IsServer() || !actor || ERR_MissionRules.Terminal(m_Phase)) return;
|
||||
m_Roster.Spawned(id, actor);
|
||||
m_Participants.Insert(id);
|
||||
m_StartedPlayers = true;
|
||||
m_NoSurvivorsSince = -1;
|
||||
}
|
||||
@@ -196,6 +226,10 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
if (!System.IsConsoleApp()) GetGame().GetCallqueue().CallLater(BindLocalObserver, 250, true);
|
||||
if (!Replication.IsServer()) return;
|
||||
m_Settings = new ERR_Settings();
|
||||
m_Extraction = new ERR_ExtractionService(m_Roster);
|
||||
PublishEscapeResults(true);
|
||||
m_Extraction.OnResolved().Insert(ExtractionResolved);
|
||||
OnStop().Insert(m_Extraction.Stop);
|
||||
m_Discovery = new ERR_DiscoveryService(this);
|
||||
OnTick().Insert(m_Discovery.Tick);
|
||||
OnStop().Insert(m_Discovery.Stop);
|
||||
@@ -236,15 +270,38 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
FailPreparation(detail);
|
||||
return;
|
||||
}
|
||||
if (!m_Extraction || !m_Extraction.Begin(m_EscapeCenter, m_EscapeRadius))
|
||||
{
|
||||
FailPreparation("The extraction vehicle must be a helicopter with a pilot position and at least twelve pilot/passenger seats, with a valid escape boundary.");
|
||||
return;
|
||||
}
|
||||
if (!m_Time.Start(this)) { FailPreparation("The world clock could not be initialized."); return; }
|
||||
Transition(ERR_Phase.PRISON);
|
||||
}
|
||||
|
||||
protected void ExtractionResolved(ERR_ExtractionState result)
|
||||
{
|
||||
if (!Replication.IsServer() || !Active()) return;
|
||||
if (result == ERR_ExtractionState.LOST)
|
||||
{
|
||||
m_EndReason = "The extraction helicopter was lost. No replacement is available.";
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
}
|
||||
else if (result == ERR_ExtractionState.ESCAPED)
|
||||
{
|
||||
m_EndReason = string.Format("Escape complete: %1 escaped; %2 did not escape.", m_Extraction.EscapedCount(), m_Extraction.LeftBehindCount());
|
||||
Transition(ERR_Phase.VICTORY);
|
||||
}
|
||||
}
|
||||
|
||||
protected void ServerTick()
|
||||
{
|
||||
if (!Replication.IsServer() || !Active()) return;
|
||||
float now = GetWorld().GetWorldTime();
|
||||
array<IEntity> players = {};
|
||||
ERR_Actors.Players(players);
|
||||
m_Extraction.Tick(m_Phase);
|
||||
if (!Active()) return;
|
||||
if (GetGame().GetPlayerManager().GetPlayerCount() == 0)
|
||||
{
|
||||
m_NoSurvivorsSince = -1;
|
||||
@@ -254,7 +311,12 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
if (m_StartedPlayers && !m_Roster.HasSurvivor())
|
||||
{
|
||||
if (m_NoSurvivorsSince < 0) m_NoSurvivorsSince = now;
|
||||
if (now - m_NoSurvivorsSince >= 30000) { Transition(ERR_Phase.DEFEAT); return; }
|
||||
if (now - m_NoSurvivorsSince >= 30000)
|
||||
{
|
||||
m_EndReason = "No surviving prisoners remain.";
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else m_NoSurvivorsSince = -1;
|
||||
if (m_Phase == ERR_Phase.RADIO_DEFENCE)
|
||||
@@ -271,17 +333,6 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
Transition(ERR_Phase.EXTRACTION);
|
||||
}
|
||||
}
|
||||
if (m_Phase == ERR_Phase.EXTRACTION)
|
||||
{
|
||||
if (!ERR_Actors.Alive(m_ExtractionVehicle)) { Transition(ERR_Phase.DEFEAT); return; }
|
||||
foreach (IEntity actor : players)
|
||||
{
|
||||
if (CompartmentAccessComponent.GetVehicleIn(actor) != m_ExtractionVehicle) continue;
|
||||
if (vector.DistanceXZ(actor.GetOrigin(), m_EscapeCenter) <= m_EscapeRadius) continue;
|
||||
m_Escaped.Insert(GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(actor));
|
||||
}
|
||||
if (!m_Escaped.IsEmpty()) { Transition(ERR_Phase.VICTORY); return; }
|
||||
}
|
||||
array<int> connected = {};
|
||||
GetGame().GetPlayerManager().GetPlayers(connected);
|
||||
foreach (int id : connected)
|
||||
|
||||
@@ -5,6 +5,8 @@ modded class ERR_GameMode
|
||||
protected int m_ERRBaseStep;
|
||||
protected bool m_ERRStartupFailure;
|
||||
protected bool m_ERRCacheLoss;
|
||||
protected string m_ERRExtractionCase;
|
||||
protected ref ERR_NativeExtractionProbe m_ERRExtractionProbe;
|
||||
protected string m_ERRActiveCacheId = "everon-supply-1";
|
||||
protected int m_ERRMedicalDiscoveries;
|
||||
protected IEntity m_ERRSightBlocker;
|
||||
@@ -21,6 +23,9 @@ modded class ERR_GameMode
|
||||
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
|
||||
string outcome;
|
||||
m_ERRStartupFailure = System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "startup-failure";
|
||||
m_ERRExtractionCase = outcome;
|
||||
if (outcome == "extraction-startup-failure" && m_Factions) m_Factions.ExtractionPrefab = "{259EE7B78C51B624}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469.et";
|
||||
if (outcome == "extraction-boundary-failure") m_EscapeRadius = 0;
|
||||
m_ERRCacheLoss = outcome == "cache-loss";
|
||||
if (m_ERRCacheLoss) m_ERRActiveCacheId = "everon-supply-2";
|
||||
OnLocationDiscovered().Insert(ERR_BaseDiscovered);
|
||||
@@ -101,6 +106,20 @@ modded class ERR_GameMode
|
||||
{
|
||||
if (!ERR_MissionRules.Terminal(current)) return;
|
||||
GetGame().GetCallqueue().Remove(ERR_BaseTick);
|
||||
if (m_ERRExtractionCase == "early-helicopter-loss")
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 201 && Players().HasSurvivor() && Extraction().State() == ERR_ExtractionState.LOST, "lost helicopter ends prison phase despite a living prisoner")) return;
|
||||
ERR_FailureGameOverScreenInfo result = new ERR_FailureGameOverScreenInfo();
|
||||
if (!ERR_BaseCheck(EndReason().Contains("helicopter was lost") && result.GetDebriefing(0, null) == EndReason(), "early loss has a specific replicated and debriefing reason")) return;
|
||||
Print("ERR SMOKE COMPLETE: early extraction vehicle loss");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "extraction-capacity")
|
||||
{
|
||||
if (!ERR_BaseCheck(m_ERRBaseStep == 202, "capacity fixture finished before its explicit test reset")) return;
|
||||
Print("ERR SMOKE COMPLETE: physical capacity and isolated extraction results; reset requested by fixture");
|
||||
return;
|
||||
}
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.VICTORY && m_ERRBaseStep == 5, "escape and restart reached without companion services")) return;
|
||||
Print("ERR SMOKE COMPLETE: core-only escape");
|
||||
}
|
||||
@@ -108,6 +127,18 @@ modded class ERR_GameMode
|
||||
{
|
||||
float now = GetWorld().GetWorldTime();
|
||||
if (now > m_ERRBaseDeadline) { ERR_BaseCheck(false, "timeout at " + m_ERRBaseStep.ToString()); return; }
|
||||
if (m_ERRExtractionCase == "extraction-startup-failure" || m_ERRExtractionCase == "extraction-boundary-failure")
|
||||
{
|
||||
if (PreparationError().IsEmpty()) return;
|
||||
if (!ERR_BaseCheck(Phase() == ERR_Phase.PREPARING && !Active() && m_Stopped, "invalid extraction setup never starts gameplay")) return;
|
||||
array<IEntity> players = {};
|
||||
ERR_Actors.Players(players);
|
||||
if (!ERR_BaseCheck(players.IsEmpty() && Sites() && Sites().Count() == 0 && m_OwnedEntities.IsEmpty() && !ExtractionVehicle(), "late startup failure rolls back all authored stock, scenery, groups and vehicle")) return;
|
||||
if (!ERR_BaseCheck(PreparationError().Contains("twelve") && !Transition(ERR_Phase.PRISON), "invalid extraction retains actionable diagnosis and blocks progression")) return;
|
||||
GetGame().GetCallqueue().Remove(ERR_BaseTick);
|
||||
Print("ERR SMOKE COMPLETE: expected late extraction startup failure");
|
||||
return;
|
||||
}
|
||||
if (m_ERRStartupFailure)
|
||||
{
|
||||
if (PreparationError().IsEmpty()) return;
|
||||
@@ -123,6 +154,13 @@ modded class ERR_GameMode
|
||||
Print("ERR SMOKE COMPLETE: expected startup-failure");
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 200)
|
||||
{
|
||||
int result = m_ERRExtractionProbe.Tick(now);
|
||||
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
if (result == 1) { m_ERRBaseStep = 202; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (now < m_ERRBaseWait) return;
|
||||
if (m_ERRBaseStep == 0)
|
||||
{
|
||||
@@ -130,6 +168,20 @@ modded class ERR_GameMode
|
||||
ERR_Actors.Players(players, true);
|
||||
if (players.Count() != 1 || Phase() != ERR_Phase.PRISON) return;
|
||||
m_ERRBasePlayer = players[0];
|
||||
if (m_ERRExtractionCase == "early-helicopter-loss")
|
||||
{
|
||||
if (!ERR_BaseCheck(Extraction().Ready() && Players().HasSurvivor(), "ready helicopter and living prisoner before deliberate loss")) return;
|
||||
m_ERRBaseStep = 201;
|
||||
ERR_Actors.DeleteSpawned(ExtractionVehicle());
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "extraction-capacity")
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRExtractionProbe = new ERR_NativeExtractionProbe(this);
|
||||
m_ERRBaseStep = 200;
|
||||
return;
|
||||
}
|
||||
string survey;
|
||||
if (System.GetCLIParam("errSmokeOutcome", survey) && survey == "site-survey")
|
||||
{
|
||||
@@ -138,7 +190,7 @@ modded class ERR_GameMode
|
||||
GetGame().GetCallqueue().Remove(ERR_BaseTick);
|
||||
return;
|
||||
}
|
||||
if (!ERR_NativeDefinitionProbe.Run() || !ERR_NativeContractsProbe.Run(this, m_ERRBasePlayer) || !ERR_BaseCover()) return;
|
||||
if (!ERR_NativeDefinitionProbe.Run() || !ERR_NativeContractsProbe.Run(this, m_ERRBasePlayer) || !ERR_BaseCover() || !ERR_NativeTimeProbe.Run(this)) return;
|
||||
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
if (!ERR_BaseCheck(Objectives() && Objectives().Current() && Objectives().Current().GetTaskName() == "Escape the prison", "native prison objective exists")) return;
|
||||
ERR_SiteRuntime cache = Sites().Find(m_ERRActiveCacheId);
|
||||
@@ -166,7 +218,7 @@ modded class ERR_GameMode
|
||||
ERR_Actors.DeleteSpawned(Sites().Find("everon-supply-1").Container);
|
||||
if (!ERR_BaseCheck(!Sites().Find("everon-supply-1").Container, "primary cache lost before any clues are recovered")) return;
|
||||
}
|
||||
if (!ERR_BaseCheck(lock && m_ERRBaseRadio && m_ERRBaseDocuments.Count() == 2 && ERR_Actors.Alive(m_ExtractionVehicle), "gate, clues, radio and helicopter created by core")) return;
|
||||
if (!ERR_BaseCheck(lock && m_ERRBaseRadio && m_ERRBaseDocuments.Count() == 2 && ERR_Actors.Alive(ExtractionVehicle()), "gate, clues, radio and helicopter created by core")) return;
|
||||
ERR_BaseMove(lock.GetOwner().GetOrigin() + "1 0 1");
|
||||
lock.PerformAction(lock.GetOwner(), m_ERRBasePlayer);
|
||||
if (!ERR_BaseCheck(Phase() == ERR_Phase.RADIO_SEARCH, "gate action begins escape")) return;
|
||||
@@ -292,20 +344,20 @@ modded class ERR_GameMode
|
||||
}
|
||||
else if (m_ERRBaseStep == 4)
|
||||
{
|
||||
if (!ERR_BaseCheck(Phase() == ERR_Phase.EXTRACTION, "radio defence reveals extraction")) return;
|
||||
if (!ERR_BaseCheck(Phase() == ERR_Phase.EXTRACTION, "radio defence reveals extraction") || !ERR_NativeTimeProbe.VerifyProgression(this)) return;
|
||||
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(m_ERRBasePlayer.FindComponent(CompartmentAccessComponent));
|
||||
BaseCompartmentSlot slot = access.FindFreeCompartment(m_ExtractionVehicle, ECompartmentType.PILOT);
|
||||
if (!ERR_BaseCheck(slot && access.GetInVehicle(m_ExtractionVehicle, slot, true, -1, ECloseDoorAfterActions.INVALID, false), "player physically boards extraction helicopter")) return;
|
||||
BaseCompartmentSlot slot = access.FindFreeCompartment(ExtractionVehicle(), ECompartmentType.PILOT);
|
||||
if (!ERR_BaseCheck(slot && access.GetInVehicle(ExtractionVehicle(), slot, true, -1, ECloseDoorAfterActions.INVALID, false), "player physically boards extraction helicopter")) return;
|
||||
m_ERRBaseStep = 5;
|
||||
m_ERRBaseWait = now + 3000;
|
||||
}
|
||||
else if (m_ERRBaseStep == 5)
|
||||
{
|
||||
if (!ERR_BaseCheck(CompartmentAccessComponent.GetVehicleIn(m_ERRBasePlayer) == m_ExtractionVehicle, "extraction requires real occupancy")) return;
|
||||
if (!ERR_BaseCheck(CompartmentAccessComponent.GetVehicleIn(m_ERRBasePlayer) == ExtractionVehicle(), "extraction requires real occupancy")) return;
|
||||
vector transform[4];
|
||||
m_ExtractionVehicle.GetWorldTransform(transform);
|
||||
ExtractionVehicle().GetWorldTransform(transform);
|
||||
transform[3] = m_EscapeCenter + Vector(m_EscapeRadius + 100, 150, 0);
|
||||
m_ExtractionVehicle.SetWorldTransform(transform);
|
||||
ExtractionVehicle().SetWorldTransform(transform);
|
||||
m_ERRBaseWait = now + 4000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
#ifdef WORKBENCH
|
||||
// Real vehicle/character compartments with an isolated synthetic identity roster.
|
||||
// This is not twelve network clients, manual boarding, medical treatment or flight.
|
||||
class ERR_NativeExtractionProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected Vehicle m_Vehicle;
|
||||
protected ref ERR_PlayerRoster m_Roster = new ERR_PlayerRoster();
|
||||
protected ref ERR_ExtractionService m_All;
|
||||
protected ref ERR_ExtractionService m_Partial;
|
||||
protected ref array<IEntity> m_Actors = {};
|
||||
protected ref array<UUID> m_Accounts = {};
|
||||
protected ref array<BaseCompartmentSlot> m_Seats = {};
|
||||
protected vector m_Original[4];
|
||||
protected int m_Step;
|
||||
protected int m_Boarding;
|
||||
protected int m_Resolutions;
|
||||
protected float m_Wait;
|
||||
protected UUID m_NewAccount = "00e77200-0000-0000-0000-000000000099";
|
||||
|
||||
void ERR_NativeExtractionProbe(ERR_GameMode mode) { m_Mode = mode; }
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: extraction " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: extraction " + detail, LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
protected void Resolved(ERR_ExtractionState state) { m_Resolutions++; }
|
||||
protected IEntity Character(int index)
|
||||
{
|
||||
IEntity actor = ERR_Actors.Spawn(m_Mode.Factions().PrisonerPrefab, ERR_Actors.Ground(m_Original[3] + Vector(20 + index * 2, 0, 15)));
|
||||
if (!actor) return null;
|
||||
m_Mode.Own(actor);
|
||||
m_Actors.Insert(actor);
|
||||
DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
return actor;
|
||||
}
|
||||
protected void MoveHelicopter(bool outside)
|
||||
{
|
||||
vector transform[4];
|
||||
m_Vehicle.GetWorldTransform(transform);
|
||||
transform[3] = m_Original[3];
|
||||
if (outside) transform[3] = m_Original[3] + "650 150 0";
|
||||
m_Vehicle.SetWorldTransform(transform);
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
if (!ERR_NativeTimeProbe.Run(m_Mode)) return -1;
|
||||
m_Vehicle = m_Mode.ExtractionVehicle();
|
||||
if (!Check(m_Vehicle && m_Mode.Extraction().Ready(), "authored transport is ready for twelve prisoners")) return -1;
|
||||
int count = m_Mode.Extraction().Seats(m_Seats);
|
||||
PrintFormat("ERR EXTRACTION CAPACITY: usable pilot/passenger positions=%1", count);
|
||||
if (!Check(count >= 12 && !m_Mode.SetExtractionVehicle(m_Vehicle), "running mission cannot rebind its extraction vehicle")) return -1;
|
||||
m_Vehicle.GetWorldTransform(m_Original);
|
||||
DamageManagerComponent.Cast(m_Vehicle.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
IEntity actor = Character(i);
|
||||
UUID account = "00e77200-0000-0000-0000-" + (i + 1).ToString(12, true);
|
||||
if (!Check(actor && m_Roster.Bind(10001 + i, account), "isolated participant created")) return -1;
|
||||
m_Accounts.Insert(account);
|
||||
m_Roster.Spawned(10001 + i, actor);
|
||||
}
|
||||
m_All = new ERR_ExtractionService(m_Roster);
|
||||
if (!Check(m_All.Assign(m_Vehicle) && !m_All.Begin(m_Original[3], 0) && m_All.Begin(m_Original[3], 500), "invalid boundary rejected without preventing a valid service start")) return -1;
|
||||
if (!Check(!m_All.Assign(m_Vehicle) && !m_All.Begin(m_Original[3], 500), "duplicate binding and start rejected")) return -1;
|
||||
m_All.OnResolved().Insert(Resolved);
|
||||
m_Step = 1;
|
||||
m_Wait = now + 1000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(m_Actors[m_Boarding].FindComponent(CompartmentAccessComponent));
|
||||
if (!Check(access && access.GetInVehicle(m_Vehicle, m_Seats[m_Boarding], true, -1, ECloseDoorAfterActions.INVALID, false), "native boarding request accepted")) return -1;
|
||||
m_Boarding++;
|
||||
m_Wait = now + 500;
|
||||
if (m_Boarding == 12) { m_Step = 2; m_Wait = now + 3000; }
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 2)
|
||||
{
|
||||
for (int i = 0; i < 12; i++)
|
||||
if (!Check(CompartmentAccessComponent.GetVehicleIn(m_Actors[i]) == m_Vehicle && m_Seats[i].GetOccupant() == m_Actors[i], "participant occupies a distinct physical seat")) return -1;
|
||||
m_All.Tick(ERR_Phase.EXTRACTION);
|
||||
if (!Check(m_All.State() == ERR_ExtractionState.WAITING && m_All.EscapedCount() == 0, "boarding inside boundary does not win")) return -1;
|
||||
MoveHelicopter(true);
|
||||
m_Step = 3;
|
||||
m_Wait = now + 2500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
m_All.Tick(ERR_Phase.RADIO_SEARCH);
|
||||
if (!Check(m_All.State() == ERR_ExtractionState.WAITING, "leaving before extraction phase does not win")) return -1;
|
||||
m_All.Tick(ERR_Phase.EXTRACTION);
|
||||
array<int> winners = {};
|
||||
m_All.ConnectedWinners(winners);
|
||||
if (!Check(m_All.EscapedCount() == 12 && m_All.LeftBehindCount() == 0 && winners.Count() == 12 && m_Resolutions == 1, "all twelve physical occupants escape exactly once")) return -1;
|
||||
m_All.Tick(ERR_Phase.EXTRACTION);
|
||||
if (!Check(m_Resolutions == 1 && !m_All.Assign(m_Vehicle), "resolved extraction neither repeats nor replaces its vehicle")) return -1;
|
||||
MoveHelicopter(false);
|
||||
m_Step = 4;
|
||||
m_Wait = now + 2500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 4)
|
||||
{
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(m_Actors[0].FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
damage.EnableDamageHandling(true);
|
||||
damage.Kill(Instigator.CreateInstigator(m_Actors[0]));
|
||||
vector exitTransform[4];
|
||||
m_Actors[1].GetWorldTransform(exitTransform);
|
||||
exitTransform[3] = ERR_Actors.Ground(m_Original[3] + "30 0 0");
|
||||
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(m_Actors[1].FindComponent(CompartmentAccessComponent));
|
||||
if (!Check(access.GetOutVehicle_NoDoor(exitTransform, false, false), "one survivor leaves the helicopter")) return -1;
|
||||
CharacterControllerComponent.Cast(m_Actors[2].FindComponent(CharacterControllerComponent)).SetUnconscious(true);
|
||||
m_Roster.Disconnect(10004);
|
||||
IEntity newcomer = Character(12);
|
||||
if (!Check(newcomer && m_Roster.Bind(10004, m_NewAccount), "departed connection ID reused by a distinct account")) return -1;
|
||||
m_Roster.Spawned(10004, newcomer);
|
||||
m_Partial = new ERR_ExtractionService(m_Roster);
|
||||
if (!Check(m_Partial.Assign(m_Vehicle) && m_Partial.Begin(m_Original[3], 500), "isolated partial escape case starts")) return -1;
|
||||
m_Step = 5;
|
||||
m_Wait = now + 3000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 5)
|
||||
{
|
||||
if (!Check(!ERR_Actors.Alive(m_Actors[0]) && !CompartmentAccessComponent.GetVehicleIn(m_Actors[1]), "dead occupant and left-behind survivor are physically distinct cases")) return -1;
|
||||
if (!Check(ERR_Actors.Alive(m_Actors[2]) && !ERR_Actors.CanFight(m_Actors[2]) && CompartmentAccessComponent.GetVehicleIn(m_Actors[2]) == m_Vehicle, "unconscious passenger remains physically aboard")) return -1;
|
||||
MoveHelicopter(true);
|
||||
m_Step = 6;
|
||||
m_Wait = now + 2500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 6)
|
||||
{
|
||||
m_Partial.Tick(ERR_Phase.EXTRACTION);
|
||||
array<int> winners = {};
|
||||
m_Partial.ConnectedWinners(winners);
|
||||
if (!Check(m_Partial.EscapedCount() == 10 && m_Partial.LeftBehindCount() == 3 && winners.Count() == 9, "partial escape counts only living aboard participants")) return -1;
|
||||
if (!Check(m_Partial.DidEscape(m_Accounts[2]) && m_Partial.DidEscape(m_Accounts[3]) && !m_Partial.DidEscape(m_Accounts[0]) && !m_Partial.DidEscape(m_Accounts[1]) && !m_Partial.DidEscape(m_NewAccount), "unconscious and reserved occupants escape; dead and absent people do not")) return -1;
|
||||
if (!Check(winners.Contains(10003) && !winners.Contains(10004), "recycled connection ID does not inherit another account's win")) return -1;
|
||||
if (!Check(m_Roster.Bind(10040, m_Accounts[3]), "escaped reserved account can rebind to a new connection")) return -1;
|
||||
m_Partial.ConnectedWinners(winners);
|
||||
if (!Check(winners.Count() == 10 && winners.Contains(10040) && !winners.Contains(10004), "result follows escaped identity after rebinding")) return -1;
|
||||
m_Partial.Tick(ERR_Phase.EXTRACTION);
|
||||
if (!Check(m_Partial.EscapedCount() == 10 && m_Partial.LeftBehindCount() == 3, "terminal result snapshot remains stable")) return -1;
|
||||
ERR_PlayerRoster emptyRoster = new ERR_PlayerRoster();
|
||||
ERR_ExtractionService empty = new ERR_ExtractionService(emptyRoster);
|
||||
if (!Check(empty.Assign(m_Vehicle) && empty.Begin(m_Original[3], 500), "empty-roster control starts")) return -1;
|
||||
empty.Tick(ERR_Phase.EXTRACTION);
|
||||
if (!Check(empty.State() == ERR_ExtractionState.WAITING, "unregistered occupants cannot win the mission")) return -1;
|
||||
ERR_ExtractionService stopped = new ERR_ExtractionService(m_Roster);
|
||||
if (!Check(stopped.Assign(m_Vehicle) && stopped.Begin(m_Original[3], 500), "stopped-service control starts")) return -1;
|
||||
stopped.Stop();
|
||||
stopped.Tick(ERR_Phase.EXTRACTION);
|
||||
if (!Check(stopped.State() == ERR_ExtractionState.WAITING, "stopped extraction cannot resolve")) return -1;
|
||||
if (!ERR_NativeResultProbe.Run(m_Partial, m_Accounts[3], m_Accounts[0]) || !ERR_NativeTimeProbe.VerifyProgression(m_Mode)) return -1;
|
||||
MoveHelicopter(false);
|
||||
for (int i = m_Actors.Count() - 1; i >= 0; i--) ERR_Actors.DeleteSpawned(m_Actors[i]);
|
||||
m_Step = 7;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,68 @@
|
||||
#ifdef WORKBENCH
|
||||
// Local controller and native presentation provider; no network clients are
|
||||
// simulated. The extraction snapshot was produced by real occupied seats.
|
||||
class ERR_NativeResultProbe
|
||||
{
|
||||
protected static Widget s_Content;
|
||||
protected static bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: result " + detail); return true; }
|
||||
if (s_Content) s_Content.RemoveFromHierarchy();
|
||||
s_Content = null;
|
||||
Print("ERR SMOKE FAIL: result " + detail, LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
protected static bool NativeText(string title, string debrief)
|
||||
{
|
||||
if (!s_Content) return false;
|
||||
TextWidget titleWidget = TextWidget.Cast(s_Content.FindAnyWidget("GameOver_State"));
|
||||
TextWidget debriefWidget = TextWidget.Cast(s_Content.FindAnyWidget("GameOver_Description"));
|
||||
return titleWidget && debriefWidget && titleWidget.GetText() == title && debriefWidget.GetText() == debrief;
|
||||
}
|
||||
static bool Run(ERR_ExtractionService extraction, UUID escapedAccount, UUID missedAccount)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
if (!Check(controller && extraction && extraction.State() == ERR_ExtractionState.ESCAPED, "local controller and physical extraction snapshot exist")) return false;
|
||||
int playerId = controller.GetPlayerId();
|
||||
ERR_EscapeGameOverScreenInfo info = new ERR_EscapeGameOverScreenInfo();
|
||||
array<int> staleIds = {playerId};
|
||||
SCR_GameModeEndData staleData = SCR_GameModeEndData.Create(EGameOverTypes.VICTORY, staleIds);
|
||||
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;
|
||||
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;
|
||||
s_Content.SetVisible(false);
|
||||
SCR_GameOverScreenContentUIComponent contentHandler = SCR_GameOverScreenContentUIComponent.Cast(s_Content.FindHandler(SCR_GameOverScreenContentUIComponent));
|
||||
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;
|
||||
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());
|
||||
controller.ERR_PublishEscapeResult(roster, extraction);
|
||||
array<int> noWinners = {};
|
||||
if (!Check(controller.ERR_EscapeResult() == ERR_PersonalEscapeResult.ESCAPED && info.GetTitle(playerId, noWinners) == "You escaped Everon", "identity snapshot publishes victory despite missing end-data ID")) return false;
|
||||
if (!Check(NativeText("You escaped Everon", "You crossed the extraction boundary aboard the helicopter."), "already-open native content refreshes when personal victory arrives")) return false;
|
||||
if (!Check(roster.Bind(playerId, missedAccount), "local presentation connection ID is recycled to non-escapee")) return false;
|
||||
roster.Spawned(playerId, controller.GetControlledEntity());
|
||||
controller.ERR_PublishEscapeResult(roster, extraction);
|
||||
if (!Check(controller.ERR_EscapeResult() == ERR_PersonalEscapeResult.MISSED && info.GetTitle(playerId, copiedIds) == "The helicopter escaped" && info.GetDebriefing(playerId, copiedIds) == "You did not escape with the helicopter.", "recycled winner ID cannot grant escape or misdescribe dead occupants")) return false;
|
||||
if (!Check(NativeText("The helicopter escaped", "You did not escape with the helicopter."), "native text refresh removes the previous account's win")) return false;
|
||||
if (!Check(roster.Bind(playerId, escapedAccount), "escaped presentation account returns after simulated rebind")) return false;
|
||||
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;
|
||||
s_Content.RemoveFromHierarchy();
|
||||
s_Content = null;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
controller.ERR_PublishEscapeResult(mode.Players(), mode.Extraction());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,117 @@
|
||||
#ifdef WORKBENCH
|
||||
class ERR_NativeSettingsReader : ERR_Settings
|
||||
{
|
||||
bool Probe(string path, out map<string, int> values) { return Read(path, values); }
|
||||
}
|
||||
// Runs inside the disposable native test profile. No original settings touched.
|
||||
class ERR_NativeTimeProbe
|
||||
{
|
||||
protected static float s_EngineTime;
|
||||
protected static float s_ClockHours;
|
||||
protected static int s_Multiplier;
|
||||
protected static bool s_Started;
|
||||
static bool VerifyProgression(ERR_GameMode mode)
|
||||
{
|
||||
ChimeraWorld world = ChimeraWorld.CastFrom(mode.GetWorld());
|
||||
TimeAndWeatherManagerEntity manager = world.GetTimeAndWeatherManager();
|
||||
if (!Check(s_Started && manager && manager.GetIsDayAutoAdvanced(), "clock advancement remains enabled")) return false;
|
||||
float elapsed = manager.GetEngineTime() - s_EngineTime;
|
||||
if (!Check(elapsed >= 10, "progression sampled across at least ten engine seconds")) return false;
|
||||
float actual = manager.GetTimeOfTheDay() - s_ClockHours;
|
||||
if (actual < 0) actual += 24;
|
||||
float expected = elapsed * s_Multiplier / 3600.0;
|
||||
return Check(Math.AbsFloat(actual - expected) < 0.001, "native clock advances at configured rate without periodic resets");
|
||||
}
|
||||
protected static bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: mission time " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: mission time " + detail, LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
protected static bool WriteFixture(string path, string omit = "", bool clock = false, float minutes = 840, bool speed = false, float multiplier = 1)
|
||||
{
|
||||
JsonSaveContext json = new JsonSaveContext();
|
||||
json.EnableTypeDiscriminator(false);
|
||||
if (omit != "reportIntervalSeconds") json.WriteValue("reportIntervalSeconds", 20);
|
||||
json.WriteValue("supportSquadLimit", 2);
|
||||
json.WriteValue("responseRadiusMetres", 1500);
|
||||
json.WriteValue("aiIntelChancePercent", 0);
|
||||
json.WriteValue("intelRevealChancePercent", 100);
|
||||
json.WriteValue("radioDefenceSeconds", 120);
|
||||
if (clock) json.WriteValue("startTimeMinutes", minutes);
|
||||
if (speed) json.WriteValue("timeMultiplier", multiplier);
|
||||
return json.SaveToFile(path);
|
||||
}
|
||||
protected static bool SettingsMigration(string path)
|
||||
{
|
||||
if (!Check(WriteFixture(path), "legacy fixture created")) return false;
|
||||
ERR_Settings legacy = new ERR_Settings(path);
|
||||
if (!Check(legacy.Get("reportIntervalSeconds") == 20 && legacy.Get("radioDefenceSeconds") == 120 && legacy.Get("aiIntelChancePercent") == 0 && legacy.Get("intelRevealChancePercent") == 100, "existing six-key settings preserved")) return false;
|
||||
if (!Check(legacy.Get("startTimeMinutes") == 840 && legacy.Get("timeMultiplier") == 1, "only absent time settings receive defaults")) return false;
|
||||
JsonLoadContext unchanged = new JsonLoadContext();
|
||||
if (!Check(unchanged.LoadFromFile(path) && !unchanged.DoesKeyExist("startTimeMinutes") && !unchanged.DoesKeyExist("timeMultiplier"), "loading does not rewrite legacy file")) return false;
|
||||
if (!Check(legacy.Set("startTimeMinutes", 375) && legacy.Set("timeMultiplier", 4), "explicit save upgrades valid legacy settings")) return false;
|
||||
ERR_Settings reloaded = new ERR_Settings(path);
|
||||
if (!Check(reloaded.Get("startTimeMinutes") == 375 && reloaded.Get("timeMultiplier") == 4 && reloaded.Get("responseRadiusMetres") == 1500 && reloaded.Get("radioDefenceSeconds") == 120, "new keys and old values survive reload")) return false;
|
||||
ERR_NativeSettingsReader reader = new ERR_NativeSettingsReader();
|
||||
map<string, int> values;
|
||||
if (!WriteFixture(path, "", true, 1440)) return false;
|
||||
if (!Check(!reader.Probe(path, values), "present out-of-range time cannot silently default")) return false;
|
||||
if (!WriteFixture(path, "", true, 375.5)) return false;
|
||||
if (!Check(!reader.Probe(path, values), "fractional clock setting rejected")) return false;
|
||||
if (!WriteFixture(path, "", false, 0, true, 0)) return false;
|
||||
if (!Check(!reader.Probe(path, values), "zero multiplier rejected")) return false;
|
||||
if (!WriteFixture(path, "reportIntervalSeconds")) return false;
|
||||
if (!Check(!reader.Probe(path, values), "missing legacy setting remains invalid")) return false;
|
||||
if (!WriteFixture(path, "", true, 375)) return false;
|
||||
if (!Check(reader.Probe(path, values) && values.Get("startTimeMinutes") == 375 && values.Get("timeMultiplier") == 1, "partially upgraded file defaults only missing new field")) return false;
|
||||
if (!WriteFixture(path, "", false, 0, true, 25)) return false;
|
||||
ERR_Settings recovered = new ERR_Settings(path);
|
||||
if (!Check(recovered.Get("startTimeMinutes") == 375 && recovered.Get("timeMultiplier") == 1 && recovered.Get("reportIntervalSeconds") == 20, "valid saved backup recovers invalid primary")) return false;
|
||||
return true;
|
||||
}
|
||||
static bool Run(ERR_GameMode mode)
|
||||
{
|
||||
int minutes;
|
||||
if (!Check(ERR_TimeService.ParseClock("00:00", minutes) && minutes == 0 && ERR_TimeService.ParseClock("23:59", minutes) && minutes == 1439 && ERR_TimeService.Clock(375) == "06:15", "clock parsing includes midnight and final minute")) return false;
|
||||
if (!Check(!ERR_TimeService.ParseClock("24:00", minutes) && !ERR_TimeService.ParseClock("12:60", minutes) && !ERR_TimeService.ParseClock("1:00", minutes) && !ERR_TimeService.ParseClock("ab:cd", minutes) && !ERR_TimeService.ParseClock("06:15junk", minutes), "malformed clock commands rejected")) return false;
|
||||
string path = "$profile:erri-native-time-probe.json";
|
||||
if (!Check(!FileIO.FileExists(path) && !FileIO.FileExists(path + ".bak") && !FileIO.FileExists(path + ".tmp"), "isolated fixture paths unused")) return false;
|
||||
bool migrated = SettingsMigration(path);
|
||||
FileIO.DeleteFile(path); FileIO.DeleteFile(path + ".bak"); FileIO.DeleteFile(path + ".tmp");
|
||||
if (!migrated) return false;
|
||||
if (!Check(mode.Time() && mode.Time().Started(), "server starts world clock before prison phase")) return false;
|
||||
ChimeraWorld world = ChimeraWorld.CastFrom(mode.GetWorld());
|
||||
TimeAndWeatherManagerEntity manager = world.GetTimeAndWeatherManager();
|
||||
if (!Check(manager && manager.GetIsDayAutoAdvanced() && Math.AbsFloat(manager.GetDayDuration() - ERR_TimeService.DayDuration(mode.Time().Multiplier())) < 0.1, "engine progression matches applied multiplier")) return false;
|
||||
int originalTime = mode.Settings().Get("startTimeMinutes");
|
||||
int originalSpeed = mode.Settings().Get("timeMultiplier");
|
||||
array<string> replies = {};
|
||||
mode.Admin().Execute(mode, false, "mission time set 06:15", replies);
|
||||
if (!Check(mode.Settings().Get("startTimeMinutes") == originalTime && replies[0] == "Administrator access required.", "unauthorized clock mutation rejected")) return false;
|
||||
mode.Admin().Execute(mode, true, "mission time set 24:00", replies);
|
||||
if (!Check(mode.Settings().Get("startTimeMinutes") == originalTime && replies[0].Contains("Invalid"), "invalid clock command preserves setting")) return false;
|
||||
float currentHours = manager.GetTimeOfTheDay();
|
||||
float currentDuration = manager.GetDayDuration();
|
||||
int appliedStart = mode.Time().StartMinutes();
|
||||
int appliedSpeed = mode.Time().Multiplier();
|
||||
mode.Admin().Execute(mode, true, "mission time set 06:15", replies);
|
||||
bool savedTime = mode.Settings().Get("startTimeMinutes") == 375 && replies[0].Contains("next round");
|
||||
mode.Admin().Execute(mode, true, "mission speed set 4", replies);
|
||||
bool savedSpeed = mode.Settings().Get("timeMultiplier") == 4 && replies[0].Contains("next round");
|
||||
bool idempotent = mode.Time().Start(mode) && Math.AbsFloat(manager.GetTimeOfTheDay() - currentHours) < 0.01 && manager.GetDayDuration() == currentDuration && mode.Time().StartMinutes() == appliedStart && mode.Time().Multiplier() == appliedSpeed;
|
||||
mode.Admin().Execute(mode, true, "mission time show", replies);
|
||||
bool shown = replies.Count() == 2 && replies[0].Contains("Clock") && replies[1] == "Next round: 06:15, 4x progression.";
|
||||
bool restoredTime = mode.Settings().Set("startTimeMinutes", originalTime);
|
||||
bool restoredSpeed = mode.Settings().Set("timeMultiplier", originalSpeed);
|
||||
if (!Check(savedTime && savedSpeed && restoredTime && restoredSpeed, "authorized settings persist and test restores originals")) return false;
|
||||
if (!Check(idempotent, "saved changes and repeated start leave current clock and speed intact")) return false;
|
||||
if (!Check(shown, "admin sees current clock separately from next round settings")) return false;
|
||||
s_EngineTime = manager.GetEngineTime();
|
||||
s_ClockHours = manager.GetTimeOfTheDay();
|
||||
s_Multiplier = mode.Time().Multiplier();
|
||||
s_Started = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -63,7 +63,7 @@ class ERR_Objectives
|
||||
return string.Format("Transmission in progress: up to %1 seconds remaining. Keep a conscious survivor within 60 metres of the radio. Leaving resets progress.", displayed);
|
||||
}
|
||||
if (phase == ERR_Phase.EXTRACTION)
|
||||
return string.Format("Reach the helicopter marked on your map. Board and fly beyond %1 kilometres from the prison. Everyone aboard escapes; survivors left behind do not.", m_Mode.EscapeRadius() / 1000.0);
|
||||
return string.Format("Reach the helicopter marked on your map. Board and fly beyond %1 kilometres from the prison. Living prisoners aboard escape, including unconscious passengers. Dead or absent prisoners do not.", m_Mode.EscapeRadius() / 1000.0);
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
enum ERR_PersonalEscapeResult { UNKNOWN, ESCAPED, MISSED }
|
||||
|
||||
// Only the server resolves account identities. Clients receive their controller's
|
||||
// result, never a persistent account ID or a reused end-data connection ID.
|
||||
modded class SCR_PlayerController
|
||||
{
|
||||
[RplProp(onRplName: "ERR_OnEscapeResultReplicated")]
|
||||
protected ERR_PersonalEscapeResult m_ERREscapeResult;
|
||||
[RplProp(onRplName: "ERR_OnEscapeResultReplicated")]
|
||||
protected RplId m_ERREscapeResultMission = RplId.Invalid();
|
||||
protected static ref ScriptInvoker s_ERREscapeResultChanged = new ScriptInvoker();
|
||||
|
||||
static ScriptInvoker ERR_OnEscapeResultChanged() { return s_ERREscapeResultChanged; }
|
||||
protected RplId ERR_CurrentResultMission()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode) return RplId.Invalid();
|
||||
RplComponent component = RplComponent.Cast(mode.FindComponent(RplComponent));
|
||||
if (!component) return RplId.Invalid();
|
||||
return component.Id();
|
||||
}
|
||||
ERR_PersonalEscapeResult ERR_EscapeResult()
|
||||
{
|
||||
RplId mission = ERR_CurrentResultMission();
|
||||
if (mission == RplId.Invalid() || mission != m_ERREscapeResultMission) return ERR_PersonalEscapeResult.UNKNOWN;
|
||||
return m_ERREscapeResult;
|
||||
}
|
||||
void ERR_ResetEscapeResult()
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
m_ERREscapeResult = ERR_PersonalEscapeResult.UNKNOWN;
|
||||
m_ERREscapeResultMission = ERR_CurrentResultMission();
|
||||
Replication.BumpMe();
|
||||
ERR_OnEscapeResultReplicated();
|
||||
}
|
||||
void ERR_PublishEscapeResult(ERR_PlayerRoster roster, ERR_ExtractionService extraction)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
ERR_PersonalEscapeResult result = ERR_PersonalEscapeResult.UNKNOWN;
|
||||
ERR_PlayerLife life;
|
||||
if (roster) life = roster.Get(GetPlayerId());
|
||||
if (life && life.Connected && life.ConnectionId == GetPlayerId() && life.UsedLife && extraction && extraction.State() == ERR_ExtractionState.ESCAPED)
|
||||
{
|
||||
result = ERR_PersonalEscapeResult.MISSED;
|
||||
if (extraction.DidEscape(life.AccountId)) result = ERR_PersonalEscapeResult.ESCAPED;
|
||||
}
|
||||
m_ERREscapeResult = result;
|
||||
m_ERREscapeResultMission = ERR_CurrentResultMission();
|
||||
Replication.BumpMe();
|
||||
ERR_OnEscapeResultReplicated();
|
||||
}
|
||||
protected void ERR_OnEscapeResultReplicated()
|
||||
{
|
||||
if (GetGame() && this == GetGame().GetPlayerController()) s_ERREscapeResultChanged.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ modded class SCR_PlayerController
|
||||
else if (mode.Phase() == ERR_Phase.RADIO_DEFENCE)
|
||||
message = "Keep a conscious survivor within 60 metres of the radio until the transmission finishes. Leaving the area resets the defence timer.";
|
||||
else if (mode.Phase() == ERR_Phase.EXTRACTION)
|
||||
message = "The extraction helicopter is marked on your map. Board it and fly at least 9 kilometres from the prison. Everyone aboard escapes; players left behind do not.";
|
||||
message = "The extraction helicopter is marked on your map. Board it and fly at least 9 kilometres from the prison. Living prisoners aboard escape, including unconscious passengers. Dead or absent prisoners do not.";
|
||||
if (!message.IsEmpty()) hints.ShowCustom(message, "Escape Everon", 15);
|
||||
m_ERRGuidePhase = mode.Phase();
|
||||
m_ERRGuideError = mode.PreparationError();
|
||||
|
||||
@@ -71,6 +71,12 @@ class ERR_PlayerRoster
|
||||
m_Connections.Remove(playerId);
|
||||
m_OnChanged.Invoke(playerId);
|
||||
}
|
||||
void Participants(out array<ref ERR_PlayerLife> lives)
|
||||
{
|
||||
lives.Clear();
|
||||
foreach (UUID identity, ERR_PlayerLife life : m_Lives)
|
||||
if (life.UsedLife) lives.Insert(life);
|
||||
}
|
||||
bool HasSurvivor()
|
||||
{
|
||||
// Native reconnect reservations retain the original actor until expiry.
|
||||
|
||||
@@ -4,10 +4,11 @@ class ERR_Settings
|
||||
static const string PATH = "$profile:rereintroduced.json";
|
||||
protected ref map<string, int> m_Values = new map<string, int>();
|
||||
protected bool m_ReadOnly;
|
||||
protected string m_Path;
|
||||
|
||||
static array<string> Keys()
|
||||
{
|
||||
return {"reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds"};
|
||||
return {"reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier"};
|
||||
}
|
||||
|
||||
static bool Valid(string key, int value)
|
||||
@@ -17,21 +18,33 @@ class ERR_Settings
|
||||
if (key == "responseRadiusMetres") return value >= 500 && value <= 5000;
|
||||
if (key == "aiIntelChancePercent" || key == "intelRevealChancePercent") return value >= 0 && value <= 100;
|
||||
if (key == "radioDefenceSeconds") return value >= 30 && value <= 1800;
|
||||
if (key == "startTimeMinutes") return value >= 0 && value <= 1439;
|
||||
if (key == "timeMultiplier") return value >= 1 && value <= 24;
|
||||
return false;
|
||||
}
|
||||
|
||||
void ERR_Settings()
|
||||
static int AddedDefault(string key)
|
||||
{
|
||||
if (key == "startTimeMinutes") return 840;
|
||||
if (key == "timeMultiplier") return 1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
void ERR_Settings(string path = PATH)
|
||||
{
|
||||
m_Path = path;
|
||||
m_Values.Set("reportIntervalSeconds", 10);
|
||||
m_Values.Set("supportSquadLimit", 4);
|
||||
m_Values.Set("responseRadiusMetres", 2000);
|
||||
m_Values.Set("aiIntelChancePercent", 60);
|
||||
m_Values.Set("intelRevealChancePercent", 15);
|
||||
m_Values.Set("radioDefenceSeconds", 180);
|
||||
m_Values.Set("startTimeMinutes", AddedDefault("startTimeMinutes"));
|
||||
m_Values.Set("timeMultiplier", AddedDefault("timeMultiplier"));
|
||||
if (!Replication.IsServer()) { m_ReadOnly = true; return; }
|
||||
map<string, int> loaded;
|
||||
if (Read(PATH, loaded) || Read(PATH + ".bak", loaded)) m_Values = loaded;
|
||||
else if (FileIO.FileExists(PATH) || FileIO.FileExists(PATH + ".bak"))
|
||||
if (Read(m_Path, loaded) || Read(m_Path + ".bak", loaded)) m_Values = loaded;
|
||||
else if (FileIO.FileExists(m_Path) || FileIO.FileExists(m_Path + ".bak"))
|
||||
{
|
||||
m_ReadOnly = true;
|
||||
Print("Rereintroduced: invalid settings; defaults active, existing files preserved", LogLevel.ERROR);
|
||||
@@ -47,6 +60,12 @@ class ERR_Settings
|
||||
values = new map<string, int>();
|
||||
foreach (string key : Keys())
|
||||
{
|
||||
// Only newly introduced keys may be absent in an older valid file.
|
||||
if (!json.DoesKeyExist(key) && AddedDefault(key) >= 0)
|
||||
{
|
||||
values.Set(key, AddedDefault(key));
|
||||
continue;
|
||||
}
|
||||
float number;
|
||||
if (!json.ReadValue(key, number)) return false;
|
||||
int value = Math.Round(number);
|
||||
@@ -68,7 +87,7 @@ class ERR_Settings
|
||||
json.WriteValue(option, saved);
|
||||
}
|
||||
map<string, int> checked;
|
||||
if (!json.SaveToFile(PATH + ".tmp") || !Read(PATH + ".tmp", checked)) return false;
|
||||
if (!json.SaveToFile(m_Path + ".tmp") || !Read(m_Path + ".tmp", checked)) return false;
|
||||
foreach (string option : Keys())
|
||||
{
|
||||
int expected = Get(option);
|
||||
@@ -76,10 +95,10 @@ class ERR_Settings
|
||||
if (checked.Get(option) != expected) return false;
|
||||
}
|
||||
map<string, int> previous;
|
||||
if (Read(PATH, previous) && !FileIO.CopyFile(PATH, PATH + ".bak")) return false;
|
||||
if (!FileIO.CopyFile(PATH + ".tmp", PATH) || !Read(PATH, checked))
|
||||
if (Read(m_Path, previous) && !FileIO.CopyFile(m_Path, m_Path + ".bak")) return false;
|
||||
if (!FileIO.CopyFile(m_Path + ".tmp", m_Path) || !Read(m_Path, checked))
|
||||
{
|
||||
if (FileIO.FileExists(PATH + ".bak")) FileIO.CopyFile(PATH + ".bak", PATH);
|
||||
if (FileIO.FileExists(m_Path + ".bak")) FileIO.CopyFile(m_Path + ".bak", m_Path);
|
||||
return false;
|
||||
}
|
||||
foreach (string option : Keys())
|
||||
@@ -88,7 +107,7 @@ class ERR_Settings
|
||||
if (option == key) expected = value;
|
||||
if (checked.Get(option) != expected)
|
||||
{
|
||||
if (FileIO.FileExists(PATH + ".bak")) FileIO.CopyFile(PATH + ".bak", PATH);
|
||||
if (FileIO.FileExists(m_Path + ".bak")) FileIO.CopyFile(m_Path + ".bak", m_Path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ class ERR_SiteService
|
||||
{
|
||||
Vehicle helicopter = Vehicle.Cast(Spawn(m_Mode.Factions().ExtractionPrefab, position + "12 0 0"));
|
||||
if (!helicopter) return Fail("extraction vehicle");
|
||||
m_Mode.SetExtractionVehicle(helicopter);
|
||||
if (!m_Mode.SetExtractionVehicle(helicopter)) return Fail("extraction vehicle registration");
|
||||
}
|
||||
if (!spec.Container.IsEmpty())
|
||||
{
|
||||
|
||||
@@ -20,11 +20,14 @@ class ERR_SpawnLogic : SCR_SpawnLogic
|
||||
}
|
||||
#endif
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
SCR_PlayerController resultController = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(playerId));
|
||||
if (resultController) resultController.ERR_ResetEscapeResult();
|
||||
if (!mode || !mode.Players().Bind(playerId, identity))
|
||||
{
|
||||
Print("Rereintroduced: player identity could not be bound; spawn withheld", LogLevel.ERROR);
|
||||
return;
|
||||
}
|
||||
if (resultController) resultController.ERR_PublishEscapeResult(mode.Players(), mode.Extraction());
|
||||
m_Identities.Set(playerId, identity);
|
||||
if (ResolveReconnection(playerId))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// The authoritative engine manager owns progression and replication. This
|
||||
// service sets it once per mission instance; saved changes apply next round.
|
||||
class ERR_TimeService
|
||||
{
|
||||
protected TimeAndWeatherManagerEntity m_Manager;
|
||||
protected bool m_Started;
|
||||
protected int m_StartMinutes;
|
||||
protected int m_Multiplier;
|
||||
|
||||
static float DayDuration(int multiplier)
|
||||
{
|
||||
if (!ERR_Settings.Valid("timeMultiplier", multiplier)) return -1;
|
||||
return 86400.0 / multiplier;
|
||||
}
|
||||
static string Clock(int minutes)
|
||||
{
|
||||
if (!ERR_Settings.Valid("startTimeMinutes", minutes)) return "unavailable";
|
||||
int hours = minutes / 60;
|
||||
int remainder = minutes % 60;
|
||||
string hourText = hours.ToString();
|
||||
string minuteText = remainder.ToString();
|
||||
if (hours < 10) hourText = "0" + hourText;
|
||||
if (remainder < 10) minuteText = "0" + minuteText;
|
||||
return hourText + ":" + minuteText;
|
||||
}
|
||||
static bool ParseClock(string text, out int minutes)
|
||||
{
|
||||
if (text.Length() != 5 || text.Substring(2, 1) != ":") return false;
|
||||
int hours;
|
||||
int remainder;
|
||||
if (!ERR_CommandArguments.Integer(text.Substring(0, 2), hours) || !ERR_CommandArguments.Integer(text.Substring(3, 2), remainder)) return false;
|
||||
if (hours > 23 || remainder > 59) return false;
|
||||
minutes = hours * 60 + remainder;
|
||||
return true;
|
||||
}
|
||||
bool Start(ERR_GameMode mode)
|
||||
{
|
||||
if (!Replication.IsServer() || !mode || !mode.Settings()) return false;
|
||||
if (m_Started) return true;
|
||||
ChimeraWorld world = ChimeraWorld.CastFrom(mode.GetWorld());
|
||||
if (!world) return false;
|
||||
m_Manager = world.GetTimeAndWeatherManager();
|
||||
if (!m_Manager) return false;
|
||||
int startMinutes = mode.Settings().Get("startTimeMinutes");
|
||||
int multiplier = mode.Settings().Get("timeMultiplier");
|
||||
if (!ERR_Settings.Valid("startTimeMinutes", startMinutes) || !ERR_Settings.Valid("timeMultiplier", multiplier)) return false;
|
||||
float previousDuration = m_Manager.GetDayDuration();
|
||||
bool previousAdvancement = m_Manager.GetIsDayAutoAdvanced();
|
||||
if (!m_Manager.SetDayDuration(DayDuration(multiplier))) return false;
|
||||
if (!m_Manager.SetIsDayAutoAdvanced(true))
|
||||
{
|
||||
m_Manager.SetDayDuration(previousDuration);
|
||||
return false;
|
||||
}
|
||||
if (!m_Manager.SetTimeOfTheDay(startMinutes / 60.0, true))
|
||||
{
|
||||
m_Manager.SetDayDuration(previousDuration);
|
||||
m_Manager.SetIsDayAutoAdvanced(previousAdvancement);
|
||||
return false;
|
||||
}
|
||||
m_StartMinutes = startMinutes;
|
||||
m_Multiplier = multiplier;
|
||||
m_Started = true;
|
||||
PrintFormat("Rereintroduced [time]: started at %1 with %2x progression", Clock(m_StartMinutes), m_Multiplier);
|
||||
return true;
|
||||
}
|
||||
bool Started() { return m_Started; }
|
||||
int StartMinutes() { return m_StartMinutes; }
|
||||
int Multiplier() { return m_Multiplier; }
|
||||
int CurrentMinutes()
|
||||
{
|
||||
if (!m_Started || !m_Manager) return -1;
|
||||
int minutes = Math.Floor(m_Manager.GetTimeOfTheDay() * 60);
|
||||
return minutes % 1440;
|
||||
}
|
||||
string Status()
|
||||
{
|
||||
if (!m_Started) return "Clock not started.";
|
||||
return "Clock " + Clock(CurrentMinutes()) + ", " + m_Multiplier.ToString() + "x progression (round started " + Clock(m_StartMinutes) + ").";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user