Prepare independent successor playtest with spawn recovery and casualty feedback
This commit is contained in:
@@ -31,7 +31,8 @@ ERR_ScenarioDefinition {
|
||||
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 8 }
|
||||
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 16 }
|
||||
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 12 }
|
||||
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
|
||||
ERR_SiteStock { Prefab "{13772C903CB5E4F7}Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et" Count 1 }
|
||||
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
|
||||
}
|
||||
RevealOnEscape 1
|
||||
}
|
||||
@@ -82,6 +83,7 @@ ERR_ScenarioDefinition {
|
||||
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 4 }
|
||||
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 8 }
|
||||
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 6 }
|
||||
ERR_SiteStock { Prefab "{13772C903CB5E4F7}Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et" Count 1 }
|
||||
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Presentation only. The service still owns roster, settling and seat authority.
|
||||
class ERR_CasualtyFeedback
|
||||
{
|
||||
static bool Visible(ERR_GameMode mode, IEntity caller, IEntity casualty)
|
||||
{
|
||||
if (!mode || !mode.Active() || mode.Phase() != ERR_Phase.EXTRACTION || !caller || caller == casualty || !ERR_CasualtyLoadingRules.Down(casualty)) return false;
|
||||
Vehicle aircraft = mode.CasualtyLoadingAircraft();
|
||||
Faction faction = mode.PlayerFaction();
|
||||
return aircraft && faction && SCR_FactionManager.SGetFaction(caller) == faction && SCR_FactionManager.SGetFaction(casualty) == faction && ERR_CasualtyLoadingRules.Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin()));
|
||||
}
|
||||
static string PhysicalReason(ERR_GameMode mode, IEntity caller, IEntity casualty)
|
||||
{
|
||||
if (!mode || !mode.Active() || mode.Phase() != ERR_Phase.EXTRACTION) return "Wait until helicopter extraction is available";
|
||||
if (!caller || caller == casualty || !ERR_Actors.CanFight(caller)) return "A living, conscious rescuer is required";
|
||||
if (CompartmentAccessComponent.GetVehicleIn(caller)) return "Leave the helicopter before loading a casualty";
|
||||
if (!ERR_CasualtyLoadingRules.GroundSettled(casualty)) return "Place the living, unconscious casualty on stable ground";
|
||||
Vehicle aircraft = mode.CasualtyLoadingAircraft();
|
||||
if (!aircraft || !ERR_CasualtyLoadingRules.Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin()))) return "Stay beside the casualty and within twelve metres of the extraction helicopter";
|
||||
if (!ERR_CasualtyLoadingRules.AircraftReady(aircraft)) return "The helicopter must be upright, grounded and stationary";
|
||||
if (!ERR_CasualtyLoadingRules.Nearby(mode, caller, casualty)) return "Keep clear access to a fellow prisoner's body";
|
||||
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(aircraft.FindComponent(BaseCompartmentManagerComponent));
|
||||
if (!manager) return "No accessible transport seat is available";
|
||||
array<BaseCompartmentSlot> seats = {}; manager.GetCompartments(seats);
|
||||
foreach (BaseCompartmentSlot seat : seats)
|
||||
if (ERR_CasualtyLoadingRules.TransportBench(seat, aircraft) && !seat.GetOccupant() && !seat.IsReserved() && seat.IsCompartmentAccessible() && !seat.IsGetInLockedFor(casualty)) return string.Empty;
|
||||
return "No accessible transport seat is available";
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_PlayerController
|
||||
{
|
||||
// Only the server addresses the still-current initiating rescuer's controller.
|
||||
void ERR_CasualtyFeedback(ERR_GameMode mode, IEntity actor, string message)
|
||||
{
|
||||
if (!Replication.IsServer() || !mode || mode != ERR_GameMode.Get() || !mode.Active() || !actor || actor != GetControlledEntity() || !mode.Players() || !mode.Players().CurrentActor(actor)) return;
|
||||
if (GetGame().GetPlayerManager().GetPlayerController(GetPlayerId()) != this) return;
|
||||
RplId modeId = ERR_CasualtyLoadingRules.EntityId(mode);
|
||||
RplId actorId = ERR_CasualtyLoadingRules.EntityId(actor);
|
||||
if (modeId == RplId.Invalid() || actorId == RplId.Invalid() || message.IsEmpty()) return;
|
||||
if (this == GetGame().GetPlayerController()) ERR_CasualtyFeedbackOwner(modeId, actorId, message);
|
||||
else Rpc(ERR_CasualtyFeedbackOwner, modeId, actorId, message);
|
||||
}
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
|
||||
protected void ERR_CasualtyFeedbackOwner(RplId modeId, RplId actorId, string message)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!GetGame() || this != GetGame().GetPlayerController() || !mode || !mode.Active() || mode.Phase() != ERR_Phase.EXTRACTION || ERR_CasualtyLoadingRules.EntityId(mode) != modeId || ERR_CasualtyLoadingRules.EntityId(GetControlledEntity()) != actorId || message.IsEmpty()) return;
|
||||
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
|
||||
if (!hints || !hints.ShowCustom(message, "Casualty loading", 10)) SCR_ChatComponent.RadioProtocolMessage(message);
|
||||
}
|
||||
}
|
||||
@@ -131,6 +131,7 @@ class ERR_CasualtyLoadTicket
|
||||
int CommittedAt;
|
||||
bool Committed;
|
||||
bool OwnerReported;
|
||||
bool FeedbackSubmitted; // presentation only; never authorizes entry
|
||||
}
|
||||
class ERR_CasualtyLoadingService
|
||||
{
|
||||
@@ -204,6 +205,34 @@ class ERR_CasualtyLoadingService
|
||||
if (!CurrentLife(caller) || !CurrentLife(casualty) || !Settled(casualty, System.GetTickCount()) || PendingActor(caller, casualty)) return false;
|
||||
return AvailableSeat(casualty, m_Mode.ExtractionVehicle()) != null;
|
||||
}
|
||||
// Reasons never replace CanLoad/Begin authority or reserve a seat.
|
||||
string RejectionReason(IEntity caller, IEntity casualty)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped) return "Casualty loading is unavailable";
|
||||
string physical = ERR_CasualtyFeedback.PhysicalReason(m_Mode, caller, casualty);
|
||||
if (!physical.IsEmpty()) return physical;
|
||||
if (!CurrentLife(caller) || !CurrentLife(casualty)) return "Both prisoners must still be connected to this mission";
|
||||
if (!Settled(casualty, System.GetTickCount())) return "Let the casualty rest on the ground for five seconds";
|
||||
if (PendingActor(caller, casualty)) return "A casualty loading request is already in progress";
|
||||
if (!AvailableSeat(casualty, m_Mode.ExtractionVehicle())) return "No accessible transport seat is available";
|
||||
return string.Empty;
|
||||
}
|
||||
protected void NotifyCaller(IEntity caller, string message)
|
||||
{
|
||||
if (!Replication.IsServer() || !m_Mode || !m_Mode.Active() || m_Mode.Phase() != ERR_Phase.EXTRACTION || message.IsEmpty()) return;
|
||||
ERR_PlayerLife life = CurrentLife(caller);
|
||||
if (!life) return;
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(life.ConnectionId));
|
||||
if (!controller || controller.GetControlledEntity() != caller) return;
|
||||
controller.ERR_CasualtyFeedback(m_Mode, caller, message);
|
||||
}
|
||||
void Request(IEntity caller, IEntity casualty)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
string reason = RejectionReason(caller, casualty);
|
||||
if (!reason.IsEmpty()) { NotifyCaller(caller, reason); return; }
|
||||
if (!Begin(caller, casualty)) NotifyCaller(caller, "Loading could not start. Check the casualty and available transport seats, then try again.");
|
||||
}
|
||||
bool Begin(IEntity caller, IEntity casualty)
|
||||
{
|
||||
if (!CanLoad(caller, casualty) || m_NextNonce >= 2147483646) return false;
|
||||
@@ -221,6 +250,7 @@ class ERR_CasualtyLoadingService
|
||||
m_Pending.Insert(ticket);
|
||||
ticket.Seat.SetReserved(casualty);
|
||||
if (!ticket.Seat.IsReservedBy(casualty)) { Resolve(ticket, false); return false; }
|
||||
ticket.FeedbackSubmitted = true;
|
||||
ScheduleMaintenance();
|
||||
// Preparation never moves the actor. Its owner starts a local real-time lease.
|
||||
controller.ERR_PrepareCasualtyLoad(ERR_CasualtyLoadingRules.EntityId(m_Mode), ticket.Nonce);
|
||||
@@ -264,12 +294,18 @@ class ERR_CasualtyLoadingService
|
||||
{
|
||||
return m_Mode && m_Mode.Active() && m_Mode.Phase() == ERR_Phase.EXTRACTION && ticket.Aircraft == m_Mode.ExtractionVehicle() && ERR_Actors.Alive(ticket.Aircraft) && ERR_Actors.Alive(ticket.Casualty) && SameLife(ticket.Caller, ticket.CallerAccount, ticket.CallerConnection) && SameLife(ticket.Casualty, ticket.CasualtyAccount, ticket.CasualtyConnection) && ERR_CasualtyLoadingRules.TransportBench(ticket.Seat, ticket.Aircraft);
|
||||
}
|
||||
protected void NotifyResolution(ERR_CasualtyLoadTicket ticket, bool success)
|
||||
{
|
||||
if (!success && ticket.FeedbackSubmitted && !m_Stopped && SameLife(ticket.Caller, ticket.CallerAccount, ticket.CallerConnection))
|
||||
NotifyCaller(ticket.Caller, "Casualty boarding could not be confirmed. Check the helicopter before retrying; keep it stationary with an accessible transport seat.");
|
||||
}
|
||||
protected void Resolve(ERR_CasualtyLoadTicket ticket, bool success)
|
||||
{
|
||||
int index = m_Pending.Find(ticket); if (index < 0) return;
|
||||
m_Pending.Remove(index);
|
||||
if (ticket.Seat && ticket.Casualty && ticket.Seat.IsReservedBy(ticket.Casualty)) ticket.Seat.SetReserved(null);
|
||||
if (m_Pending.IsEmpty()) CancelMaintenance();
|
||||
NotifyResolution(ticket, success);
|
||||
m_OnResolved.Invoke(ticket.Casualty, success);
|
||||
}
|
||||
void Tick(float worldNow)
|
||||
|
||||
@@ -35,6 +35,7 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
[RplProp()]
|
||||
protected string m_RecoveryStatus;
|
||||
protected bool m_StartedPlayers;
|
||||
protected ref map<int, PlayerController> m_ExhaustedInitialAdmissions = new map<int, PlayerController>();
|
||||
protected bool m_Stopped;
|
||||
protected bool m_NativeEndHandled;
|
||||
protected float m_NoSurvivorsSince = -1;
|
||||
@@ -263,6 +264,36 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
m_NoSurvivorsSince = -1;
|
||||
}
|
||||
|
||||
// Exhaustion is admission state, never a spawned participant or spent life.
|
||||
void InitialAdmissionStarted(int playerId)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
m_ExhaustedInitialAdmissions.Remove(playerId);
|
||||
if (!m_StartedPlayers) m_NoSurvivorsSince = -1;
|
||||
}
|
||||
void InitialAdmissionExhausted(int playerId, PlayerController controller)
|
||||
{
|
||||
if (!Replication.IsServer() || !Active() || m_StartedPlayers || !controller) return;
|
||||
ERR_PlayerLife life = m_Roster.Get(playerId);
|
||||
if (!life || !life.Connected || life.UsedLife || life.Actor) return;
|
||||
if (GetGame().GetPlayerManager().GetPlayerController(playerId) != controller || controller.GetControlledEntity()) return;
|
||||
m_ExhaustedInitialAdmissions.Set(playerId, controller);
|
||||
}
|
||||
protected bool InitialAdmissionsExhausted()
|
||||
{
|
||||
array<int> connected = {};
|
||||
GetGame().GetPlayerManager().GetPlayers(connected);
|
||||
if (connected.IsEmpty()) return false;
|
||||
foreach (int id : connected)
|
||||
{
|
||||
PlayerController controller = GetGame().GetPlayerManager().GetPlayerController(id);
|
||||
if (!controller || !m_ExhaustedInitialAdmissions.Contains(id) || m_ExhaustedInitialAdmissions.Get(id) != controller || controller.GetControlledEntity()) return false;
|
||||
ERR_PlayerLife life = m_Roster.Get(id);
|
||||
if (!life || !life.Connected || life.UsedLife || life.Actor) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected string SettingsPath() { return ERR_Settings.PATH; }
|
||||
|
||||
protected override void EOnInit(IEntity owner)
|
||||
@@ -379,12 +410,13 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
m_NoSurvivorsSince = -1;
|
||||
return;
|
||||
}
|
||||
if (m_StartedPlayers && !m_Roster.HasSurvivor())
|
||||
if ((m_StartedPlayers || InitialAdmissionsExhausted()) && !m_Roster.HasSurvivor())
|
||||
{
|
||||
if (m_NoSurvivorsSince < 0) m_NoSurvivorsSince = now;
|
||||
if (now - m_NoSurvivorsSince >= 30000)
|
||||
{
|
||||
m_EndReason = "No surviving prisoners remain.";
|
||||
if (!m_StartedPlayers) m_EndReason = "No prisoners could be admitted to this round.";
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,21 +3,26 @@ class ERR_LoadCasualtyAction : ScriptedUserAction
|
||||
{
|
||||
override bool CanBeShownScript(IEntity user)
|
||||
{
|
||||
return ERR_CasualtyLoadingRules.Nearby(ERR_GameMode.Get(), user, GetOwner());
|
||||
return ERR_CasualtyFeedback.Visible(ERR_GameMode.Get(), user, GetOwner());
|
||||
}
|
||||
override bool CanBePerformedScript(IEntity user)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!ERR_CasualtyLoadingRules.Nearby(mode, user, GetOwner())) return false;
|
||||
// Clients do not own the authoritative roster. Server always checks again.
|
||||
if (!Replication.IsServer()) return true;
|
||||
return mode.CasualtyLoading() && mode.CasualtyLoading().CanLoad(user, GetOwner());
|
||||
SetCannotPerformReason(string.Empty);
|
||||
string reason = ERR_CasualtyFeedback.PhysicalReason(mode, user, GetOwner());
|
||||
if (reason.IsEmpty() && Replication.IsServer())
|
||||
{
|
||||
if (!mode.CasualtyLoading()) reason = "Casualty loading is unavailable";
|
||||
else reason = mode.CasualtyLoading().RejectionReason(user, GetOwner());
|
||||
}
|
||||
if (!reason.IsEmpty()) { SetCannotPerformReason(reason); return false; }
|
||||
return true;
|
||||
}
|
||||
override bool HasLocalEffectOnlyScript() { return false; }
|
||||
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
|
||||
{
|
||||
if (!Replication.IsServer() || pOwnerEntity != GetOwner()) return;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && mode.CasualtyLoading()) mode.CasualtyLoading().Begin(pUserEntity, pOwnerEntity);
|
||||
if (mode && mode.CasualtyLoading()) mode.CasualtyLoading().Request(pUserEntity, pOwnerEntity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@ class ERR_NativeDefinitionProbe
|
||||
cache.Id = "everon-prison-1";
|
||||
if (!Check(!definition.Valid(), "duplicate site identity rejected")) return false;
|
||||
cache.Id = "everon-supply-1";
|
||||
ERR_SiteStock documents = cache.Stock[3];
|
||||
ERR_SiteStock documents;
|
||||
foreach (ERR_SiteStock stock : cache.Stock)
|
||||
if (stock.Prefab == ERR_Resources.ObjectiveDocument()) { documents = stock; break; }
|
||||
if (!Check(documents != null, "clue stock located independently of supply order")) return false;
|
||||
documents.TargetId = "missing-radio";
|
||||
if (!Check(!definition.Valid(), "dangling document target rejected")) return false;
|
||||
documents.TargetId = "everon-prison-1";
|
||||
|
||||
@@ -94,6 +94,7 @@ class ERR_SpawnLogic : SCR_SpawnLogic
|
||||
SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(playerId)).ERR_SetObserver(ERR_ObserverReason.NONE);
|
||||
return;
|
||||
}
|
||||
mode.InitialAdmissionStarted(playerId);
|
||||
m_Attempts.Set(playerId, 0);
|
||||
DoSpawn_S(playerId);
|
||||
}
|
||||
@@ -111,7 +112,12 @@ class ERR_SpawnLogic : SCR_SpawnLogic
|
||||
// for the same server preparation result before consuming its retry budget.
|
||||
if (!mode.Active()) { RetryLater(playerId); return; }
|
||||
int attempts = m_Attempts.Get(playerId);
|
||||
if (attempts >= 30) { Observe(playerId, ERR_ObserverReason.SPAWN_UNAVAILABLE); return; }
|
||||
if (attempts >= 30)
|
||||
{
|
||||
Observe(playerId, ERR_ObserverReason.SPAWN_UNAVAILABLE);
|
||||
mode.InitialAdmissionExhausted(playerId, controller);
|
||||
return;
|
||||
}
|
||||
if (!mode.Players().CanEnter(playerId, mode.Phase()))
|
||||
{
|
||||
ERR_ObserverReason reason = ERR_ObserverReason.SPENT_LIFE;
|
||||
@@ -333,7 +339,11 @@ class ERR_SpawnLogic : SCR_SpawnLogic
|
||||
CancelAdmission(playerId, ERR_ObserverReason.NONE);
|
||||
m_Identities.Remove(playerId);
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode) mode.Players().Disconnect(playerId);
|
||||
if (mode)
|
||||
{
|
||||
mode.InitialAdmissionStarted(playerId);
|
||||
mode.Players().Disconnect(playerId);
|
||||
}
|
||||
if (GetPlayerRespawnComponent_S(playerId)) super.OnPlayerDisconnected_S(playerId, cause, timeout);
|
||||
}
|
||||
bool Authorizes(int playerId, SCR_SpawnData data)
|
||||
|
||||
Reference in New Issue
Block a user