From 67cb84455029531efe460e3b2e664c23ae07befb Mon Sep 17 00:00:00 2001 From: smiki Date: Sun, 24 Aug 2025 15:07:27 +0200 Subject: [PATCH] Validate and Reposition Ground Units algorithm [ADDED] UTILS.ValidateAndRepositionGroundUnits [ADDED] SPAWN:InitValidateAndRepositionGroundUnits [ADDED] CTLD.validateAndRepositionUnits --- Moose Development/Moose/Core/Spawn.lua | 24 +++- Moose Development/Moose/Ops/CTLD.lua | 8 +- Moose Development/Moose/Utilities/Utils.lua | 121 ++++++++++++++++++++ 3 files changed, 151 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index c81aea59d..6e95e285b 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1049,6 +1049,22 @@ function SPAWN:InitSetUnitAbsolutePositions(Positions) return self end + +--- Uses Disposition and other fallback logic to find better ground positions for ground units. +--- NOTE: This is not a spawn randomizer. +--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area. +--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit. +-- @param #boolean OnOff Enable/disable the feature. +-- @param #number MaxRadius (Optional) Max radius to search for valid ground locations in meters. Default is double the max radius of the units. +-- @param #number Spacing (Optional) Minimum spacing between units in meters. Default is 5% of the search radius or 5 meters, whichever is larger. +-- @return #SPAWN +function SPAWN:InitValidateAndRepositionGroundUnits(OnOff, MaxRadius, Spacing) + self.SpawnValidateAndRepositionGroundUnits = OnOff + self.SpawnValidateAndRepositionGroundUnitsRadius = MaxRadius + self.SpawnValidateAndRepositionGroundUnitsSpacing = Spacing + return self +end + --- This method is rather complicated to understand. But I'll try to explain. -- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor, -- but they will all follow the same Template route and have the same prefix name. @@ -1829,7 +1845,13 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) if self.SpawnHiddenOnMap then SpawnTemplate.hidden=self.SpawnHiddenOnMap end - + + if self.SpawnValidateAndRepositionGroundUnits then + local units = SpawnTemplate.units + local gPos = { x = SpawnTemplate.x, y = SpawnTemplate.y } + UTILS.ValidateAndRepositionGroundUnits(gPos, units, self.SpawnValidateAndRepositionGroundUnitsRadius, self.SpawnValidateAndRepositionGroundUnitsSpacing) + end + -- Set country, coalition and category. SpawnTemplate.CategoryID = self.SpawnInitCategory or SpawnTemplate.CategoryID SpawnTemplate.CountryID = self.SpawnInitCountry or SpawnTemplate.CountryID diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 7b46d1439..40f2dc8e3 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -1564,7 +1564,9 @@ function CTLD:New(Coalition, Prefixes, Alias) self.FixedMinAngels = 165 -- for troop/cargo drop via chute self.FixedMaxAngels = 2000 -- for troop/cargo drop via chute self.FixedMaxSpeed = 77 -- 280 kph or 150kn eq 77 mps - + + self.validateAndRepositionUnits = false -- 280 kph or 150kn eq 77 mps + -- message suppression self.suppressmessages = false @@ -3735,6 +3737,7 @@ function CTLD:_UnloadTroops(Group, Unit) self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) :InitDelayOff() :InitSetUnitAbsolutePositions(Positions) + :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end) :SpawnFromVec2(randomcoord:GetVec2()) self:__TroopsDeployed(1, Group, Unit, self.DroppedTroops[self.TroopCounter],type) @@ -4181,11 +4184,13 @@ function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation,Mult self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) --:InitRandomizeUnits(true,20,2) :InitDelayOff() + :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end) :SpawnFromVec2(randomcoord) else -- don't random position of e.g. SAM units build as FOB self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias) :InitDelayOff() + :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end) :SpawnFromVec2(randomcoord) end @@ -5211,6 +5216,7 @@ function CTLD:_UnloadSingleTroopByID(Group, Unit, chunkID) self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template, alias) :InitDelayOff() :InitSetUnitAbsolutePositions(Positions) + :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits) :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end) :SpawnFromVec2(randomcoord:GetVec2()) self:__TroopsDeployed(1, Group, Unit, self.DroppedTroops[self.TroopCounter], cType) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index a9a76d535..c7543b3a2 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -4871,3 +4871,124 @@ function UTILS.FindNearestPointOnCircle(Vec1,Radius,Vec2) return {x=qx, y=qy} end + +--- This function uses Disposition and other fallback logic to find better ground positions for a group of ground units. +--- NOTE: This is not a spawn randomizer. +--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area and modifies the provided positions table. +--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit. +-- @param table Positions A table of DCS#Vec2 or DCS#Vec3, can be a units table from the group template. +-- @param DCS#Vec2 Anchor (Optional) DCS#Vec2 or DCS#Vec3 as anchor point to calculate offset of the units. +-- @param #number MaxRadius (Optional) Max radius to search for valid ground locations in meters. Default is double the max radius of the units. +-- @param #number Spacing (Optional) Minimum spacing between units in meters. Default is 5% of the search radius or 5 meters, whichever is larger. +function UTILS.ValidateAndRepositionGroundUnits(Anchor, Positions, MaxRadius, Spacing) + local units = Positions + Anchor = Anchor or UTILS.GetCenterPoint(units) + local gPos = { x = Anchor.x, y = Anchor.z or Anchor.y } + local maxRadius = 0 + local unitCount = 0 + for _, unit in pairs(units) do + local pos = { x = unit.x, y = unit.z or unit.y } + local dist = UTILS.VecDist2D(pos, gPos) + if dist > maxRadius then + maxRadius = dist + end + unitCount = unitCount + 1 + end + maxRadius = MaxRadius or math.max(maxRadius * 2, 10) + local spacing = Spacing or math.max(maxRadius * 0.05, 5) + if unitCount > 0 and maxRadius > 5 then + local spots = UTILS.GetSimpleZones(UTILS.Vec2toVec3(gPos), maxRadius, spacing, 1000) + if spots and #spots > 0 then + local validSpots = {} + for _, spot in pairs(spots) do -- Disposition sometimes returns points on roads, hence this filter. + if land.getSurfaceType(spot) == land.SurfaceType.LAND then + table.insert(validSpots, spot) + end + end + spots = validSpots + end + + local step = spacing + for _, unit in pairs(units) do + local pos = { x = unit.x, y = unit.z or unit.y } + local isOnLand = land.getSurfaceType(pos) == land.SurfaceType.LAND + local isValid = false + if spots and #spots > 0 then + local si = 1 + local sid = 0 + local closestDist = 100000000 + local closestSpot + for _, spot in pairs(spots) do + local dist = UTILS.VecDist2D(pos, spot) + if dist < closestDist then + local skip = false + for _, unit2 in pairs(units) do + local pos2 = { x = unit2.x, y = unit2.z or unit2.y } + local dist2 = UTILS.VecDist2D(spot, pos2) + if dist2 < spacing and isOnLand then + skip = true + break + end + end + if not skip then + closestDist = dist + closestSpot = spot + sid = si + end + end + si = si + 1 + end + if closestSpot then + if closestDist >= spacing then + pos = closestSpot + end + isValid = true + table.remove(spots, sid) + end + end + + -- Failsafe calculation + if not isValid and not isOnLand then + + local h = UTILS.HdgTo(pos, gPos) + local retries = 0 + while not isValid and retries < 500 do + + local dist = UTILS.VecDist2D(pos, gPos) + pos = UTILS.Vec2Translate(pos, step, h) + + local skip = false + for _, unit2 in pairs(units) do + if unit ~= unit2 then + local pos2 = { x = unit2.x, y = unit2.z or unit2.y } + local dist2 = UTILS.VecDist2D(pos, pos2) + if dist2 < 12 then + isValid = false + skip = true + break + end + end + end + + if not skip and dist > step and land.getSurfaceType(pos) == land.SurfaceType.LAND then + isValid = true + break + elseif dist <= step then + break + end + + retries = retries + 1 + end + end + + if isValid then + unit.x = pos.x + if unit.z then + unit.z = pos.y + else + unit.y = pos.y + end + end + end + end +end