diff --git a/CHANGELOG.md b/CHANGELOG.md index 914f431d..4a05b045 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ Latest published Workshop version: **0.0.146** +## 2026-11-07 — Shore boats + +- Added optional Motorboat spawns along accessible shores, with better chances on coasts facing another island. +- Added boat limits and coastal spawn chances under Content → Shore boats. +- Added Motorboat to Recommended addons with dependency checks and a settings shortcut. + ## 2026-11-06 — Crash-site salvage - Added configurable smoking helicopter and armored wreck sites with finite loot crates. diff --git a/README.md b/README.md index 63f6202c..15b865f1 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,8 @@ The [development source bundle tool](docs/development-source-bundle.md) previews Random field hospitals and camps, their permanent/mobile guard split, finite supplies and authoring options are documented in [Field sites](docs/field-sites.md). +Occasional transport boats and island-facing coast preferences: [Shore boats](docs/shore-boats.md). + Configurable smoking wrecks and finite weapon crates are documented in [Crash-site salvage](docs/crash-sites.md). Town population, configurable factory guards and safe tower/roof posts: [town and object garrisons](docs/town-garrisons.md). diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c index 9eea92aa..567d716c 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c @@ -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 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 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); diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenuFields.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenuFields.c index ab7fa86a..a3adf0a6 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenuFields.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenuFields.c @@ -3,7 +3,7 @@ class ERR_AdminMenuFields { static array 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"; diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenuPages.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenuPages.c index 03cf8de6..14eb940c 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenuPages.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenuPages.c @@ -31,17 +31,18 @@ enum ERR_AdminPage VEHICLE_POOL = 27, PACKS = 28, CRASH_SITES = 29, + BOATS = 30, } class ERR_AdminMenuPages { - static array 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 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 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 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 MainButtons() { return {"Main_Mission", "Main_Forces", "Main_HQ", "Main_Intel", "Main_Content", "Main_Audio", "Main_Status"}; } static array Sidebars() { return {"Sidebar_Mission", "Sidebar_Forces", "Sidebar_HQ", "Sidebar_Intel", "Sidebar_Content", "Sidebar_Audio", "Sidebar_Status"}; } static int Group(int page) { - array 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 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]; } diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c index 230ce196..40946b77 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c @@ -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 ""; diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminSession.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminSession.c index 815fac85..466dadd7 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminSession.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminSession.c @@ -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; diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c index 66853f39..6e63cb5a 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c @@ -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 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); } diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_ItemPreferences.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_ItemPreferences.c index a451f401..20961589 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_ItemPreferences.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_ItemPreferences.c @@ -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 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 sources = SCR_AddonTool.GetResourceAddons(prefab); diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_ServerVehicleCatalog.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_ServerVehicleCatalog.c index 74447520..9c9e30f8 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_ServerVehicleCatalog.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_ServerVehicleCatalog.c @@ -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) { diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_ServerVehiclesMenu.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_ServerVehiclesMenu.c index d307c074..e6641bde 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_ServerVehiclesMenu.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_ServerVehiclesMenu.c @@ -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(); } diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c index 44f88755..2be53a51 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c @@ -9,11 +9,14 @@ class ERR_Settings static array 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")); diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_OptionalMotorbikes.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_OptionalMotorbikes.c index b44ecb39..69573533 100644 --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_OptionalMotorbikes.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_OptionalMotorbikes.c @@ -63,6 +63,7 @@ class ERR_RecommendedAddons { array 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()); diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_ShoreBoats.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_ShoreBoats.c new file mode 100644 index 00000000..417cdfcf --- /dev/null +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_ShoreBoats.c @@ -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 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 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 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 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 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 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 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. + } +} diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_ShoreMap.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_ShoreMap.c new file mode 100644 index 00000000..064303c7 --- /dev/null +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_ShoreMap.c @@ -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 Parents = {}; + ref array 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; + } +} diff --git a/addons/core/UI/layouts/ERR/AdminMenu.layout b/addons/core/UI/layouts/ERR/AdminMenu.layout index 3cf5e440..6e127c73 100644 --- a/addons/core/UI/layouts/ERR/AdminMenu.layout +++ b/addons/core/UI/layouts/ERR/AdminMenu.layout @@ -82,14 +82,16 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenVehiclePool" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 152 OffsetTop 152 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -155 } Color 0.9 0.66 0.25 1 } ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenVehicles" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 162 OffsetTop 162 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -204 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Vehicle spawning" } } } ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenVehicles" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 206 OffsetTop 206 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -209 } Color 0.9 0.66 0.25 1 } -ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenItems" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 216 OffsetTop 216 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -258 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Equipment chance" } } } -ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenItems" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 260 OffsetTop 260 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -263 } Color 0.9 0.66 0.25 1 } -ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenWeights" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 270 OffsetTop 270 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -312 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Advanced weights" } } } -ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenWeights" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 314 OffsetTop 314 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -317 } Color 0.9 0.66 0.25 1 } -ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenItemPresets" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 324 OffsetTop 324 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -366 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Presets" } } } -ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenItemPresets" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 368 OffsetTop 368 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -371 } Color 0.9 0.66 0.25 1 } -ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenStarter" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 378 OffsetTop 378 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -420 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Starting backpack" } } } -ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenStarter" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 422 OffsetTop 422 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -425 } Color 0.9 0.66 0.25 1 } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenBoats" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 216 OffsetTop 216 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -258 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Shore boats" } } } +ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenBoats" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 260 OffsetTop 260 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -263 } Color 0.9 0.66 0.25 1 } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenItems" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 270 OffsetTop 270 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -312 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Equipment chance" } } } +ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenItems" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 314 OffsetTop 314 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -317 } Color 0.9 0.66 0.25 1 } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenWeights" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 324 OffsetTop 324 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -366 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Advanced weights" } } } +ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenWeights" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 368 OffsetTop 368 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -371 } Color 0.9 0.66 0.25 1 } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenItemPresets" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 378 OffsetTop 378 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -420 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Presets" } } } +ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenItemPresets" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 422 OffsetTop 422 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -425 } Color 0.9 0.66 0.25 1 } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenStarter" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 432 OffsetTop 432 SizeX 180 OffsetRight -180 SizeY 42 OffsetBottom -474 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Starting backpack" } } } +ImageWidgetClass { "Is Visible" 0 Name "Selected_OpenStarter" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 476 OffsetTop 476 SizeX 180 OffsetRight -180 SizeY 3 OffsetBottom -479 } Color 0.9 0.66 0.25 1 } } } ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Main_Audio" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 758 OffsetLeft 758 PositionY 106 OffsetTop 106 SizeX 138 OffsetRight -896 SizeY 42 OffsetBottom -148 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Audio" } } } ImageWidgetClass { "Is Visible" 0 Name "Selected_Main_Audio" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 758 OffsetLeft 758 PositionY 150 OffsetTop 150 SizeX 138 OffsetRight -896 SizeY 3 OffsetBottom -153 } Color 0.9 0.66 0.25 1 } @@ -174,6 +176,26 @@ TextWidgetClass { Name "Saved_garageSpawnIntervalSeconds" Slot FrameWidgetSlot { TextWidgetClass { Name "Help_garageSpawnIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 372 OffsetTop 372 SizeX 456 OffsetRight -484 SizeY 28 OffsetBottom -400 } Color 0.9 0.92 0.94 1 Text "Default 5 s. Increase to spread out spawning. Next round." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } TextWidgetClass { Name "GarageSettingsNote" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 284 OffsetTop 284 SizeX 456 OffsetRight -964 SizeY 110 OffsetBottom -394 } Color 0.9 0.92 0.94 1 Text "Parking settings apply next round. Used vehicles stay. Garages and clear yards fill while hidden. Rally adds cars and occasional farm tractors." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } } } +FrameWidgetClass { "Is Visible" 0 Name "BoatsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1220 SizeY 532 OffsetBottom -692 } { +TextWidgetClass { Name "BoatsHeading" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 4 OffsetTop 4 SizeX 936 OffsetRight -964 SizeY 30 OffsetBottom -34 } Color 0.9 0.92 0.94 1 Text "Shore boats - next round" "Font Size" 22 "Min Font Size" 22 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "BoatsHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 40 OffsetTop 40 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -90 } Color 0.9 0.92 0.94 1 Text "Occasional usable boats near accessible shores. Coasts facing another island are favored. Requires Motorboat loaded on the server." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "Label_shoreBoatCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 108 OffsetTop 108 SizeX 456 OffsetRight -484 SizeY 26 OffsetBottom -134 } Color 0.9 0.92 0.94 1 Text "Shore boats (0-16; 0 off)" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "shoreBoatCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 136 OffsetTop 136 SizeX 180 OffsetRight -208 SizeY 32 OffsetBottom -168 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_shoreBoatCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 136 OffsetTop 136 SizeX 100 OffsetRight -320 SizeY 32 OffsetBottom -168 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } } +TextWidgetClass { Name "Saved_shoreBoatCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 172 OffsetTop 172 SizeX 456 OffsetRight -484 SizeY 24 OffsetBottom -196 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "Help_shoreBoatCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 136 OffsetTop 136 SizeX 456 OffsetRight -964 SizeY 60 OffsetBottom -196 } Color 0.9 0.92 0.94 1 Text "Maximum for the round. Used or destroyed boats are not replaced." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "Label_shoreBoatChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 234 OffsetTop 234 SizeX 456 OffsetRight -484 SizeY 26 OffsetBottom -260 } Color 0.9 0.92 0.94 1 Text "Ordinary coast chance (%)" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "shoreBoatChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 262 OffsetTop 262 SizeX 180 OffsetRight -208 SizeY 32 OffsetBottom -294 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_shoreBoatChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 262 OffsetTop 262 SizeX 100 OffsetRight -320 SizeY 32 OffsetBottom -294 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } } +TextWidgetClass { Name "Saved_shoreBoatChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 298 OffsetTop 298 SizeX 456 OffsetRight -484 SizeY 24 OffsetBottom -322 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "Help_shoreBoatChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 262 OffsetTop 262 SizeX 456 OffsetRight -964 SizeY 60 OffsetBottom -322 } Color 0.9 0.92 0.94 1 Text "Chance per surveyed shore candidate; default 20%." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "Label_shoreBoatCrossingChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 360 OffsetTop 360 SizeX 456 OffsetRight -484 SizeY 26 OffsetBottom -386 } Color 0.9 0.92 0.94 1 Text "Island-facing coast chance (%)" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "shoreBoatCrossingChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 388 OffsetTop 388 SizeX 180 OffsetRight -208 SizeY 32 OffsetBottom -420 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_shoreBoatCrossingChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 388 OffsetTop 388 SizeX 100 OffsetRight -320 SizeY 32 OffsetBottom -420 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } } +TextWidgetClass { Name "Saved_shoreBoatCrossingChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 424 OffsetTop 424 SizeX 456 OffsetRight -484 SizeY 24 OffsetBottom -448 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "Help_shoreBoatCrossingChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 388 OffsetTop 388 SizeX 456 OffsetRight -964 SizeY 60 OffsetBottom -448 } Color 0.9 0.92 0.94 1 Text "Default 70%; never lower than the ordinary coast chance." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "BoatsFooter" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 486 OffsetTop 486 SizeX 936 OffsetRight -964 SizeY 40 OffsetBottom -526 } Color 0.9 0.92 0.94 1 Text "Default: up to 6 unarmed boats, at least 700 m apart, with fuel. Boat and mod switches are respected. Status shows availability and survey progress." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +} } FrameWidgetClass { "Is Visible" 0 Name "TelemetryPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1220 SizeY 532 OffsetBottom -692 } { TextWidgetClass { Name "TelemetryHeading" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 10 OffsetTop 10 SizeX 936 OffsetRight -964 SizeY 32 OffsetBottom -42 } Color 0.9 0.92 0.94 1 Text "Mission statistics" "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } TextWidgetClass { Name "TelemetryDescription" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 55 OffsetTop 55 SizeX 936 OffsetRight -964 SizeY 80 OffsetBottom -135 } Color 0.9 0.92 0.94 1 Text "Share mission starts, progress, results, terrain, loaded mods and player counts with the configured statistics service. Enabled by default." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } @@ -1223,18 +1245,21 @@ ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_motorbikeCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 764 OffsetLeft 764 PositionY 80 OffsetTop 80 SizeX 200 OffsetRight -964 SizeY 38 OffsetBottom -118 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } } TextWidgetClass { Name "Saved_motorbikeCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 640 OffsetLeft 640 PositionY 120 OffsetTop 120 SizeX 324 OffsetRight -964 SizeY 24 OffsetBottom -144 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } TextWidgetClass { Name "RecommendedBikeID" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 116 OffsetTop 116 SizeX 580 OffsetRight -608 SizeY 24 OffsetBottom -140 } Color 0.9 0.92 0.94 1 Text "Workshop: 6503091A7B264FC3" "Font Size" 15 "Min Font Size" 15 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } -TextWidgetClass { Name "recommended_bikes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 145 OffsetTop 145 SizeX 936 OffsetRight -964 SizeY 25 OffsetBottom -170 } Color 0.9 0.92 0.94 1 Text "Waiting for server addon list" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } -TextWidgetClass { Name "Help_motorbikeCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 175 OffsetTop 175 SizeX 936 OffsetRight -964 SizeY 27 OffsetBottom -202 } Color 0.9 0.92 0.94 1 Text "Two special-forces crew per bike, alongside UAZ hunters. Count applies next round; losses stay lost." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } -TextWidgetClass { Name "RecommendedBikeRuntime" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 207 OffsetTop 207 SizeX 936 OffsetRight -964 SizeY 36 OffsetBottom -243 } Color 0.9 0.92 0.94 1 Text "Current round: waiting for server" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } -TextWidgetClass { Name "RecommendedPlaneLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 256 OffsetTop 256 SizeX 740 OffsetRight -768 SizeY 28 OffsetBottom -284 } Color 0.9 0.92 0.94 1 Text "Cessna 172 - reconnaissance patrol" "Font Size" 21 "Min Font Size" 21 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } -ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "RecommendedPlane" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 788 OffsetLeft 788 PositionY 252 OffsetTop 252 SizeX 176 OffsetRight -964 SizeY 38 OffsetBottom -290 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Configure Cessna" } } } -TextWidgetClass { Name "RecommendedPlaneID" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 289 OffsetTop 289 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -313 } Color 0.9 0.92 0.94 1 Text "Workshop: 69BAEE0D439966FE | Propeller Flight Core: 69A8A34027DA65C5" "Font Size" 15 "Min Font Size" 15 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } -TextWidgetClass { Name "recommended_plane" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 316 OffsetTop 316 SizeX 936 OffsetRight -964 SizeY 25 OffsetBottom -341 } Color 0.9 0.92 0.94 1 Text "Waiting for server addon list" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } -TextWidgetClass { Name "RecommendedPlaneHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 345 OffsetTop 345 SizeX 936 OffsetRight -964 SizeY 26 OffsetBottom -371 } Color 0.9 0.92 0.94 1 Text "Patrols and reports sightings. Requires a usable airport on the selected terrain." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } -TextWidgetClass { Name "RecommendedWeaponsLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 385 OffsetTop 385 SizeX 740 OffsetRight -768 SizeY 28 OffsetBottom -413 } Color 0.9 0.92 0.94 1 Text "Phaseline Marksman Rifles - extra weapons" "Font Size" 21 "Min Font Size" 21 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } -ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "RecommendedWeapons" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 788 OffsetLeft 788 PositionY 381 OffsetTop 381 SizeX 176 OffsetRight -964 SizeY 38 OffsetBottom -419 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Manage items" } } } -TextWidgetClass { Name "RecommendedWeaponsID" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 416 OffsetTop 416 SizeX 936 OffsetRight -964 SizeY 22 OffsetBottom -438 } Color 0.9 0.92 0.94 1 Text "Workshop: BFC890A6F1603EC0" "Font Size" 15 "Min Font Size" 15 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } -TextWidgetClass { Name "recommended_weapons" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 441 OffsetTop 441 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -465 } Color 0.9 0.92 0.94 1 Text "Waiting for server addon list" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "recommended_bikes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 137 OffsetTop 137 SizeX 936 OffsetRight -964 SizeY 25 OffsetBottom -162 } Color 0.9 0.92 0.94 1 Text "Waiting for server addon list" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "Help_motorbikeCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 165 OffsetTop 165 SizeX 936 OffsetRight -964 SizeY 22 OffsetBottom -187 } Color 0.9 0.92 0.94 1 Text "Two special-forces crew per bike, alongside UAZ hunters. Count applies next round; losses stay lost." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "RecommendedBikeRuntime" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 188 OffsetTop 188 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -212 } Color 0.9 0.92 0.94 1 Text "Current round: waiting for server" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "RecommendedPlaneLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 220 OffsetTop 220 SizeX 740 OffsetRight -768 SizeY 28 OffsetBottom -248 } Color 0.9 0.92 0.94 1 Text "Cessna 172 - reconnaissance (requires an airport)" "Font Size" 21 "Min Font Size" 21 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "RecommendedPlane" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 788 OffsetLeft 788 PositionY 216 OffsetTop 216 SizeX 176 OffsetRight -964 SizeY 38 OffsetBottom -254 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Configure Cessna" } } } +TextWidgetClass { Name "RecommendedPlaneID" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 250 OffsetTop 250 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -274 } Color 0.9 0.92 0.94 1 Text "Workshop: 69BAEE0D439966FE | Propeller Flight Core: 69A8A34027DA65C5" "Font Size" 15 "Min Font Size" 15 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "recommended_plane" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 276 OffsetTop 276 SizeX 936 OffsetRight -964 SizeY 25 OffsetBottom -301 } Color 0.9 0.92 0.94 1 Text "Waiting for server addon list" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "RecommendedWeaponsLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 320 OffsetTop 320 SizeX 740 OffsetRight -768 SizeY 28 OffsetBottom -348 } Color 0.9 0.92 0.94 1 Text "Phaseline Marksman Rifles - extra weapons" "Font Size" 21 "Min Font Size" 21 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "RecommendedWeapons" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 788 OffsetLeft 788 PositionY 316 OffsetTop 316 SizeX 176 OffsetRight -964 SizeY 38 OffsetBottom -354 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Manage items" } } } +TextWidgetClass { Name "RecommendedWeaponsID" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 351 OffsetTop 351 SizeX 936 OffsetRight -964 SizeY 22 OffsetBottom -373 } Color 0.9 0.92 0.94 1 Text "Workshop: BFC890A6F1603EC0" "Font Size" 15 "Min Font Size" 15 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "recommended_weapons" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 376 OffsetTop 376 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -400 } Color 0.9 0.92 0.94 1 Text "Waiting for server addon list" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "RecommendedBoatsLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 412 OffsetTop 412 SizeX 740 OffsetRight -768 SizeY 28 OffsetBottom -440 } Color 0.9 0.92 0.94 1 Text "Motorboat - shoreline transport" "Font Size" 21 "Min Font Size" 21 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "RecommendedBoats" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 788 OffsetLeft 788 PositionY 408 OffsetTop 408 SizeX 176 OffsetRight -964 SizeY 38 OffsetBottom -446 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Shore boats" } } } +TextWidgetClass { Name "RecommendedBoatsID" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 445 OffsetTop 445 SizeX 936 OffsetRight -964 SizeY 22 OffsetBottom -467 } Color 0.9 0.92 0.94 1 Text "Workshop: 610C8532D3756F2D | Requires Boat Core, AI Radio and VehicleCore" "Font Size" 15 "Min Font Size" 15 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "recommended_boats" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 470 OffsetTop 470 SizeX 936 OffsetRight -964 SizeY 52 OffsetBottom -522 } Color 0.9 0.92 0.94 1 Text "Waiting for server addon list" "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } } } FrameWidgetClass { "Is Visible" 0 Name "WeightsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1220 SizeY 532 OffsetBottom -692 } { TextWidgetClass { Name "WeightsHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 4 OffsetTop 4 SizeX 936 OffsetRight -964 SizeY 48 OffsetBottom -52 } Color 0.9 0.92 0.94 1 Text "Make selected mods or items more common. 1 = normal, 100 = very high, 0 = never. These are relative weights, not percentages." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } diff --git a/docs/enemy-loadouts.md b/docs/enemy-loadouts.md index 1313b1a0..aaaefd0a 100644 --- a/docs/enemy-loadouts.md +++ b/docs/enemy-loadouts.md @@ -99,10 +99,13 @@ There are no new entity scans, prefab loads, network messages or per-frame updat ## Optional motorbike hunters -Mods → Recommended addons lists WCS Motorbikes (`6503091A7B264FC3`), Cessna 172 -and Phaseline Marksman Rifles, using the **server's mounted addons**. Missing +Mission → Recommended addons lists WCS Motorbikes (`6503091A7B264FC3`), Cessna 172, +Phaseline Marksman Rifles and Motorboat (`610C8532D3756F2D`), using the **server's mounted addons**. Missing supporting addons are named. This page does not download mods or introduce new -Workshop dependencies. Cessna and weapon buttons open their existing controls. +Workshop dependencies. Cessna, weapon and Shore boats buttons open their existing controls. Motorboat requires +Modest_Boat_Core (`696A0CE876246081`), Modest AI Radio (`61A6B4C21F6FCF22`) and +Modest VehicleCore (`6A7D4E7B69969355`); the page names any missing dependencies. +See [Shore boats](shore-boats.md) for supported versions and test results. Motorbike count is 0–6 (default 2, 0 disables), saved for the next round. When WCS Motorbikes, its Motorbike base (`61B84088181A8FA4`) and WCS Armaments are mounted, diff --git a/docs/shore-boats.md b/docs/shore-boats.md new file mode 100644 index 00000000..eac82b76 --- /dev/null +++ b/docs/shore-boats.md @@ -0,0 +1,39 @@ +# Shore boats + +Optional integration uses **[Motorboat](https://reforger.armaplatform.com/workshop/610C8532D3756F2D)** by Modest23. Version 0.0.13 is built for Reforger 1.9.0.37. It requires Modest_Boat_Core, Modest AI Radio and Modest VehicleCore. REAPER is not required. Vanilla boat scenery is not usable transport. + +**Mission → Recommended addons** lists Motorboat, names missing server dependencies and opens its settings through **Shore boats**. + +**Content → Shore boats** controls occasional unarmed transport boats. Changes apply next round. + +| Setting | Default | Range | +| --- | ---: | ---: | +| Maximum boats per round | 6 | 0–16; zero disables | +| Ordinary coast candidate chance | 20% | 0–100% | +| Island-facing coast candidate chance | 70% | 0–100%; at least the ordinary chance | + +The server must load Motorboat (`610C8532D3756F2D`) and its Workshop dependencies. The mission discovers the inspected unarmed `Prefabs/Vehicles/Boats/Motorboat/Motorboat_Base.et` dynamically. No new mandatory mission dependency or third-party asset is included. Without Motorboat, Status explains why no boats spawn. Mod switches and **Content → Vehicles → Use for: Shore boats** assignments apply. Other boat mods are not automatically selected. Invalid drivetrains are rejected before a boat is kept. + +A bounded background terrain survey identifies connected landmasses. Shore candidates facing a different landmass within 4 km receive the higher probability; an opposite bank on the same island does not. The grid uses 64 m cells on smaller maps and expands to keep each axis below 257 cells, with intermediate land checks across each connection. Very small islands and narrow land connections can fall below this survey resolution. Probabilities apply once per randomized shore candidate, not once per island. Boats are occasional transport, not a guaranteed escape route for every island. + +Precise placement checks reject cliffs, shallow hull footprints, nearby obstructions and blocked departure water. Boats sit 12–36 m off the shore, at least 700 m apart, with 50–90% fuel. Placement is withheld near players or when their bodies/spectator targets can see it; unknown views or an open editor also veto placement. Normal vehicle replication handles the boats. Empty boats have no forced faction, allowing first-occupant ownership. + +The round owns the finite fleet. No distance cleanup, respawn or replacement happens after use or destruction. The maximum is an upper bound: missing/disabled content or unsuitable shores may yield fewer boats. + +Authenticated commands: + +- `/esc mission shoreboats set 6` +- `/esc mission boatchance set 20` +- `/esc mission boatcrossings set 70` + +Each also supports `show` and `help`. Status → Mission status reports survey progress, missing mod, disabled state and created count. + +## Validation + +Native Workbench test on 2026-11-07, Reforger 1.9.0.37: Motorboat 0.0.13, Modest_Boat_Core 1.0.1, Modest AI Radio 1.0.33 and Modest VehicleCore 1.0.1. The final production catalog and drivetrain guard spawned a fueled, unarmed boat at a surveyed shore; a USSR test soldier entered its neutral driver seat. Native throttle moved it 16.1 m in the 15-second acceleration phase, reaching 9.84 km/h and 4,587 RPM; native steering changed its yaw by 95.5 degrees. The finite survey and native menu bounds checks passed, as did the shoreline-grid, worker, menu and vehicle-role regression checks. + +This is scripted local integration evidence, not a dedicated-server multiplayer or manual-control playtest. The diagnostic completed and cleaned up, with source and dependency fingerprints unchanged. Its strict overall result remains nonzero because the log contains resource errors: Motorboat radio/horn references and separate Rally DLC resources in the private Workbench profile. Motorboat also reports clutch-torque and wheel-axis warnings, but its drivetrain initializes and the movement checks pass. REAPER RHIB 1.0.2 failed drivetrain initialization in the earlier test; it is no longer selected. + +### Horn audio + +Motorboat 0.0.13 has a silent horn in the 1.9.0.37 native test. Two four-second bursts used its `SOUND_VEHICLE_HORN_LP` event with `HornState=1`, compared with a vanilla UAZ under the same isolated audio recording setup. Motorboat stayed at background levels (about 43–54 PCM RMS); the UAZ produced clear horn output (about 656–823 PCM RMS versus 33–57 background). The boat references the unavailable `Sounds/Horns/Horn_-_Mono_-_Loop.wav` resource (`8A27DC8302B06ACF`). No third-party horn asset or compatibility override was added. This verifies the configured sound output; it does not qualify multiplayer or the player's horn key binding. diff --git a/tests/test_admin_menu.py b/tests/test_admin_menu.py index ade42114..13847f04 100644 --- a/tests/test_admin_menu.py +++ b/tests/test_admin_menu.py @@ -55,7 +55,7 @@ assert 'Apply_planeReconEnabled' not in names # Saved-state toggle pair, not a assert len(names) == len(set(names)), 'Menu widget names must be unique' for key in keys: assert all(name in names for name in (key, 'Apply_' + key, 'Saved_' + key, 'Help_' + key)) - if key not in ('crashLootMode','crashSiteCount','crashHelicopterChancePercent','crashRifleCount','crashMagazineCount','crashAntiAirChancePercent','crashRocketChancePercent','radioIntelSiteCount','droneReconCount','droneCooldownSeconds','sarkaAngerPerCar','emptyRestartHours','aiDoctrine','aiShadow','motorbikeCount','planeAltitudeMetres','planeReplacementSeconds','planeSectorSeconds','fieldCampCount','fieldHospitalCount','startMode','garageVehicleCount','garageChancePercent','garageRadiusMetres','garageSpawnIntervalSeconds','civilianTrafficCount','roadblockCount','largeRoadblockCount','mainRoadWidthMetres','hunterSpawnDistanceMetres','objectGuardCount','objectGuardEvery','objectGuardHQMode','startWeaponCount','startMagazineCount'): assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names + if key not in ('shoreBoatCount','shoreBoatChancePercent','shoreBoatCrossingChancePercent','crashLootMode','crashSiteCount','crashHelicopterChancePercent','crashRifleCount','crashMagazineCount','crashAntiAirChancePercent','crashRocketChancePercent','radioIntelSiteCount','droneReconCount','droneCooldownSeconds','sarkaAngerPerCar','emptyRestartHours','aiDoctrine','aiShadow','motorbikeCount','planeAltitudeMetres','planeReplacementSeconds','planeSectorSeconds','fieldCampCount','fieldHospitalCount','startMode','garageVehicleCount','garageChancePercent','garageRadiusMetres','garageSpawnIntervalSeconds','civilianTrafficCount','roadblockCount','largeRoadblockCount','mainRoadWidthMetres','hunterSpawnDistanceMetres','objectGuardCount','objectGuardEvery','objectGuardHQMode','startWeaponCount','startMagazineCount'): assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names else: assert 'Random_startMode' not in names for name in ('Close', 'SettingsPage', 'HQPage', 'JournalPage', 'JournalText', 'JournalContent', 'JournalScroll', 'PauseJournal', 'LatestJournal', 'Status'): assert name in names diff --git a/tests/test_admin_setting_policy.py b/tests/test_admin_setting_policy.py index 9f536328..c2d20b27 100644 --- a/tests/test_admin_setting_policy.py +++ b/tests/test_admin_setting_policy.py @@ -11,6 +11,10 @@ rules=(rules.replace('string ', 'std::string ').replace('out int ', 'int& ') .replace(' < 0) return false;', ' == std::string::npos) return false;').replace('text.ToInt()', 'std::stoi(text)')) run('#include \n#include \n'+rules+r''' int main(){ + assert(CoreSetting("mission","shoreboats")=="shoreBoatCount"); assert(Valid("shoreBoatCount",0)&&Valid("shoreBoatCount",16)&&!Valid("shoreBoatCount",-1)&&!Valid("shoreBoatCount",17)); + assert(CoreSetting("mission","boatchance")=="shoreBoatChancePercent"); assert(Valid("shoreBoatChancePercent",0)&&Valid("shoreBoatChancePercent",100)&&!Valid("shoreBoatChancePercent",-1)&&!Valid("shoreBoatChancePercent",101)); + assert(CoreSetting("mission","boatcrossings")=="shoreBoatCrossingChancePercent"); assert(Valid("shoreBoatCrossingChancePercent",0)&&Valid("shoreBoatCrossingChancePercent",100)&&!Valid("shoreBoatCrossingChancePercent",-1)&&!Valid("shoreBoatCrossingChancePercent",101)); + assert(CoreSetting("mission","crashloot")=="crashLootMode"); assert(Valid("crashLootMode",0)&&Valid("crashLootMode",1)&&!Valid("crashLootMode",-1)&&!Valid("crashLootMode",2)); assert(CoreSetting("mission","crashsites")=="crashSiteCount"); diff --git a/tests/test_item_preferences.py b/tests/test_item_preferences.py index 6eafe183..27262c7a 100644 --- a/tests/test_item_preferences.py +++ b/tests/test_item_preferences.py @@ -7,7 +7,7 @@ methods=methods.replace('role.ToString()', 'string(std::to_string(role))').repla methods=methods.replace('protected ', '').replace('array', 'Array').replace('set keys = new set();','Array keys;') methods=methods.replace('foreach (', 'for (').replace('ResourceName','string').replace('ERR_TerrainCatalog.Key(prefab)', 'prefab').replace('SCR_AddonTool.GetResourceAddons(prefab)', 'sources') methods=methods.replace('Array sources = sources;', 'Array resourceSources = sources;').replace('project : sources','project : resourceSources') -methods=methods.replace('"01234".IndexOf', 'string("01234").IndexOf') +methods=methods.replace('"012345".IndexOf', 'string("012345").IndexOf') methods=methods.replace('"0123456789abcdef".IndexOf','string("0123456789abcdef").IndexOf').replace('!records || ', '') run(r''' #include @@ -88,9 +88,13 @@ int main(){ assert(p.SetVehicleRole(a,j,3,false)&&!p.AllowsVehicleRole(j,3,false)&&p.AllowsVehicleRole(j,2,false)); assert(p.Preset("save","replacement")&&p.SetVehicleRole(a,j,2,false)&&p.Preset("load","replacement")&&p.AllowsVehicleRole(j,2,false)); assert(p.SetSwitch(a,"mod",false)&&!p.AllowsVehicleRole(j,2,true)&&p.SetSwitch(a,"mod",true)&&p.AllowsVehicleRole(j,2,false)); + assert(p.SetVehicleRole(a,j,5,false)&&!p.AllowsVehicleRole(j,5,true)); + assert(p.SetVehicleRole(a,j,5,true)&&p.AllowsVehicleRole(j,5,true)); + assert(p.Preset("save","replacement")&&p.SetVehicleRole(a,j,5,false)&&p.Preset("load","replacement")&&p.AllowsVehicleRole(j,5,true)); + assert(p.Valid({"active","active|1111111111111111|r5*|1"})); int before=p.m_Revision;p.fail=true;assert(!p.SetVehicleRole(a,j,2,false)&&p.m_Revision==before&&p.AllowsVehicleRole(j,2,false));p.fail=false; - assert(!p.SetVehicleRole(a,j,5,true)&&!p.SetVehicleRole(a,j,-1,true)&&!p.SetVehicleRole(a,"bad",0,true)); - assert(!p.Valid({"active","active|1111111111111111|r5*|1"})); + assert(!p.SetVehicleRole(a,j,6,true)&&!p.SetVehicleRole(a,j,-1,true)&&!p.SetVehicleRole(a,"bad",0,true)); + assert(!p.Valid({"active","active|1111111111111111|r6*|1"})); assert(!p.Set("../x",i,true)&&!p.Set(a,"unknown",true)); } ''') diff --git a/tests/test_settings_migration.py b/tests/test_settings_migration.py index fc3b7ea1..d47b0739 100644 --- a/tests/test_settings_migration.py +++ b/tests/test_settings_migration.py @@ -5,7 +5,7 @@ from policy_harness import ROOT,body,run spec=importlib.util.spec_from_file_location('import_settings',BASE/'tools/import_settings.py') m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m) result,deferred=m.merge({'configVersion':1,'reportIntervalSeconds':20.0,'supportSquadLimit':2,'responseRadiusMetres':1500,'convoyCount':3},{'aiIntelChancePercent':0,'intelRevealChancePercent':100}) -assert result==dict(crashLootMode=1,crashSiteCount=3,crashHelicopterChancePercent=50,crashRifleCount=3,crashMagazineCount=4,crashAntiAirChancePercent=25,crashRocketChancePercent=50,telemetryEnabled=1,droneReconCount=0,droneCooldownSeconds=300,aiDoctrine=0,aiShadow=0,planeAltitudeMetres=250,planeReplacementSeconds=60,planeSectorSeconds=120,planeReconEnabled=1,roadblockCount=4,largeRoadblockCount=4,mainRoadWidthMetres=6,civilianTrafficCount=3,garageVehicleCount=16,garageChancePercent=70,garageRadiusMetres=1000,garageSpawnIntervalSeconds=5,startMode=0,keepWeaponOnUnconscious=1,selfEpinephrine=0,restartOnEmpty=1,emptyRestartHours=5,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioIntelSiteCount=5,showAllSites=0,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,viewDistanceMetres=0,automaticEquipmentChancePercent=25,antiAirChancePercent=7,antiPersonnelMineChancePercent=60,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15) +assert result==dict(shoreBoatCount=6,shoreBoatChancePercent=20,shoreBoatCrossingChancePercent=70,crashLootMode=1,crashSiteCount=3,crashHelicopterChancePercent=50,crashRifleCount=3,crashMagazineCount=4,crashAntiAirChancePercent=25,crashRocketChancePercent=50,telemetryEnabled=1,droneReconCount=0,droneCooldownSeconds=300,aiDoctrine=0,aiShadow=0,planeAltitudeMetres=250,planeReplacementSeconds=60,planeSectorSeconds=120,planeReconEnabled=1,roadblockCount=4,largeRoadblockCount=4,mainRoadWidthMetres=6,civilianTrafficCount=3,garageVehicleCount=16,garageChancePercent=70,garageRadiusMetres=1000,garageSpawnIntervalSeconds=5,startMode=0,keepWeaponOnUnconscious=1,selfEpinephrine=0,restartOnEmpty=1,emptyRestartHours=5,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioIntelSiteCount=5,showAllSites=0,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,viewDistanceMetres=0,automaticEquipmentChancePercent=25,antiAirChancePercent=7,antiPersonnelMineChancePercent=60,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15) assert deferred==[] for count in (0,1,3,20): values,notes=m.merge(ai={'convoyCount':count}) diff --git a/tests/test_shore_boats.py b/tests/test_shore_boats.py new file mode 100644 index 00000000..332ef663 --- /dev/null +++ b/tests/test_shore_boats.py @@ -0,0 +1,82 @@ +"""Execute production finite boat-worker decisions with engine calls doubled.""" +from policy_harness import ROOT,body,run +p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ShoreBoats.c' +step=body(p,'protected void Step(').replace('protected ','').replace('Replication.','Replication::').replace('ERR_GameMode.Get()','ERR_GameMode::Get()').replace('ERR_MissionRules.','ERR_MissionRules::').replace('m_Mode.','m_Mode->').replace('Math.RandomInt','RandomInt').replace('ERR_ShoreMap.Chance','Chance') +chance=body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ShoreMap.c','static int Chance(').replace('Math.Max','std::max') +run(r''' +#include +#include +#include +#include +using vector=int; +namespace Replication {bool server=true;bool IsServer(){return server;}} +namespace ERR_MissionRules {bool Terminal(int phase){return phase==2;}} +struct ERR_GameMode {int phase=0;bool Active(){return phase==1;}int Phase(){return phase;}static ERR_GameMode* Get();} mode; +ERR_GameMode* ERR_GameMode::Get(){return &mode;} +struct Array:std::vector{using std::vector::vector;bool IsEmpty(){return empty();}int Count(){return size();}void Remove(int i){erase(begin()+i);}}; +struct Map {Array Edges={1,2,3,4,5};bool ready=false;int work=0;bool Ready(){return ready;}void Step(){work++;}}; +int roll=10;int RandomInt(int,int end){if(end==100)return roll;return 0;} +''' + chance + r''' +struct Worker { + ERR_GameMode* m_Mode=&mode;Map m_Map;bool m_Stopped=false,clear=true,cross=false,success=true; + int m_Target=2,m_Attempts=0,m_Chance=20,m_CrossingChance=70,created=0,spawns=0;std::string m_State; + int Count(){return created;}void Stop(){m_Stopped=true;} + bool Survey(int,vector&,vector&,bool& crossing){crossing=cross;return clear;} + bool Spawn(vector,vector){spawns++;if(success)created++;return success;} +''' + step + r''' +}; +int main(){ + Worker w;for(int i=0;i<100;i++){w.Step();}assert(w.m_Map.work==0&&!w.m_Stopped); + mode.phase=1;w.Step();assert(w.m_Map.work==1&&w.spawns==0);w.m_Map.ready=true; + for(int i=0;i<100;i++){w.Step();}assert(w.spawns==2&&w.created==2&&w.m_Stopped); + Worker ordinary;ordinary.m_Map.ready=true;roll=50;for(int i=0;i<20;i++){ordinary.Step();}assert(ordinary.spawns==0&&ordinary.m_Attempts==5&&ordinary.m_Stopped); + Worker facing;facing.m_Map.ready=true;facing.cross=true;for(int i=0;i<20;i++){facing.Step();}assert(facing.created==2); + Worker blocked;blocked.m_Map.ready=true;blocked.clear=false;for(int i=0;i<20;i++){blocked.Step();}assert(blocked.m_Stopped&&blocked.spawns==0); + Worker failed;failed.m_Map.ready=true;failed.success=false;roll=10;for(int i=0;i<20;i++){failed.Step();}assert(failed.spawns==5&&failed.m_Stopped); + Worker bounded;bounded.m_Map.ready=true;bounded.m_Attempts=4096;bounded.Step();assert(bounded.m_Stopped&&bounded.spawns==0); + Worker client;Replication::server=false;client.Step();assert(client.m_Stopped&&client.m_Map.work==0); + Replication::server=true;mode.phase=2;Worker ended;ended.Step();assert(ended.m_Stopped&&ended.spawns==0); +} +''') +# The audited integration selects only the playable unarmed Motorboat, never scenery, +# armed alternatives or a boat's child light entity. +candidate=body(p,'static bool Candidate(').replace('string ','Text ') +run(r''' +#include +#include +#include +struct Text:std::string {using std::string::string;void ToLower(){std::transform(begin(),end(),begin(),::tolower);}bool EndsWith(std::string suffix){return size()>=suffix.size()&&compare(size()-suffix.size(),suffix.size(),suffix)==0;}}; +'''+candidate+r''' +int main(){ + assert(Candidate("{ABC}Prefabs/Vehicles/Boats/Motorboat/Motorboat_Base.et")); + for(auto s:{"Prefabs/Vehicles/RHIB/REAPER_RHIB_BK_ARMED.et","Prefabs/Vehicles/RHIB/REAPER_RHIB_Wreck.et","Prefabs/Vehicles/RHIB/REAPER_RHIB_LightEnt.et","Prefabs/Vehicles/Boats/Motorboat/Lights/Left_Red_Nav_ES.et","Prefabs/Vehicles/Boats/MOD_Boat_Base.et","Prefabs/Props/Boat.et"})assert(!Candidate(s)); +} +''') +print('PASS: preparation/client/end guards, finite shuffled candidates, probability once per edge, no replenishment, failed placement bounds and unarmed boat catalog') + +start=body(p,'void Start(').replace('Replication.','Replication::').replace('m_Mode.Settings().Get(', 'settings.Get(').replace('vector low, high; GetGame().GetWorldEntity().GetWorldBounds(low, high);','vector low=0, high=100;').replace('GetGame().GetCallqueue().CallLater(Step, 100, true)','schedules++') +run(r''' +#include +#include +#include +using vector=int; +namespace Replication{bool server=true;bool IsServer(){return server;}} +struct Settings{std::map v={{"shoreBoatCount",6},{"shoreBoatChancePercent",20},{"shoreBoatCrossingChancePercent",70}};int Get(std::string key){return v.at(key);}}; +struct Map{int setups=0;void Setup(vector,vector){setups++;}}; +struct Pool{bool empty=true;bool IsEmpty(){return empty;}}; +struct Catalog{Pool Prefabs;bool loaded=false;int finds=0;void Find(){finds++;Prefabs.empty=!loaded;}}; +struct Service{ + bool m_Started=false,m_Stopped=false,m_Mode=true;int m_Target=0,m_Chance=0,m_CrossingChance=0,schedules=0;std::string m_State; + Settings settings;Catalog m_Catalog;Map m_Map; +''' + start + r''' +}; +int main(){ + Service normal;normal.m_Catalog.loaded=true;normal.Start();normal.Start();assert(normal.schedules==1&&normal.m_Map.setups==1&&normal.m_Target==6&&normal.m_CrossingChance==70&&normal.m_Chance==20); + normal.settings.v["shoreBoatCount"]=0;normal.Start();assert(normal.m_Target==6); // saved edits wait until next round + Service disabled;disabled.settings.v["shoreBoatCount"]=0;disabled.Start();assert(disabled.schedules==0&&disabled.m_Catalog.finds==0&&disabled.m_State=="disabled"); + Service missing;missing.Start();assert(missing.schedules==0&&missing.m_Map.setups==0&&missing.m_State=="Motorboat not loaded"); + Service stopped;stopped.m_Stopped=true;stopped.Start();assert(stopped.schedules==0&&stopped.m_Catalog.finds==0); + Service client;Replication::server=false;client.Start();assert(client.schedules==0&&client.m_Catalog.finds==0); +} +''') +print('PASS: actual startup snapshots settings once, skips disabled/missing-mod surveys, and refuses client/stopped scheduling') diff --git a/tests/test_shore_map.py b/tests/test_shore_map.py new file mode 100644 index 00000000..fbc0369b --- /dev/null +++ b/tests/test_shore_map.py @@ -0,0 +1,46 @@ +"""Execute production coastal topology and island-facing selection, not an engine test.""" +from policy_harness import ROOT,body,run +p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ShoreMap.c' +methods='\n'.join(body(p,s) for s in ['void Setup(', 'vector Point(', 'int Index(', 'int Root(', 'protected void Join(', 'int Neighbor(', 'bool Ready(', 'void Step(', 'bool FacingIsland(', 'static int Chance(']) +methods=methods.replace('protected ','').replace('vector.Lerp','Vec::Lerp').replace('vector ','Vec ').replace('Math.Max','Max').replace('Math.Ceil','ceil').replace('Math.Floor','floor').replace('Math.Clamp','Clamp').replace('Vector(','Vec(').replace('System.GetTickCount()','0').replace('ERR_CampTerrain.Rotate','Rotate') +run(r''' +#include +#include +#include +#include +#include +float Max(float a,float b){return std::max(a,b);} int Clamp(int v,int a,int b){return std::clamp(v,a,b);} +struct Vec { float v[3]; Vec(float x=0,float y=0,float z=0):v{x,y,z}{} float& operator[](int i){return v[i];} + Vec operator+(Vec b){return {v[0]+b[0],v[1]+b[1],v[2]+b[2]};} Vec operator*(float n){return {v[0]*n,v[1]*n,v[2]*n};} + static Vec Lerp(Vec a,Vec b,float t){return a*(1-t)+b*t;} }; +Vec Rotate(Vec v,float angle){float a=angle*3.14159265/180;return {v[0]*cosf(a)+v[2]*sinf(a),0,v[2]*cosf(a)-v[0]*sinf(a)};} +templatestruct Array:std::vector{using std::vector::vector;void Clear(){this->clear();}void Insert(T v){this->push_back(v);}int Count(){return this->size();}}; +struct Map { + Vec Low;float Cell=64;int Width=0,Height=0,m_Cursor=0,m_Phase=0;Array Parents,Edges; + std::function terrain=[](Vec){return false;};int samples=0; + bool Land(Vec p){samples++;return terrain(p);} +''' + methods + r''' + void Build(){for(int i=0;i<10000&&!Ready();i++){int n=samples;Step();assert(samples-n<=64*7);}assert(Ready());} +}; +int main(){ + Map m;m.Setup({0,0,0},{1024,0,1024});assert(m.Width==16&&m.Height==16&&m.Cell==64); + // Two islands separated by a four-cell sea channel. + m.terrain=[](Vec p){return p[0]<256 || (p[0]>=512&&p[0]<768);};m.Build(); + assert(m.Root(0)!=m.Root(8));assert(m.FacingIsland(0,{256,0,400},{1,0,0})); + assert(!m.FacingIsland(0,{0,0,400},{-1,0,0})); // open sea + assert(m.Index({-1,0,0})==-1&&m.Index({1024,0,0})==-1); + assert(m.Neighbor(15,0)==-1&&m.Neighbor(0,1)==-1); + assert(m.Chance(true,20,70)==70&&m.Chance(false,20,70)==20); + assert(m.Chance(true,80,30)==80&&m.Chance(false,0,70)==0); + // A land connection along the north edge makes the same shape a bay, not islands. + m.Setup({0,0,0},{1024,0,1024});m.terrain=[](Vec p){return p[2]<64||p[0]<256||(p[0]>=512&&p[0]<768);};m.Build(); + assert(m.Root(0)==m.Root(8));assert(!m.FacingIsland(0,{256,0,400},{1,0,0})); + // Channels smaller than one cell still prevent a link when intermediate samples see water. + m.Setup({0,0,0},{256,0,256});m.terrain=[](Vec p){return p[0]<50||p[0]>78;};m.Build();assert(m.Root(0)!=m.Root(1)); + // All water / empty terrain creates no shore candidates; very large worlds stay bounded. + m.Setup({0,0,0},{256,0,256});m.terrain=[](Vec){return false;};m.Build();assert(m.Edges.empty()); + m.Setup({0,0,0},{0,0,0});m.Build();assert(m.Edges.empty()); + m.Setup({0,0,0},{100000,0,100000});assert(m.Width<=256&&m.Height<=256); +} +''') +print('PASS: actual bounded shoreline grid, connected bays versus separate islands, narrow channels, outward fan, map limits and probability boost') diff --git a/tools/build_admin_menu.py b/tools/build_admin_menu.py index fc1fd1c9..1b2e82f3 100644 --- a/tools/build_admin_menu.py +++ b/tools/build_admin_menu.py @@ -29,7 +29,7 @@ def navigation(source, tokens): if (len(members) != len(set(members)) or set(members) != keys.keys() or len(set(names)) != len(names) or any(not re.fullmatch(r'[A-Za-z][A-Za-z0-9]*', n) for n in names) - or any(not 1 <= len(g['pages']) <= 8 for g in groups)): + or any(not 1 <= len(g['pages']) <= 9 for g in groups)): raise ValueError('Every page must belong to exactly one valid sidebar group') for page in pages: if not isinstance(page['label'], str) or any(c in page['label'] for c in '\n\r"\\'): diff --git a/tools/check.py b/tools/check.py index 30591feb..54597ff7 100644 --- a/tools/check.py +++ b/tools/check.py @@ -34,6 +34,8 @@ def check(): subprocess.run([sys.executable,str(ROOT/'tests/test_rescue_recovery.py')],check=True) subprocess.run([sys.executable,str(ROOT/'tests/test_casualty_fleet.py')],check=True) subprocess.run([sys.executable,str(ROOT/'tests/test_plane_boarding.py')],check=True) + subprocess.run([sys.executable,str(ROOT/'tests/test_shore_map.py')],check=True) + subprocess.run([sys.executable,str(ROOT/'tests/test_shore_boats.py')],check=True) subprocess.run([sys.executable,str(ROOT/'tests/test_crash_sites_lifecycle.py')],check=True) subprocess.run([sys.executable,str(ROOT/'tests/test_crash_loot_plan.py')],check=True) subprocess.run([sys.executable,str(ROOT/'tests/test_crash_random_loot.py')],check=True) diff --git a/tools/import_settings.py b/tools/import_settings.py index aac58897..8952f38a 100644 --- a/tools/import_settings.py +++ b/tools/import_settings.py @@ -3,6 +3,10 @@ import argparse,json,os,tempfile,sys from pathlib import Path SCHEMA={ + 'shoreBoatCount':(6,0,16), + 'shoreBoatChancePercent':(20,0,100), + 'shoreBoatCrossingChancePercent':(70,0,100), + 'crashLootMode':(1,0,1), 'crashSiteCount':(3,0,12), 'crashHelicopterChancePercent':(50,0,100), diff --git a/ui/admin_menu/navigation.json b/ui/admin_menu/navigation.json index 918d28a4..9361bcc0 100644 --- a/ui/admin_menu/navigation.json +++ b/ui/admin_menu/navigation.json @@ -46,6 +46,7 @@ "ALLOWED_ITEMS", "VEHICLE_POOL", "VEHICLES", + "BOATS", "ITEMS", "WEIGHTS", "ITEM_PRESETS", @@ -276,6 +277,13 @@ "widget": "CrashSitesPage", "button": "OpenCrashSites", "label": "Crash sites" + }, + { + "id": 30, + "key": "BOATS", + "widget": "BoatsPage", + "button": "OpenBoats", + "label": "Shore boats" } ] } diff --git a/ui/admin_menu/pages/BoatsPage.layout.in b/ui/admin_menu/pages/BoatsPage.layout.in new file mode 100644 index 00000000..7dd7a0bc --- /dev/null +++ b/ui/admin_menu/pages/BoatsPage.layout.in @@ -0,0 +1,20 @@ +FrameWidgetClass { "Is Visible" {{VISIBLE_BoatsPage}} Name "BoatsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1220 SizeY 532 OffsetBottom -692 } { +TextWidgetClass { Name "BoatsHeading" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 4 OffsetTop 4 SizeX 936 OffsetRight -964 SizeY 30 OffsetBottom -34 } Color {{COLOR_TEXT}} Text "Shore boats - next round" "Font Size" {{SIZE_HEADING}} "Min Font Size" {{SIZE_HEADING}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +TextWidgetClass { Name "BoatsHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 40 OffsetTop 40 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -90 } Color {{COLOR_TEXT}} Text "Occasional usable boats near accessible shores. Coasts facing another island are favored. Requires Motorboat loaded on the server." "Font Size" {{SIZE_HELP}} "Min Font Size" {{SIZE_HELP}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +TextWidgetClass { Name "Label_shoreBoatCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 108 OffsetTop 108 SizeX 456 OffsetRight -484 SizeY 26 OffsetBottom -134 } Color {{COLOR_TEXT}} Text "Shore boats (0-16; 0 off)" "Font Size" {{SIZE_HELP}} "Min Font Size" {{SIZE_HELP}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "shoreBoatCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 136 OffsetTop 136 SizeX 180 OffsetRight -208 SizeY 32 OffsetBottom -168 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_shoreBoatCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 136 OffsetTop 136 SizeX 100 OffsetRight -320 SizeY 32 OffsetBottom -168 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } } +TextWidgetClass { Name "Saved_shoreBoatCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 172 OffsetTop 172 SizeX 456 OffsetRight -484 SizeY 24 OffsetBottom -196 } Color {{COLOR_TEXT}} Text "Waiting for server" "Font Size" {{SIZE_CAPTION_SMALL}} "Min Font Size" {{SIZE_CAPTION_SMALL}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +TextWidgetClass { Name "Help_shoreBoatCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 136 OffsetTop 136 SizeX 456 OffsetRight -964 SizeY 60 OffsetBottom -196 } Color {{COLOR_TEXT}} Text "Maximum for the round. Used or destroyed boats are not replaced." "Font Size" {{SIZE_HELP}} "Min Font Size" {{SIZE_HELP}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +TextWidgetClass { Name "Label_shoreBoatChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 234 OffsetTop 234 SizeX 456 OffsetRight -484 SizeY 26 OffsetBottom -260 } Color {{COLOR_TEXT}} Text "Ordinary coast chance (%)" "Font Size" {{SIZE_HELP}} "Min Font Size" {{SIZE_HELP}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "shoreBoatChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 262 OffsetTop 262 SizeX 180 OffsetRight -208 SizeY 32 OffsetBottom -294 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_shoreBoatChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 262 OffsetTop 262 SizeX 100 OffsetRight -320 SizeY 32 OffsetBottom -294 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } } +TextWidgetClass { Name "Saved_shoreBoatChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 298 OffsetTop 298 SizeX 456 OffsetRight -484 SizeY 24 OffsetBottom -322 } Color {{COLOR_TEXT}} Text "Waiting for server" "Font Size" {{SIZE_CAPTION_SMALL}} "Min Font Size" {{SIZE_CAPTION_SMALL}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +TextWidgetClass { Name "Help_shoreBoatChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 262 OffsetTop 262 SizeX 456 OffsetRight -964 SizeY 60 OffsetBottom -322 } Color {{COLOR_TEXT}} Text "Chance per surveyed shore candidate; default 20%." "Font Size" {{SIZE_HELP}} "Min Font Size" {{SIZE_HELP}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +TextWidgetClass { Name "Label_shoreBoatCrossingChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 360 OffsetTop 360 SizeX 456 OffsetRight -484 SizeY 26 OffsetBottom -386 } Color {{COLOR_TEXT}} Text "Island-facing coast chance (%)" "Font Size" {{SIZE_HELP}} "Min Font Size" {{SIZE_HELP}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "shoreBoatCrossingChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 388 OffsetTop 388 SizeX 180 OffsetRight -208 SizeY 32 OffsetBottom -420 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_shoreBoatCrossingChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 220 OffsetLeft 220 PositionY 388 OffsetTop 388 SizeX 100 OffsetRight -320 SizeY 32 OffsetBottom -420 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } } +TextWidgetClass { Name "Saved_shoreBoatCrossingChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 424 OffsetTop 424 SizeX 456 OffsetRight -484 SizeY 24 OffsetBottom -448 } Color {{COLOR_TEXT}} Text "Waiting for server" "Font Size" {{SIZE_CAPTION_SMALL}} "Min Font Size" {{SIZE_CAPTION_SMALL}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +TextWidgetClass { Name "Help_shoreBoatCrossingChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 388 OffsetTop 388 SizeX 456 OffsetRight -964 SizeY 60 OffsetBottom -448 } Color {{COLOR_TEXT}} Text "Default 70%; never lower than the ordinary coast chance." "Font Size" {{SIZE_HELP}} "Min Font Size" {{SIZE_HELP}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +TextWidgetClass { Name "BoatsFooter" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 486 OffsetTop 486 SizeX 936 OffsetRight -964 SizeY 40 OffsetBottom -526 } Color {{COLOR_TEXT}} Text "Default: up to 6 unarmed boats, at least 700 m apart, with fuel. Boat and mod switches are respected. Status shows availability and survey progress." "Font Size" {{SIZE_CAPTION_SMALL}} "Min Font Size" {{SIZE_CAPTION_SMALL}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +} } diff --git a/ui/admin_menu/pages/RecommendationsPage.layout.in b/ui/admin_menu/pages/RecommendationsPage.layout.in index bed784cf..f2cb763e 100644 --- a/ui/admin_menu/pages/RecommendationsPage.layout.in +++ b/ui/admin_menu/pages/RecommendationsPage.layout.in @@ -8,16 +8,19 @@ ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_motorbikeCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 764 OffsetLeft 764 PositionY 80 OffsetTop 80 SizeX 200 OffsetRight -964 SizeY 38 OffsetBottom -118 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } } TextWidgetClass { Name "Saved_motorbikeCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 640 OffsetLeft 640 PositionY 120 OffsetTop 120 SizeX 324 OffsetRight -964 SizeY 24 OffsetBottom -144 } Color {{COLOR_TEXT}} Text "Waiting for server" "Font Size" {{SIZE_CAPTION}} "Min Font Size" {{SIZE_CAPTION}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } TextWidgetClass { Name "RecommendedBikeID" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 116 OffsetTop 116 SizeX 580 OffsetRight -608 SizeY 24 OffsetBottom -140 } Color {{COLOR_TEXT}} Text "Workshop: 6503091A7B264FC3" "Font Size" {{SIZE_MINOR}} "Min Font Size" {{SIZE_MINOR}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } -TextWidgetClass { Name "recommended_bikes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 145 OffsetTop 145 SizeX 936 OffsetRight -964 SizeY 25 OffsetBottom -170 } Color {{COLOR_TEXT}} Text "Waiting for server addon list" "Font Size" {{SIZE_HELP}} "Min Font Size" {{SIZE_HELP}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } -TextWidgetClass { Name "Help_motorbikeCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 175 OffsetTop 175 SizeX 936 OffsetRight -964 SizeY 27 OffsetBottom -202 } Color {{COLOR_TEXT}} Text "Two special-forces crew per bike, alongside UAZ hunters. Count applies next round; losses stay lost." "Font Size" {{SIZE_HELP}} "Min Font Size" {{SIZE_HELP}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } -TextWidgetClass { Name "RecommendedBikeRuntime" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 207 OffsetTop 207 SizeX 936 OffsetRight -964 SizeY 36 OffsetBottom -243 } Color {{COLOR_TEXT}} Text "Current round: waiting for server" "Font Size" {{SIZE_CAPTION}} "Min Font Size" {{SIZE_CAPTION}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } -TextWidgetClass { Name "RecommendedPlaneLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 256 OffsetTop 256 SizeX 740 OffsetRight -768 SizeY 28 OffsetBottom -284 } Color {{COLOR_TEXT}} Text "Cessna 172 - reconnaissance patrol" "Font Size" {{SIZE_LABEL_LARGE}} "Min Font Size" {{SIZE_LABEL_LARGE}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } -ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "RecommendedPlane" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 788 OffsetLeft 788 PositionY 252 OffsetTop 252 SizeX 176 OffsetRight -964 SizeY 38 OffsetBottom -290 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Configure Cessna" } } } -TextWidgetClass { Name "RecommendedPlaneID" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 289 OffsetTop 289 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -313 } Color {{COLOR_TEXT}} Text "Workshop: 69BAEE0D439966FE | Propeller Flight Core: 69A8A34027DA65C5" "Font Size" {{SIZE_MINOR}} "Min Font Size" {{SIZE_MINOR}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } -TextWidgetClass { Name "recommended_plane" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 316 OffsetTop 316 SizeX 936 OffsetRight -964 SizeY 25 OffsetBottom -341 } Color {{COLOR_TEXT}} Text "Waiting for server addon list" "Font Size" {{SIZE_HELP}} "Min Font Size" {{SIZE_HELP}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } -TextWidgetClass { Name "RecommendedPlaneHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 345 OffsetTop 345 SizeX 936 OffsetRight -964 SizeY 26 OffsetBottom -371 } Color {{COLOR_TEXT}} Text "Patrols and reports sightings. Requires a usable airport on the selected terrain." "Font Size" {{SIZE_HELP}} "Min Font Size" {{SIZE_HELP}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } -TextWidgetClass { Name "RecommendedWeaponsLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 385 OffsetTop 385 SizeX 740 OffsetRight -768 SizeY 28 OffsetBottom -413 } Color {{COLOR_TEXT}} Text "Phaseline Marksman Rifles - extra weapons" "Font Size" {{SIZE_LABEL_LARGE}} "Min Font Size" {{SIZE_LABEL_LARGE}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } -ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "RecommendedWeapons" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 788 OffsetLeft 788 PositionY 381 OffsetTop 381 SizeX 176 OffsetRight -964 SizeY 38 OffsetBottom -419 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Manage items" } } } -TextWidgetClass { Name "RecommendedWeaponsID" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 416 OffsetTop 416 SizeX 936 OffsetRight -964 SizeY 22 OffsetBottom -438 } Color {{COLOR_TEXT}} Text "Workshop: BFC890A6F1603EC0" "Font Size" {{SIZE_MINOR}} "Min Font Size" {{SIZE_MINOR}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } -TextWidgetClass { Name "recommended_weapons" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 441 OffsetTop 441 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -465 } Color {{COLOR_TEXT}} Text "Waiting for server addon list" "Font Size" {{SIZE_HELP}} "Min Font Size" {{SIZE_HELP}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +TextWidgetClass { Name "recommended_bikes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 137 OffsetTop 137 SizeX 936 OffsetRight -964 SizeY 25 OffsetBottom -162 } Color {{COLOR_TEXT}} Text "Waiting for server addon list" "Font Size" {{SIZE_HELP}} "Min Font Size" {{SIZE_HELP}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +TextWidgetClass { Name "Help_motorbikeCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 165 OffsetTop 165 SizeX 936 OffsetRight -964 SizeY 22 OffsetBottom -187 } Color {{COLOR_TEXT}} Text "Two special-forces crew per bike, alongside UAZ hunters. Count applies next round; losses stay lost." "Font Size" {{SIZE_HELP}} "Min Font Size" {{SIZE_HELP}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +TextWidgetClass { Name "RecommendedBikeRuntime" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 188 OffsetTop 188 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -212 } Color {{COLOR_TEXT}} Text "Current round: waiting for server" "Font Size" {{SIZE_CAPTION}} "Min Font Size" {{SIZE_CAPTION}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +TextWidgetClass { Name "RecommendedPlaneLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 220 OffsetTop 220 SizeX 740 OffsetRight -768 SizeY 28 OffsetBottom -248 } Color {{COLOR_TEXT}} Text "Cessna 172 - reconnaissance (requires an airport)" "Font Size" {{SIZE_LABEL_LARGE}} "Min Font Size" {{SIZE_LABEL_LARGE}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "RecommendedPlane" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 788 OffsetLeft 788 PositionY 216 OffsetTop 216 SizeX 176 OffsetRight -964 SizeY 38 OffsetBottom -254 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Configure Cessna" } } } +TextWidgetClass { Name "RecommendedPlaneID" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 250 OffsetTop 250 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -274 } Color {{COLOR_TEXT}} Text "Workshop: 69BAEE0D439966FE | Propeller Flight Core: 69A8A34027DA65C5" "Font Size" {{SIZE_MINOR}} "Min Font Size" {{SIZE_MINOR}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +TextWidgetClass { Name "recommended_plane" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 276 OffsetTop 276 SizeX 936 OffsetRight -964 SizeY 25 OffsetBottom -301 } Color {{COLOR_TEXT}} Text "Waiting for server addon list" "Font Size" {{SIZE_HELP}} "Min Font Size" {{SIZE_HELP}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +TextWidgetClass { Name "RecommendedWeaponsLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 320 OffsetTop 320 SizeX 740 OffsetRight -768 SizeY 28 OffsetBottom -348 } Color {{COLOR_TEXT}} Text "Phaseline Marksman Rifles - extra weapons" "Font Size" {{SIZE_LABEL_LARGE}} "Min Font Size" {{SIZE_LABEL_LARGE}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "RecommendedWeapons" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 788 OffsetLeft 788 PositionY 316 OffsetTop 316 SizeX 176 OffsetRight -964 SizeY 38 OffsetBottom -354 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Manage items" } } } +TextWidgetClass { Name "RecommendedWeaponsID" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 351 OffsetTop 351 SizeX 936 OffsetRight -964 SizeY 22 OffsetBottom -373 } Color {{COLOR_TEXT}} Text "Workshop: BFC890A6F1603EC0" "Font Size" {{SIZE_MINOR}} "Min Font Size" {{SIZE_MINOR}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +TextWidgetClass { Name "recommended_weapons" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 376 OffsetTop 376 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -400 } Color {{COLOR_TEXT}} Text "Waiting for server addon list" "Font Size" {{SIZE_HELP}} "Min Font Size" {{SIZE_HELP}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +TextWidgetClass { Name "RecommendedBoatsLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 412 OffsetTop 412 SizeX 740 OffsetRight -768 SizeY 28 OffsetBottom -440 } Color {{COLOR_TEXT}} Text "Motorboat - shoreline transport" "Font Size" {{SIZE_LABEL_LARGE}} "Min Font Size" {{SIZE_LABEL_LARGE}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "RecommendedBoats" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 788 OffsetLeft 788 PositionY 408 OffsetTop 408 SizeX 176 OffsetRight -964 SizeY 38 OffsetBottom -446 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Shore boats" } } } +TextWidgetClass { Name "RecommendedBoatsID" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 445 OffsetTop 445 SizeX 936 OffsetRight -964 SizeY 22 OffsetBottom -467 } Color {{COLOR_TEXT}} Text "Workshop: 610C8532D3756F2D | Requires Boat Core, AI Radio and VehicleCore" "Font Size" {{SIZE_MINOR}} "Min Font Size" {{SIZE_MINOR}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } +TextWidgetClass { Name "recommended_boats" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 470 OffsetTop 470 SizeX 936 OffsetRight -964 SizeY 52 OffsetBottom -522 } Color {{COLOR_TEXT}} Text "Waiting for server addon list" "Font Size" {{SIZE_HELP}} "Min Font Size" {{SIZE_HELP}} Wrap 1 FontProperties FontProperties { Font "{{FONT}}" } } } } diff --git a/ui/admin_menu/shell.layout.in b/ui/admin_menu/shell.layout.in index 49bba6a2..5bbe0a36 100644 --- a/ui/admin_menu/shell.layout.in +++ b/ui/admin_menu/shell.layout.in @@ -20,6 +20,7 @@ FrameWidgetClass { {{NAVIGATION}} {{include:pages/SettingsPage.layout.in}} {{include:pages/VehiclesPage.layout.in}} +{{include:pages/BoatsPage.layout.in}} {{include:pages/TelemetryPage.layout.in}} {{include:pages/HQPage.layout.in}} {{include:pages/JournalPage.layout.in}}