Build independent Everon successor foundation and native regressions
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
class ERR_UnlockPrisonAction : ScriptedUserAction
|
||||
{
|
||||
override bool CanBePerformedScript(IEntity user)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
return mode && mode.Phase() == ERR_Phase.PRISON && ERR_Actors.CanFight(user);
|
||||
}
|
||||
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || !mode.UnlockPrison(pUserEntity)) return;
|
||||
BaseDoorComponent door = BaseDoorComponent.Cast(pOwnerEntity.FindComponent(BaseDoorComponent));
|
||||
if (door) door.UseDoorAction(pUserEntity);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_PrisonDoorAction : SCR_DoorUserAction
|
||||
{
|
||||
override bool CanBePerformedScript(IEntity user)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
return mode && mode.Phase() > ERR_Phase.PRISON && super.CanBePerformedScript(user);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_TuneRadioAction : ScriptedUserAction
|
||||
{
|
||||
override bool CanBePerformedScript(IEntity user)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
ERR_LocationComponent location = ERR_LocationComponent.Cast(GetOwner().FindComponent(ERR_LocationComponent));
|
||||
return mode && mode.Phase() == ERR_Phase.RADIO_SEARCH && location && location.Knowledge() == 2 && ERR_Actors.CanFight(user);
|
||||
}
|
||||
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode) mode.TuneRadio(pUserEntity, ERR_LocationComponent.Cast(pOwnerEntity.FindComponent(ERR_LocationComponent)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
class ERR_Actors
|
||||
{
|
||||
static bool Alive(IEntity actor)
|
||||
{
|
||||
if (!actor) return false;
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent));
|
||||
return damage && !damage.IsDestroyed();
|
||||
}
|
||||
|
||||
static bool CanFight(IEntity actor)
|
||||
{
|
||||
if (!Alive(actor)) return false;
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(actor.FindComponent(CharacterControllerComponent));
|
||||
return controller && !controller.IsUnconscious();
|
||||
}
|
||||
|
||||
static SCR_AIGroup Group(IEntity actor)
|
||||
{
|
||||
if (!actor) return null;
|
||||
AIControlComponent control = AIControlComponent.Cast(actor.FindComponent(AIControlComponent));
|
||||
if (!control || !control.GetAIAgent()) return null;
|
||||
return SCR_AIGroup.Cast(control.GetAIAgent().GetParentGroup());
|
||||
}
|
||||
|
||||
static bool Player(IEntity actor)
|
||||
{
|
||||
return actor && GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(actor) > 0;
|
||||
}
|
||||
|
||||
static IEntity Spawn(ResourceName prefab, vector position, vector angles = "0 0 0")
|
||||
{
|
||||
if (!Replication.IsServer() || prefab.IsEmpty()) return null;
|
||||
Resource resource = Resource.Load(prefab);
|
||||
if (!resource || !resource.IsValid()) return null;
|
||||
EntitySpawnParams params = new EntitySpawnParams();
|
||||
params.TransformMode = ETransformMode.WORLD;
|
||||
Math3D.AnglesToMatrix(angles, params.Transform);
|
||||
params.Transform[3] = position;
|
||||
return GetGame().SpawnEntityPrefab(resource, GetGame().GetWorld(), params);
|
||||
}
|
||||
|
||||
static void DeleteSpawned(IEntity entity)
|
||||
{
|
||||
if (!Replication.IsServer() || !entity || Player(entity)) return;
|
||||
SCR_AIGroup group = SCR_AIGroup.Cast(entity);
|
||||
if (group)
|
||||
{
|
||||
// Never remove a group that has acquired a player since mission creation.
|
||||
if (group.GetPlayerCount(true) > 0) return;
|
||||
group.DespawnMembers();
|
||||
}
|
||||
SCR_EntityHelper.DeleteEntityAndChildren(entity);
|
||||
}
|
||||
|
||||
static vector Ground(vector position)
|
||||
{
|
||||
position[1] = GetGame().GetWorld().GetSurfaceY(position[0], position[2]) + 0.1;
|
||||
return position;
|
||||
}
|
||||
|
||||
static void Players(out array<IEntity> players, bool conscious = false)
|
||||
{
|
||||
players.Clear();
|
||||
array<int> ids = {};
|
||||
GetGame().GetPlayerManager().GetPlayers(ids);
|
||||
foreach (int id : ids)
|
||||
{
|
||||
IEntity actor = GetGame().GetPlayerManager().GetPlayerControlledEntity(id);
|
||||
if (Alive(actor) && (!conscious || CanFight(actor))) players.Insert(actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One server-owned implementation is installed by the AI addon.
|
||||
class ERR_CommandService
|
||||
{
|
||||
void Tick(float now) {}
|
||||
void Stop() {}
|
||||
void RegisterGroup(SCR_AIGroup group) {}
|
||||
void UnregisterGroup(SCR_AIGroup group) {}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
[BaseContainerProps()]
|
||||
class ERR_EscapeGameOverScreenInfo : SCR_BaseGameOverScreenInfo
|
||||
{
|
||||
override string GetTitle(int playerId, array<int> otherPlayerIds)
|
||||
{
|
||||
string title = "The helicopter escaped";
|
||||
if (otherPlayerIds && otherPlayerIds.Contains(playerId)) title = "You escaped Everon";
|
||||
#ifdef WORKBENCH
|
||||
PrintFormat("ERR RESULT UI: player=%1 title=%2", playerId, title);
|
||||
#endif
|
||||
return title;
|
||||
}
|
||||
override string GetDebriefing(int playerId, array<int> otherPlayerIds)
|
||||
{
|
||||
if (otherPlayerIds && otherPlayerIds.Contains(playerId))
|
||||
return "You crossed the extraction boundary aboard the helicopter.";
|
||||
return "You were not aboard when the helicopter crossed the extraction boundary.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Vanilla-only catalog. Optional factions will supply separate catalogs.
|
||||
class ERR_FactionCatalog
|
||||
{
|
||||
static ResourceName Prisoner() { return "{2F912ED6E399FF47}Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Unarmed.et"; }
|
||||
static ResourceName Patrol() { return "{D5A4D0D3AE1A3E26}Prefabs/ERR/Patrol.et"; }
|
||||
static ResourceName Helicopter() { return "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et"; }
|
||||
static ResourceName SearchWaypoint() { return "{B3E7B8DC2BAB8ACC}Prefabs/AI/Waypoints/AIWaypoint_SearchAndDestroy.et"; }
|
||||
static ResourceName DefendWaypoint() { return "{93291E72AC23930F}Prefabs/AI/Waypoints/AIWaypoint_Defend.et"; }
|
||||
static ResourceName IntelFolder() { return "{6D56FED1E55A8F84}Prefabs/Items/Misc/IntelligenceFolder_E_01/IntelligenceFolder_E_01.et"; }
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
class ERR_GameModeClass : SCR_BaseGameModeClass {}
|
||||
class ERR_GameMode : SCR_BaseGameMode
|
||||
{
|
||||
[Attribute("6090 0 7160", UIWidgets.Coords, "Prison position; validated by scenario author")]
|
||||
protected vector m_PrisonPosition;
|
||||
[Attribute("6090 0 7160", UIWidgets.Coords, "Centre of extraction boundary")]
|
||||
protected vector m_EscapeCenter;
|
||||
[Attribute("9000", UIWidgets.EditBox, "Helicopter must cross this radius to escape")]
|
||||
protected float m_EscapeRadius;
|
||||
[RplProp()]
|
||||
protected ERR_Phase m_Phase;
|
||||
[RplProp()]
|
||||
protected float m_DefenceSince = -1;
|
||||
protected ref ERR_Settings m_Settings;
|
||||
protected ref ERR_PlayerRoster m_Roster = new ERR_PlayerRoster();
|
||||
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 ERR_LocationComponent m_Radio;
|
||||
protected Vehicle m_ExtractionVehicle;
|
||||
protected bool m_StartedPlayers;
|
||||
protected bool m_Stopped;
|
||||
protected float m_NoSurvivorsSince = -1;
|
||||
protected ref ScriptInvoker m_OnPhaseChanged = new ScriptInvoker();
|
||||
protected ref ScriptInvoker m_OnTick = new ScriptInvoker();
|
||||
protected ref ScriptInvoker m_OnStop = new ScriptInvoker();
|
||||
protected ref ScriptInvoker m_OnGroup = new ScriptInvoker();
|
||||
|
||||
static ERR_GameMode Get()
|
||||
{
|
||||
if (!GetGame()) return null;
|
||||
return ERR_GameMode.Cast(GetGame().GetGameMode());
|
||||
}
|
||||
ERR_Phase Phase() { return m_Phase; }
|
||||
ERR_PlayerRoster Players() { return m_Roster; }
|
||||
ERR_Settings Settings() { return m_Settings; }
|
||||
ERR_CommandService Command() { return m_Command; }
|
||||
ScriptInvoker OnPhaseChanged() { return m_OnPhaseChanged; }
|
||||
ScriptInvoker OnTick() { return m_OnTick; }
|
||||
ScriptInvoker OnStop() { return m_OnStop; }
|
||||
ScriptInvoker OnGroup() { return m_OnGroup; }
|
||||
Faction EnemyFaction() { return GetGame().GetFactionManager().GetFactionByKey("USSR"); }
|
||||
vector PrisonPosition() { return ERR_Actors.Ground(m_PrisonPosition); }
|
||||
bool Active() { return m_Phase >= ERR_Phase.PRISON && !ERR_MissionRules.Terminal(m_Phase); }
|
||||
protected ERR_CommandService CreateCommandService() { return null; }
|
||||
|
||||
void RegisterLocation(ERR_LocationComponent location)
|
||||
{
|
||||
if (m_Locations.Contains(location)) return;
|
||||
foreach (ERR_LocationComponent existing : m_Locations)
|
||||
{
|
||||
if (existing.Id() == location.Id())
|
||||
{
|
||||
Print("Rereintroduced: duplicate location ID " + location.Id(), LogLevel.ERROR);
|
||||
return;
|
||||
}
|
||||
}
|
||||
m_Locations.Insert(location);
|
||||
}
|
||||
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; }
|
||||
void RegisterGroup(SCR_AIGroup group)
|
||||
{
|
||||
if (!Replication.IsServer() || !group) return;
|
||||
Own(group);
|
||||
if (m_Command) m_Command.RegisterGroup(group);
|
||||
m_OnGroup.Invoke(group);
|
||||
}
|
||||
|
||||
bool Transition(ERR_Phase phase)
|
||||
{
|
||||
if (!Replication.IsServer() || !ERR_MissionRules.CanTransition(m_Phase, phase)) return false;
|
||||
ERR_Phase previous = m_Phase;
|
||||
m_Phase = phase;
|
||||
Replication.BumpMe();
|
||||
PrintFormat("Rereintroduced [mission]: phase %1 -> %2", previous, phase);
|
||||
m_OnPhaseChanged.Invoke(previous, phase);
|
||||
if (ERR_MissionRules.Terminal(phase))
|
||||
{
|
||||
StopServices();
|
||||
array<int> winners = {};
|
||||
foreach (int playerId : m_Escaped) winners.Insert(playerId);
|
||||
EGameOverTypes reason = EGameOverTypes.LOOSE;
|
||||
if (phase == ERR_Phase.VICTORY) reason = EGameOverTypes.VICTORY;
|
||||
EndGameMode(SCR_GameModeEndData.Create(reason, winners));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UnlockPrison(IEntity player)
|
||||
{
|
||||
if (!Replication.IsServer() || !ERR_Actors.Player(player) || !ERR_Actors.CanFight(player)) return false;
|
||||
if (vector.DistanceXZ(player.GetOrigin(), PrisonPosition()) > 15) return false;
|
||||
return m_Phase == ERR_Phase.PRISON && Transition(ERR_Phase.RADIO_SEARCH);
|
||||
}
|
||||
|
||||
bool TuneRadio(IEntity player, ERR_LocationComponent location)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Phase != ERR_Phase.RADIO_SEARCH || !location || !m_Locations.Contains(location)) return false;
|
||||
if (location.Kind() != ERR_LocationKind.RADIO || location.Knowledge() < 2) return false;
|
||||
if (!ERR_Actors.Player(player) || !ERR_Actors.CanFight(player) || vector.DistanceXZ(player.GetOrigin(), location.Position()) > 5) return false;
|
||||
m_Radio = location;
|
||||
m_DefenceSince = -1;
|
||||
return Transition(ERR_Phase.RADIO_DEFENCE);
|
||||
}
|
||||
|
||||
void PlayerSpawned(int id, IEntity actor)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
if (SCR_Global.IsEditMode()) return;
|
||||
if (!System.IsConsoleApp()) GetGame().GetCallqueue().CallLater(BindLocalObserver, 250, true);
|
||||
if (!Replication.IsServer()) return;
|
||||
m_Settings = new ERR_Settings();
|
||||
m_Command = CreateCommandService();
|
||||
GetGame().GetCallqueue().CallLater(ServerTick, 1000, true);
|
||||
GetGame().GetCallqueue().CallLater(StartScenario, 1500, false);
|
||||
}
|
||||
|
||||
protected void BindLocalObserver()
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
if (!controller) return;
|
||||
controller.ERR_StartObserverMonitor();
|
||||
GetGame().GetCallqueue().Remove(BindLocalObserver);
|
||||
}
|
||||
|
||||
protected void StartScenario()
|
||||
{
|
||||
if (m_Phase != ERR_Phase.PREPARING) return;
|
||||
// Scenario content is created through a separate authoring component.
|
||||
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(FindComponent(ERR_ScenarioComponent));
|
||||
if (!scenario || !scenario.Prepare())
|
||||
{
|
||||
Print("Rereintroduced: scenario preparation failed; spawning remains disabled", LogLevel.ERROR);
|
||||
return;
|
||||
}
|
||||
Transition(ERR_Phase.PRISON);
|
||||
}
|
||||
|
||||
protected void ServerTick()
|
||||
{
|
||||
if (!Replication.IsServer() || !Active()) return;
|
||||
float now = GetWorld().GetWorldTime();
|
||||
array<IEntity> players = {};
|
||||
ERR_Actors.Players(players);
|
||||
if (GetGame().GetPlayerManager().GetPlayerCount() == 0)
|
||||
{
|
||||
m_NoSurvivorsSince = -1;
|
||||
m_DefenceSince = -1;
|
||||
return;
|
||||
}
|
||||
if (m_StartedPlayers && !m_Roster.HasSurvivor())
|
||||
{
|
||||
if (m_NoSurvivorsSince < 0) m_NoSurvivorsSince = now;
|
||||
if (now - m_NoSurvivorsSince >= 30000) { Transition(ERR_Phase.DEFEAT); return; }
|
||||
}
|
||||
else m_NoSurvivorsSince = -1;
|
||||
if (m_Phase == ERR_Phase.RADIO_DEFENCE)
|
||||
{
|
||||
bool occupied;
|
||||
foreach (IEntity actor : players)
|
||||
if (m_Radio && ERR_Actors.CanFight(actor) && vector.DistanceXZ(actor.GetOrigin(), m_Radio.Position()) < 60) occupied = true;
|
||||
if (!occupied) m_DefenceSince = -1;
|
||||
else if (m_DefenceSince < 0) m_DefenceSince = now;
|
||||
if (ERR_MissionRules.DefenceComplete(now, m_DefenceSince, m_Settings.Get("radioDefenceSeconds") * 1000, occupied))
|
||||
{
|
||||
foreach (ERR_LocationComponent location : m_Locations)
|
||||
if (location.Kind() == ERR_LocationKind.EXTRACTION) location.Discover(0, 100);
|
||||
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)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
|
||||
if (controller) controller.ERR_UpdateObserverServer();
|
||||
}
|
||||
if (m_Command) m_Command.Tick(now);
|
||||
m_OnTick.Invoke(now);
|
||||
}
|
||||
|
||||
protected void StopServices()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
GetGame().GetCallqueue().Remove(ServerTick);
|
||||
if (m_Command) m_Command.Stop();
|
||||
m_OnStop.Invoke();
|
||||
if (Replication.IsServer())
|
||||
{
|
||||
array<int> connected = {};
|
||||
GetGame().GetPlayerManager().GetPlayers(connected);
|
||||
foreach (int id : connected)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
|
||||
if (controller) controller.ERR_SetObserver(ERR_ObserverReason.NONE);
|
||||
}
|
||||
foreach (IEntity entity : m_OwnedEntities)
|
||||
if (SCR_AIGroup.Cast(entity)) ERR_Actors.DeleteSpawned(entity);
|
||||
}
|
||||
}
|
||||
|
||||
void ~ERR_GameMode()
|
||||
{
|
||||
if (GetGame())
|
||||
{
|
||||
GetGame().GetCallqueue().Remove(StartScenario);
|
||||
GetGame().GetCallqueue().Remove(BindLocalObserver);
|
||||
StopServices();
|
||||
}
|
||||
m_Locations.Clear();
|
||||
m_OwnedEntities.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
enum ERR_LocationKind { PRISON, RADIO, EXTRACTION, OUTPOST, DEPOT }
|
||||
|
||||
[ComponentEditorProps(category: "Rereintroduced", description: "Server-owned location with replicated discovery state")]
|
||||
class ERR_LocationComponentClass : ScriptComponentClass {}
|
||||
class ERR_LocationComponent : ScriptComponent
|
||||
{
|
||||
[Attribute("", UIWidgets.EditBox, "Stable scenario location ID")]
|
||||
protected string m_Id;
|
||||
[Attribute("0", UIWidgets.ComboBox, "Location kind", enums: ParamEnumArray.FromEnum(ERR_LocationKind))]
|
||||
protected ERR_LocationKind m_Kind;
|
||||
[RplProp()]
|
||||
protected int m_Knowledge;
|
||||
|
||||
string Id() { return m_Id; }
|
||||
ERR_LocationKind Kind() { return m_Kind; }
|
||||
int Knowledge() { return m_Knowledge; }
|
||||
vector Position() { return GetOwner().GetOrigin(); }
|
||||
|
||||
bool Discover(int roll, int chance)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Knowledge >= 2) return false;
|
||||
m_Knowledge = ERR_MissionRules.Discovery(m_Knowledge, roll, chance);
|
||||
Replication.BumpMe();
|
||||
return true;
|
||||
}
|
||||
|
||||
override void OnPostInit(IEntity owner)
|
||||
{
|
||||
super.OnPostInit(owner);
|
||||
if (!SCR_Global.IsEditMode()) SetEventMask(owner, EntityEvent.INIT);
|
||||
}
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode) mode.RegisterLocation(this);
|
||||
}
|
||||
protected override void OnDelete(IEntity owner)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode) mode.UnregisterLocation(this);
|
||||
super.OnDelete(owner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
enum ERR_Phase
|
||||
{
|
||||
PREPARING,
|
||||
PRISON,
|
||||
RADIO_SEARCH,
|
||||
RADIO_DEFENCE,
|
||||
EXTRACTION,
|
||||
VICTORY,
|
||||
DEFEAT
|
||||
}
|
||||
|
||||
// Pure mission policy. Entity queries and authority checks belong to the owner.
|
||||
class ERR_MissionRules
|
||||
{
|
||||
static bool Terminal(int phase)
|
||||
{
|
||||
return phase == ERR_Phase.VICTORY || phase == ERR_Phase.DEFEAT;
|
||||
}
|
||||
|
||||
static bool CanTransition(int from, int to)
|
||||
{
|
||||
if (Terminal(from) || from == to) return false;
|
||||
if (to == ERR_Phase.DEFEAT) return from >= ERR_Phase.PRISON && from <= ERR_Phase.EXTRACTION;
|
||||
if (from == ERR_Phase.PREPARING) return to == ERR_Phase.PRISON;
|
||||
if (from == ERR_Phase.PRISON) return to == ERR_Phase.RADIO_SEARCH;
|
||||
if (from == ERR_Phase.RADIO_SEARCH) return to == ERR_Phase.RADIO_DEFENCE;
|
||||
if (from == ERR_Phase.RADIO_DEFENCE) return to == ERR_Phase.EXTRACTION;
|
||||
return from == ERR_Phase.EXTRACTION && to == ERR_Phase.VICTORY;
|
||||
}
|
||||
|
||||
static bool DefenceComplete(float now, float since, float duration, bool occupied)
|
||||
{
|
||||
return occupied && since >= 0 && duration > 0 && now >= since && now - since >= duration;
|
||||
}
|
||||
|
||||
static bool ContactFresh(float observed, float now)
|
||||
{
|
||||
return observed >= 0 && now >= observed && now - observed <= 120000;
|
||||
}
|
||||
|
||||
static int Discovery(int knowledge, int roll, int identifyPercent)
|
||||
{
|
||||
if (knowledge >= 1 || roll < identifyPercent) return 2;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
enum ERR_ObserverReason
|
||||
{
|
||||
NONE,
|
||||
SPENT_LIFE,
|
||||
LATE_EXTRACTION,
|
||||
SPAWN_UNAVAILABLE
|
||||
}
|
||||
|
||||
// A local follow camera. It never possesses its target or offers free flight.
|
||||
[BaseContainerProps(), SCR_BaseManualCameraComponentTitle()]
|
||||
class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
|
||||
{
|
||||
override bool EOnCameraInit() { return true; }
|
||||
override void EOnCameraFrame(SCR_ManualCameraParam param)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
if (!controller) return;
|
||||
IEntity target = controller.ERR_ObservedEntity();
|
||||
vector aim = controller.ERR_ObserverAnchor() + "0 1.5 0";
|
||||
vector position = aim + "0 4 -6";
|
||||
IEntity exclude = target;
|
||||
if (target)
|
||||
{
|
||||
IEntity vehicle = CompartmentAccessComponent.GetVehicleIn(target);
|
||||
vector transform[4];
|
||||
if (vehicle)
|
||||
{
|
||||
vehicle.GetWorldTransform(transform);
|
||||
vector lower, upper;
|
||||
vehicle.GetWorldBounds(lower, upper);
|
||||
aim = (lower + upper) * 0.5;
|
||||
position = aim + "0 5 0" - transform[2] * 10;
|
||||
exclude = vehicle;
|
||||
}
|
||||
else
|
||||
{
|
||||
target.GetWorldTransform(transform);
|
||||
aim = transform[3] + "0 1.5 0";
|
||||
position = aim + "0 1.5 0" - transform[2] * 4;
|
||||
}
|
||||
}
|
||||
TraceParam trace = new TraceParam();
|
||||
trace.Start = aim;
|
||||
trace.End = position;
|
||||
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
|
||||
trace.LayerMask = TRACE_LAYER_CAMERA;
|
||||
trace.Exclude = exclude;
|
||||
float fraction = GetCameraEntity().GetWorld().TraceMove(trace, null);
|
||||
if (fraction < 1) position = vector.Lerp(aim, position, Math.Max(0.05, fraction - 0.05));
|
||||
Math3D.DirectionAndUpMatrix(vector.Direction(position, aim), "0 1 0", param.transform);
|
||||
param.transform[3] = position;
|
||||
param.isDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_PlayerController
|
||||
{
|
||||
[RplProp()]
|
||||
protected ERR_ObserverReason m_ERRObserverReason;
|
||||
[RplProp()]
|
||||
protected RplId m_ERRObservedId = RplId.Invalid();
|
||||
[RplProp()]
|
||||
protected vector m_ERRObserverAnchor;
|
||||
protected IEntity m_ERRServerObserved;
|
||||
protected IEntity m_ERRServerObservedVehicle;
|
||||
protected SCR_ManualCamera m_ERRObserverCamera;
|
||||
protected bool m_ERRObserverMonitor;
|
||||
protected bool m_ERRObserverReady;
|
||||
protected float m_ERRLastCycle = -1000;
|
||||
|
||||
ERR_ObserverReason ERR_ObserverState() { return m_ERRObserverReason; }
|
||||
vector ERR_ObserverAnchor() { return m_ERRObserverAnchor; }
|
||||
IEntity ERR_ObservedEntity()
|
||||
{
|
||||
RplComponent component = RplComponent.Cast(Replication.FindItem(m_ERRObservedId));
|
||||
if (!component) return null;
|
||||
return component.GetEntity();
|
||||
}
|
||||
bool ERR_HasObserverCamera() { return m_ERRObserverCamera != null; }
|
||||
|
||||
void ERR_SetObserver(ERR_ObserverReason reason)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
if (reason != ERR_ObserverReason.NONE && ERR_Actors.Alive(GetControlledEntity())) return;
|
||||
m_ERRObserverReason = reason;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && reason != ERR_ObserverReason.NONE) m_ERRObserverAnchor = mode.PrisonPosition();
|
||||
if (reason == ERR_ObserverReason.NONE) ERR_ReleaseObserved();
|
||||
Replication.BumpMe();
|
||||
ERR_UpdateObserverServer();
|
||||
}
|
||||
|
||||
protected void ERR_StreamObserved(IEntity entity, bool normalStreaming)
|
||||
{
|
||||
RplIdentity identity = GetRplIdentity();
|
||||
if (!identity.IsValid() || identity == RplIdentity.Local() || !entity) return;
|
||||
RplComponent component = RplComponent.Cast(entity.FindComponent(RplComponent));
|
||||
if (component) component.EnableStreamingConNode(identity, normalStreaming);
|
||||
}
|
||||
protected void ERR_ReleaseObserved()
|
||||
{
|
||||
ERR_StreamObserved(m_ERRServerObserved, true);
|
||||
ERR_StreamObserved(m_ERRServerObservedVehicle, true);
|
||||
m_ERRServerObserved = null;
|
||||
m_ERRServerObservedVehicle = null;
|
||||
m_ERRObservedId = RplId.Invalid();
|
||||
}
|
||||
void ERR_UpdateObserverServer(int direction = 0)
|
||||
{
|
||||
if (!Replication.IsServer() || m_ERRObserverReason == ERR_ObserverReason.NONE) return;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || !mode.Active() || ERR_Actors.Alive(GetControlledEntity()))
|
||||
{
|
||||
m_ERRObserverReason = ERR_ObserverReason.NONE;
|
||||
ERR_ReleaseObserved();
|
||||
Replication.BumpMe();
|
||||
return;
|
||||
}
|
||||
array<IEntity> survivors = {};
|
||||
ERR_Actors.Players(survivors);
|
||||
Faction friendly = GetGame().GetFactionManager().GetFactionByKey("US");
|
||||
for (int i = survivors.Count() - 1; i >= 0; i--)
|
||||
if (SCR_FactionManager.SGetFaction(survivors[i]) != friendly) survivors.RemoveOrdered(i);
|
||||
int index = survivors.Find(m_ERRServerObserved);
|
||||
if (index < 0) index = 0;
|
||||
else index += direction;
|
||||
if (index < 0) index = survivors.Count() - 1;
|
||||
if (index >= survivors.Count()) index = 0;
|
||||
IEntity target;
|
||||
if (!survivors.IsEmpty()) target = survivors[index];
|
||||
if (target != m_ERRServerObserved)
|
||||
{
|
||||
ERR_ReleaseObserved();
|
||||
m_ERRServerObserved = target;
|
||||
if (target)
|
||||
{
|
||||
RplComponent component = RplComponent.Cast(target.FindComponent(RplComponent));
|
||||
if (component) m_ERRObservedId = component.Id();
|
||||
ERR_StreamObserved(target, false);
|
||||
}
|
||||
}
|
||||
if (target)
|
||||
{
|
||||
m_ERRObserverAnchor = target.GetOrigin();
|
||||
IEntity vehicle = CompartmentAccessComponent.GetVehicleIn(target);
|
||||
if (vehicle != m_ERRServerObservedVehicle)
|
||||
{
|
||||
ERR_StreamObserved(m_ERRServerObservedVehicle, true);
|
||||
m_ERRServerObservedVehicle = vehicle;
|
||||
ERR_StreamObserved(vehicle, false);
|
||||
}
|
||||
}
|
||||
Replication.BumpMe();
|
||||
}
|
||||
void ERR_CycleObserver(int direction)
|
||||
{
|
||||
if (this == GetGame().GetPlayerController()) Rpc(ERR_RequestObserverCycle, direction);
|
||||
}
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
|
||||
protected void ERR_RequestObserverCycle(int direction)
|
||||
{
|
||||
if (!Replication.IsServer() || (direction != -1 && direction != 1)) return;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (now - m_ERRLastCycle < 250) return;
|
||||
m_ERRLastCycle = now;
|
||||
ERR_UpdateObserverServer(direction);
|
||||
}
|
||||
|
||||
void ERR_StartObserverMonitor()
|
||||
{
|
||||
if (this != GetGame().GetPlayerController() || System.IsConsoleApp() || m_ERRObserverMonitor) return;
|
||||
m_ERRObserverMonitor = true;
|
||||
ERR_OnObserverMonitorStarted();
|
||||
GetGame().GetCallqueue().CallLater(ERR_UpdateObserverClient, 250, true);
|
||||
}
|
||||
protected void ERR_OnObserverMonitorStarted() {}
|
||||
protected void ERR_UpdateObserverClient()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
bool observe = mode && mode.Active() && m_ERRObserverReason != ERR_ObserverReason.NONE && !ERR_Actors.Alive(GetControlledEntity());
|
||||
if (!observe)
|
||||
{
|
||||
ERR_CloseObserverCamera();
|
||||
return;
|
||||
}
|
||||
if (!m_ERRObserverCamera)
|
||||
{
|
||||
Resource resource = Resource.Load("{EB1210AAE1BB8F54}Prefabs/ERR/SpectatorCamera.et");
|
||||
if (!resource || !resource.IsValid()) return;
|
||||
EntitySpawnParams params = new EntitySpawnParams();
|
||||
params.TransformMode = ETransformMode.WORLD;
|
||||
params.Transform[3] = m_ERRObserverAnchor + "0 5 -6";
|
||||
m_ERRObserverCamera = SCR_ManualCamera.Cast(GetGame().SpawnEntityPrefab(resource, GetGame().GetWorld(), params));
|
||||
if (!m_ERRObserverCamera) return;
|
||||
}
|
||||
CameraManager manager = GetGame().GetCameraManager();
|
||||
if (!manager || manager.CurrentCamera() != m_ERRObserverCamera) return;
|
||||
SCR_RespawnSystemComponent respawn = SCR_RespawnSystemComponent.GetInstance();
|
||||
if (respawn) respawn.DestroyLoadingPlaceholder();
|
||||
if (!m_ERRObserverReady)
|
||||
{
|
||||
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
|
||||
if (hints) hints.ShowCustom("You are spectating the surviving prisoners until the next round.", "Spectating", 12);
|
||||
m_ERRObserverReady = true;
|
||||
}
|
||||
}
|
||||
protected void ERR_CloseObserverCamera()
|
||||
{
|
||||
m_ERRObserverReady = false;
|
||||
if (!m_ERRObserverCamera) return;
|
||||
if (GetGame().GetPlayerController() == this) m_ERRObserverCamera.TrySwitchToControlledEntityCamera();
|
||||
m_ERRObserverCamera.Terminate();
|
||||
delete m_ERRObserverCamera;
|
||||
m_ERRObserverCamera = null;
|
||||
}
|
||||
void ~SCR_PlayerController()
|
||||
{
|
||||
if (GetGame())
|
||||
{
|
||||
GetGame().GetCallqueue().Remove(ERR_UpdateObserverClient);
|
||||
ERR_CloseObserverCamera();
|
||||
if (Replication.IsServer()) ERR_ReleaseObserved();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Local presentation reads replicated state; it cannot advance objectives.
|
||||
modded class SCR_PlayerController
|
||||
{
|
||||
protected int m_ERRGuidePhase = -1;
|
||||
protected bool m_ERRGuideStarted;
|
||||
|
||||
override void OnControlledEntityChanged(IEntity from, IEntity to)
|
||||
{
|
||||
super.OnControlledEntityChanged(from, to);
|
||||
if (this != GetGame().GetPlayerController() || !ERR_GameMode.Get() || m_ERRGuideStarted) return;
|
||||
m_ERRGuideStarted = true;
|
||||
GetGame().GetCallqueue().CallLater(ERR_UpdateGuide, 1000, true);
|
||||
}
|
||||
protected void ERR_UpdateGuide()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || !GetControlledEntity() || m_ERRGuidePhase == mode.Phase()) return;
|
||||
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
|
||||
if (!hints) return;
|
||||
string message;
|
||||
if (mode.Phase() == ERR_Phase.PRISON)
|
||||
message = "Find the supplies and field map inside the prison. Pick the gate lock to begin your escape.";
|
||||
else if (mode.Phase() == ERR_Phase.RADIO_SEARCH)
|
||||
message = "Recover the two documents on the guard's table just outside the gate. They identify the radio station on your map. Enemy soldiers may carry more intel.";
|
||||
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.";
|
||||
if (!message.IsEmpty()) hints.ShowCustom(message, "Escape Everon", 15);
|
||||
m_ERRGuidePhase = mode.Phase();
|
||||
}
|
||||
void ~SCR_PlayerController()
|
||||
{
|
||||
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_UpdateGuide);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Round-owned identities survive controller IDs, death and reconnect attempts.
|
||||
class ERR_PlayerLifeRules
|
||||
{
|
||||
static bool CanEnter(bool usedLife, int phase)
|
||||
{
|
||||
return !usedLife && phase >= ERR_Phase.PRISON && phase <= ERR_Phase.RADIO_DEFENCE;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_PlayerLife
|
||||
{
|
||||
UUID AccountId;
|
||||
int ConnectionId;
|
||||
bool Connected;
|
||||
bool UsedLife;
|
||||
IEntity Actor;
|
||||
}
|
||||
|
||||
class ERR_PlayerRoster
|
||||
{
|
||||
protected ref map<UUID, ref ERR_PlayerLife> m_Lives = new map<UUID, ref ERR_PlayerLife>();
|
||||
protected ref map<int, UUID> m_Connections = new map<int, UUID>();
|
||||
protected ref ScriptInvoker m_OnChanged = new ScriptInvoker();
|
||||
|
||||
ScriptInvoker OnChanged() { return m_OnChanged; }
|
||||
bool Bind(int playerId, UUID identity)
|
||||
{
|
||||
if (!Replication.IsServer() || playerId <= 0 || identity.IsNull()) return false;
|
||||
ERR_PlayerLife life;
|
||||
if (!m_Lives.Find(identity, life))
|
||||
{
|
||||
life = new ERR_PlayerLife();
|
||||
life.AccountId = identity;
|
||||
m_Lives.Insert(identity, life);
|
||||
}
|
||||
if (life.Connected && life.ConnectionId != playerId) return false;
|
||||
// A recycled transient ID cannot inherit the previous connection's record.
|
||||
UUID previous;
|
||||
if (m_Connections.Find(playerId, previous) && previous != identity) Disconnect(playerId);
|
||||
life.ConnectionId = playerId;
|
||||
life.Connected = true;
|
||||
m_Connections.Set(playerId, identity);
|
||||
m_OnChanged.Invoke(playerId);
|
||||
return true;
|
||||
}
|
||||
ERR_PlayerLife Get(int playerId)
|
||||
{
|
||||
UUID identity;
|
||||
if (!m_Connections.Find(playerId, identity)) return null;
|
||||
return m_Lives.Get(identity);
|
||||
}
|
||||
bool CanEnter(int playerId, int phase)
|
||||
{
|
||||
ERR_PlayerLife life = Get(playerId);
|
||||
return life && life.Connected && ERR_PlayerLifeRules.CanEnter(life.UsedLife, phase);
|
||||
}
|
||||
void Spawned(int playerId, IEntity actor)
|
||||
{
|
||||
if (!Replication.IsServer() || !actor) return;
|
||||
ERR_PlayerLife life = Get(playerId);
|
||||
if (!life) return;
|
||||
life.Actor = actor;
|
||||
life.UsedLife = true;
|
||||
m_OnChanged.Invoke(playerId);
|
||||
}
|
||||
void Disconnect(int playerId)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
ERR_PlayerLife life = Get(playerId);
|
||||
if (life) life.Connected = false;
|
||||
m_Connections.Remove(playerId);
|
||||
m_OnChanged.Invoke(playerId);
|
||||
}
|
||||
bool HasSurvivor()
|
||||
{
|
||||
// Native reconnect reservations retain the original actor until expiry.
|
||||
// A reserved survivor must not lose the round while their client reconnects.
|
||||
foreach (UUID identity, ERR_PlayerLife life : m_Lives)
|
||||
if (life.UsedLife && ERR_Actors.Alive(life.Actor)) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_ReconnectComponent
|
||||
{
|
||||
override bool HandlePlayerDisconnect(int playerId, KickCauseCode cause)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || !mode.Active()) return super.HandlePlayerDisconnect(playerId, cause);
|
||||
UUID identity = SCR_PlayerIdentityUtils.GetPlayerIdentityId(playerId);
|
||||
if (identity.IsNull()) return super.HandlePlayerDisconnect(playerId, cause);
|
||||
// Voluntary and involuntary departures use the same vanilla reservation,
|
||||
// timeout, character handoff and expiry cleanup. No replacement life is issued.
|
||||
SCR_ReconnectData data = StoreData(playerId);
|
||||
if (!IsDataRelevant(data)) return false;
|
||||
m_mReconnectData.Set(identity, data);
|
||||
UpdateExpieryCheck();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
[ComponentEditorProps(category: "Rereintroduced", description: "Everon escape composition and population")]
|
||||
class ERR_ScenarioComponentClass : ScriptComponentClass {}
|
||||
class ERR_ScenarioComponent : ScriptComponent
|
||||
{
|
||||
[Attribute("", UIWidgets.ResourceNamePicker, "Prison composition", "et")]
|
||||
protected ResourceName m_Prison;
|
||||
[Attribute("", UIWidgets.ResourceNamePicker, "Radio station", "et")]
|
||||
protected ResourceName m_Radio;
|
||||
[Attribute("", UIWidgets.ResourceNamePicker, "Extraction marker", "et")]
|
||||
protected ResourceName m_Extraction;
|
||||
[Attribute("6500 0 7200", UIWidgets.Coords, "Radio site")]
|
||||
protected vector m_RadioPosition;
|
||||
[Attribute("7000 0 7500", UIWidgets.Coords, "Extraction helicopter site")]
|
||||
protected vector m_ExtractionPosition;
|
||||
protected ref array<IEntity> m_Spawned = {};
|
||||
|
||||
bool Prepare()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!Replication.IsServer() || !mode || !m_Spawned.IsEmpty()) return false;
|
||||
vector start = mode.PrisonPosition();
|
||||
vector radioPosition = ERR_Actors.Ground(m_RadioPosition);
|
||||
vector extractionPosition = ERR_Actors.Ground(m_ExtractionPosition);
|
||||
if (start[1] < 1 || radioPosition[1] < 1 || extractionPosition[1] < 1) return false;
|
||||
IEntity prison = Spawn(m_Prison, start);
|
||||
IEntity radio = Spawn(m_Radio, radioPosition);
|
||||
IEntity extraction = Spawn(m_Extraction, extractionPosition);
|
||||
Vehicle helicopter = Vehicle.Cast(Spawn(ERR_FactionCatalog.Helicopter(), ERR_Actors.Ground(extractionPosition + "12 0 0")));
|
||||
if (!prison || !radio || !extraction || !helicopter) { Rollback(); return false; }
|
||||
mode.SetExtractionVehicle(helicopter);
|
||||
// Keep equipment above vegetation and visible to newly spawned prisoners.
|
||||
IEntity supplyTable = Spawn(ERR_Supplies.IntelTable(), ERR_Actors.Ground(start + "-4 0 0"));
|
||||
if (!supplyTable) { Rollback(); return false; }
|
||||
vector supplyLow, supplyHigh;
|
||||
supplyTable.GetWorldBounds(supplyLow, supplyHigh);
|
||||
for (int i = 0; i < 5; i++)
|
||||
if (!Spawn(ERR_Supplies.Pistol(), TablePoint(supplyLow, supplyHigh, (i + 1) / 6.0, 0.3))) { Rollback(); return false; }
|
||||
for (int i = 0; i < 8; i++)
|
||||
if (!Spawn(ERR_Supplies.Magazine(), TablePoint(supplyLow, supplyHigh, (i + 1) / 9.0, 0.65))) { Rollback(); return false; }
|
||||
if (!Spawn(FieldMap(), TablePoint(supplyLow, supplyHigh, 0.5, 0.9))) { Rollback(); return false; }
|
||||
// Objective documents are guaranteed; random carried intel remains optional.
|
||||
vector cachePosition = ERR_Actors.Ground(start + "0 0 -12");
|
||||
IEntity table = Spawn(ERR_Supplies.IntelTable(), cachePosition);
|
||||
if (!table) { Rollback(); return false; }
|
||||
vector lower, upper;
|
||||
table.GetWorldBounds(lower, upper);
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
vector documentPosition = cachePosition + Vector(-0.2 + i * 0.4, 0, 0);
|
||||
documentPosition[1] = upper[1] + 0.05;
|
||||
if (!Spawn(ERR_FactionCatalog.IntelFolder(), documentPosition)) { Rollback(); return false; }
|
||||
}
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
vector centre = start + "35 0 0";
|
||||
if (i == 1) centre = radioPosition + "30 0 0";
|
||||
if (i == 2) centre = extractionPosition + "30 0 0";
|
||||
SCR_AIGroup group = SCR_AIGroup.Cast(Spawn(ERR_FactionCatalog.Patrol(), ERR_Actors.Ground(centre)));
|
||||
if (!group) { Rollback(); return false; }
|
||||
AIWaypoint defend = AIWaypoint.Cast(Spawn(ERR_FactionCatalog.DefendWaypoint(), ERR_Actors.Ground(centre)));
|
||||
if (!defend) { Rollback(); return false; }
|
||||
group.AddWaypoint(defend);
|
||||
GetGame().GetCallqueue().CallLater(RegisterGroup, 2000, false, group);
|
||||
}
|
||||
foreach (IEntity entity : m_Spawned) mode.Own(entity);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected vector TablePoint(vector lower, vector upper, float along, float across)
|
||||
{
|
||||
float x = along;
|
||||
float z = across;
|
||||
if (upper[2] - lower[2] > upper[0] - lower[0]) { x = across; z = along; }
|
||||
return Vector(lower[0] + (upper[0] - lower[0]) * x, upper[1] + 0.05, lower[2] + (upper[2] - lower[2]) * z);
|
||||
}
|
||||
|
||||
protected ResourceName FieldMap() { return ERR_Supplies.Map(); }
|
||||
|
||||
protected IEntity Spawn(ResourceName resource, vector position)
|
||||
{
|
||||
IEntity entity = ERR_Actors.Spawn(resource, position);
|
||||
if (entity) m_Spawned.Insert(entity);
|
||||
return entity;
|
||||
}
|
||||
protected void RegisterGroup(SCR_AIGroup group)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && mode.Active() && group) mode.RegisterGroup(group);
|
||||
}
|
||||
protected void Rollback()
|
||||
{
|
||||
GetGame().GetCallqueue().Remove(RegisterGroup);
|
||||
foreach (IEntity entity : m_Spawned) ERR_Actors.DeleteSpawned(entity);
|
||||
m_Spawned.Clear();
|
||||
}
|
||||
protected override void OnDelete(IEntity owner)
|
||||
{
|
||||
if (GetGame()) GetGame().GetCallqueue().Remove(RegisterGroup);
|
||||
super.OnDelete(owner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// One configuration owner; never mutate runtime state after a failed save.
|
||||
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;
|
||||
|
||||
static array<string> Keys()
|
||||
{
|
||||
return {"reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds"};
|
||||
}
|
||||
|
||||
static bool Valid(string key, int value)
|
||||
{
|
||||
if (key == "reportIntervalSeconds") return value >= 5 && value <= 60;
|
||||
if (key == "supportSquadLimit") return value >= 0 && value <= 8;
|
||||
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;
|
||||
return false;
|
||||
}
|
||||
|
||||
void ERR_Settings()
|
||||
{
|
||||
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);
|
||||
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"))
|
||||
{
|
||||
m_ReadOnly = true;
|
||||
Print("Rereintroduced: invalid settings; defaults active, existing files preserved", LogLevel.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
int Get(string key) { return m_Values.Get(key); }
|
||||
|
||||
protected bool Read(string path, out map<string, int> values)
|
||||
{
|
||||
JsonLoadContext json = new JsonLoadContext();
|
||||
if (!json.LoadFromFile(path)) return false;
|
||||
values = new map<string, int>();
|
||||
foreach (string key : Keys())
|
||||
{
|
||||
float number;
|
||||
if (!json.ReadValue(key, number)) return false;
|
||||
int value = Math.Round(number);
|
||||
if (number != value || !Valid(key, value)) return false;
|
||||
values.Set(key, value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Set(string key, int value)
|
||||
{
|
||||
if (!Replication.IsServer() || m_ReadOnly || !Valid(key, value)) return false;
|
||||
JsonSaveContext json = new JsonSaveContext();
|
||||
json.EnableTypeDiscriminator(false);
|
||||
foreach (string option : Keys())
|
||||
{
|
||||
int saved = Get(option);
|
||||
if (option == key) saved = value;
|
||||
json.WriteValue(option, saved);
|
||||
}
|
||||
map<string, int> checked;
|
||||
if (!json.SaveToFile(PATH + ".tmp") || !Read(PATH + ".tmp", checked)) return false;
|
||||
foreach (string option : Keys())
|
||||
{
|
||||
int expected = Get(option);
|
||||
if (option == key) expected = value;
|
||||
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 (FileIO.FileExists(PATH + ".bak")) FileIO.CopyFile(PATH + ".bak", PATH);
|
||||
return false;
|
||||
}
|
||||
foreach (string option : Keys())
|
||||
{
|
||||
int expected = Get(option);
|
||||
if (option == key) expected = value;
|
||||
if (checked.Get(option) != expected)
|
||||
{
|
||||
if (FileIO.FileExists(PATH + ".bak")) FileIO.CopyFile(PATH + ".bak", PATH);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
m_Values = checked;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
[BaseContainerProps(category: "Respawn")]
|
||||
class ERR_SpawnLogic : SCR_SpawnLogic
|
||||
{
|
||||
protected ref map<int, int> m_Attempts = new map<int, int>();
|
||||
protected ref map<int, UUID> m_Identities = new map<int, UUID>();
|
||||
protected ref map<int, UUID> m_Retries = new map<int, UUID>();
|
||||
protected ref map<int, vector> m_AuthorizedPositions = new map<int, vector>();
|
||||
protected ref set<int> m_Pending = new set<int>();
|
||||
|
||||
override void OnPlayerAuditSuccess_S(int playerId)
|
||||
{
|
||||
super.OnPlayerAuditSuccess_S(playerId);
|
||||
UUID identity = SCR_PlayerIdentityUtils.GetPlayerIdentityId(playerId);
|
||||
#ifdef WORKBENCH
|
||||
if (identity.IsNull() && RplSession.Mode() != RplMode.Dedicated)
|
||||
{
|
||||
string name = GetGame().GetPlayerManager().GetPlayerName(playerId);
|
||||
string suffix = Math.AbsInt(name.Hash()).ToString(12, true);
|
||||
identity = "00e77100-0000-0000-0000-" + suffix;
|
||||
}
|
||||
#endif
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || !mode.Players().Bind(playerId, identity))
|
||||
{
|
||||
Print("Rereintroduced: player identity could not be bound; spawn withheld", LogLevel.ERROR);
|
||||
return;
|
||||
}
|
||||
m_Identities.Set(playerId, identity);
|
||||
if (ResolveReconnection(playerId))
|
||||
{
|
||||
mode.PlayerSpawned(playerId, GetGame().GetPlayerManager().GetPlayerControlledEntity(playerId));
|
||||
SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(playerId)).ERR_SetObserver(ERR_ObserverReason.NONE);
|
||||
return;
|
||||
}
|
||||
m_Attempts.Set(playerId, 0);
|
||||
DoSpawn_S(playerId);
|
||||
}
|
||||
|
||||
protected override void DoSpawn_S(int playerId)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(playerId));
|
||||
if (!controller || !mode || !m_Identities.Contains(playerId) || !m_Attempts.Contains(playerId) || m_Pending.Contains(playerId)) return;
|
||||
if (controller.GetControlledEntity()) return;
|
||||
if (ERR_MissionRules.Terminal(mode.Phase())) return;
|
||||
int attempts = m_Attempts.Get(playerId);
|
||||
if (attempts >= 30) { Observe(playerId, ERR_ObserverReason.SPAWN_UNAVAILABLE); return; }
|
||||
m_Attempts.Set(playerId, attempts + 1);
|
||||
if (!mode.Active()) { RetryLater(playerId); return; }
|
||||
if (!mode.Players().CanEnter(playerId, mode.Phase()))
|
||||
{
|
||||
ERR_ObserverReason reason = ERR_ObserverReason.SPENT_LIFE;
|
||||
ERR_PlayerLife life = mode.Players().Get(playerId);
|
||||
if (life && !life.UsedLife) reason = ERR_ObserverReason.LATE_EXTRACTION;
|
||||
Observe(playerId, reason);
|
||||
return;
|
||||
}
|
||||
SCR_PlayerFactionAffiliationComponent affiliation = GetPlayerFactionComponent_S(playerId);
|
||||
SCR_RespawnComponent respawn = GetPlayerRespawnComponent_S(playerId);
|
||||
Faction faction = GetGame().GetFactionManager().GetFactionByKey("US");
|
||||
if (!affiliation || !respawn || !faction) { RetryLater(playerId); return; }
|
||||
affiliation.RequestFaction(faction);
|
||||
vector position = SpawnPosition(mode, playerId);
|
||||
SCR_FreeSpawnData request = new SCR_FreeSpawnData(ERR_FactionCatalog.Prisoner(), position);
|
||||
if (!respawn.CanSpawn(request)) { RetryLater(playerId); return; }
|
||||
m_AuthorizedPositions.Set(playerId, position);
|
||||
m_Pending.Insert(playerId);
|
||||
if (!respawn.RequestSpawn(request)) OnPlayerSpawnFailed_S(playerId);
|
||||
}
|
||||
|
||||
protected vector SpawnPosition(ERR_GameMode mode, int playerId)
|
||||
{
|
||||
int column = playerId % 3;
|
||||
int row = playerId % 4;
|
||||
vector offset = Vector(column - 1, 0, row - 1);
|
||||
vector prison = ERR_Actors.Ground(mode.PrisonPosition() + offset);
|
||||
if (mode.Phase() == ERR_Phase.PRISON) return prison;
|
||||
array<IEntity> survivors = {};
|
||||
ERR_Actors.Players(survivors, true);
|
||||
foreach (IEntity survivor : survivors)
|
||||
{
|
||||
if (CompartmentAccessComponent.GetVehicleIn(survivor)) continue;
|
||||
vector anchor = survivor.GetOrigin();
|
||||
vector ground = ERR_Actors.Ground(anchor);
|
||||
if (Math.AbsFloat(anchor[1] - ground[1]) > 1.5) continue;
|
||||
vector position = ERR_Actors.Ground(anchor + "3 0 0" + offset);
|
||||
if (position[1] < 1 || Math.AbsFloat(position[1] - anchor[1]) > 1.5) continue;
|
||||
TraceParam trace = new TraceParam();
|
||||
trace.Start = anchor + "0 1 0";
|
||||
trace.End = position + "0 1 0";
|
||||
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
|
||||
trace.LayerMask = TRACE_LAYER_CAMERA;
|
||||
trace.Exclude = survivor;
|
||||
if (GetGame().GetWorld().TraceMove(trace, null) < 1) continue;
|
||||
trace.Start = position + "0 0.15 0";
|
||||
trace.End = position + "0 2 0";
|
||||
if (GetGame().GetWorld().TraceMove(trace, null) < 1) continue;
|
||||
return position;
|
||||
}
|
||||
return prison;
|
||||
}
|
||||
|
||||
protected void ClearPending(int playerId)
|
||||
{
|
||||
int index = m_Pending.Find(playerId);
|
||||
if (index >= 0) m_Pending.Remove(index);
|
||||
}
|
||||
protected void RetryLater(int playerId)
|
||||
{
|
||||
UUID identity;
|
||||
if (!m_Identities.Find(playerId, identity) || !m_Attempts.Contains(playerId)) return;
|
||||
if (m_Retries.Contains(playerId) && m_Retries.Get(playerId) == identity) return;
|
||||
m_Retries.Set(playerId, identity);
|
||||
GetGame().GetCallqueue().CallLater(RetrySpawn, 1000, false, playerId, identity);
|
||||
}
|
||||
protected void RetrySpawn(int playerId, UUID identity)
|
||||
{
|
||||
if (!m_Identities.Contains(playerId) || m_Identities.Get(playerId) != identity || m_Retries.Get(playerId) != identity) return;
|
||||
m_Retries.Remove(playerId);
|
||||
DoSpawn_S(playerId);
|
||||
}
|
||||
protected override void OnPlayerSpawnFailed_S(int playerId)
|
||||
{
|
||||
ClearPending(playerId);
|
||||
m_AuthorizedPositions.Remove(playerId);
|
||||
RetryLater(playerId);
|
||||
}
|
||||
protected void Observe(int playerId, ERR_ObserverReason reason)
|
||||
{
|
||||
m_Attempts.Remove(playerId);
|
||||
m_Retries.Remove(playerId);
|
||||
ClearPending(playerId);
|
||||
m_AuthorizedPositions.Remove(playerId);
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(playerId));
|
||||
if (controller) controller.ERR_SetObserver(reason);
|
||||
}
|
||||
protected override void OnPlayerEntityLost_S(int playerId)
|
||||
{
|
||||
super.OnPlayerEntityLost_S(playerId);
|
||||
if (!m_Identities.Contains(playerId)) return;
|
||||
GetGame().GetCallqueue().CallLater(ObserveAfterLoss, 100, false, playerId, m_Identities.Get(playerId));
|
||||
}
|
||||
protected void ObserveAfterLoss(int playerId, UUID identity)
|
||||
{
|
||||
if (!m_Identities.Contains(playerId) || m_Identities.Get(playerId) != identity) return;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
ERR_PlayerLife life;
|
||||
if (mode) life = mode.Players().Get(playerId);
|
||||
if (life && life.UsedLife) Observe(playerId, ERR_ObserverReason.SPENT_LIFE);
|
||||
}
|
||||
override void OnPlayerSpawned_S(int playerId, IEntity entity)
|
||||
{
|
||||
super.OnPlayerSpawned_S(playerId, entity);
|
||||
ClearPending(playerId);
|
||||
m_AuthorizedPositions.Remove(playerId);
|
||||
m_Attempts.Remove(playerId);
|
||||
m_Retries.Remove(playerId);
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode) mode.PlayerSpawned(playerId, entity);
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(playerId));
|
||||
if (controller) controller.ERR_SetObserver(ERR_ObserverReason.NONE);
|
||||
PrintFormat("Rereintroduced [spawn]: player %1 at %2", playerId, entity.GetOrigin());
|
||||
}
|
||||
override void OnPlayerDisconnected_S(int playerId, KickCauseCode cause, int timeout)
|
||||
{
|
||||
m_Attempts.Remove(playerId);
|
||||
m_Retries.Remove(playerId);
|
||||
ClearPending(playerId);
|
||||
m_AuthorizedPositions.Remove(playerId);
|
||||
m_Identities.Remove(playerId);
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode) mode.Players().Disconnect(playerId);
|
||||
if (GetPlayerRespawnComponent_S(playerId)) super.OnPlayerDisconnected_S(playerId, cause, timeout);
|
||||
}
|
||||
bool Authorizes(int playerId, SCR_SpawnData data)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!Replication.IsServer() || !mode || !data || !mode.Players().CanEnter(playerId, mode.Phase())) return false;
|
||||
if (!m_Pending.Contains(playerId) || !m_AuthorizedPositions.Contains(playerId) || data.GetPrefab() != ERR_FactionCatalog.Prisoner()) return false;
|
||||
return vector.Distance(data.GetPosition(), m_AuthorizedPositions.Get(playerId)) < 0.1;
|
||||
}
|
||||
void ~ERR_SpawnLogic()
|
||||
{
|
||||
if (!GetGame()) return;
|
||||
GetGame().GetCallqueue().Remove(RetrySpawn);
|
||||
GetGame().GetCallqueue().Remove(ObserveAfterLoss);
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_RespawnSystemComponent
|
||||
{
|
||||
override bool CanRequestSpawn_S(SCR_SpawnRequestComponent requestComponent, SCR_SpawnHandlerComponent handlerComponent, SCR_SpawnData data, out SCR_ESpawnResult result = SCR_ESpawnResult.SPAWN_NOT_ALLOWED)
|
||||
{
|
||||
if (ERR_GameMode.Get())
|
||||
{
|
||||
ERR_SpawnLogic logic = ERR_SpawnLogic.Cast(GetSpawnLogic());
|
||||
if (!requestComponent || !logic || !logic.Authorizes(requestComponent.GetPlayerId(), data))
|
||||
{
|
||||
result = SCR_ESpawnResult.SPAWN_NOT_ALLOWED;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return super.CanRequestSpawn_S(requestComponent, handlerComponent, data, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
class ERR_Supplies
|
||||
{
|
||||
static ResourceName Pistol() { return "{C0F7DD85A86B2900}Prefabs/Weapons/Handguns/PM/Handgun_PM.et"; }
|
||||
static ResourceName Magazine() { return "{8B853CDD11BA916E}Prefabs/Weapons/Magazines/Magazine_9x18_PM_8rnd_Ball.et"; }
|
||||
static ResourceName Map() { return "{13772C903CB5E4F7}Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et"; }
|
||||
static ResourceName IntelTable() { return "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et"; }
|
||||
}
|
||||
Reference in New Issue
Block a user