Add garage car variants and yard parking; record Workshop 0.0.142
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -202,10 +202,10 @@ class ERR_AdminService
|
||||
if (key == "telemetryEnabled") return "Default on. Sends mission results and player identities to the configured statistics service. Disabling clears unsent events immediately.";
|
||||
if (key == "restartOnEmpty") return "On (1) restarts after the last disconnect; Off (0) preserves the round for reconnects. Default On. Applies immediately; normal round endings still apply.";
|
||||
if (key == "emptyRestartHours") return "0 disables idle restarts; 1..168 real hours continuously empty. Default 5. Applies immediately, measured from mission start or the last disconnect. Connected players, including spectators, prevent idle restarts.";
|
||||
if (key == "garageChancePercent") return "Garage selection chance (0-100%). Default 70%; 100 selects every suitable garage. Next round.";
|
||||
if (key == "garageChancePercent") return "Parking-site selection chance (0-100%). Default 70%; applies to suitable garages and residential yards. Next round.";
|
||||
if (key == "garageRadiusMetres") return "Spawn radius (250-1500 metres). Distance around players; default 1000 m. Next round.";
|
||||
if (key == "garageSpawnIntervalSeconds") return "Time between spawns (5-60 seconds). Default 5 s. Increase to spread out spawning. Next round.";
|
||||
if (key == "garageVehicleCount") return "0..24 unclaimed garage vehicles near players; default 16. Used vehicles are kept; distant unused vehicles retire. Optional bikes. Applies next round.";
|
||||
if (key == "garageVehicleCount") return "0..24 unclaimed cars in garages and residential yards; default 16. Used vehicles stay. Optional Rally cars and bikes. Applies next round.";
|
||||
if (key == "civilianTrafficCount") return "Set 0..6 civilian cars (default 3). Zero disables new traffic. Unclaimed cars retire only far from all players and out of sight; player-entered cars are preserved.";
|
||||
if (key == "showAllSites") return "0 disabled (default), 1 enabled. Show prepared sites at their positions as question marks; recovered intel identifies their types. Applies immediately to everyone.";
|
||||
if (key == "selfEpinephrine") return "0 disabled (default), 1 enabled. Unconscious players can press Space to use a carried epinephrine injector. Applies immediately; consumes the item and keeps its normal medical effect.";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Nearby garages are discovered incrementally. Each callback performs one
|
||||
// Nearby garages and residential yards are discovered incrementally. Each callback performs one
|
||||
// survey, maintenance, planning, resource-load or native-spawn step.
|
||||
class ERR_GarageVehicleRules
|
||||
{
|
||||
@@ -14,6 +14,26 @@ class ERR_GarageVehicleRules
|
||||
&& low[1] >= -0.3 && high[1] <= 1.9
|
||||
&& high[0] > low[0] && high[1] > low[1] && high[2] > low[2];
|
||||
}
|
||||
static bool Yard(string path)
|
||||
{
|
||||
path.ToLower();
|
||||
return path.EndsWith("/house_village_e_1i01.et") || path.EndsWith("/house_village_e_1i01_beige.et")
|
||||
|| path.EndsWith("/house_village_e_1i01_v2.et") || path.EndsWith("/house_village_e_1i01_beige_v2.et")
|
||||
|| path.EndsWith("/house_village_e_1i02.et") || path.EndsWith("/house_village_e_1i02_white.et")
|
||||
|| path.EndsWith("/house_village_e_1i03.et") || path.EndsWith("/house_village_e_1i03_yellow.et")
|
||||
|| path.EndsWith("/house_village_e_1i06.et") || path.EndsWith("/house_village_e_1i06_brown.et")
|
||||
|| path.EndsWith("/house_village_e_1i06_white.et");
|
||||
}
|
||||
static vector YardOffset(vector low, vector high, int side)
|
||||
{
|
||||
vector centre = (low + high) * 0.5; centre[1] = 0;
|
||||
// Three metres from the wall leaves almost two metres beside the car.
|
||||
if (side == 0) centre[0] = low[0] - 3;
|
||||
else if (side == 1) centre[0] = high[0] + 3;
|
||||
else if (side == 2) centre[2] = low[2] - 3;
|
||||
else centre[2] = high[2] + 3;
|
||||
return centre;
|
||||
}
|
||||
}
|
||||
class ERR_GarageSite
|
||||
{
|
||||
@@ -21,6 +41,8 @@ class ERR_GarageSite
|
||||
bool Consumed;
|
||||
bool Selected;
|
||||
float NextTry;
|
||||
bool Yard;
|
||||
int Side;
|
||||
}
|
||||
class ERR_ParkedVehicle
|
||||
{
|
||||
@@ -36,6 +58,7 @@ class ERR_GarageVehicles
|
||||
protected ref array<ref ERR_ParkedVehicle> m_Parked = {};
|
||||
protected ref map<string, bool> m_KnownCells = new map<string, bool>();
|
||||
protected ref ERR_MotorbikeCatalog m_Bikes = new ERR_MotorbikeCatalog();
|
||||
protected ref ERR_ParkedCarCatalog m_Cars = new ERR_ParkedCarCatalog();
|
||||
protected ref array<ResourceName> m_LoadedNames = {};
|
||||
protected ref array<ref Resource> m_LoadedResources = {};
|
||||
protected ref ERR_GarageSite m_Pending;
|
||||
@@ -61,6 +84,7 @@ class ERR_GarageVehicles
|
||||
m_CleanupRadius = Math.Max(1400, m_SpawnRadius + 400);
|
||||
if (m_Requested <= 0 || m_GarageChance <= 0) { Stop(); return; }
|
||||
m_Bikes.Find();
|
||||
m_Cars.Find();
|
||||
GetGame().GetCallqueue().CallLater(Step, 250, true);
|
||||
}
|
||||
protected bool Observers(array<vector> positions, array<IEntity> actors)
|
||||
@@ -124,9 +148,13 @@ class ERR_GarageVehicles
|
||||
protected bool Remember(IEntity entity)
|
||||
{
|
||||
if (m_Sites.Count() >= 2048) return false;
|
||||
if (!entity.GetPrefabData() || !ERR_GarageVehicleRules.Garage(entity.GetPrefabData().GetPrefabName())) return true;
|
||||
if (!entity.GetPrefabData()) return true;
|
||||
string prefab = entity.GetPrefabData().GetPrefabName();
|
||||
bool yard = ERR_GarageVehicleRules.Yard(prefab);
|
||||
if (!yard && !ERR_GarageVehicleRules.Garage(prefab)) return true;
|
||||
foreach (ERR_GarageSite site : m_Sites) if (site.Garage == entity) return true;
|
||||
ERR_GarageSite added = new ERR_GarageSite(); added.Garage = entity;
|
||||
added.Yard = yard; added.Side = Math.RandomInt(0, 4);
|
||||
added.Selected = Math.RandomInt(0, 100) < m_GarageChance;
|
||||
m_Sites.Insert(added);
|
||||
return true;
|
||||
@@ -202,6 +230,22 @@ class ERR_GarageVehicles
|
||||
foreach (ERR_ParkedVehicle parked : m_Parked) if (parked.Site == site) return true;
|
||||
return false;
|
||||
}
|
||||
protected bool YardGround(vector position, vector transform[4])
|
||||
{
|
||||
SCR_AIWorld world = SCR_AIWorld.Cast(GetGame().GetAIWorld());
|
||||
if (!world || !world.GetRoadNetworkManager()) return false;
|
||||
BaseRoad road; float distance;
|
||||
world.GetRoadNetworkManager().GetClosestRoad(position, road, distance, skipNavlinks: true);
|
||||
if (road && (!ERR_ForceRules.Finite(distance) || distance < road.GetWidth() * 0.5 + 4)) return false;
|
||||
float ground = SCR_TerrainHelper.GetTerrainY(position);
|
||||
for (int x = -1; x <= 1; x++)
|
||||
for (int z = -1; z <= 1; z++)
|
||||
{
|
||||
vector sample = position + transform[0] * (x * 1.1) + transform[2] * (z * 2.5);
|
||||
if (!ERR_Actors.DryGround(sample) || Math.AbsFloat(SCR_TerrainHelper.GetTerrainY(sample) - ground) > 0.18) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected void Plan(array<vector> observers, array<IEntity> actors, float now)
|
||||
{
|
||||
if (observers.IsEmpty() || now < m_NextSpawn || m_Parked.Count() >= m_Requested || m_Parked.Count() + m_Claimed >= 128 || m_Sites.IsEmpty()) return;
|
||||
@@ -211,20 +255,38 @@ class ERR_GarageVehicles
|
||||
if (m_SiteCursor >= m_Sites.Count()) m_SiteCursor = 0;
|
||||
ERR_GarageSite site = m_Sites[m_SiteCursor++];
|
||||
if (!site.Selected || site.Consumed || !site.Garage || now < site.NextTry || Occupied(site)) continue;
|
||||
vector position = site.Garage.CoordToParent("0 0 -0.5");
|
||||
vector offset = "0 0 -0.5";
|
||||
vector angles = site.Garage.GetYawPitchRoll();
|
||||
if (site.Yard)
|
||||
{
|
||||
vector low, high; site.Garage.GetBounds(low, high);
|
||||
if (!ERR_ForceRules.FiniteVector(low) || !ERR_ForceRules.FiniteVector(high) || (high - low).Length() > 40) continue;
|
||||
int side = site.Side; site.Side = (site.Side + 1) % 4;
|
||||
offset = ERR_GarageVehicleRules.YardOffset(low, high, side);
|
||||
angles[1] = 0; angles[2] = 0;
|
||||
if (side >= 2) angles[0] = angles[0] + 90;
|
||||
}
|
||||
vector position = site.Garage.CoordToParent(offset);
|
||||
if (!Nearby(position, observers, m_SpawnRadius)) continue;
|
||||
site.NextTry = now + 15000;
|
||||
if (!ERR_CivilianTrafficRules.Far(position, observers, 35) || !ERR_Actors.Alive(site.Garage)) continue;
|
||||
vector transform[4]; site.Garage.GetWorldTransform(transform);
|
||||
TraceParam floor = new TraceParam(); floor.Start = position + "0 1 0"; floor.End = position - "0 1 0";
|
||||
floor.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
|
||||
float fraction = GetGame().GetWorld().TraceMove(floor, null);
|
||||
if (!ERR_ForceRules.Finite(fraction) || fraction >= 1 || floor.TraceNorm[1] < 0.98) return;
|
||||
position = vector.Lerp(floor.Start, floor.End, fraction) + "0 0.08 0";
|
||||
vector angles = site.Garage.GetYawPitchRoll();
|
||||
vector transform[4]; Math3D.AnglesToMatrix(angles, transform);
|
||||
if (site.Yard)
|
||||
{
|
||||
position = ERR_Actors.Ground(position) + "0 0.08 0";
|
||||
if (!YardGround(position, transform)) return;
|
||||
}
|
||||
else
|
||||
{
|
||||
TraceParam floor = new TraceParam(); floor.Start = position + "0 1 0"; floor.End = position - "0 1 0";
|
||||
floor.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
|
||||
float fraction = GetGame().GetWorld().TraceMove(floor, null);
|
||||
if (!ERR_ForceRules.Finite(fraction) || fraction >= 1 || floor.TraceNorm[1] < 0.98) return;
|
||||
position = vector.Lerp(floor.Start, floor.End, fraction) + "0 0.08 0";
|
||||
}
|
||||
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), position) || !Clear(position, transform) || !Hidden(position, angles, actors)) return;
|
||||
m_Pending = site; m_Position = position; m_Angles = angles;
|
||||
m_Prefab = ERR_CivilianTraffic.CarPrefab(Math.RandomInt(0, 2));
|
||||
m_Prefab = m_Cars.Prefabs[Math.RandomInt(0, m_Cars.Prefabs.Count())];
|
||||
m_PendingBike = m_Spawned % 4 == 3 && m_BikeCandidate < m_Bikes.Prefabs.Count();
|
||||
if (m_PendingBike) m_Prefab = m_Bikes.Prefabs[m_BikeCandidate];
|
||||
m_Stage = 1; return;
|
||||
@@ -246,6 +308,7 @@ class ERR_GarageVehicles
|
||||
if (!site.Garage || site.Consumed || !ERR_Actors.Alive(site.Garage) || !Nearby(m_Position, observers, m_SpawnRadius)
|
||||
|| !ERR_CivilianTrafficRules.Far(m_Position, observers, 35) || !Hidden(m_Position, m_Angles, actors)) return;
|
||||
vector transform[4]; Math3D.AnglesToMatrix(m_Angles, transform);
|
||||
if (site.Yard && !YardGround(m_Position, transform)) return;
|
||||
if (!Clear(m_Position, transform)) return;
|
||||
m_NextSpawn = Now() + m_SpawnInterval;
|
||||
IEntity entity = ERR_Actors.Spawn(m_Prefab, m_Position, m_Angles);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Discover only complete civilian cars. Rally is optional and must already be
|
||||
// mounted; do not add a DLC dependency or bypass its normal access checks.
|
||||
class ERR_ParkedCarCatalog
|
||||
{
|
||||
ref array<ResourceName> Prefabs = {};
|
||||
protected bool m_Checked;
|
||||
static bool Candidate(string path, bool rally)
|
||||
{
|
||||
path.ToLower();
|
||||
if (!path.EndsWith(".et") || path.Contains("_base") || path.Contains("_fullkit")
|
||||
|| path.Contains("_randomized") || path.Contains("_fia")) return false;
|
||||
if (!path.Contains("/s105/s105_") && !path.Contains("/t601/t601_") && !path.Contains("/v2101/v2101_")) return false;
|
||||
if (rally) return path.Contains("_rally_0");
|
||||
return !path.Contains("_rally");
|
||||
}
|
||||
void Find()
|
||||
{
|
||||
if (m_Checked) return;
|
||||
m_Checked = true;
|
||||
Prefabs.Insert(ERR_CivilianTraffic.CarPrefab(0));
|
||||
Prefabs.Insert(ERR_CivilianTraffic.CarPrefab(1));
|
||||
SearchResourcesFilter filter = new SearchResourcesFilter();
|
||||
filter.fileExtensions = {"et"}; filter.recursive = true;
|
||||
filter.rootPath = "$" + GameProject.GetAddonID("58D0FB3206B6F859") + ":Prefabs/Vehicles/Wheeled";
|
||||
ResourceDatabase.SearchResources(filter, AddRoad);
|
||||
array<string> loaded = {}; GameProject.GetLoadedAddons(loaded);
|
||||
if (!loaded.Contains("6566A2D70498DA6B")) return;
|
||||
filter.rootPath = "$" + GameProject.GetAddonID("6566A2D70498DA6B") + ":Prefabs/Vehicles/Wheeled";
|
||||
ResourceDatabase.SearchResources(filter, AddRally);
|
||||
}
|
||||
protected void AddRoad(ResourceName resource, string path = "")
|
||||
{
|
||||
if (Candidate(resource, false) && Prefabs.Count() < 64 && !Prefabs.Contains(resource)) Prefabs.Insert(resource);
|
||||
}
|
||||
protected void AddRally(ResourceName resource, string path = "")
|
||||
{
|
||||
if (Candidate(resource, true) && Prefabs.Count() < 64 && !Prefabs.Contains(resource)) Prefabs.Insert(resource);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user