Combine Workshop addon and fix prison startup

Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
This commit is contained in:
2026-09-20 16:42:52 +02:00
parent 7d94953b53
commit cab998fb64
513 changed files with 18940 additions and 862 deletions
@@ -280,7 +280,7 @@ class ERR_CasualtyLoadingService
ERR_CasualtyLoadTicket ticket = Ticket(controller, nonce);
if (!ticket || !ticket.Committed || ticket.OwnerReported) return;
ticket.OwnerReported = true;
if (!accepted) Print("Rereintroduced [casualty]: owner declined loading; observing physical outcome until expiry.");
if (!accepted) Print("Reintroduced [casualty]: owner declined loading; observing physical outcome until expiry.");
// Neither positive nor negative client acknowledgements release a reserved
// seat while an authorized owner command could still be in flight.
}
@@ -70,7 +70,7 @@ class ERR_DiscoveryService
ERR_LocationComponent chosen = eligible.GetRandomElement();
if (!chosen.Discover(Math.RandomInt(0, 100), m_Mode.Settings().Get("intelRevealChancePercent"))) continue;
m_Spent.Insert(folder);
PrintFormat("Rereintroduced [intel]: player %1 recovered folder; location=%2 knowledge=%3", GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(player), chosen.Id(), chosen.Knowledge());
PrintFormat("Reintroduced [intel]: player %1 recovered folder; location=%2 knowledge=%3", GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(player), chosen.Id(), chosen.Knowledge());
}
}
@@ -1,4 +1,4 @@
[ComponentEditorProps(category: "Rereintroduced", description: "Physical document identifying one authored objective")]
[ComponentEditorProps(category: "Reintroduced", description: "Physical document identifying one authored objective")]
class ERR_DocumentComponentClass : ScriptComponentClass {}
class ERR_DocumentComponent : ScriptComponent
{
@@ -108,7 +108,7 @@ class ERR_EnvironmentService
m_DateCode = dateCode;
m_Profile = profile;
m_Started = true;
PrintFormat("Rereintroduced [environment]: date=%1 weather=%2", Date(dateCode), ProfileName(profile));
PrintFormat("Reintroduced [environment]: date=%1 weather=%2", Date(dateCode), ProfileName(profile));
return true;
}
bool Started() { return m_Started && !m_Stopped; }
@@ -41,7 +41,7 @@ class ERR_ExtractionPlacement
protected ERR_PrepareResult Fail(string reason)
{
m_Failure = reason; m_Result = ERR_PrepareResult.FAILED;
Print("Rereintroduced [extraction placement]: " + reason, LogLevel.ERROR);
Print("Reintroduced [extraction placement]: " + reason, LogLevel.ERROR);
return m_Result;
}
protected void Diagnostic(bool ready)
@@ -98,7 +98,7 @@ class ERR_ExtractionPlacement
if (Stable(m_StableSince, m_LastObservation, now))
{
if (!QualificationAllowed(m_Result == ERR_PrepareResult.READY, now, m_Deadline)) return Fail("initial aircraft stable qualification missed the 30-second preparation deadline");
if (m_Result != ERR_PrepareResult.READY) Print("Rereintroduced [extraction placement]: native supported aircraft stable for one second, fuel present, exactly one initial wake");
if (m_Result != ERR_PrepareResult.READY) Print("Reintroduced [extraction placement]: native supported aircraft stable for one second, fuel present, exactly one initial wake");
m_Result = ERR_PrepareResult.READY;
m_LastObservation = now;
return m_Result;
@@ -106,7 +106,7 @@ class ERR_ForceLedger
// formation cannot replace its casualties with a fresh full-strength unit.
if (m_Records.Count() >= 128 || m_Sources.Count() >= 128)
{
if (!m_CapLogged) Print("Rereintroduced [ledger]: 128-formation registration cap reached.");
if (!m_CapLogged) Print("Reintroduced [ledger]: 128-formation registration cap reached.");
m_CapLogged = true;
return null;
}
@@ -153,7 +153,7 @@ class ERR_GameMode : SCR_BaseGameMode
if (!Replication.IsServer() || m_Phase != ERR_Phase.PREPARING || !m_PreparationError.IsEmpty()) return;
m_PreparationError = detail;
Replication.BumpMe();
Print("Rereintroduced: scenario preparation failed; spawning remains disabled: " + detail, LogLevel.ERROR);
Print("Reintroduced: scenario preparation failed; spawning remains disabled: " + detail, LogLevel.ERROR);
StopServices();
ERR_SiteService sites = Sites();
if (sites) sites.Rollback();
@@ -184,7 +184,7 @@ class ERR_GameMode : SCR_BaseGameMode
{
if (existing.Id() == location.Id())
{
Print("Rereintroduced: duplicate location ID " + location.Id(), LogLevel.ERROR);
Print("Reintroduced: duplicate location ID " + location.Id(), LogLevel.ERROR);
return;
}
}
@@ -222,7 +222,7 @@ class ERR_GameMode : SCR_BaseGameMode
{
m_ForceSerial++;
ERR_ForceRecord record = m_Forces.Register("formation-" + m_ForceSerial.ToString(), ERR_ForceKind.GARRISON, group.GetOrigin(), strength, null, group, GetGame().GetWorld().GetWorldTime());
if (!record) { Print("Rereintroduced: force ledger refused physical formation", LogLevel.ERROR); return; }
if (!record) { Print("Reintroduced: force ledger refused physical formation", LogLevel.ERROR); return; }
}
}
if (m_Command) m_Command.RegisterGroup(group);
@@ -259,10 +259,19 @@ class ERR_GameMode : SCR_BaseGameMode
if (!Replication.IsServer() || !m_PreparationError.IsEmpty() || !ERR_MissionRules.CanTransition(m_Phase, phase)) return false;
ERR_Phase previous = m_Phase;
m_Phase = phase;
if (previous == ERR_Phase.PRISON && phase == ERR_Phase.RADIO_SEARCH)
{
// Dormant patrols must not catch up the time spent waiting in prison.
for (int i = 0; i < m_Forces.Count(); i++)
{
ERR_ForceRecord force = m_Forces.At(i);
if (force) force.LastSimulatedAt = GetWorld().GetWorldTime();
}
}
RefreshRadioFeedback();
if (ERR_MissionRules.Terminal(phase)) PublishEscapeResults();
Replication.BumpMe();
PrintFormat("Rereintroduced [mission]: phase %1 -> %2", previous, phase);
PrintFormat("Reintroduced [mission]: phase %1 -> %2", previous, phase);
m_OnPhaseChanged.Invoke(previous, phase);
if (ERR_MissionRules.Terminal(phase))
{
@@ -451,7 +460,7 @@ class ERR_GameMode : SCR_BaseGameMode
if (m_Command) m_Command.Maintain(now);
// Maintenance can finish or stop the round; do not resume tactical work.
if (!Active()) return;
TickForces(now);
if (m_Phase >= ERR_Phase.RADIO_SEARCH) TickForces(now);
if (GetGame().GetPlayerManager().GetPlayerCount() == 0)
{
m_NoSurvivorsSince = -1;
@@ -476,7 +485,7 @@ class ERR_GameMode : SCR_BaseGameMode
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
if (controller) controller.ERR_UpdateObserverServer();
}
if (m_Command) m_Command.Tick(now);
if (m_Command && m_Phase >= ERR_Phase.RADIO_SEARCH) m_Command.Tick(now);
m_OnTick.Invoke(now);
}
@@ -67,7 +67,7 @@ class ERR_LocalAdmission
m_Selected = true;
if (m_Key.IsNull()) { m_Key = UUID.GenV4(); m_LocalKey = true; }
if (m_Key.IsNull()) { m_Failed = true; return UUID.NULL_UUID; }
if (m_LocalKey) Print("Rereintroduced [admission]: sole offline controller uses a generated local-round roster key; backend identity and permissions unchanged.");
if (m_LocalKey) Print("Reintroduced [admission]: sole offline controller uses a generated local-round roster key; backend identity and permissions unchanged.");
return m_Key;
}
bool Validate()
@@ -1,6 +1,6 @@
enum ERR_LocationKind { PRISON, RADIO, EXTRACTION, OUTPOST, DEPOT }
[ComponentEditorProps(category: "Rereintroduced", description: "Server-owned location with replicated discovery state")]
[ComponentEditorProps(category: "Reintroduced", description: "Server-owned location with replicated discovery state")]
class ERR_LocationComponentClass : ScriptComponentClass {}
class ERR_LocationComponent : ScriptComponent
{
@@ -99,11 +99,21 @@ class ERR_NativePrisonBreachProbe
if (gate) door = BaseDoorComponent.Cast(gate.FindComponent(BaseDoorComponent));
if (!Check(door && !escape.GateLost() && Math.AbsFloat(door.GetNormalizedDoorState()) < 0.01 && !door.IsOpening() && m_Mode.Phase() == ERR_Phase.PRISON && m_Transitions == 0, "direct AI/player PerformAction cannot open locked native gate or advance progression")) return -1;
SCR_DestructionMultiPhaseComponent damage = SCR_DestructionMultiPhaseComponent.Cast(gate.FindComponent(SCR_DestructionMultiPhaseComponent));
if (!Check(damage && damage.GetDefaultHitZone(), "actual gate has its inherited multiphase destruction component and hit zone")) return -1;
PrintFormat("ERR PRISON BREACH DAMAGE: health=%1 enabled=%2 threshold=%3", damage.GetHealth(), damage.IsDamageHandlingEnabled(), damage.GetTotalDestructionThreshold());
vector hit[3]; hit[0] = gate.GetOrigin(); hit[1] = "0 0 1"; hit[2] = "0 0 -1";
SCR_DamageContext context = new SCR_DamageContext(EDamageType.TRUE, 100000, hit, gate, damage.GetDefaultHitZone(), Instigator.CreateInstigator(m_Player), null, -1, -1);
damage.HandleDamage(context);
if (damage)
{
if (!Check(damage.GetDefaultHitZone() != null, "destructible gate has its native hit zone")) return -1;
PrintFormat("ERR PRISON BREACH DAMAGE: health=%1 enabled=%2 threshold=%3", damage.GetHealth(), damage.IsDamageHandlingEnabled(), damage.GetTotalDestructionThreshold());
vector hit[3]; hit[0] = gate.GetOrigin(); hit[1] = "0 0 1"; hit[2] = "0 0 -1";
SCR_DamageContext context = new SCR_DamageContext(EDamageType.TRUE, 100000, hit, gate, damage.GetDefaultHitZone(), Instigator.CreateInstigator(m_Player), null, -1, -1);
damage.HandleDamage(context);
}
else
{
// The original framed metal door is not destructible. Exercise actual
// entity loss explicitly; this is not evidence of a damage-driven breach.
Print("ERR PRISON BREACH: deleting non-destructible original metal door for gate-loss fixture");
SCR_EntityHelper.DeleteEntityAndChildren(gate);
}
m_Step = 2; m_Wait = now + 1500;
return 0;
}
@@ -116,7 +126,7 @@ class ERR_NativePrisonBreachProbe
bool refusedLate = !late.Registered() && !late.GateLost();
late.Stop();
if (!Check(refusedLate, "fresh registration after native gate loss stays unregistered and cannot grant escape")) return -1;
if (!Check(m_Mode.Phase() == ERR_Phase.PRISON && m_Transitions == 0, "native gate destruction alone cannot advance the round")) return -1;
if (!Check(m_Mode.Phase() == ERR_Phase.PRISON && m_Transitions == 0, "native gate loss alone cannot advance the round")) return -1;
Move("1.5 0 -60");
m_Step = 3; m_Wait = now + 2500;
return 0;
@@ -15,7 +15,7 @@ class ERR_Objectives
{
if (!Replication.IsServer() || m_Stopped) return;
SCR_TaskSystem tasks = SCR_TaskSystem.GetInstance();
if (!tasks) { Print("Rereintroduced: native task system missing", LogLevel.ERROR); return; }
if (!tasks) { Print("Reintroduced: native task system missing", LogLevel.ERROR); return; }
if (m_Current)
{
SCR_ETaskState result = SCR_ETaskState.COMPLETED;
@@ -27,7 +27,7 @@ class ERR_Objectives
string title = ERR_ObjectiveText.Title(current);
if (title.IsEmpty()) return;
m_Current = tasks.CreateTask("{1D0F815858EE24AD}Prefabs/Tasks/BaseTask.et", Id(current), title, Description());
if (!m_Current) { Print("Rereintroduced: objective creation failed", LogLevel.ERROR); return; }
if (!m_Current) { Print("Reintroduced: objective creation failed", LogLevel.ERROR); return; }
// Location markers use the discovery registry. A task cannot expose an unknown site.
tasks.SetTaskUIVisibility(m_Current, SCR_ETaskUIVisibility.LIST_ONLY);
m_Mode.Own(m_Current);
@@ -11,6 +11,22 @@ modded class SCR_RespawnSystemComponent
protected bool m_ERRPreparationSpinnerWasVisible;
protected string m_ERRPreparationLastTitle;
protected string m_ERRPreparationLastDescription;
protected bool m_ERRKeepSpawnSceneLoading;
void ERR_KeepSpawnSceneLoading(bool keep) { m_ERRKeepSpawnSceneLoading = keep; }
void ERR_ShowSpawnSceneLoading()
{
ERR_GameMode mode = ERR_GameMode.Get();
if (System.IsConsoleApp() || !mode || m_pGameMode != mode) return;
if (!m_wLoadingPlaceholder) CreateLoadingPlaceholder();
UpdateLoadingPlaceholder(0);
}
protected string ERR_PreparationDescription(ERR_GameMode mode)
{
if (mode.Phase() == ERR_Phase.PREPARING) return ERR_ObjectiveText.Description(mode, false);
return "Please wait while your spawn area and equipment finish loading. You will take control when preparation is complete.";
}
protected void ERR_ClearPreparationStatus()
{
@@ -32,7 +48,10 @@ modded class SCR_RespawnSystemComponent
super.UpdateLoadingPlaceholder(dt);
// Vanilla OnInit(IEntity owner) stores its owning mode in m_pGameMode.
ERR_GameMode mode = ERR_GameMode.Get();
if (System.IsConsoleApp() || !mode || m_pGameMode != mode || mode.Phase() != ERR_Phase.PREPARING || !m_wLoadingPlaceholder)
// Server preparation can finish before this client's spawn-area preload.
// Keep the message on the native black placeholder until the native spawn
// response (or spectator handoff) destroys it, including late arrivals.
if (System.IsConsoleApp() || !mode || m_pGameMode != mode || (mode.Phase() != ERR_Phase.PREPARING && !mode.Active()) || !m_wLoadingPlaceholder)
{
ERR_ClearPreparationStatus();
return;
@@ -52,7 +71,7 @@ modded class SCR_RespawnSystemComponent
bool failed = !mode.PreparationError().IsEmpty();
string title = "Preparing mission";
if (failed) title = "Mission could not start";
string description = ERR_ObjectiveText.Description(mode, false);
string description = ERR_PreparationDescription(mode);
if (title != m_ERRPreparationLastTitle)
{
m_ERRPreparationTitle.SetText(title);
@@ -78,6 +97,7 @@ modded class SCR_RespawnSystemComponent
override void DestroyLoadingPlaceholder()
{
if (m_ERRKeepSpawnSceneLoading) return;
ERR_ClearPreparationStatus();
super.DestroyLoadingPlaceholder();
}
@@ -97,7 +117,7 @@ modded class SCR_RespawnSystemComponent
if (!m_ERRPreparationRoot || m_ERRPreparationParent != m_wLoadingPlaceholder || m_ERRPreparationRoot.GetParent() != m_wLoadingPlaceholder || !m_ERRPreparationRoot.IsVisible()) return false;
if (!ERR_PreparationWidgetVisible(m_ERRPreparationTitle) || !ERR_PreparationWidgetVisible(m_ERRPreparationDescription) || !ERR_PreparationWidgetVisible(m_ERRPreparationRoot)) return false;
string title = "Preparing mission"; if (failed) title = "Mission could not start";
if (m_ERRPreparationTitle.GetText() != title || m_ERRPreparationDescription.GetText() != ERR_ObjectiveText.Description(mode, false)) return false;
if (m_ERRPreparationTitle.GetText() != title || m_ERRPreparationDescription.GetText() != ERR_PreparationDescription(mode)) return false;
if (failed) return m_ERRPreparationSpinner && m_ERRPreparationSpinnerHidden && !m_ERRPreparationSpinner.IsVisible();
return !m_ERRPreparationSpinnerHidden;
}
@@ -0,0 +1,79 @@
// Match the original opening: physical enemies exist for preparation/streaming,
// but their character AI stays disabled until the prison objective completes.
// Scope the hooks to this mission and its enemy faction, including RHS units.
modded class SCR_ChimeraAIAgent
{
protected ERR_GameMode m_ERRPrisonMode;
void ERR_HoldForPrison()
{
if (!Replication.IsServer() || SCR_Global.IsEditMode()) return;
ERR_GameMode mode = ERR_GameMode.Get();
IEntity actor = GetControlledEntity();
if (!mode || mode.Phase() > ERR_Phase.PRISON || !actor || ERR_Actors.Player(actor) || IsPlayerPending_S()) return;
if (!mode.EnemyFaction() || SCR_FactionManager.SGetFaction(actor) != mode.EnemyFaction()) return;
DeactivateAI();
if (m_ERRPrisonMode) return;
m_ERRPrisonMode = mode;
mode.OnPhaseChanged().Insert(ERR_PrisonPhaseChanged);
mode.OnStop().Insert(ERR_PrisonStopped);
}
protected void ERR_PrisonUnsubscribe()
{
if (!m_ERRPrisonMode) return;
m_ERRPrisonMode.OnPhaseChanged().Remove(ERR_PrisonPhaseChanged);
m_ERRPrisonMode.OnStop().Remove(ERR_PrisonStopped);
m_ERRPrisonMode = null;
}
protected void ERR_PrisonPhaseChanged(ERR_Phase previous, ERR_Phase current)
{
if (current != ERR_Phase.RADIO_SEARCH) return;
ERR_PrisonUnsubscribe();
IEntity actor = GetControlledEntity();
if (actor && !ERR_Actors.Player(actor) && !IsPlayerPending_S() && ERR_Actors.CanFight(actor)) ActivateAI();
}
protected void ERR_PrisonStopped()
{
ERR_PrisonUnsubscribe();
// Aborted preparation/end of round must not wake the waiting enemies.
DeactivateAI();
}
override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
ERR_HoldForPrison();
// Native group population activates the character after its init callback.
if (Replication.IsServer() && !SCR_Global.IsEditMode()) GetGame().GetCallqueue().Call(ERR_HoldForPrison);
}
override void OnLifeStateChanged(ECharacterLifeState previousLifeState, ECharacterLifeState newLifeState)
{
super.OnLifeStateChanged(previousLifeState, newLifeState);
ERR_HoldForPrison();
}
void ~SCR_ChimeraAIAgent()
{
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_HoldForPrison);
ERR_PrisonUnsubscribe();
}
}
modded class SCR_AIGroup
{
override bool AddAIEntityToGroup(IEntity entity)
{
bool added = super.AddAIEntityToGroup(entity);
if (added)
{
AIControlComponent control = AIControlComponent.Cast(entity.FindComponent(AIControlComponent));
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(control.GetControlAIAgent());
if (agent) agent.ERR_HoldForPrison();
}
return added;
}
}
@@ -73,7 +73,7 @@ class ERR_PrisonEscapeService
{
ResourceName raw;
if (entity.GetPrefabData()) raw = entity.GetPrefabData().GetPrefabName();
PrintFormat("Rereintroduced [prison]: gate candidate raw=%1 resolved=%2 lock=%3 door=%4 position=%5", raw, SCR_ResourceNameUtils.GetPrefabName(entity), lock, door, entity.GetOrigin());
PrintFormat("Reintroduced [prison]: gate candidate raw=%1 resolved=%2 lock=%3 door=%4 position=%5", raw, SCR_ResourceNameUtils.GetPrefabName(entity), lock, door, entity.GetOrigin());
}
if (entity != prison && lock && door) { found = entity; matches++; }
IEntity child = entity.GetChildren();
@@ -112,25 +112,28 @@ class ERR_PrisonEscapeService
if (prisons != 1) return;
ResourceName raw;
if (prison.GetPrefabData()) raw = prison.GetPrefabData().GetPrefabName();
PrintFormat("Rereintroduced [prison]: owned composition raw=%1 resolved=%2 position=%3", raw, SCR_ResourceNameUtils.GetPrefabName(prison), prison.GetOrigin());
PrintFormat("Reintroduced [prison]: owned composition raw=%1 resolved=%2 position=%3", raw, SCR_ResourceNameUtils.GetPrefabName(prison), prison.GetOrigin());
IEntity found;
int gates;
bool bounded = FindGate(prison, found, gates);
SCR_DestructionMultiPhaseComponent damage;
if (found) damage = SCR_DestructionMultiPhaseComponent.Cast(found.FindComponent(SCR_DestructionMultiPhaseComponent));
bool intact = damage && damage.GetDefaultHitZone() && !damage.GetDestroyed() && !damage.IsDestructionQueued();
if (damage) PrintFormat("Rereintroduced [prison]: destruction health=%1 destroyed=%2 queued=%3", damage.GetHealth(), damage.GetDestroyed(), damage.IsDestructionQueued());
// The original framed metal door has no destruction component. It is still
// an intact physical lock; deletion/crossing remain observable below.
bool intact = found != null;
if (damage) intact = damage.GetDefaultHitZone() && !damage.GetDestroyed() && !damage.IsDestructionQueued();
if (damage) PrintFormat("Reintroduced [prison]: destruction health=%1 destroyed=%2 queued=%3", damage.GetHealth(), damage.GetDestroyed(), damage.IsDestructionQueued());
if (!ERR_PrisonEscapeRules.Registration(true, gates, bounded, intact))
{
m_RegistrationReason = string.Format("gate registration refused: candidates=%1 bounded=%2 intact=%3", gates, bounded, intact);
Print("Rereintroduced [prison]: " + m_RegistrationReason);
Print("Reintroduced [prison]: " + m_RegistrationReason);
return;
}
m_Gate = found;
m_Center = prison.GetOrigin();
m_Registered = true;
m_RegistrationReason = "one intact successor lock in owned prison";
Print("Rereintroduced [prison]: " + m_RegistrationReason);
Print("Reintroduced [prison]: " + m_RegistrationReason);
}
void RosterChanged(int connection)
{
@@ -1,4 +1,4 @@
[ComponentEditorProps(category: "Rereintroduced", description: "Authored scenario definition and finite population lifecycle")]
[ComponentEditorProps(category: "Reintroduced", description: "Authored scenario definition and finite population lifecycle")]
class ERR_ScenarioComponentClass : ScriptComponentClass {}
class ERR_ScenarioComponent : ScriptComponent
{
@@ -75,7 +75,7 @@ class ERR_Settings
else if (FileIO.FileExists(m_Path) || FileIO.FileExists(m_Path + ".bak"))
{
m_ReadOnly = true;
Print("Rereintroduced: invalid settings; defaults active, existing files preserved", LogLevel.ERROR);
Print("Reintroduced: invalid settings; defaults active, existing files preserved", LogLevel.ERROR);
}
}
@@ -44,6 +44,28 @@ class ERR_SiteService
void ERR_SiteService(ERR_GameMode mode) { m_Mode = mode; }
int Count() { return m_Sites.Count(); }
bool Owns(IEntity entity) { return entity && m_Entities.Contains(entity); }
// Snapshot only nearby mission-owned objects. Distant sites must not block
// arrival, and vanilla map streaming remains the native preload's job.
bool SpawnScene(vector position, out array<IEntity> result)
{
result = {};
array<IEntity> pending = {};
foreach (IEntity root : m_Entities)
if (root && vector.DistanceXZ(root.GetOrigin(), position) <= 120) pending.Insert(root);
int visited;
while (!pending.IsEmpty())
{
if (++visited > 512) return false;
IEntity entity = pending[0]; pending.Remove(0);
if (!entity) continue;
RplComponent component = RplComponent.Cast(entity.FindComponent(RplComponent));
if (component && !result.Contains(entity)) result.Insert(entity);
if (result.Count() > 256) return false;
IEntity child = entity.GetChildren();
while (child) { pending.Insert(child); child = child.GetSibling(); }
}
return true;
}
int TransportCount()
{
int count;
@@ -75,7 +97,7 @@ class ERR_SiteService
m_Obstructed = false;
vector ground = ERR_Actors.Ground(position);
GetGame().GetWorld().QueryEntitiesByAABB(ground + "-1.3 0.1 -1.3", ground + "1.3 2 1.3", CheckClearance);
PrintFormat("Rereintroduced [sites]: cache clearance position=%1 clear=%2", ground, !m_Obstructed);
PrintFormat("Reintroduced [sites]: cache clearance position=%1 clear=%2", ground, !m_Obstructed);
return !m_Obstructed;
}
protected bool CreateGuardOrders(ERR_SiteRuntime site, vector anchor)
@@ -149,7 +171,7 @@ class ERR_SiteService
vector high = Vector(position[0] + radius, highest + 3, position[2] + radius);
GetGame().GetWorld().QueryEntitiesByAABB(low, high, CheckClearance);
bool clear = lowest >= 1 && highest - lowest <= maximumRange && !m_Obstructed;
PrintFormat("Rereintroduced [sites]: transport parking=%1 radius=%2 terrainRange=%3 obstructed=%4 clear=%5", ERR_Actors.Ground(position), radius, highest - lowest, m_Obstructed, clear);
PrintFormat("Reintroduced [sites]: transport parking=%1 radius=%2 terrainRange=%3 obstructed=%4 clear=%5", ERR_Actors.Ground(position), radius, highest - lowest, m_Obstructed, clear);
return clear;
}
protected bool CreateTransport(ERR_SiteRuntime site, vector sitePosition)
@@ -181,13 +203,13 @@ class ERR_SiteService
float radius = site.Definition.Transport.ClearanceRadius - 0.5;
if (Math.AbsFloat(low[0] - center[0]) > radius || Math.AbsFloat(high[0] - center[0]) > radius || Math.AbsFloat(low[2] - center[2]) > radius || Math.AbsFloat(high[2] - center[2]) > radius)
{
if (GetGame().GetWorld().GetWorldTime() > m_Deadline) PrintFormat("Rereintroduced [sites]: transport footprint low=%1 high=%2 surveyedCenter=%3 origin=%4 allowedRadius=%5", low, high, center, vehicle.GetOrigin(), radius);
if (GetGame().GetWorld().GetWorldTime() > m_Deadline) PrintFormat("Reintroduced [sites]: transport footprint low=%1 high=%2 surveyedCenter=%3 origin=%4 allowedRadius=%5", low, high, center, vehicle.GetOrigin(), radius);
return false;
}
FuelManagerComponent fuel = FuelManagerComponent.Cast(vehicle.FindComponent(FuelManagerComponent));
if (!fuel || !fuel.HasFuel())
{
if (GetGame().GetWorld().GetWorldTime() > m_Deadline) PrintFormat("Rereintroduced [sites]: transport fuel manager=%1 hasFuel=%2", fuel != null, fuel && fuel.HasFuel());
if (GetGame().GetWorld().GetWorldTime() > m_Deadline) PrintFormat("Reintroduced [sites]: transport fuel manager=%1 hasFuel=%2", fuel != null, fuel && fuel.HasFuel());
return false;
}
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(vehicle.FindComponent(BaseCompartmentManagerComponent));
@@ -202,13 +224,13 @@ class ERR_SiteService
if (type == ECompartmentType.PILOT) driver = true;
if (type == ECompartmentType.CARGO) passenger = true;
}
if ((!driver || !passenger) && GetGame().GetWorld().GetWorldTime() > m_Deadline) PrintFormat("Rereintroduced [sites]: transport seats=%1 driver=%2 passenger=%3", seats.Count(), driver, passenger);
if ((!driver || !passenger) && GetGame().GetWorld().GetWorldTime() > m_Deadline) PrintFormat("Reintroduced [sites]: transport seats=%1 driver=%2 passenger=%3", seats.Count(), driver, passenger);
return driver && passenger;
}
protected bool Fail(string reason)
{
m_FailureReason = reason;
Print("Rereintroduced [sites]: preparation failed: " + reason, LogLevel.ERROR);
Print("Reintroduced [sites]: preparation failed: " + reason, LogLevel.ERROR);
Rollback();
m_State = ERR_PrepareResult.FAILED;
return false;
@@ -301,7 +323,7 @@ class ERR_SiteService
if (member.IsEmpty()) return Fail("guard profile has an empty member resource: " + spec.Id);
if (!CreateGuardOrders(site, guardPosition)) return Fail("guard initial orders: " + spec.Id);
}
PrintFormat("Rereintroduced [sites]: authored %1 at %2", spec.Id, position);
PrintFormat("Reintroduced [sites]: authored %1 at %2", spec.Id, position);
}
return true;
}
@@ -340,7 +362,7 @@ class ERR_SiteService
bool complete = site.Group.IsExpandComplete();
if (site.LastInitialPhysical != physical.Count() || site.LastInitialConscious != conscious)
{
PrintFormat("Rereintroduced [guards]: initial site=%1 requested=%2 physical=%3 conscious=%4 nativeComplete=%5", site.Definition.Id, site.InitialGuardCount, physical.Count(), conscious, complete);
PrintFormat("Reintroduced [guards]: initial site=%1 requested=%2 physical=%3 conscious=%4 nativeComplete=%5", site.Definition.Id, site.InitialGuardCount, physical.Count(), conscious, complete);
site.LastInitialPhysical = physical.Count();
site.LastInitialConscious = conscious;
}
@@ -356,7 +378,7 @@ class ERR_SiteService
tileLoaded = navmesh.IsTileLoaded(site.Group.GetOrigin());
tileRequested = navmesh.IsTileRequested(site.Group.GetOrigin());
}
PrintFormat("Rereintroduced [guards]: timeout detail site=%1 navmesh=%2 tileLoaded=%3 tileRequested=%4 dormantAlive=%5 dormantDead=%6", site.Definition.Id, navmesh != null, tileLoaded, tileRequested, site.Group.GetDormantAliveCount(), site.Group.GetDormantDeadCount());
PrintFormat("Reintroduced [guards]: timeout detail site=%1 navmesh=%2 tileLoaded=%3 tileRequested=%4 dormantAlive=%5 dormantDead=%6", site.Definition.Id, navmesh != null, tileLoaded, tileRequested, site.Group.GetDormantAliveCount(), site.Group.GetDormantDeadCount());
Fail(string.Format("initial guard population incomplete or invalid: site=%1 requested=%2 physical=%3 conscious=%4 nativeComplete=%5; limit=90s; no replacement requested", site.Definition.Id, site.InitialGuardCount, physical.Count(), conscious, complete));
}
return result;
@@ -419,7 +441,7 @@ class ERR_SiteService
if (pendingGuards > 0 || aircraftPlacement == ERR_PrepareResult.PENDING) return m_State;
foreach (IEntity entity : m_Entities) m_Mode.Own(entity);
m_State = ERR_PrepareResult.READY;
PrintFormat("Rereintroduced [sites]: %1 sites and %2 physical stock items ready", m_Sites.Count(), m_Placements.Count());
PrintFormat("Reintroduced [sites]: %1 sites and %2 physical stock items ready", m_Sites.Count(), m_Placements.Count());
m_Placements.Clear();
return m_State;
}
@@ -0,0 +1,139 @@
// A native resource preload can finish before runtime replication delivers the
// prison. Keep the existing admission pending until the owner resolves every
// required replication node. No client can choose the server's spawn contents.
modded class SCR_SpawnRequestComponent
{
protected int m_ERRSceneToken;
protected bool m_ERRScenePending;
protected bool m_ERRSceneAcknowledged;
protected bool m_ERRSceneTimedOut;
protected float m_ERRSceneDeadline;
protected ref array<IEntity> m_ERRSceneEntities = {};
protected int m_ERRClientSceneToken;
protected bool m_ERRClientScenePending;
protected ref array<RplId> m_ERRClientSceneNodes = {};
protected void ERR_StreamScene(IEntity entity, bool normalStreaming)
{
if (!entity || !GetPlayerController()) return;
RplIdentity identity = GetPlayerController().GetRplIdentity();
if (!identity.IsValid() || identity == RplIdentity.Local()) return;
RplComponent component = RplComponent.Cast(entity.FindComponent(RplComponent));
if (component) component.EnableStreamingConNode(identity, normalStreaming);
}
void ERR_BeginSpawnScene(array<IEntity> entities, bool valid)
{
if (!Replication.IsServer()) return;
ERR_EndSpawnScene();
m_ERRSceneToken++;
m_ERRScenePending = true;
m_ERRSceneDeadline = GetGame().GetWorld().GetWorldTime() + 60000;
array<RplId> nodes = {};
foreach (IEntity entity : entities)
{
RplComponent component;
if (entity) component = RplComponent.Cast(entity.FindComponent(RplComponent));
if (!component || component.Id() == RplId.Invalid()) { valid = false; break; }
if (nodes.Contains(component.Id())) continue;
nodes.Insert(component.Id());
m_ERRSceneEntities.Insert(entity);
ERR_StreamScene(entity, false);
}
if (!valid)
{
m_ERRSceneTimedOut = true;
Print("Reintroduced [spawn scene]: invalid server scene snapshot; admission withheld", LogLevel.ERROR);
return;
}
PrintFormat("Reintroduced [spawn scene]: player=%1 token=%2 waiting for %3 replicated objects", GetPlayerId(), m_ERRSceneToken, nodes.Count());
Rpc(ERR_ReceiveSpawnScene, m_ERRSceneToken, nodes);
}
bool ERR_SpawnSceneReady()
{
return m_ERRScenePending && m_ERRSceneAcknowledged && !m_ERRSceneTimedOut;
}
bool ERR_SpawnSceneMayFinish()
{
if (!m_ERRScenePending) return true;
if (!m_ERRSceneAcknowledged && !m_ERRSceneTimedOut && GetGame().GetWorld().GetWorldTime() >= m_ERRSceneDeadline)
{
m_ERRSceneTimedOut = true;
PrintFormat("Reintroduced [spawn scene]: player=%1 token=%2 timed out; refusing assignment and retrying", GetPlayerId(), m_ERRSceneToken);
}
// A timeout reaches the existing failure/actor-cleanup path, never assignment.
return m_ERRSceneAcknowledged || m_ERRSceneTimedOut;
}
void ERR_EndSpawnScene()
{
if (!Replication.IsServer()) return;
foreach (IEntity entity : m_ERRSceneEntities) ERR_StreamScene(entity, true);
m_ERRSceneEntities.Clear();
if (m_ERRScenePending) Rpc(ERR_CancelClientSpawnScene, m_ERRSceneToken);
m_ERRScenePending = false;
m_ERRSceneAcknowledged = false;
m_ERRSceneTimedOut = false;
}
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_ReceiveSpawnScene(int token, array<RplId> nodes)
{
ERR_ClearClientSpawnScene();
m_ERRClientSceneToken = token;
m_ERRClientScenePending = true;
m_ERRClientSceneNodes.Copy(nodes);
SCR_RespawnSystemComponent respawn = SCR_RespawnSystemComponent.GetInstance();
if (respawn) respawn.ERR_ShowSpawnSceneLoading();
PrintFormat("Reintroduced [spawn scene]: client token=%1 waiting for %2 replicated objects", token, nodes.Count());
GetGame().GetCallqueue().CallLater(ERR_CheckClientSpawnScene, 100, true);
}
protected void ERR_CheckClientSpawnScene()
{
if (!m_ERRClientScenePending || !ERR_GameMode.Get()) { ERR_ClearClientSpawnScene(); return; }
foreach (RplId id : m_ERRClientSceneNodes)
{
RplComponent component = RplComponent.Cast(Replication.FindItem(id));
if (!component || !component.GetEntity()) return;
}
int token = m_ERRClientSceneToken;
PrintFormat("Reintroduced [spawn scene]: client token=%1 all %2 replicated objects present", token, m_ERRClientSceneNodes.Count());
ERR_ClearClientSpawnScene();
Rpc(ERR_AcknowledgeSpawnScene, token);
}
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
protected void ERR_AcknowledgeSpawnScene(int token)
{
if (!Replication.IsServer() || !m_ERRScenePending || m_ERRSceneTimedOut || token != m_ERRSceneToken) return;
m_ERRSceneAcknowledged = true;
}
protected void ERR_ClearClientSpawnScene()
{
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_CheckClientSpawnScene);
m_ERRClientScenePending = false;
m_ERRClientSceneNodes.Clear();
}
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_CancelClientSpawnScene(int token)
{
if (token == m_ERRClientSceneToken) ERR_ClearClientSpawnScene();
}
protected override void Rpc_SendResponse_O(SCR_ESpawnResult response)
{
ERR_ClearClientSpawnScene();
SCR_RespawnSystemComponent respawn = SCR_RespawnSystemComponent.GetInstance();
ERR_GameMode mode = ERR_GameMode.Get();
bool retry = mode && mode.Active() && response != SCR_ESpawnResult.OK;
if (respawn) respawn.ERR_KeepSpawnSceneLoading(retry);
super.Rpc_SendResponse_O(response);
if (respawn) respawn.ERR_KeepSpawnSceneLoading(false);
}
void ~SCR_SpawnRequestComponent()
{
if (!GetGame()) return;
ERR_ClearClientSpawnScene();
ERR_EndSpawnScene();
}
}
@@ -28,6 +28,7 @@ modded class SCR_SpawnRequestComponent
void ERR_CancelAdmission(SCR_SpawnData data, bool notifyOwner)
{
if (!GetGame()) return;
ERR_EndSpawnScene();
GetGame().GetCallqueue().Remove(AwaitFinalization_S);
if (!Replication.IsServer()) return;
if (notifyOwner && GetPlayerController() && GetRespawnComponent()) SendResponse_S(SCR_ESpawnResult.SPAWN_NOT_ALLOWED, data);
@@ -36,6 +37,23 @@ modded class SCR_SpawnRequestComponent
modded class SCR_FreeSpawnHandlerComponent
{
override void OnFinalizeBegin_S(SCR_SpawnRequestComponent requestComponent, SCR_SpawnData data, IEntity entity)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (mode && requestComponent && data && entity)
{
array<IEntity> scene = {};
bool valid = mode.Sites() && mode.Sites().SpawnScene(data.GetPosition(), scene);
scene.Insert(entity);
requestComponent.ERR_BeginSpawnScene(scene, valid);
}
super.OnFinalizeBegin_S(requestComponent, data, entity);
}
override bool CanFinalize_S(SCR_SpawnRequestComponent requestComponent, SCR_SpawnData data, IEntity entity)
{
if (ERR_GameMode.Get() && requestComponent && !requestComponent.ERR_SpawnSceneMayFinish()) return false;
return super.CanFinalize_S(requestComponent, data, entity);
}
protected ERR_SpawnLogic ERR_AdmissionLogic()
{
SCR_RespawnSystemComponent system = GetRespawnSystemComponent();
@@ -72,12 +90,14 @@ modded class SCR_FreeSpawnHandlerComponent
if (logic) ticket = logic.TakeSpawnTicket(requestComponent, data, entity);
// No ticket means no ownership proof: reject without deleting arbitrary input.
if (!ticket) return SCR_ESpawnResult.SPAWN_NOT_ALLOWED;
if (!logic.MayFinalizeSpawn(ticket))
if (!requestComponent.ERR_SpawnSceneReady() || !logic.MayFinalizeSpawn(ticket))
{
requestComponent.ERR_EndSpawnScene();
ERR_RejectOwnedActor(requestComponent, entity, data);
return SCR_ESpawnResult.SPAWN_NOT_ALLOWED;
}
SCR_ESpawnResult result = super.FinalizeRequest_S(requestComponent, data, entity);
requestComponent.ERR_EndSpawnScene();
if (result != SCR_ESpawnResult.OK) ERR_RejectOwnedActor(requestComponent, entity, data);
return result;
}
@@ -79,7 +79,7 @@ class ERR_SpawnLogic : SCR_SpawnLogic
}
if (!mode || !mode.Players().Bind(playerId, identity))
{
Print("Rereintroduced: player identity could not be bound; spawn withheld", LogLevel.ERROR);
Print("Reintroduced: player identity could not be bound; spawn withheld", LogLevel.ERROR);
return;
}
if (resultController)
@@ -331,7 +331,7 @@ class ERR_SpawnLogic : SCR_SpawnLogic
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());
PrintFormat("Reintroduced [spawn]: player %1 at %2", playerId, entity.GetOrigin());
}
override void OnPlayerDisconnected_S(int playerId, KickCauseCode cause, int timeout)
{

Some files were not shown because too many files have changed in this diff Show More