Add configurable main-road checkpoints and reconnect replacements; record Workshop 0.0.136
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -3,7 +3,7 @@ class ERR_AdminMenuFields
|
||||
{
|
||||
static array<string> Keys()
|
||||
{
|
||||
return {"aiDoctrine", "aiShadow", "automaticEquipmentChancePercent", "sarkaAngerPerCar", "motorbikeCount", "droneReconCount", "droneCooldownSeconds", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "garageVehicleCount", "garageChancePercent", "garageRadiusMetres", "garageSpawnIntervalSeconds", "emptyRestartHours", "startMode", "radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "radioIntelSiteCount", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount", "antiAirChancePercent", "antiPersonnelMineChancePercent", "heliTimerMinutes"};
|
||||
return {"aiDoctrine", "aiShadow", "automaticEquipmentChancePercent", "sarkaAngerPerCar", "motorbikeCount", "droneReconCount", "droneCooldownSeconds", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "largeRoadblockCount", "mainRoadWidthMetres", "civilianTrafficCount", "garageVehicleCount", "garageChancePercent", "garageRadiusMetres", "garageSpawnIntervalSeconds", "emptyRestartHours", "startMode", "radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "radioIntelSiteCount", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount", "antiAirChancePercent", "antiPersonnelMineChancePercent", "heliTimerMinutes"};
|
||||
}
|
||||
static bool Available(string key, bool hq, bool intel)
|
||||
{
|
||||
@@ -63,6 +63,8 @@ class ERR_AdminMenuFields
|
||||
else if (key == "fieldCampCount") prefix = "mission camps";
|
||||
else if (key == "fieldHospitalCount") prefix = "mission hospitals";
|
||||
else if (key == "roadblockCount") prefix = "mission roadblocks";
|
||||
else if (key == "largeRoadblockCount") prefix = "mission largeroadblocks";
|
||||
else if (key == "mainRoadWidthMetres") prefix = "mission mainroadwidth";
|
||||
else if (key == "emptyRestartHours") prefix = "mission emptyhours";
|
||||
else if (key == "garageChancePercent") prefix = "mission garagechance";
|
||||
else if (key == "garageRadiusMetres") prefix = "mission garageradius";
|
||||
|
||||
@@ -27,17 +27,18 @@ enum ERR_AdminPage
|
||||
RADIO_INTEL = 23,
|
||||
TELEMETRY = 24,
|
||||
VEHICLES = 25,
|
||||
CHECKPOINTS = 26,
|
||||
}
|
||||
|
||||
class ERR_AdminMenuPages
|
||||
{
|
||||
static array<string> Pages() { return {"SettingsPage", "HQPage", "JournalPage", "ModsPage", "FactionsPage", "WorldPage", "IntelPage", "StatusPage", "TerrainsPage", "ItemsPage", "TTSPage", "ForcesPage", "GuardsPage", "SitesPage", "PlanePage", "RecommendationsPage", "WeightsPage", "AllowedItemsPage", "ItemPresetsPage", "AIPage", "StarterPage", "SarkaPage", "DronesPage", "RadioIntelPage", "TelemetryPage", "VehiclesPage"}; }
|
||||
static array<string> Navigation() { return {"OpenSettings", "OpenHQ", "OpenJournal", "OpenMods", "OpenFactions", "OpenWorld", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems", "OpenTTS", "OpenForces", "OpenGuards", "OpenSites", "OpenPlane", "OpenRecommendations", "OpenWeights", "OpenAllowedItems", "OpenItemPresets", "OpenAI", "OpenStarter", "OpenSarka", "OpenDrones", "OpenRadioIntel", "OpenTelemetry", "OpenVehicles"}; }
|
||||
static array<string> Pages() { return {"SettingsPage", "HQPage", "JournalPage", "ModsPage", "FactionsPage", "WorldPage", "IntelPage", "StatusPage", "TerrainsPage", "ItemsPage", "TTSPage", "ForcesPage", "GuardsPage", "SitesPage", "PlanePage", "RecommendationsPage", "WeightsPage", "AllowedItemsPage", "ItemPresetsPage", "AIPage", "StarterPage", "SarkaPage", "DronesPage", "RadioIntelPage", "TelemetryPage", "VehiclesPage", "CheckpointsPage"}; }
|
||||
static array<string> Navigation() { return {"OpenSettings", "OpenHQ", "OpenJournal", "OpenMods", "OpenFactions", "OpenWorld", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems", "OpenTTS", "OpenForces", "OpenGuards", "OpenSites", "OpenPlane", "OpenRecommendations", "OpenWeights", "OpenAllowedItems", "OpenItemPresets", "OpenAI", "OpenStarter", "OpenSarka", "OpenDrones", "OpenRadioIntel", "OpenTelemetry", "OpenVehicles", "OpenCheckpoints"}; }
|
||||
static array<string> MainButtons() { return {"Main_Mission", "Main_Forces", "Main_HQ", "Main_Intel", "Main_Items", "Main_Mods", "Main_Audio", "Main_Status"}; }
|
||||
static array<string> Sidebars() { return {"Sidebar_Mission", "Sidebar_Forces", "Sidebar_HQ", "Sidebar_Intel", "Sidebar_Items", "Sidebar_Mods", "Sidebar_Audio", "Sidebar_Status"}; }
|
||||
static int Group(int page)
|
||||
{
|
||||
array<int> groups = {0, 2, 7, 5, 1, 0, 3, 7, 0, 4, 6, 1, 1, 1, 2, 5, 4, 4, 4, 2, 4, 1, 2, 3, 0, 0};
|
||||
array<int> groups = {0, 2, 7, 5, 1, 0, 3, 7, 0, 4, 6, 1, 1, 1, 2, 5, 4, 4, 4, 2, 4, 1, 2, 3, 0, 0, 1};
|
||||
if (page < 0 || page >= groups.Count()) return -1;
|
||||
return groups[page];
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ class ERR_CommandArguments
|
||||
if (section == "mission" && option == "objectguards") return "objectGuardCount";
|
||||
if (section == "mission" && option == "hunterdistance") return "hunterSpawnDistanceMetres";
|
||||
if (section == "mission" && option == "roadblocks") return "roadblockCount";
|
||||
if (section == "mission" && option == "largeroadblocks") return "largeRoadblockCount";
|
||||
if (section == "mission" && option == "mainroadwidth") return "mainRoadWidthMetres";
|
||||
if (section == "mission" && option == "garagechance") return "garageChancePercent";
|
||||
if (section == "mission" && option == "garageradius") return "garageRadiusMetres";
|
||||
if (section == "mission" && option == "garageinterval") return "garageSpawnIntervalSeconds";
|
||||
@@ -194,7 +196,9 @@ class ERR_AdminService
|
||||
if (key == "objectGuardHQMode") return "0: all hold their posts; 1: half hold, half respond to HQ; 2: all may respond to HQ. All report contacts. Next round.";
|
||||
if (key == "objectGuardCount") return "0..16 soldiers per factory/industrial, military or infrastructure site; default 8, zero off. Next round. HQ participation is configured separately; all report. Towers and chimneys use suitable platforms. Lost guards are not replaced.";
|
||||
if (key == "hunterSpawnDistanceMetres") return "Opening UAZ and troop-truck patrol placement. Applies next round.";
|
||||
if (key == "roadblockCount") return "Set 0..30 roadblocks (default 4; 0 disables). Applies next round. Each has four guards; unsuitable sites are skipped.";
|
||||
if (key == "largeRoadblockCount") return "Set 0..30 main-road checkpoints (default 4; 0 disables large posts). Eight guards per post. Applies next round; unsuitable sites are skipped.";
|
||||
if (key == "mainRoadWidthMetres") return "Set main-road minimum width to 5..12 metres (default 6). Wider roads use large checkpoints; narrower roads use small ones. Applies next round.";
|
||||
if (key == "roadblockCount") return "Set 0..30 small-road checkpoints (default 4; 0 disables small posts). Four guards per post. Applies next round; unsuitable sites are skipped.";
|
||||
if (key == "telemetryEnabled") return "Default on. Sends mission results and player identities to the configured statistics service. Disabling clears unsent events immediately.";
|
||||
if (key == "restartOnEmpty") return "On (1) restarts after the last disconnect; Off (0) preserves the round for reconnects. Default On. Applies immediately; normal round endings still apply.";
|
||||
if (key == "emptyRestartHours") return "0 disables idle restarts; 1..168 real hours continuously empty. Default 5. Applies immediately, measured from mission start or the last disconnect. Connected players, including spectators, prevent idle restarts.";
|
||||
@@ -217,7 +221,7 @@ class ERR_AdminService
|
||||
{
|
||||
if (key == "radioIntelSiteCount" || key == "garageVehicleCount" || key == "garageChancePercent" || key == "garageRadiusMetres" || key == "garageSpawnIntervalSeconds") return " for next round";
|
||||
if (key == "aiDoctrine" || key == "aiShadow" || key == "droneReconCount" || key == "droneCooldownSeconds") return " for next round";
|
||||
if (key == "motorbikeCount" || key == "planeAltitudeMetres" || key == "planeReplacementSeconds" || key == "planeSectorSeconds" || key == "planeReconEnabled" || key == "fieldCampCount" || key == "fieldHospitalCount" || key == "startWeaponCount" || key == "startMagazineCount" || key == "objectGuardCount" || key == "objectGuardEvery" || key == "objectGuardHQMode" || key == "hunterSpawnDistanceMetres" || key == "roadblockCount") return " for next round";
|
||||
if (key == "motorbikeCount" || key == "planeAltitudeMetres" || key == "planeReplacementSeconds" || key == "planeSectorSeconds" || key == "planeReconEnabled" || key == "fieldCampCount" || key == "fieldHospitalCount" || key == "startWeaponCount" || key == "startMagazineCount" || key == "objectGuardCount" || key == "objectGuardEvery" || key == "objectGuardHQMode" || key == "hunterSpawnDistanceMetres" || key == "roadblockCount" || key == "largeRoadblockCount" || key == "mainRoadWidthMetres") return " for next round";
|
||||
return "";
|
||||
}
|
||||
protected void SettingCommand(ERR_Settings settings, string key, array<string> parts, array<string> replies)
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
#ifdef WORKBENCH
|
||||
class ERR_RoadblockProbeRules
|
||||
{
|
||||
static int Target()
|
||||
static int SmallTarget()
|
||||
{
|
||||
string value;
|
||||
if (System.GetCLIParam("errRoadblockProbeCount", value)) return Math.Clamp(value.ToInt(), 1, 30);
|
||||
if (System.GetCLIParam("errRoadblockProbeCount", value)) return Math.Clamp(value.ToInt(), 0, 30);
|
||||
return 30;
|
||||
}
|
||||
static int LargeTarget()
|
||||
{
|
||||
string value;
|
||||
if (System.GetCLIParam("errLargeRoadblockProbeCount", value)) return Math.Clamp(value.ToInt(), 0, 30);
|
||||
return 0;
|
||||
}
|
||||
static int Target() { return SmallTarget() + LargeTarget(); }
|
||||
static int Guards() { return SmallTarget() * 4 + LargeTarget() * 8; }
|
||||
static int Props() { return SmallTarget() * 8 + LargeTarget() * 14; }
|
||||
|
||||
}
|
||||
modded class ERR_HQ
|
||||
{
|
||||
@@ -23,7 +33,11 @@ modded class ERR_Roadblocks
|
||||
{
|
||||
string outcome;
|
||||
if (!m_Begun && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "roadblocks")
|
||||
m_Mode.Settings().Set("roadblockCount", ERR_RoadblockProbeRules.Target());
|
||||
{
|
||||
m_Mode.Settings().Set("roadblockCount", ERR_RoadblockProbeRules.SmallTarget());
|
||||
m_Mode.Settings().Set("largeRoadblockCount", ERR_RoadblockProbeRules.LargeTarget());
|
||||
m_Mode.Settings().Set("mainRoadWidthMetres", 6);
|
||||
}
|
||||
return super.Prepare(definition);
|
||||
}
|
||||
protected int m_ERRLayoutPosts;
|
||||
@@ -40,7 +54,7 @@ modded class ERR_Roadblocks
|
||||
for (int i = 0; i < m_BuildCovers.Count() && valid; i++)
|
||||
{
|
||||
ERR_RoadblockProp prop = m_BuildCovers[i];
|
||||
IEntity entity = m_BuildSpawned[i + 4];
|
||||
IEntity entity = m_BuildSpawned[i + m_BuildPositions.Count()];
|
||||
vector low, high; entity.GetBounds(low, high);
|
||||
if (m_ERRLayoutPosts == 0) PrintFormat("ERR ROADBLOCK PROP NATIVE: prefab=%1 low=%2 high=%3", prop.Prefab, low, high);
|
||||
if (Math.Max(Math.AbsFloat(low[0]), Math.AbsFloat(high[0])) > prop.HalfWidth
|
||||
@@ -102,28 +116,29 @@ modded class ERR_Roadblocks
|
||||
foreach (SCR_AIGroup group : m_Groups)
|
||||
{
|
||||
ERR_ForceRecord record = m_Mode.Forces().FindGroup(group);
|
||||
if (!group || !record || record.Kind != ERR_ForceKind.CHECKPOINT || record.InitialStrength != 4 || m_Mode.ForceLiving(group) != 4) return false;
|
||||
if (!group || !record || record.Kind != ERR_ForceKind.CHECKPOINT || (record.InitialStrength != 4 && record.InitialStrength != 8) || m_Mode.ForceLiving(group) != record.InitialStrength) return false;
|
||||
if (group.GetFaction() != m_Mode.EnemyFaction() || !ERR_HQ.Get().ERR_ProbeRoadblockGroup(group)) return false;
|
||||
}
|
||||
if (m_SmallPlaced != ERR_RoadblockProbeRules.SmallTarget() || m_LargePlaced != ERR_RoadblockProbeRules.LargeTarget()) return false;
|
||||
int count = m_Positions.Count();
|
||||
BackgroundStep();
|
||||
PrintFormat("ERR ROADBLOCK BACKGROUND NATIVE: posts=%1 guards=%2 steps=%3 workMs=%4 maxStepMs=%5 attempts=%6", count, count * 4, m_BackgroundSteps, m_BackgroundWork, m_MaxBackgroundStep, m_Attempts);
|
||||
return m_Positions.Count() == count && m_BackgroundSteps > 0;
|
||||
PrintFormat("ERR ROADBLOCK BACKGROUND NATIVE: posts=%1 guards=%2 steps=%3 workMs=%4 maxStepMs=%5 attempts=%6", count, ERR_RoadblockProbeRules.Guards(), m_BackgroundSteps, m_BackgroundWork, m_MaxBackgroundStep, m_Attempts);
|
||||
return m_Positions.Count() == count && (m_BackgroundSteps > 0 || ERR_RoadblockProbeRules.Target() <= 4);
|
||||
}
|
||||
bool ERR_ProbePreparedRoadblocks()
|
||||
{
|
||||
if (!m_Ready || m_Target != 4 || m_Groups.IsEmpty() || m_Groups.Count() > m_Target || m_Groups.Count() != m_Positions.Count()) return false;
|
||||
if (!m_Ready || m_Target != m_SmallTarget + m_LargeTarget || m_Groups.IsEmpty() || m_Groups.Count() > m_Target || m_Groups.Count() != m_Positions.Count()) return false;
|
||||
int count = m_Groups.Count();
|
||||
foreach (SCR_AIGroup group : m_Groups)
|
||||
{
|
||||
ERR_ForceRecord record = m_Mode.Forces().FindGroup(group);
|
||||
if (!record || record.Kind != ERR_ForceKind.CHECKPOINT || record.InitialStrength != 4 || m_Mode.ForceLiving(group) != 4
|
||||
if (!record || record.Kind != ERR_ForceKind.CHECKPOINT || (record.InitialStrength != 4 && record.InitialStrength != 8) || m_Mode.ForceLiving(group) != record.InitialStrength
|
||||
|| group.GetFaction() != m_Mode.EnemyFaction() || !ERR_HQ.Get().ERR_ProbeRoadblockGroup(group)) return false;
|
||||
}
|
||||
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf"));
|
||||
// Repeated preparation cannot replenish or create another squad.
|
||||
if (Prepare(definition) != ERR_PrepareResult.READY || m_Groups.Count() != count) return false;
|
||||
PrintFormat("ERR ROADBLOCK NATIVE: posts=%1 soldiers=%2 ledger=checkpoint HQ=available repeatedPrepare=unchanged", count, count * 4);
|
||||
PrintFormat("ERR ROADBLOCK NATIVE: posts=%1 soldiers=%2 ledger=checkpoint HQ=available repeatedPrepare=unchanged", count, m_SmallPlaced * 4 + m_LargePlaced * 8);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -156,13 +171,13 @@ modded class ERR_GameMode
|
||||
IEntity player; if (controller) player = controller.GetControlledEntity();
|
||||
if (!player || !Players().CurrentActor(player)) return;
|
||||
DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
if (!posts.ERR_ProbeBackgroundPending() || posts.ERR_ProbePostCount() >= ERR_RoadblockProbeRules.Target() || !UnlockPrison(player)) { ERR_RoadblockProbeFail("did not enter background search with prison unlocked"); return; }
|
||||
if ((ERR_RoadblockProbeRules.Target() > 4 && (!posts.ERR_ProbeBackgroundPending() || posts.ERR_ProbePostCount() >= ERR_RoadblockProbeRules.Target())) || !UnlockPrison(player)) { ERR_RoadblockProbeFail("did not enter background search with prison unlocked"); return; }
|
||||
m_ERRRoadblockInitial = posts.ERR_ProbePostCount();
|
||||
PrintFormat("ERR SMOKE PASS: roadblocks prison unlocked with %1/%2 posts; background still pending", m_ERRRoadblockInitial, ERR_RoadblockProbeRules.Target());
|
||||
PrintFormat("ERR SMOKE PASS: roadblocks prison unlocked with %1/%2 posts; placement observation started", m_ERRRoadblockInitial, ERR_RoadblockProbeRules.Target());
|
||||
}
|
||||
if (posts.ERR_ProbeBackgroundPending()) return;
|
||||
if (!posts.ERR_ProbeCompleteRoadblocks() || posts.ERR_ProbePostCount() <= m_ERRRoadblockInitial) { ERR_RoadblockProbeFail("physical four-guard posts, replicated props, ledger and HQ admission"); return; }
|
||||
PrintFormat("ERR SMOKE PASS: roadblocks %1 physical posts / %2 guards / %3 replicated checkpoint props with distance streaming disabled; verified model footprints and centre lane; background completed after prison unlock, no extra posts on repeat tick", posts.ERR_ProbePostCount(), posts.ERR_ProbePostCount() * 4, posts.ERR_ProbePostCount() * ERR_RoadblockRules.PropCount());
|
||||
if (!posts.ERR_ProbeCompleteRoadblocks() || (ERR_RoadblockProbeRules.Target() > 4 && posts.ERR_ProbePostCount() <= m_ERRRoadblockInitial)) { ERR_RoadblockProbeFail("physical small/large posts, independent quotas, replicated props, ledger and HQ admission"); return; }
|
||||
PrintFormat("ERR SMOKE PASS: roadblocks %1 physical posts / %2 guards / %3 replicated checkpoint props with distance streaming disabled; verified model footprints and centre lane; placement complete after prison unlock, no extra posts on repeat tick", posts.ERR_ProbePostCount(), ERR_RoadblockProbeRules.Guards(), ERR_RoadblockProbeRules.Props());
|
||||
ERR_StopRoadblockProbe();
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
Print("ERR SMOKE COMPLETE: roadblocks background completion on Everon; scripted local fixture");
|
||||
|
||||
@@ -43,6 +43,22 @@ modded class SCR_ReconnectComponent
|
||||
PrintFormat("Reintroduced [reconnect]: player %1 original body reserved; timeoutSeconds=%2", playerId, seconds);
|
||||
return true;
|
||||
}
|
||||
// Only an actual disconnected account may receive a replacement. Retire an
|
||||
// orphaned living body through native cleanup before admitting a fresh one.
|
||||
protected void PrepareReplacement(int playerId, ERR_GameMode mode, ERR_PlayerLife life)
|
||||
{
|
||||
if (!life.ReconnectPending || !ERR_PlayerLifeRules.CanEnter(false, mode.Phase())) return;
|
||||
if (GetGame().GetPlayerManager().GetPlayerControlledEntity(playerId)) return;
|
||||
if (life.Actor && GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(life.Actor) > 0) return;
|
||||
if (ERR_Actors.Alive(life.Actor))
|
||||
{
|
||||
SCR_ReconnectData orphan = new SCR_ReconnectData();
|
||||
orphan.m_ReservedEntity = life.Actor;
|
||||
HandleDataExpiery(orphan);
|
||||
}
|
||||
if (mode.Players().AllowReconnectReplacement(playerId, mode.Phase()))
|
||||
PrintFormat("Reintroduced [reconnect]: player %1 has no recoverable reservation; fresh admission allowed", playerId);
|
||||
}
|
||||
override bool HandlePlayerReconnect(int playerId)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
@@ -54,11 +70,21 @@ modded class SCR_ReconnectComponent
|
||||
UUID identity = SCR_PlayerIdentityUtils.GetPlayerIdentityId(playerId);
|
||||
if (!life || !life.Connected || !life.UsedLife || identity.IsNull() || life.AccountId != identity) return false;
|
||||
SCR_ReconnectData data;
|
||||
if (!m_mReconnectData.Find(identity, data)) return false;
|
||||
if (!m_mReconnectData.Find(identity, data))
|
||||
{
|
||||
PrepareReplacement(playerId, mode, life);
|
||||
return false;
|
||||
}
|
||||
IEntity actor = data.m_ReservedEntity;
|
||||
int owner;
|
||||
if (actor) owner = GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(actor);
|
||||
if (!actor || actor != life.Actor || (owner > 0 && owner != playerId))
|
||||
if (!actor)
|
||||
{
|
||||
m_mReconnectData.Remove(identity); UpdateExpieryCheck();
|
||||
PrepareReplacement(playerId, mode, life);
|
||||
return false;
|
||||
}
|
||||
if (actor != life.Actor || (owner > 0 && owner != playerId))
|
||||
{
|
||||
m_mReconnectData.Remove(identity); UpdateExpieryCheck();
|
||||
PrintFormat("Reintroduced [reconnect]: player %1 rejected mismatched body reservation", playerId);
|
||||
@@ -69,7 +95,7 @@ modded class SCR_ReconnectComponent
|
||||
if (IsDataExpired(data, GetGame().GetWorld().GetTimestamp()) || !IsDataRelevant(data))
|
||||
{
|
||||
m_mReconnectData.Remove(identity); HandleDataExpiery(data); UpdateExpieryCheck();
|
||||
PrintFormat("Reintroduced [reconnect]: player %1 reservation expired or body lost; no replacement life", playerId);
|
||||
PrepareReplacement(playerId, mode, life);
|
||||
return false;
|
||||
}
|
||||
bool restored = super.HandlePlayerReconnect(playerId);
|
||||
|
||||
@@ -13,6 +13,7 @@ class ERR_PlayerLife
|
||||
int ConnectionId;
|
||||
bool Connected;
|
||||
bool UsedLife;
|
||||
bool ReconnectPending;
|
||||
IEntity Actor;
|
||||
}
|
||||
|
||||
@@ -66,6 +67,18 @@ class ERR_PlayerRoster
|
||||
ERR_PlayerLife life = Get(playerId);
|
||||
return life && life.Connected && ERR_PlayerLifeRules.CanEnter(life.UsedLife, phase);
|
||||
}
|
||||
bool AllowReconnectReplacement(int playerId, int phase)
|
||||
{
|
||||
if (!Replication.IsServer()) return false;
|
||||
ERR_PlayerLife life = Get(playerId);
|
||||
if (!life || !life.Connected || !life.UsedLife || !life.ReconnectPending || !ERR_PlayerLifeRules.CanEnter(false, phase)) return false;
|
||||
if (ERR_Actors.Alive(life.Actor)) return false;
|
||||
life.Actor = null;
|
||||
life.UsedLife = false;
|
||||
life.ReconnectPending = false;
|
||||
m_OnChanged.Invoke(playerId);
|
||||
return true;
|
||||
}
|
||||
void Spawned(int playerId, IEntity actor)
|
||||
{
|
||||
if (!Replication.IsServer() || !actor) return;
|
||||
@@ -73,13 +86,18 @@ class ERR_PlayerRoster
|
||||
if (!life) return;
|
||||
life.Actor = actor;
|
||||
life.UsedLife = true;
|
||||
life.ReconnectPending = false;
|
||||
m_OnChanged.Invoke(playerId);
|
||||
}
|
||||
void Disconnect(int playerId)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
ERR_PlayerLife life = Get(playerId);
|
||||
if (life) life.Connected = false;
|
||||
if (life)
|
||||
{
|
||||
life.Connected = false;
|
||||
life.ReconnectPending = life.UsedLife;
|
||||
}
|
||||
m_Connections.Remove(playerId);
|
||||
m_OnChanged.Invoke(playerId);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ class ERR_Settings
|
||||
|
||||
static array<string> Keys()
|
||||
{
|
||||
return {"telemetryEnabled", "aiDoctrine", "aiShadow", "sarkaAngerPerCar", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "planeReconEnabled", "droneReconCount", "droneCooldownSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "garageVehicleCount", "garageChancePercent", "garageRadiusMetres", "garageSpawnIntervalSeconds", "startMode", "keepWeaponOnUnconscious", "selfEpinephrine", "restartOnEmpty", "emptyRestartHours", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioIntelSiteCount", "showAllSites", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "antiAirChancePercent", "antiPersonnelMineChancePercent", "automaticEquipmentChancePercent", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
|
||||
return {"telemetryEnabled", "aiDoctrine", "aiShadow", "sarkaAngerPerCar", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "planeReconEnabled", "droneReconCount", "droneCooldownSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "largeRoadblockCount", "mainRoadWidthMetres", "civilianTrafficCount", "garageVehicleCount", "garageChancePercent", "garageRadiusMetres", "garageSpawnIntervalSeconds", "startMode", "keepWeaponOnUnconscious", "selfEpinephrine", "restartOnEmpty", "emptyRestartHours", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioIntelSiteCount", "showAllSites", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "antiAirChancePercent", "antiPersonnelMineChancePercent", "automaticEquipmentChancePercent", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
|
||||
}
|
||||
|
||||
static bool Valid(string key, int value)
|
||||
@@ -33,6 +33,8 @@ class ERR_Settings
|
||||
if (key == "objectGuardCount") return value >= 0 && value <= 16;
|
||||
if (key == "hunterSpawnDistanceMetres") return value >= 800 && value <= 3000;
|
||||
if (key == "roadblockCount") return value >= 0 && value <= 30;
|
||||
if (key == "largeRoadblockCount") return value >= 0 && value <= 30;
|
||||
if (key == "mainRoadWidthMetres") return value >= 5 && value <= 12;
|
||||
if (key == "garageChancePercent") return value >= 0 && value <= 100;
|
||||
if (key == "garageRadiusMetres") return value >= 250 && value <= 1500;
|
||||
if (key == "garageSpawnIntervalSeconds") return value >= 5 && value <= 60;
|
||||
@@ -144,6 +146,8 @@ class ERR_Settings
|
||||
if (key == "objectGuardCount") return 8;
|
||||
if (key == "hunterSpawnDistanceMetres") return 1000;
|
||||
if (key == "roadblockCount") return 4;
|
||||
if (key == "largeRoadblockCount") return 4;
|
||||
if (key == "mainRoadWidthMetres") return 6;
|
||||
if (key == "civilianTrafficCount") return 3;
|
||||
if (key == "garageChancePercent") return 70;
|
||||
if (key == "garageRadiusMetres") return 1000;
|
||||
@@ -177,6 +181,8 @@ class ERR_Settings
|
||||
m_Values.Set("garageSpawnIntervalSeconds", AddedDefault("garageSpawnIntervalSeconds"));
|
||||
m_Values.Set("garageVehicleCount", AddedDefault("garageVehicleCount"));
|
||||
m_Values.Set("motorbikeCount", AddedDefault("motorbikeCount"));
|
||||
m_Values.Set("droneReconCount", AddedDefault("droneReconCount"));
|
||||
m_Values.Set("droneCooldownSeconds", AddedDefault("droneCooldownSeconds"));
|
||||
m_Values.Set("planeAltitudeMetres", AddedDefault("planeAltitudeMetres"));
|
||||
m_Values.Set("planeReplacementSeconds", AddedDefault("planeReplacementSeconds"));
|
||||
m_Values.Set("planeSectorSeconds", AddedDefault("planeSectorSeconds"));
|
||||
@@ -190,6 +196,8 @@ class ERR_Settings
|
||||
m_Values.Set("objectGuardHQMode", AddedDefault("objectGuardHQMode"));
|
||||
m_Values.Set("hunterSpawnDistanceMetres", AddedDefault("hunterSpawnDistanceMetres"));
|
||||
m_Values.Set("roadblockCount", AddedDefault("roadblockCount"));
|
||||
m_Values.Set("largeRoadblockCount", AddedDefault("largeRoadblockCount"));
|
||||
m_Values.Set("mainRoadWidthMetres", AddedDefault("mainRoadWidthMetres"));
|
||||
m_Values.Set("civilianTrafficCount", AddedDefault("civilianTrafficCount"));
|
||||
m_Values.Set("startMode", AddedDefault("startMode"));
|
||||
m_Values.Set("keepWeaponOnUnconscious", AddedDefault("keepWeaponOnUnconscious"));
|
||||
|
||||
@@ -4,7 +4,22 @@ class ERR_RoadblockRules
|
||||
{
|
||||
// Admission never waits longer for a high count. Remaining posts are built
|
||||
// by the same safe placement path in background, including after prison unlock.
|
||||
static int PropCount() { return 8; }
|
||||
static int PropCount(bool large = false)
|
||||
{
|
||||
if (large) return 14;
|
||||
return 8;
|
||||
}
|
||||
static int GuardCount(bool large)
|
||||
{
|
||||
if (large) return 8;
|
||||
return 4;
|
||||
}
|
||||
static bool LargeRoad(float width, float threshold) { return width >= threshold; }
|
||||
static bool Needed(bool large, int smallPlaced, int smallTarget, int largePlaced, int largeTarget)
|
||||
{
|
||||
if (large) return largePlaced < largeTarget;
|
||||
return smallPlaced < smallTarget;
|
||||
}
|
||||
static int SearchMilliseconds() { return 12000; }
|
||||
static int SearchAttempts() { return 300; }
|
||||
static bool Road(float width, float length, float heightDifference)
|
||||
@@ -51,6 +66,8 @@ class ERR_Roadblocks
|
||||
protected int m_Target, m_Attempts, m_Started, m_CandidateStarted, m_Candidates, m_Planned;
|
||||
protected vector m_Low, m_High, m_Centre, m_Forward, m_Right;
|
||||
protected float m_Shoulder;
|
||||
protected bool m_Large;
|
||||
protected int m_SmallTarget, m_LargeTarget, m_SmallPlaced, m_LargePlaced, m_MainRoadWidth;
|
||||
protected SCR_AIGroup m_BuildGroup;
|
||||
protected AIWaypoint m_BuildHold;
|
||||
protected ref array<IEntity> m_BuildSpawned = {};
|
||||
@@ -72,7 +89,7 @@ class ERR_Roadblocks
|
||||
if (m_Ready && m_Positions.Count() < m_Target) progress = "placement stopped; see server log";
|
||||
if (m_Background) progress = "background placement pending";
|
||||
else if (!m_Ready) progress = "initial placement in progress";
|
||||
return string.Format("Roadblocks: %1/%2 placed this round; next-round setting %3. Four guards per post, no replacements. %4.", m_Positions.Count(), m_Target, m_Mode.Settings().Get("roadblockCount"), progress);
|
||||
return string.Format("Checkpoints: small %1/%2 (4 guards), large %3/%4 (8 guards). Main-road cutoff %5 m. No replacements. %6.", m_SmallPlaced, m_SmallTarget, m_LargePlaced, m_LargeTarget, m_MainRoadWidth, progress);
|
||||
}
|
||||
protected ERR_PrepareResult Finish(string reason)
|
||||
{
|
||||
@@ -119,11 +136,15 @@ class ERR_Roadblocks
|
||||
if (i > 0) length += vector.DistanceXZ(points[i - 1], points[i]);
|
||||
}
|
||||
if (!ERR_RoadblockRules.Road(road.GetWidth(), length, 0)) return false;
|
||||
float distance = Math.RandomFloat(20, length - 20);
|
||||
m_Large = ERR_RoadblockRules.LargeRoad(road.GetWidth(), m_MainRoadWidth);
|
||||
if (!ERR_RoadblockRules.Needed(m_Large, m_SmallPlaced, m_SmallTarget, m_LargePlaced, m_LargeTarget)) return false;
|
||||
float margin = 20;
|
||||
if (m_Large) { if (length < 60) return false; margin = 23; }
|
||||
float distance = Math.RandomFloat(margin, length - margin);
|
||||
vector centre, before, after, beforeDirection, afterDirection;
|
||||
if (!ERR_ConvoyRoadPlan.Sample(points, distance, centre, m_Forward)
|
||||
|| !ERR_ConvoyRoadPlan.Sample(points, distance - 15, before, beforeDirection)
|
||||
|| !ERR_ConvoyRoadPlan.Sample(points, distance + 15, after, afterDirection)) return false;
|
||||
|| !ERR_ConvoyRoadPlan.Sample(points, distance - margin, before, beforeDirection)
|
||||
|| !ERR_ConvoyRoadPlan.Sample(points, distance + margin, after, afterDirection)) return false;
|
||||
if (vector.Dot(m_Forward, beforeDirection) < 0.95 || vector.Dot(m_Forward, afterDirection) < 0.95) return false;
|
||||
m_Centre = ERR_Actors.Ground(centre);
|
||||
if (!ERR_RoadblockRules.Road(road.GetWidth(), length, m_Centre[1] - centre[1])) return false;
|
||||
@@ -184,6 +205,20 @@ class ERR_Roadblocks
|
||||
protected void Layout(out array<ref ERR_RoadblockProp> covers)
|
||||
{
|
||||
covers.Clear();
|
||||
if (m_Large)
|
||||
{
|
||||
// Four protected guard bays on each shoulder, with two barriers per approach.
|
||||
// Extend along the road instead of closing the traffic lane.
|
||||
for (int largeSide = -1; largeSide <= 1; largeSide += 2)
|
||||
{
|
||||
for (int bay = 0; bay < 4; bay++)
|
||||
AddProp(covers, COVER, largeSide * m_Shoulder, bay * 6 - 9, m_Right, 1.9, 0.7, 1);
|
||||
AddProp(covers, BARRIER, largeSide * 3.3, largeSide * 14, m_Forward, 1.2, 0.4, 1.3);
|
||||
AddProp(covers, BARRIER, largeSide * 3.3, largeSide * 18, m_Forward, 1.2, 0.4, 1.3);
|
||||
AddProp(covers, SIGN, largeSide * 3, largeSide * 21, m_Forward * (largeSide * 1.0), 0.45, 0.4, 0.8);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Footprints enclose the native model bounds, including small placement margins.
|
||||
for (int side = -1; side <= 1; side += 2)
|
||||
{
|
||||
@@ -209,7 +244,7 @@ class ERR_Roadblocks
|
||||
// Start a transaction; physical pieces are created separately in BuildPost.
|
||||
protected bool SpawnPost(array<vector> positions, array<ref ERR_RoadblockProp> covers)
|
||||
{
|
||||
if (!PlayersClear() || m_Mode.Forces().Count() >= 250 || m_BuildGroup || positions.Count() != 4 || covers.Count() != ERR_RoadblockRules.PropCount()) return false;
|
||||
if (!PlayersClear() || m_Mode.Forces().Count() >= 250 || m_BuildGroup || positions.Count() != ERR_RoadblockRules.GuardCount(m_Large) || covers.Count() != ERR_RoadblockRules.PropCount(m_Large)) return false;
|
||||
m_BuildGroup = SCR_AIGroup.Cast(ERR_Actors.Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", m_Centre));
|
||||
if (!m_BuildGroup) return false;
|
||||
m_BuildGroup.SetFaction(m_Mode.EnemyFaction()); m_BuildGroup.SetSpawnImmediately(false); m_BuildGroup.SetDeleteWhenEmpty(false); m_BuildGroup.SetPermanentLOD(0);
|
||||
@@ -225,12 +260,12 @@ class ERR_Roadblocks
|
||||
m_BuildPositions.Clear(); m_BuildCovers.Clear();
|
||||
}
|
||||
// A free Game Master camera does not force-stream bare, non-editable props
|
||||
// with the nearby editable guards. Keep this bounded bank (<=240 props) on
|
||||
// with the nearby editable guards. Keep this bounded bank (<=660 props across both quotas) on
|
||||
// clients, including late joiners, rather than tying it to the player's body.
|
||||
// Do not report a completed post if any physical prop lacks a network node.
|
||||
protected bool ShareProps()
|
||||
{
|
||||
if (m_BuildCovers.Count() != ERR_RoadblockRules.PropCount() || m_BuildSpawned.Count() != m_BuildPositions.Count() + m_BuildCovers.Count() + 1) return false;
|
||||
if (m_BuildCovers.Count() != ERR_RoadblockRules.PropCount(m_Large) || m_BuildSpawned.Count() != m_BuildPositions.Count() + m_BuildCovers.Count() + 1) return false;
|
||||
for (int i = m_BuildPositions.Count(); i < m_BuildPositions.Count() + m_BuildCovers.Count(); i++)
|
||||
{
|
||||
IEntity entity = m_BuildSpawned[i];
|
||||
@@ -283,21 +318,24 @@ class ERR_Roadblocks
|
||||
CancelPost(); return;
|
||||
}
|
||||
string id = "roadblock-" + m_Positions.Count().ToString();
|
||||
if (!m_Mode.Forces().Register(id, ERR_ForceKind.CHECKPOINT, m_Centre, 4, null, m_BuildGroup, GetGame().GetWorld().GetWorldTime()))
|
||||
if (!m_Mode.Forces().Register(id, ERR_ForceKind.CHECKPOINT, m_Centre, m_BuildPositions.Count(), null, m_BuildGroup, GetGame().GetWorld().GetWorldTime()))
|
||||
{ CancelPost(); return; }
|
||||
foreach (IEntity ownedEntity : m_BuildSpawned) m_Mode.Own(ownedEntity);
|
||||
m_Mode.Own(m_BuildGroup); m_Groups.Insert(m_BuildGroup); m_Positions.Insert(m_Centre);
|
||||
if (m_Large) m_LargePlaced++; else m_SmallPlaced++;
|
||||
m_BuildGroup.AddWaypoint(m_BuildHold);
|
||||
m_Mode.RegisterGroup(m_BuildGroup); m_BuildGroup.ActivateAI();
|
||||
m_Mining.Register(m_BuildGroup, m_BuildHold, m_Right);
|
||||
PrintFormat("Reintroduced [roadblocks]: %1 at %2 guards=4 props=%3 networkedProps=%3 propStreaming=always roadPassage=open HQ=registered", id, m_Centre, m_BuildCovers.Count());
|
||||
PrintFormat("Reintroduced [roadblocks]: %1 at %2 large=%3 guards=%4 props=%5 networkedProps=%5 propStreaming=always roadPassage=open HQ=registered", id, m_Centre, m_Large, m_BuildPositions.Count(), m_BuildCovers.Count());
|
||||
m_BuildSpawned.Clear(); m_BuildGroup = null; m_BuildHold = null;
|
||||
m_BuildPositions.Clear(); m_BuildCovers.Clear();
|
||||
m_NextSearch = System.GetTickCount() + 2000;
|
||||
}
|
||||
protected vector GuardPosition(int side, int row)
|
||||
{
|
||||
return ERR_Actors.Ground(m_Centre + m_Right * (side * (m_Shoulder + 1.5)) + m_Forward * (side * (row - 1.5)));
|
||||
float along = side * (row * 2 - 2.5);
|
||||
if (m_Large) along = row * 6 - 9;
|
||||
return ERR_Actors.Ground(m_Centre + m_Right * (side * (m_Shoulder + 1.5)) + m_Forward * along);
|
||||
}
|
||||
// 0: reject, 1: awaiting native tile, 2: ready.
|
||||
protected int Plan(out array<vector> positions, out array<ref ERR_RoadblockProp> covers)
|
||||
@@ -311,7 +349,7 @@ class ERR_Roadblocks
|
||||
map<int, vector> reserved = new map<int, vector>();
|
||||
for (int side = -1; side <= 1; side += 2)
|
||||
{
|
||||
for (int row = -1; row <= 1; row += 2)
|
||||
for (int row = 0; row < ERR_RoadblockRules.GuardCount(m_Large) / 2; row++)
|
||||
{
|
||||
vector point = GuardPosition(side, row);
|
||||
if (!mesh.IsTileLoaded(point)) { mesh.LoadTileIn(point); return 1; }
|
||||
@@ -328,7 +366,10 @@ class ERR_Roadblocks
|
||||
if (m_Ready) return ERR_PrepareResult.READY;
|
||||
if (!m_Begun)
|
||||
{
|
||||
m_Begun = true; m_Started = System.GetTickCount(); m_Target = m_Mode.Settings().Get("roadblockCount");
|
||||
m_Begun = true; m_Started = System.GetTickCount(); m_SmallTarget = m_Mode.Settings().Get("roadblockCount");
|
||||
m_LargeTarget = m_Mode.Settings().Get("largeRoadblockCount");
|
||||
m_MainRoadWidth = m_Mode.Settings().Get("mainRoadWidthMetres");
|
||||
m_Target = m_SmallTarget + m_LargeTarget;
|
||||
if (m_Target == 0) return Finish("disabled");
|
||||
#ifdef WORKBENCH
|
||||
// The legacy convoy/hq-memory fixtures use the original 60-body allowance and six-squad layout.
|
||||
|
||||
Reference in New Issue
Block a user