Add optional shoreline Motorboats and recommended addon controls
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-11-07 00:32:01 +01:00
parent a7452f8be6
commit 161100cf08
31 changed files with 624 additions and 56 deletions
@@ -47,7 +47,7 @@ class ERR_AdminMenu : ChimeraMenuBase
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
m_Mode = ERR_GameMode.Cast(GetGame().GetGameMode());
if (!m_Controller) { Close(); return; }
array<string> names = {"TelemetryOn", "TelemetryOff","SitesOn", "SitesOff", "PlaneOn", "PlaneOff", "Close", "PauseJournal", "JournalDetails", "LatestJournal", "KeepWeaponOn", "KeepWeaponOff", "SelfEpiOn", "SelfEpiOff", "IntelSummary", "RerollIntel", "RandomDay_startTimeMinutes", "Default_viewDistanceMetres", "RestartEmptyOn", "RestartEmptyOff", "RecommendedPlane", "RecommendedWeapons", "RecommendedDrones"};
array<string> names = {"TelemetryOn", "TelemetryOff","SitesOn", "SitesOff", "PlaneOn", "PlaneOff", "Close", "PauseJournal", "JournalDetails", "LatestJournal", "KeepWeaponOn", "KeepWeaponOff", "SelfEpiOn", "SelfEpiOff", "IntelSummary", "RerollIntel", "RandomDay_startTimeMinutes", "Default_viewDistanceMetres", "RestartEmptyOn", "RestartEmptyOff", "RecommendedBoats", "RecommendedPlane", "RecommendedWeapons", "RecommendedDrones"};
foreach (string mainButton : ERR_AdminMenuPages.MainButtons()) names.Insert(mainButton);
foreach (string pageButton : Navigation()) names.Insert(pageButton);
for (int density = 1; density <= 4; density++) names.Insert("GuardEvery_" + density.ToString());
@@ -360,6 +360,7 @@ class ERR_AdminMenu : ChimeraMenuBase
int section = ERR_AdminMenuPages.MainButtons().Find(name);
if (section >= 0) { ShowPage(ERR_AdminMenuPages.MainPage(section)); return; }
if (name == "RecommendedDrones") { ShowPage(ERR_AdminPage.DRONES); return; }
if (name == "RecommendedBoats") { ShowPage(ERR_AdminPage.BOATS); return; }
if (name == "RecommendedPlane") { ShowPage(ERR_AdminPage.PLANE); return; }
if (name == "RecommendedWeapons") { ShowPage(ERR_AdminPage.ITEMS); return; }
int page = Navigation().Find(name);
@@ -3,7 +3,7 @@ class ERR_AdminMenuFields
{
static array<string> Keys()
{
return {"crashLootMode", "crashSiteCount", "crashHelicopterChancePercent", "crashRifleCount", "crashMagazineCount", "crashAntiAirChancePercent", "crashRocketChancePercent", "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"};
return {"shoreBoatCount", "shoreBoatChancePercent", "shoreBoatCrossingChancePercent", "crashLootMode", "crashSiteCount", "crashHelicopterChancePercent", "crashRifleCount", "crashMagazineCount", "crashAntiAirChancePercent", "crashRocketChancePercent", "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)
{
@@ -68,6 +68,9 @@ class ERR_AdminMenuFields
else if (key == "crashMagazineCount") prefix = "mission crashammo";
else if (key == "crashAntiAirChancePercent") prefix = "mission crashaa";
else if (key == "crashRocketChancePercent") prefix = "mission crashrockets";
else if (key == "shoreBoatCount") prefix = "mission shoreboats";
else if (key == "shoreBoatChancePercent") prefix = "mission boatchance";
else if (key == "shoreBoatCrossingChancePercent") prefix = "mission boatcrossings";
else if (key == "fieldCampCount") prefix = "mission camps";
else if (key == "fieldHospitalCount") prefix = "mission hospitals";
else if (key == "roadblockCount") prefix = "mission roadblocks";
@@ -31,17 +31,18 @@ enum ERR_AdminPage
VEHICLE_POOL = 27,
PACKS = 28,
CRASH_SITES = 29,
BOATS = 30,
}
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", "CheckpointsPage", "VehiclePoolPage", "PacksPage", "CrashSitesPage"}; }
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", "OpenVehiclePool", "OpenPacks", "OpenCrashSites"}; }
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", "VehiclePoolPage", "PacksPage", "CrashSitesPage", "BoatsPage"}; }
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", "OpenVehiclePool", "OpenPacks", "OpenCrashSites", "OpenBoats"}; }
static array<string> MainButtons() { return {"Main_Mission", "Main_Forces", "Main_HQ", "Main_Intel", "Main_Content", "Main_Audio", "Main_Status"}; }
static array<string> Sidebars() { return {"Sidebar_Mission", "Sidebar_Forces", "Sidebar_HQ", "Sidebar_Intel", "Sidebar_Content", "Sidebar_Audio", "Sidebar_Status"}; }
static int Group(int page)
{
array<int> groups = {0, 2, 6, 4, 1, 0, 3, 6, 0, 4, 5, 1, 1, 1, 2, 0, 4, 4, 4, 2, 4, 1, 2, 3, 0, 4, 1, 4, 1, 1};
array<int> groups = {0, 2, 6, 4, 1, 0, 3, 6, 0, 4, 5, 1, 1, 1, 2, 0, 4, 4, 4, 2, 4, 1, 2, 3, 0, 4, 1, 4, 1, 1, 4};
if (page < 0 || page >= groups.Count()) return -1;
return groups[page];
}
@@ -23,6 +23,9 @@ class ERR_CommandArguments
if (section == "mission" && option == "crashammo") return "crashMagazineCount";
if (section == "mission" && option == "crashaa") return "crashAntiAirChancePercent";
if (section == "mission" && option == "crashrockets") return "crashRocketChancePercent";
if (section == "mission" && option == "shoreboats") return "shoreBoatCount";
if (section == "mission" && option == "boatchance") return "shoreBoatChancePercent";
if (section == "mission" && option == "boatcrossings") return "shoreBoatCrossingChancePercent";
if (section == "mission" && option == "hospitals") return "fieldHospitalCount";
if (section == "mission" && option == "startweapons") return "startWeaponCount";
if (section == "mission" && option == "startammo") return "startMagazineCount";
@@ -207,6 +210,9 @@ class ERR_AdminService
if (key == "crashMagazineCount") return "1..8; default 4. Matching ammunition for each selected rifle. Custom loot mode only; next round.";
if (key == "crashAntiAirChancePercent") return "0..100; default 25. One loaded Stinger, if installed and enabled. Custom loot mode only; next round.";
if (key == "crashRocketChancePercent") return "0..100; default 50. One scoped RPG-7 with two matching rockets. Custom loot mode only; next round.";
if (key == "shoreBoatCount") return "0..16; default 6. Maximum for the round. Used or destroyed boats are not replaced. Requires loaded Motorboat. Next round.";
if (key == "shoreBoatChancePercent") return "0..100; default 20. Chance per surveyed shore candidate; default 20%. Requires loaded Motorboat. Next round.";
if (key == "shoreBoatCrossingChancePercent") return "0..100; default 70. Default 70%; never lower than the ordinary coast chance. Requires loaded Motorboat. Next round.";
if (key == "fieldHospitalCount") return "1..8 field hospitals; default 6. Applies next round. The required radio-clue hospital is retained.";
if (key == "objectGuardEvery") return "Guard every 1st, 2nd, 3rd or 4th eligible site; default every third. Nearby buildings share one site. Next round.";
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.";
@@ -235,7 +241,7 @@ class ERR_AdminService
}
protected string RoadblockTiming(string key)
{
if (key == "crashLootMode" || key == "crashSiteCount" || key == "crashHelicopterChancePercent" || key == "crashRifleCount" || key == "crashMagazineCount" || key == "crashAntiAirChancePercent" || key == "crashRocketChancePercent" || key == "radioIntelSiteCount" || key == "garageVehicleCount" || key == "garageChancePercent" || key == "garageRadiusMetres" || key == "garageSpawnIntervalSeconds") return " for next round";
if (key == "shoreBoatCount" || key == "shoreBoatChancePercent" || key == "shoreBoatCrossingChancePercent" || key == "crashLootMode" || key == "crashSiteCount" || key == "crashHelicopterChancePercent" || key == "crashRifleCount" || key == "crashMagazineCount" || key == "crashAntiAirChancePercent" || key == "crashRocketChancePercent" || 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" || key == "largeRoadblockCount" || key == "mainRoadWidthMetres") return " for next round";
return "";
@@ -185,6 +185,7 @@ class ERR_AdminSession
GetGame().ERR_StartingSupplies().Snapshot(values);
m_Mode.Admin().Execute(m_Mode, true, "status", status);
if (m_Mode.CrashSites()) status.Insert(m_Mode.CrashSites().Status());
if (m_Mode.ShoreBoats()) status.Insert(m_Mode.ShoreBoats().Status());
m_Controller.ERR_AdminDeliverSnapshot(m_Token, true, values, status);
}
void Log(int cursor, bool latest)
@@ -243,7 +244,7 @@ class ERR_AdminSession
if (parts.Count() == 6 && parts[1] == "role" && catalog.Find(parts[2]))
{
int role;
if (!ERR_CommandArguments.Integer(parts[3], role) || role < 0 || role > 4 || (parts[5] != "0" && parts[5] != "1")) return true;
if (!ERR_CommandArguments.Integer(parts[3], role) || role < 0 || role > 5 || (parts[5] != "0" && parts[5] != "1")) return true;
if (parts[4] != "*" && (!catalog.Contains(parts[2], parts[4]) || (parts[5] == "1" && !catalog.Capable(parts[4], role)))) return true;
if (GetGame().ERR_ItemSelections().SetVehicleRole(parts[2], parts[4], role, parts[5] == "1")) replies.Insert("Vehicle assignment saved for future spawns. Existing vehicles stay.");
else replies.Insert("Could not save assignment. Previous choices retained.");
@@ -252,7 +253,7 @@ class ERR_AdminSession
if (parts.Count() >= 6 && parts[1] == "browse" && catalog.Find(parts[2]))
{
int page, filter, role;
if (!ERR_CommandArguments.Integer(parts[3], page) || page < 0 || page > 2730 || !ERR_CommandArguments.Integer(parts[4], filter) || filter < 0 || filter > 2 || !ERR_CommandArguments.Integer(parts[5], role) || role < 0 || role > 4) return true;
if (!ERR_CommandArguments.Integer(parts[3], page) || page < 0 || page > 2730 || !ERR_CommandArguments.Integer(parts[4], filter) || filter < 0 || filter > 2 || !ERR_CommandArguments.Integer(parts[5], role) || role < 0 || role > 5) return true;
string query;
for (int q = 6; q < parts.Count(); q++) { if (!query.IsEmpty()) query += " "; query += parts[q]; }
if (query.Length() > 64) return true;
@@ -67,6 +67,7 @@ class ERR_GameMode : SCR_BaseGameMode
protected ref ERR_CivilianTraffic m_CivilianTraffic;
protected ref ERR_GarageVehicles m_GarageVehicles;
protected ref ERR_CrashSites m_CrashSites;
protected ref ERR_ShoreBoats m_ShoreBoats;
protected ref ERR_ForceLedger m_Forces = new ERR_ForceLedger();
protected ref ERR_ForceSimulation m_ForceSimulation;
protected ref array<AIAgent> m_ForceAgents = {};
@@ -147,6 +148,7 @@ class ERR_GameMode : SCR_BaseGameMode
ERR_CivilianTraffic CivilianTraffic() { return m_CivilianTraffic; }
ERR_GarageVehicles GarageVehicles() { return m_GarageVehicles; }
ERR_CrashSites CrashSites() { return m_CrashSites; }
ERR_ShoreBoats ShoreBoats() { return m_ShoreBoats; }
ERR_Objectives Objectives() { return m_Objectives; }
ERR_ExtractionService Extraction() { return m_Extraction; }
string EndReason() { return m_EndReason; }
@@ -556,6 +558,9 @@ class ERR_GameMode : SCR_BaseGameMode
m_CrashSites = new ERR_CrashSites(this);
OnStop().Insert(m_CrashSites.Stop);
m_CrashSites.Start();
m_ShoreBoats = new ERR_ShoreBoats(this);
OnStop().Insert(m_ShoreBoats.Stop);
m_ShoreBoats.Start();
GetGame().GetCallqueue().CallLater(ServerTick, 1000, true);
GetGame().GetCallqueue().CallLater(StartScenario, 1500, false);
}
@@ -44,7 +44,7 @@ class ERR_ItemPreferences
static bool RoleKey(string key)
{
if (key.Length() != 3 && key.Length() != 18) return false;
if (key.Substring(0, 1) != "r" || "01234".IndexOf(key.Substring(1, 1)) < 0) return false;
if (key.Substring(0, 1) != "r" || "012345".IndexOf(key.Substring(1, 1)) < 0) return false;
return key.Substring(2, key.Length() - 2) == "*" || Guid(key.Substring(2, key.Length() - 2));
}
static bool WeightKey(string key)
@@ -171,7 +171,7 @@ class ERR_ItemPreferences
}
bool SetVehicleRole(string addon, string item, int role, bool enabled)
{
if (!Guid(addon) || (item != "*" && !Guid(item)) || role < 0 || role > 4) return false;
if (!Guid(addon) || (item != "*" && !Guid(item)) || role < 0 || role > 5) return false;
string prefix = "r" + role.ToString(), key = prefix + item;
array<string> next = {};
foreach (string row : m_Records)
@@ -199,7 +199,7 @@ class ERR_ItemPreferences
}
bool AllowsVehicleRole(ResourceName prefab, int role, bool defaultEnabled)
{
if (role < 0 || role > 4 || !AllowsVehicle(prefab)) return false;
if (role < 0 || role > 5 || !AllowsVehicle(prefab)) return false;
string item = ERR_TerrainCatalog.Key(prefab);
bool enabled = defaultEnabled;
array<string> sources = SCR_AddonTool.GetResourceAddons(prefab);
@@ -18,6 +18,7 @@ class ERR_ServerVehicleCatalog
protected bool m_Truncated;
static string RoleName(int role)
{
if (role == 5) return "Shore boats";
if (role == 0) return "Garages / yards";
if (role == 1) return "Civilian traffic";
if (role == 2) return "Enemy patrols";
@@ -26,6 +27,7 @@ class ERR_ServerVehicleCatalog
}
static string RoleHint(int role)
{
if (role == 5) return "Shore boats: inspected unarmed Motorboat; coastal water and boarding-space checks apply.";
if (role == 0) return "Garages / yards: unarmed vehicles that fit the parking space.";
if (role == 1) return "Traffic: vehicles with AI driving support; road size limits apply.";
if (role == 2) return "Enemy patrols: driver and crew, up to 15 seats; your enemy faction supplies the soldiers.";
@@ -75,6 +77,8 @@ class ERR_ServerVehicleCatalog
foreach (ResourceName tractor : cars.Tractors) Seed(tractor, 0);
ERR_MotorbikeCatalog bikes = new ERR_MotorbikeCatalog(); bikes.Find();
foreach (ResourceName bike : bikes.Prefabs) Seed(bike, 0);
ERR_ShoreBoatCatalog boats = new ERR_ShoreBoatCatalog(); boats.Find();
foreach (ResourceName boat : boats.Prefabs) Seed(boat, 5);
Seed(ERR_HunterFleetRules.VehiclePrefab(0), 2); Seed(ERR_HunterFleetRules.VehiclePrefab(2), 2);
Seed(ERR_ConvoyResources.VehiclePrefab(0), 3); Seed(ERR_ConvoyResources.VehiclePrefab(1), 4);
Catalog(manager.GetEntityCatalogOfType(EEntityCatalogType.VEHICLE, false));
@@ -141,6 +145,7 @@ class ERR_ServerVehicleCatalog
if (wheeled) capabilities = capabilities | 2;
if (Has(resource, InventoryStorageManagerComponent)) capabilities = capabilities | 16;
}
if (ERR_ShoreBoatCatalog.Candidate(prefab)) capabilities = 32;
entry.Capabilities = capabilities;
ERR_ServerItem item = new ERR_ServerItem(); item.Prefab = prefab; item.Eligible = true;
item.Label = FilePath.StripPath(prefab.GetPath()); item.Label.Replace(".et", ""); item.Label.Replace("_", " "); item.SearchLabel = item.Label;
@@ -169,14 +174,14 @@ class ERR_ServerVehicleCatalog
void Reject(ResourceName prefab, int role)
{
ERR_VehicleEntry entry = m_Entries.Get(ERR_TerrainCatalog.Key(prefab));
if (!entry || role < 0 || role > 4) return;
if (!entry || role < 0 || role > 5) return;
entry.Capabilities = entry.Capabilities & ~(1 << role);
PrintFormat("Reintroduced [vehicle assignment]: rejected %1 for %2: physical fit, driver, crew seats or storage unavailable; excluded until next round", prefab, RoleName(role));
}
bool Capable(string key, int role)
{
ERR_VehicleEntry entry = m_Entries.Get(key);
return role >= 0 && role < 5 && entry && (entry.Capabilities & (1 << role)) != 0;
return role >= 0 && role < 6 && entry && (entry.Capabilities & (1 << role)) != 0;
}
bool Allowed(ResourceName prefab, int role)
{
@@ -27,7 +27,7 @@ modded class ERR_AdminMenu
SCR_ComboBoxComponent filter = SCR_ComboBoxComponent.GetComboBoxComponent("VehiclesFilter", GetRootWidget());
filter.AddItem("All"); filter.AddItem("Enabled"); filter.AddItem("Disabled"); filter.SetCurrentItem(0, false, false, false);
SCR_ComboBoxComponent roles = SCR_ComboBoxComponent.GetComboBoxComponent("VehiclesRole", GetRootWidget());
for (int role = 0; role < 5; role++) roles.AddItem("Use for: " + ERR_ServerVehicleCatalog.RoleName(role));
for (int role = 0; role < 6; role++) roles.AddItem("Use for: " + ERR_ServerVehicleCatalog.RoleName(role));
roles.SetCurrentItem(0, false, false, false);
UpdateActions();
}
@@ -9,11 +9,14 @@ class ERR_Settings
static array<string> Keys()
{
return {"crashLootMode", "crashSiteCount", "crashHelicopterChancePercent", "crashRifleCount", "crashMagazineCount", "crashAntiAirChancePercent", "crashRocketChancePercent", "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"};
return {"shoreBoatCount", "shoreBoatChancePercent", "shoreBoatCrossingChancePercent", "crashLootMode", "crashSiteCount", "crashHelicopterChancePercent", "crashRifleCount", "crashMagazineCount", "crashAntiAirChancePercent", "crashRocketChancePercent", "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)
{
if (key == "shoreBoatCount") return value >= 0 && value <= 16;
if (key == "shoreBoatChancePercent") return value >= 0 && value <= 100;
if (key == "shoreBoatCrossingChancePercent") return value >= 0 && value <= 100;
if (key == "crashLootMode") return value == 0 || value == 1;
if (key == "crashSiteCount") return value >= 0 && value <= 12;
if (key == "crashHelicopterChancePercent") return value >= 0 && value <= 100;
@@ -134,6 +137,9 @@ class ERR_Settings
static int AddedDefault(string key)
{
if (key == "shoreBoatCount") return 6;
if (key == "shoreBoatChancePercent") return 20;
if (key == "shoreBoatCrossingChancePercent") return 70;
if (key == "crashLootMode") return 1;
if (key == "crashSiteCount") return 3;
if (key == "crashHelicopterChancePercent") return 50;
@@ -186,6 +192,9 @@ class ERR_Settings
void ERR_Settings(string path = PATH)
{
m_Path = path;
m_Values.Set("shoreBoatCount", AddedDefault("shoreBoatCount"));
m_Values.Set("shoreBoatChancePercent", AddedDefault("shoreBoatChancePercent"));
m_Values.Set("shoreBoatCrossingChancePercent", AddedDefault("shoreBoatCrossingChancePercent"));
m_Values.Set("crashLootMode", AddedDefault("crashLootMode"));
m_Values.Set("crashSiteCount", AddedDefault("crashSiteCount"));
m_Values.Set("crashHelicopterChancePercent", AddedDefault("crashHelicopterChancePercent"));
@@ -63,6 +63,7 @@ class ERR_RecommendedAddons
{
array<string> loaded = {}; GameProject.GetLoadedAddons(loaded);
Entry(values, "bikes", loaded, {"6503091A7B264FC3", "61B84088181A8FA4", "629B2BA37EFFD577"}, {"WCS Motorbikes", "Motorbike", "WCS Armaments"}, mode.Settings().Get("motorbikeCount") > 0 || (mode.Settings().Get("garageVehicleCount") > 0 && mode.Settings().Get("garageChancePercent") > 0));
Entry(values, "boats", loaded, {"610C8532D3756F2D", "696A0CE876246081", "61A6B4C21F6FCF22", "6A7D4E7B69969355"}, {"Motorboat", "Modest Boat Core", "Modest AI Radio", "Modest VehicleCore"}, mode.Settings().Get("shoreBoatCount") > 0 && mode.Settings().Get("shoreBoatCrossingChancePercent") + mode.Settings().Get("shoreBoatChancePercent") > 0);
Entry(values, "plane", loaded, {"69BAEE0D439966FE", "69A8A34027DA65C5"}, {"Cessna 172", "Propeller Flight Core"}, mode.Settings().Get("planeReconEnabled") == 1);
Entry(values, "weapons", loaded, {"BFC890A6F1603EC0"}, {"Phaseline Marksman Rifles"}, mode.Settings().Get("automaticEquipmentChancePercent") != 0);
ERR_HQ hq = ERR_HQ.Cast(mode.Command());
@@ -0,0 +1,180 @@
// Soft integration: use only the inspected unarmed Motorboat when loaded.
// No foreign script type, copied asset or mandatory Workshop dependency.
class ERR_ShoreBoatCatalog
{
ref array<ResourceName> Prefabs = {};
static bool Candidate(string path)
{
path.ToLower(); return path.EndsWith("/vehicles/boats/motorboat/motorboat_base.et");
}
protected void Add(ResourceName resource, string path = "")
{
if (Candidate(resource) && !Prefabs.Contains(resource)) Prefabs.Insert(resource);
}
void Find()
{
Prefabs.Clear(); array<string> loaded = {}; GameProject.GetLoadedAddons(loaded);
if (!loaded.Contains("610C8532D3756F2D")) return;
SearchResourcesFilter filter = new SearchResourcesFilter(); filter.fileExtensions = {"et"}; filter.recursive = true;
filter.rootPath = "$" + GameProject.GetAddonID("610C8532D3756F2D") + ":Prefabs";
ResourceDatabase.SearchResources(filter, Add);
}
}
class ERR_ShoreBoats
{
protected ERR_GameMode m_Mode;
protected ref ERR_ShoreMap m_Map = new ERR_ShoreMap();
protected ref ERR_ShoreBoatCatalog m_Catalog = new ERR_ShoreBoatCatalog();
protected ref array<vector> m_Origins = {};
protected bool m_Started, m_Stopped;
protected int m_Target, m_Chance, m_CrossingChance, m_Attempts;
protected string m_State = "waiting";
void ERR_ShoreBoats(ERR_GameMode mode) { m_Mode = mode; }
int Count() { return m_Origins.Count(); }
string Status() { return string.Format("Shore boats: %1/%2; %3. Used boats remain; no replacements.", Count(), m_Target, m_State); }
void Start()
{
if (m_Started || m_Stopped || !Replication.IsServer() || !m_Mode) return;
m_Started = true; m_Target = m_Mode.Settings().Get("shoreBoatCount");
m_Chance = m_Mode.Settings().Get("shoreBoatChancePercent"); m_CrossingChance = m_Mode.Settings().Get("shoreBoatCrossingChancePercent");
if (m_Target <= 0) { m_State = "disabled"; return; }
m_Catalog.Find();
if (m_Catalog.Prefabs.IsEmpty()) { m_State = "Motorboat not loaded"; return; }
vector low, high; GetGame().GetWorldEntity().GetWorldBounds(low, high);
m_Map.Setup(low, high); m_State = "surveying coastlines";
GetGame().GetCallqueue().CallLater(Step, 100, true);
}
protected bool Unobserved(vector position)
{
array<int> ids = {}; GetGame().GetPlayerManager().GetPlayers(ids);
SCR_EditorManagerCore editors = SCR_EditorManagerCore.Cast(SCR_EditorManagerCore.GetInstance(SCR_EditorManagerCore));
foreach (int id : ids)
{
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
if (!controller) return false;
SCR_EditorManagerEntity editor; if (editors) editor = editors.GetEditorManager(id);
if (editor && editor.IsOpened()) return false;
array<IEntity> actors = {}; IEntity body = controller.GetControlledEntity(); if (body) actors.Insert(body);
if (controller.ERR_ObserverState() != ERR_ObserverReason.NONE)
{
IEntity watched = controller.ERR_ObservedEntity(); if (!watched) return false; actors.Insert(watched);
}
else if (!body) return false;
foreach (IEntity actor : actors)
{
if (!ERR_ForceRules.FiniteVector(actor.GetOrigin()) || vector.DistanceXZ(actor.GetOrigin(), position) < 500) return false;
TraceParam ray = new TraceParam(); ray.Start = actor.GetOrigin() + "0 1.7 0"; ray.End = position + "0 2 0";
ray.Flags = TraceFlags.WORLD | TraceFlags.ENTS; ray.LayerMask = TRACE_LAYER_CAMERA;
array<IEntity> excluded = {actor}; IEntity ride = CompartmentAccessComponent.GetVehicleIn(actor); if (ride) excluded.Insert(ride); ray.ExcludeArray = excluded;
float fraction = GetGame().GetWorld().TraceMove(ray, null);
if (!ERR_ForceRules.Finite(fraction) || fraction >= 0.995) return false;
}
}
return true;
}
protected float Depth(vector point)
{
return GetGame().GetWorld().GetOceanHeight(point[0], point[2]) - SCR_TerrainHelper.GetTerrainY(point);
}
protected bool Clear(vector position, vector angles, IEntity excluded = null)
{
vector matrix[4]; Math3D.AnglesToMatrix(angles, matrix);
// Hull, boarding space and a short navigable outward departure corridor.
for (int z = -7; z <= 21; z += 7)
for (int x = -3; x <= 3; x += 3)
{
vector sample = position + matrix[0] * (x * 1.0) + matrix[2] * (z * 1.0);
float depth = Depth(sample); if (!ERR_ForceRules.Finite(depth) || depth < 2) return false;
}
TraceOBB hull = new TraceOBB(); hull.Start = position; hull.End = position + "0 0.01 0";
hull.Mins = "-3 -1.5 -7"; hull.Maxs = "3 4 7";
for (int i = 0; i < 3; i++) hull.Mat[i] = matrix[i];
hull.Flags = TraceFlags.WORLD | TraceFlags.ENTS; hull.Exclude = excluded;
float fraction = GetGame().GetWorld().TraceMove(hull, null);
return ERR_ForceRules.Finite(fraction) && fraction >= 1;
}
protected bool Survey(int edge, out vector position, out vector angles, out bool crossing)
{
int land = Math.Floor(edge / 4), water = m_Map.Neighbor(land, edge % 4);
if (water < 0) return false;
vector dry = m_Map.Point(land), wet = m_Map.Point(water);
if (Depth(dry) >= -0.5 || Depth(wet) <= 0) return false;
vector forward = wet - dry; forward[1] = 0; forward.Normalize(); angles = forward.VectorToAngles();
for (int n = 0; n < 10; n++)
{
vector middle = (dry + wet) * 0.5;
if (Depth(middle) > 0) wet = middle; else dry = middle;
}
// Reject cliffs: a person must be able to approach the shoreline from land.
vector approach = dry - forward * 8;
if (Depth(approach) < -3 || Depth(approach) > -0.2) return false;
crossing = m_Map.FacingIsland(land, wet, forward);
for (int offset = 12; offset <= 36; offset += 6)
{
position = wet + forward * (offset * 1.0); position[1] = GetGame().GetWorld().GetOceanHeight(position[0], position[2]);
if (!ERR_ForceRules.FiniteVector(position)) return false;
bool nearby;
foreach (vector existing : m_Origins) if (vector.DistanceXZ(existing, position) < 700) nearby = true;
if (nearby) return false;
if (Depth(position) > 8 || !Clear(position, angles)) continue;
return Unobserved(position);
}
return false;
}
protected bool Spawn(vector position, vector angles)
{
ResourceName prefab = ERR_VehicleSelection.Choose(m_Catalog.Prefabs, 5);
if (prefab.IsEmpty() || !Unobserved(position) || !Clear(position, angles)) return false;
IEntity spawned = ERR_Actors.Spawn(prefab, position, angles);
Vehicle boat = Vehicle.Cast(spawned);
if (!boat) { ERR_Actors.DeleteSpawned(spawned); return false; }
VehicleWheeledSimulation simulation = VehicleWheeledSimulation.Cast(boat.FindComponent(VehicleWheeledSimulation));
if (!simulation || !simulation.IsValid())
{
m_State = "boat mod has an incompatible drivetrain; spawning stopped";
GetGame().ERR_ServerVehicles().Reject(prefab, 5);
ERR_Actors.DeleteSpawned(boat); Stop(); return false;
}
array<BaseCompartmentSlot> seats = {}; bool driver; bool invalid;
if (!ERR_ConvoyRules.CollectSeats(boat, seats)) invalid = true;
foreach (BaseCompartmentSlot seat : seats)
{
if (seat.GetOccupant() || seat.GetType() == ECompartmentType.TURRET) invalid = true;
if (seat.GetType() == ECompartmentType.PILOT && seat.IsCompartmentAccessible()) driver = true;
}
vector low, high; boat.GetBounds(low, high);
if (!driver || invalid || !ERR_ForceRules.FiniteVector(low) || !ERR_ForceRules.FiniteVector(high)
|| low[0] < -3 || high[0] > 3 || low[2] < -7 || high[2] > 7 || !Clear(position, angles, boat))
{ ERR_Actors.DeleteSpawned(boat); return false; }
// Empty unaffiliated vehicle: first occupant receives faction ownership.
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(boat.FindComponent(FactionAffiliationComponent));
if (affiliation) affiliation.SetAffiliatedFaction(null);
SCR_FuelManagerComponent fuel = SCR_FuelManagerComponent.Cast(boat.FindComponent(SCR_FuelManagerComponent));
if (fuel) fuel.SetTotalFuelPercentage(Math.RandomFloat(0.5, 0.9));
SCR_GarbageSystem garbage = SCR_GarbageSystem.GetByEntityWorld(boat);
if (garbage) garbage.UpdateBlacklist(boat, true);
m_Mode.Own(boat); m_Origins.Insert(position);
return true;
}
protected void Step()
{
if (m_Stopped || !Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || ERR_MissionRules.Terminal(m_Mode.Phase())) { Stop(); return; }
if (!m_Mode.Active()) return;
if (!m_Map.Ready()) { m_Map.Step(); return; }
if (Count() >= m_Target || m_Map.Edges.IsEmpty() || m_Attempts >= 4096)
{ m_State = "survey complete"; Stop(); return; }
// Each edge is considered once, in random order. Never reroll a rejected chance.
int chosen = Math.RandomInt(0, m_Map.Edges.Count()), edge = m_Map.Edges[chosen];
m_Map.Edges.Remove(chosen); m_Attempts++;
vector position, angles; bool crossing;
if (!Survey(edge, position, angles, crossing)) return;
if (Math.RandomInt(0, 100) >= ERR_ShoreMap.Chance(crossing, m_Chance, m_CrossingChance)) return;
Spawn(position, angles);
}
void Stop()
{
m_Stopped = true;
if (GetGame()) GetGame().GetCallqueue().Remove(Step);
// Boats are finite round-owned transport; movement, use or destruction never replenishes them.
}
}
@@ -0,0 +1,107 @@
// Bounded coarse land connectivity, followed by precise placement in ERR_ShoreBoats.
// A bay on the same landmass must not receive the inter-island probability bonus.
class ERR_ShoreMap
{
vector Low;
float Cell;
int Width, Height;
ref array<int> Parents = {};
ref array<int> Edges = {};
protected int m_Cursor, m_Phase;
void Setup(vector low, vector high)
{
Low = low; Cell = Math.Max(64, Math.Ceil(Math.Max(high[0] - low[0], high[2] - low[2]) / 256));
Width = Math.Clamp(Math.Floor((high[0] - low[0]) / Cell), 0, 256);
Height = Math.Clamp(Math.Floor((high[2] - low[2]) / Cell), 0, 256);
Parents.Clear(); Edges.Clear(); m_Cursor = 0; m_Phase = 0;
}
vector Point(int index)
{
int column = index % Width, row = Math.Floor(index / Width);
return Low + Vector((column + 0.5) * Cell, 0, (row + 0.5) * Cell);
}
int Index(vector point)
{
int x = Math.Floor((point[0] - Low[0]) / Cell), z = Math.Floor((point[2] - Low[2]) / Cell);
if (x < 0 || z < 0 || x >= Width || z >= Height) return -1;
return z * Width + x;
}
int Root(int index)
{
if (index < 0 || index >= Parents.Count() || Parents[index] < 0) return -1;
int root = index;
while (Parents[root] != root) root = Parents[root];
while (Parents[index] != index) { int next = Parents[index]; Parents[index] = root; index = next; }
return root;
}
protected bool Land(vector point)
{
float ground = SCR_TerrainHelper.GetTerrainY(point), sea = GetGame().GetWorld().GetOceanHeight(point[0], point[2]);
point[1] = ground;
return ERR_ForceRules.Finite(ground) && ERR_ForceRules.Finite(sea) && ground > sea + 0.5
&& !ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point);
}
protected void Join(int a, int b)
{
int other = Root(b); if (other < 0) return;
// Do not connect sampled land across an intervening narrow channel.
for (int sample = 1; sample <= 3; sample++) if (!Land(vector.Lerp(Point(a), Point(b), sample * 0.25))) return;
Parents[Root(a)] = other;
}
int Neighbor(int index, int direction)
{
int x = index % Width, z = Math.Floor(index / Width);
if (direction == 0) x++; else if (direction == 1) x--; else if (direction == 2) z++; else z--;
if (x < 0 || z < 0 || x >= Width || z >= Height) return -1;
return z * Width + x;
}
bool Ready() { return m_Phase == 2; }
void Step()
{
int began = System.GetTickCount();
for (int work = 0; work < 64 && m_Phase < 2; work++)
{
if (m_Cursor >= Width * Height) { m_Cursor = 0; m_Phase++; continue; }
int index = m_Cursor++;
if (m_Phase == 0)
{
if (!Land(Point(index))) Parents.Insert(-1);
else
{
Parents.Insert(index);
if (index % Width > 0) Join(index, index - 1);
if (index >= Width) Join(index, index - Width);
}
}
else if (Root(index) >= 0)
for (int direction = 0; direction < 4; direction++)
{
int next = Neighbor(index, direction);
if (next >= 0 && Root(next) < 0 && Edges.Count() < 32768) Edges.Insert(index * 4 + direction);
}
if (System.GetTickCount() - began >= 4) return;
}
}
bool FacingIsland(int land, vector water, vector forward)
{
int own = Root(land); if (own < 0) return false;
// Scan an outward fan. Stop at the FIRST land on each ray, including own bays.
for (int ray = -1; ray <= 1; ray++)
{
vector direction = ERR_CampTerrain.Rotate(forward, ray * 30);
for (float distance = Cell; distance <= 4000; distance += Cell * 0.5)
{
int index = Index(water + direction * distance); if (index < 0) break;
int other = Root(index); if (other < 0) continue;
if (other != own) return true;
break;
}
}
return false;
}
static int Chance(bool facing, int ordinary, int crossing)
{
if (facing && crossing > ordinary) return crossing;
return ordinary;
}
}