Build independent Everon successor foundation and native regressions

This commit is contained in:
2026-09-19 01:37:41 +02:00
commit 2550bc0423
155 changed files with 15098 additions and 0 deletions
@@ -0,0 +1,21 @@
SCR_GameOverScreenConfig : "{651583FFFB7FC765}Configs/GameOverScreen/BaseGameOverScreensConfig.conf" {
m_aGameOverScreens {
ERR_EscapeGameOverScreenInfo "{6B193DC8DDC63507}" {
m_GameOverScreenId VICTORY
m_GameOverContentLayout "{C4507D07F3C54831}UI/layouts/HUD/GameOver/EndScreen/EndScreenContent_NoImage.layout"
m_OptionalParams SCR_BaseGameOverScreenInfoOptional "{4984A5C1BFEFC04E}" {
m_sTitle "Escape complete"
m_sAudioOneShot "SOUND_ONVICTORY"
}
}
SCR_BaseGameOverScreenInfo "{E66F29A7A8AD7438}" {
m_GameOverScreenId LOOSE
m_GameOverContentLayout "{C4507D07F3C54831}UI/layouts/HUD/GameOver/EndScreen/EndScreenContent_NoImage.layout"
m_OptionalParams SCR_BaseGameOverScreenInfoOptional "{3DBF0BDE715945FA}" {
m_sTitle "Escape failed"
m_sDebriefing "No surviving prisoners can complete the escape, or the extraction helicopter has been lost."
m_sAudioOneShot "SOUND_ONDEFEAT"
}
}
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{F6633922A25F9441}Configs/ERR/GameOverScreens.conf"
Configurations {
ResourceClass PC {
}
ResourceClass HEADLESS : PC {
}
}
}
+6
View File
@@ -0,0 +1,6 @@
GenericEntity {
components {
ERR_LocationComponent "{C17CA14398A1CD7E}" { m_Id "everon-extraction-1" m_Kind 2 }
RplComponent "{0BF5BAAA5FBFEECE}" { Streamable Disabled }
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{033503110AC34EAC}Prefabs/ERR/Extraction.et"
Configurations {
EntityTemplateResourceClass PC {
}
EntityTemplateResourceClass HEADLESS : PC {
}
}
}
+16
View File
@@ -0,0 +1,16 @@
ERR_GameMode : "{0F307326459A1395}Prefabs/MP/Modes/GameMode_Base.et" {
components {
SCR_GameOverScreenManagerComponent "{56DEC646298AD219}" {
m_GameOverScreenConfig SCR_GameOverScreenConfig "{56DEC6462FFDD82D}" : "{F6633922A25F9441}Configs/ERR/GameOverScreens.conf" {}
}
ERR_ScenarioComponent "{B78E910E88DE30D2}" {
m_Prison "{BE6B8D56349038A6}Prefabs/ERR/Prison.et"
m_Radio "{F7E7E3D6ACF0C860}Prefabs/ERR/Radio.et"
m_Extraction "{033503110AC34EAC}Prefabs/ERR/Extraction.et"
}
SCR_RespawnSystemComponent "{56B2B4793051E7C9}" {
m_SpawnLogic ERR_SpawnLogic "{79AD722B2EFD595F}" {}
}
}
m_bUseSpawnPreload 0
}
+9
View File
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{978E045E53EF36C8}Prefabs/ERR/GameMode.et"
Configurations {
EntityTemplateResourceClass PC {
}
EntityTemplateResourceClass HEADLESS : PC {
}
}
}
+10
View File
@@ -0,0 +1,10 @@
SCR_AIGroup : "{E552DABF3636C2AD}Prefabs/Groups/OPFOR/Group_USSR_RifleSquad.et" {
m_aUnitPrefabSlots {
"{5436629450D8387A}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SL.et"
"{612F43A4D5AE765F}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_RTO.et"
"{23ADBBC31B6A3DC6}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_AR.et"
"{1C78331E156A3D65}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_AT.et"
"{333DA6244C7DA34C}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SR.et"
"{BF643BE4ADBDFDD3}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_LAT.et"
}
}
+7
View File
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{D5A4D0D3AE1A3E26}Prefabs/ERR/Patrol.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS {}
}
}
+89
View File
@@ -0,0 +1,89 @@
GenericEntity {
components {
Hierarchy "{30ADADE77D1EC3B8}" {}
RplComponent "{7FA5168C6FBD0C22}" {}
}
{
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "2879356FFFBE36E0"
coords -6 0 6
angles 0 0 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "C99EF9AFCEBA863D"
coords -3 0 6
angles 0 0 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "91048277F04ACA61"
coords 0 0 6
angles 0 0 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "8F2200A922FB0A6E"
coords 3 0 6
angles 0 0 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "3C442429BBCF5D2F"
coords -6 0 -6
angles 0 -90 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "9E855CC802C592CA"
coords 6 0 -6
angles 0 -90 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "778DAC42C9208F67"
coords -6 0 -3
angles 0 -90 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "7E350EB9053673C4"
coords 6 0 -3
angles 0 -90 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "AD17884D5EA37D37"
coords -6 0 0
angles 0 -90 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "1A157E23E1A995D8"
coords 6 0 0
angles 0 -90 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "3A78A6F3ACB7ACB1"
coords -6 0 3
angles 0 -90 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "275B472B565FFF6C"
coords 6 0 3
angles 0 -90 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "1B20E11B3FC7B59A"
coords -6 0 -6
angles 0 0 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "2735B52246DD6213"
coords -3 0 -6
angles 0 0 0
}
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
ID "28101A0E33470B38"
coords 3 0 -6
angles 0 0 0
}
GenericEntity : "{C3FDC4DBCD58C2F5}Prefabs/ERR/PrisonDoor.et" {
ID "54D8F96ECC743335"
scale 1.5
coords 0 -0.1 -6
angles 0 0 0
}
}
}
+9
View File
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{BE6B8D56349038A6}Prefabs/ERR/Prison.et"
Configurations {
EntityTemplateResourceClass PC {
}
EntityTemplateResourceClass HEADLESS : PC {
}
}
}
+19
View File
@@ -0,0 +1,19 @@
GenericEntity : "{34B08027DBDE322A}Prefabs/Structures/BuildingParts/Doors/Door_MetalGate_01_Short_LEFT_EXT_ST_B.et" {
components {
DoorComponent "{221C667A9F7D4B69}" {
DoorAction ERR_PrisonDoorAction "{50888D3646A61640}" {
ParentContextList { "SCR_DoorUserAction" }
}
}
ActionsManagerComponent "{F32B667C3C823800}" {
ActionContexts { UserActionContext "{50888D37F529F720}" { ContextName "SCR_DoorUserAction" Radius 2 } }
additionalActions {
ERR_UnlockPrisonAction "{CA7918FEA177BFB8}" {
ParentContextList { "SCR_DoorUserAction" }
UIInfo SCR_ActionUIInfo "{F734C81E11AA6BEF}" { Name "Pick prison lock" }
Duration 4
}
}
}
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{C3FDC4DBCD58C2F5}Prefabs/ERR/PrisonDoor.et"
Configurations {
EntityTemplateResourceClass PC {
}
EntityTemplateResourceClass HEADLESS : PC {
}
}
}
+8
View File
@@ -0,0 +1,8 @@
GenericEntity {
components {
Hierarchy "{4F20A90EDBD625A4}" {}
MeshObject "{4F20A90EDBD625A1}" { Object "{0B96D139FEBCA006}Assets/Structures/Walls/Concrete/ConcreteWall_01_3m.xob" }
RigidBody "{4F20A90EDBD625A2}" { ModelGeometry 1 Static 1 }
RplComponent "{4F20A90EDBD625A3}" {}
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et"
Configurations {
EntityTemplateResourceClass PC {
}
EntityTemplateResourceClass HEADLESS : PC {
}
}
}
+16
View File
@@ -0,0 +1,16 @@
GenericEntity {
components {
MeshObject "{111C06CB740ED195}" { Object "{77C9FCF3FB1246E9}Assets/Items/Equipment/Radios/Radio_R107M/Radio_R107M_01_item.xob" }
RigidBody "{4C174A44D07B91BA}" { ModelGeometry 1 Static 1 }
ERR_LocationComponent "{C19CA6CC83470879}" { m_Id "everon-radio-1" m_Kind 1 }
RplComponent "{54ED7463161A5622}" { Streamable Disabled }
ActionsManagerComponent "{B081EC0768636EAB}" {
ActionContexts { UserActionContext "{C1A75A5D8B7EF1E4}" { ContextName "ERR_Radio" Radius 2 } }
additionalActions { ERR_TuneRadioAction "{7ABBB6B5D73DDD95}" {
ParentContextList { "ERR_Radio" }
UIInfo SCR_ActionUIInfo "{1B42BB7CEDECB994}" { Name "Contact extraction HQ" }
Duration 8
} }
}
}
}
+9
View File
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{F7E7E3D6ACF0C860}Prefabs/ERR/Radio.et"
Configurations {
EntityTemplateResourceClass PC {
}
EntityTemplateResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,7 @@
SCR_ManualCamera {
m_bInputEnabled 0
m_bStartAtEntity 1
m_aComponents {
ERR_FollowSurvivorCameraComponent "{965C2E8B2339B926}" {}
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{EB1210AAE1BB8F54}Prefabs/ERR/SpectatorCamera.et"
Configurations {
EntityTemplateResourceClass PC {
}
EntityTemplateResourceClass HEADLESS : PC {
}
}
}
@@ -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)));
}
}
+81
View File
@@ -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"; }
}
+239
View File
@@ -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;
}
}
+225
View File
@@ -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"; }
}
+3
View File
@@ -0,0 +1,3 @@
SubScene {
Parent "{853E92315D1D9EFE}worlds/Eden/Eden.ent"
}
+9
View File
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{B6C516F86302C679}Worlds/ERR_Everon.ent"
Configurations {
WorldResourceClass PC {
}
WorldResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,35 @@
ERR_GameMode Rereintroduced : "{978E045E53EF36C8}Prefabs/ERR/GameMode.et" {
}
SCR_AIWorld AIWorld : "{E0A05C76552E7F58}Prefabs/AI/SCR_AIWorld.et" {
components {
NavmeshWorldComponent "{5584F30E67F617AD}" {
NavmeshSettings NavmeshWorld "{50FC63BEBE3973C5}" {
NavmeshFilesConfig BaseNavmeshFilesConfig "{60CA9BE5536BF701}" : "{8BBC73472092902E}Configs/Navmesh/Navmesh_GM_Eden_Soldier.conf" {}
}
}
NavmeshWorldComponent "{5584F30EEFEE1223}" {
NavmeshSettings ChimeraNavmeshWorld "{50FC63BEBE3973C5}" {
NavmeshFilesConfig BaseNavmeshFilesConfig "{60CA9BE5536BF701}" : "{0228A79846FD8A97}Configs/Navmesh/Navmesh_GM_Eden_Vehicle.conf" {}
}
}
NavmeshWorldComponent "{5C8C9B750D124A63}" {
NavmeshSettings NavmeshWorld "{5C8C9B750B60C6E2}" {
NavmeshFilesConfig BaseNavmeshFilesConfig "{5C90BD0EC793647D}" {
NavmeshFile "{5E19E60F26327356}worlds/MP/Navmeshes/LowResEden.nmn"
}
}
}
}
}
PerceptionManager Perception : "{028DAEAD63E056BE}Prefabs/World/Game/PerceptionManager.et" {
}
SCR_FactionManager Factions : "{589540B35EBA8CDC}Prefabs/MP/Managers/Factions/FactionManager_USxUSSR.et" {
}
SCR_LoadoutManager Loadouts : "{97EF4C1A41F63DDB}Prefabs/MP/Managers/Loadouts/LoadoutManager_USxUSSR.et" {
}
RadioManagerEntity RadioManager : "{B8E09FAB91C4ECCD}Prefabs/Systems/Radio/RadioManager.et" {
}
ScriptedChatEntity Chat : "{F69BC912AC8236F9}Prefabs/MP/ScriptedChatEntity.et" {
}
+8
View File
@@ -0,0 +1,8 @@
GameProject {
ID "ERR_Core"
GUID "2528466DE220D91C"
TITLE "Escapists Rereintroduced - Core"
Dependencies {
"58D0FB3206B6F859"
}
}