From 163e59443b33f396c08996329d20eaf60082a3fd Mon Sep 17 00:00:00 2001 From: dutchie031 Date: Sat, 15 Aug 2026 12:30:11 +0200 Subject: [PATCH 1/5] crate spawning update --- RELEASENOTES.md | 4 + .../stageClasses/helpers/MaxLoadConfig.lua | 20 -- .../stageClasses/helpers/SupplyLoadConfig.lua | 42 ++++ .../helpers/SupplyUnitsTracker.lua | 204 ++++++++++++++---- 4 files changed, 211 insertions(+), 59 deletions(-) delete mode 100644 src/classes/stageClasses/helpers/MaxLoadConfig.lua create mode 100644 src/classes/stageClasses/helpers/SupplyLoadConfig.lua diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fb2d13d..95dd183 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,6 +7,10 @@ ### Bug Fixes +- Issue #11 + PR #17 + Supply crate spawning now checks for free space and will not spawn if the area is too crowded. + Additionally different units will spawn in different areas depending on loading side. ## [0.12.1] 2026-07 diff --git a/src/classes/stageClasses/helpers/MaxLoadConfig.lua b/src/classes/stageClasses/helpers/MaxLoadConfig.lua deleted file mode 100644 index 0d7f407..0000000 --- a/src/classes/stageClasses/helpers/MaxLoadConfig.lua +++ /dev/null @@ -1,20 +0,0 @@ ----@class MaxLoadConfig ----@field maxInternalLoad number - ----@type table -local MaxLoadConfig = { - ["Mi-8MT"] = { - maxInternalLoad = 4000, - }, - ["CH-47Fbl1"] = { - maxInternalLoad = 10000 - }, - ["Mi-24P"] = { - maxInternalLoad = 2000 - }, - ["UH-1H"] = { - maxInternalLoad = 2000 - } -} - -return MaxLoadConfig \ No newline at end of file diff --git a/src/classes/stageClasses/helpers/SupplyLoadConfig.lua b/src/classes/stageClasses/helpers/SupplyLoadConfig.lua new file mode 100644 index 0000000..83c651d --- /dev/null +++ b/src/classes/stageClasses/helpers/SupplyLoadConfig.lua @@ -0,0 +1,42 @@ +---@class DropZoneSlice +---@field centerAngle number Angle in degrees (0=forward, 90=right, 180=rear, 270=left) +---@field angleWidth number Total width of the slice in degrees (e.g. 60 = ±30°) +---@field minRadius number Minimum search radius in meters (safe distance from helicopter) +---@field maxRadius number Maximum search radius in meters +---@field spacing number Distance increment when searching outward in meters + +---@class SupplyLoadConfig +---@field maxInternalLoad number +---@field dropZones Array + +---@type table +local SupplyLoadConfig = { + ["Mi-8MT"] = { + maxInternalLoad = 4000, + dropZones = { + { centerAngle = 180, angleWidth = 60, minRadius = 20, maxRadius = 50, spacing = 5 }, + } + }, + ["CH-47Fbl1"] = { + maxInternalLoad = 10000, + dropZones = { + { centerAngle = 180, angleWidth = 60, minRadius = 20, maxRadius = 75, spacing = 5 }, + } + }, + ["Mi-24P"] = { + maxInternalLoad = 2000, + dropZones = { + { centerAngle = 270, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 }, + { centerAngle = 90, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 }, + } + }, + ["UH-1H"] = { + maxInternalLoad = 2000, + dropZones = { + { centerAngle = 270, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 }, + { centerAngle = 90, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 }, + } + } +} + +return SupplyLoadConfig \ No newline at end of file diff --git a/src/classes/stageClasses/helpers/SupplyUnitsTracker.lua b/src/classes/stageClasses/helpers/SupplyUnitsTracker.lua index 7362529..d26d863 100644 --- a/src/classes/stageClasses/helpers/SupplyUnitsTracker.lua +++ b/src/classes/stageClasses/helpers/SupplyUnitsTracker.lua @@ -3,7 +3,7 @@ local Util = require("classes.util.Util") local DcsUtil = require("classes.util.DcsUtil") local SpearheadEvents = require("classes.spearhead_events") local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHelper") -local MaxLoadConfig = require("classes.stageClasses.helpers.MaxLoadConfig") +local SupplyLoadConfig = require("classes.stageClasses.helpers.SupplyLoadConfig") ---@class SupplyUnitEventListener ---@field supplyUnitSpawned fun(self:SupplyUnitEventListener, unit:Unit) | nil @@ -283,9 +283,6 @@ function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHe if group == nil then return end - - self:RemoveCargoFromUnit(unitID, crateType) - self:UpdateWeightForUnit(unit) local cargoConfig = SupplyConfigHelper.getSupplyConfig(crateType) @@ -294,7 +291,16 @@ function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHe return end - local cargoPos = self:GetCargoPlacePosition(unit) + local cargoPos = self:GetCargoPlacePosition(unit, cargoConfig.staticType) + + if cargoPos == nil then + self._logger:warn("No valid position found to drop cargo for unit: " .. unit:getName()) + trigger.action.outTextForUnit(unit:getID(), "No valid position to drop cargo. Unloading area is too crowded.", 10) + return + end + + self:RemoveCargoFromUnit(unitID, crateType) + self:UpdateWeightForUnit(unit) cargoCount = cargoCount + 1 local cargoSpawnObject = { @@ -394,7 +400,7 @@ function SupplyUnitsTracker:TryLoadCrateInUnit(unit, crateType, commandHelper) end end - local unitConfig = MaxLoadConfig[unit:getTypeName()] + local unitConfig = SupplyLoadConfig[unit:getTypeName()] if unitConfig == nil then trigger.action.outTextForUnit(unit:getID(), "Your unit type is not configured for logistics: " .. crateType, 5) self._logger:error("Invalid unit type: " .. unit:getTypeName()) @@ -437,53 +443,173 @@ function SupplyUnitsTracker:UnitRequestCrateSpawn(groupID, crateType) end end +---@class SupplyUnitsBoundingBox +---@field min Vec3 World space minimum +---@field max Vec3 World space maximum + +---@param foundObject Object +---@return SupplyUnitsBoundingBox? +function SupplyUnitsTracker:GetBoundingBoxes(foundObject) + + local desc = foundObject:getDesc() + if foundObject:getCategory() == Object.Category.SCENERY then + desc = SceneryObject.getDescByName(foundObject:getTypeName()) + end + + if desc == nil or desc.box == nil then + return nil + end + + local objPos = foundObject:getPoint() + local box = desc.box + + -- Convert relative bbox to world space by adding object position + ---@type SupplyUnitsBoundingBox + return { + min = { + x = objPos.x + box.min.x, + y = objPos.y + box.min.y, + z = objPos.z + box.min.z + }, + max = { + x = objPos.x + box.max.x, + y = objPos.y + box.max.y, + z = objPos.z + box.max.z + } + } +end + +---Check if two axis-aligned bounding boxes collide with safety margin +---@param crateBBox SupplyUnitsBoundingBox The crate's bbox in world space +---@param objBBox SupplyUnitsBoundingBox The existing object's bbox in world space +---@param safetyMargin number Safety margin around objects +---@return boolean True if collision detected +function SupplyUnitsTracker:CheckBBoxCollision(crateBBox, objBBox, safetyMargin) + + -- Apply safety margin to object bbox + local objMin = { + x = objBBox.min.x - safetyMargin, + y = objBBox.min.y - safetyMargin, + z = objBBox.min.z - safetyMargin + } + local objMax = { + x = objBBox.max.x + safetyMargin, + y = objBBox.max.y + safetyMargin, + z = objBBox.max.z + safetyMargin + } + + -- AABB collision detection + return crateBBox.min.x <= objMax.x and crateBBox.max.x >= objMin.x and + crateBBox.min.y <= objMax.y and crateBBox.max.y >= objMin.y and + crateBBox.min.z <= objMax.z and crateBBox.max.z >= objMin.z +end ---@private ---@param unit Unit ----@return Vec3 -function SupplyUnitsTracker:GetCargoPlacePosition(unit) +---@param crateTypeName string +---@return Vec3? +function SupplyUnitsTracker:GetCargoPlacePosition(unit, crateTypeName) - local pos = unit:getPosition() - local preferredPos = { - x = pos.p.x - 10 * pos.x.x, - y = pos.p.y - 10 * pos.x.y, - z = pos.p.z - 10 * pos.x.z + local unitPos = unit:getPosition() + + -- Get crate bbox - relative to placement position + local crateDesc = StaticObject.getDescByName(crateTypeName) --[[@as table]] + if crateDesc == nil or crateDesc.box == nil then + self._logger:error("Could not get bbox for crate type: " .. crateTypeName) + return nil + end + + local crateRelativeBBox = crateDesc.box + + -- Get drop zone config for this unit + local dropZones = { + { centerAngle = 180, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 } + } + + if SupplyLoadConfig[unit:getTypeName()] ~= nil then + dropZones = SupplyLoadConfig[unit:getTypeName()].dropZones + end + + -- Get occupied objects with their bboxes + local searchVolume = { + id = world.VolumeType.SPHERE, + params = { + point = { + x = unitPos.p.x, + y = unitPos.p.y, + z = unitPos.p.z + }, + radius = 100 -- Search a large area + } } - return preferredPos + local occupiedObjects = {} + local found = function(foundItem, val) + local bbox = self:GetBoundingBoxes(foundItem) + if bbox then + table.insert(occupiedObjects, { + pos = foundItem:getPoint(), + bbox = bbox + }) + end + end + world.searchObjects(searchVolume.id, searchVolume, found) - -- local volume = { - -- id = world.VolumeType.SPHERE, - -- params = { - -- point = preferredPos, - -- radius = 10 - -- } - -- } + local safetyMargin = 3 -- Safety margin around objects - -- local occupiedPosX = {} - -- local occupiedPosZ = {} + -- Search through each slice + for _, zone in ipairs(dropZones) do + -- Calculate angle range for this slice + local minAngle = zone.centerAngle - (zone.angleWidth / 2) + local maxAngle = zone.centerAngle + (zone.angleWidth / 2) - -- ---@param foundItem Object - -- local found = function(foundItem, val) + -- Search outward in rings starting from minRadius + for distance = zone.minRadius, zone.maxRadius, zone.spacing do + -- Check multiple positions within the angular slice + local angleStep = math.min(15, zone.angleWidth / 3) -- Divide slice into sections + + for angle = minAngle, maxAngle, angleStep do + local radians = math.rad(angle) + + -- Calculate position at this angle and distance + -- Angle 0 = forward, 90 = right, 180 = rear, 270 = left + local candidateX = unitPos.p.x + distance * math.sin(radians) + local candidateY = unitPos.p.y + distance * math.cos(radians) + local candidateZ = land.getHeight({ x = candidateX, y = candidateY }) - -- local foundPos = foundItem:getPoint() + -- Convert crate's relative bbox to world space at this position + local crateBBoxWorldSpace = { + min = { + x = candidateX + crateRelativeBBox.min.x, + y = candidateZ + crateRelativeBBox.min.y, + z = candidateY + crateRelativeBBox.min.z + }, + max = { + x = candidateX + crateRelativeBBox.max.x, + y = candidateZ + crateRelativeBBox.max.y, + z = candidateY + crateRelativeBBox.max.z + } + } - -- local z = math.floor(foundPos.z) - -- for i = z - 3 , z + 3 do - -- occupiedPosZ[i] = true - -- end + -- Check if crate bbox collides with any existing objects + local collides = false + for _, obj in ipairs(occupiedObjects) do + if self:CheckBBoxCollision(crateBBoxWorldSpace, obj.bbox, safetyMargin) then + collides = true + break + end + end - -- local x = math.floor(foundPos.x) - -- for i = x - 3 , x + 3 do - -- occupiedPosX[i] = true - -- end - -- end + if not collides then + return { x = candidateX, y = candidateZ, z = candidateY } + end + end + end + end - -- world.searchObjects(volume.id, volume.params, found) - - - + -- No free spot found + return nil end return SupplyUnitsTracker \ No newline at end of file -- 2.54.0 From 4b30cc3a32c6716e2e354f74f5dee5df32815ed5 Mon Sep 17 00:00:00 2001 From: dutchie031 Date: Sat, 15 Aug 2026 12:32:30 +0200 Subject: [PATCH 2/5] crate spawning update --- RELEASENOTES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 95dd183..d7eed88 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,9 +7,9 @@ ### Bug Fixes -- Issue #11 - PR #17 - Supply crate spawning now checks for free space and will not spawn if the area is too crowded. +- Issue #11
+ PR #17
+ Supply crate spawning now checks for free space and will not spawn if the area is too crowded.
Additionally different units will spawn in different areas depending on loading side. ## [0.12.1] 2026-07 -- 2.54.0 From 1edcbb366669fd56fc2b9bff37f58d5d172954b5 Mon Sep 17 00:00:00 2001 From: dutchie031 Date: Sat, 15 Aug 2026 16:32:15 +0200 Subject: [PATCH 3/5] updated ignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index cd31b20..5d688a1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /dist -.vscode/settings.json \ No newline at end of file +.vscode/settings.json +**\settings.json \ No newline at end of file -- 2.54.0 From b9c5c7df8f5c8668e0b1c6fb5740011dd2d3c9e3 Mon Sep 17 00:00:00 2001 From: dutchie031 Date: Sat, 15 Aug 2026 16:32:43 +0200 Subject: [PATCH 4/5] removed settings files --- .vscode/settings.json | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 7674b97..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "cSpell.words": [ - "farp", - "Sams", - "SEAD" - ], - "Lua.diagnostics.globals": [ - "lfs" - ], - "maptz.regionfolder": { - "[lua]" : { - "foldEnd": "--endregion", - "foldEndRegex": "[\\s]*--endregion", - "foldStart": "--region [NAME]", - "foldStartRegex": "[\\s]*--region[\\s]*(.*)", - "disableFolding": false - } - }, - "livePreview.serverRoot": "/_docs", - "Lua.workspace.library": [ - "/Users/ex61wi/Developer/personal/DcsMissionScriptingTools/dutchies-dcs-scripting-tools/lua-addons", - "c:\\Users\\Tim\\.vscode\\extensions\\dutchie031.dutchies-dcs-scripting-tools-0.0.3\\lua-addons" - ], - "dutchies-dcs-scripting-tools.compileAt": "onSave", -} \ No newline at end of file -- 2.54.0 From 24608d36c546e5c7233fee179fee6aa01ae84246 Mon Sep 17 00:00:00 2001 From: dutchie031 Date: Sun, 16 Aug 2026 13:06:07 +0200 Subject: [PATCH 5/5] Updated cargo crates spawning and logic --- .../stageClasses/helpers/SupplyLoadConfig.lua | 2 +- .../helpers/SupplyUnitsTracker.lua | 99 ++++++++++++++++--- 2 files changed, 84 insertions(+), 17 deletions(-) diff --git a/src/classes/stageClasses/helpers/SupplyLoadConfig.lua b/src/classes/stageClasses/helpers/SupplyLoadConfig.lua index 83c651d..90b42db 100644 --- a/src/classes/stageClasses/helpers/SupplyLoadConfig.lua +++ b/src/classes/stageClasses/helpers/SupplyLoadConfig.lua @@ -20,7 +20,7 @@ local SupplyLoadConfig = { ["CH-47Fbl1"] = { maxInternalLoad = 10000, dropZones = { - { centerAngle = 180, angleWidth = 60, minRadius = 20, maxRadius = 75, spacing = 5 }, + { centerAngle = 180, angleWidth = 30, minRadius = 20, maxRadius = 75, spacing = 5 }, } }, ["Mi-24P"] = { diff --git a/src/classes/stageClasses/helpers/SupplyUnitsTracker.lua b/src/classes/stageClasses/helpers/SupplyUnitsTracker.lua index f6376f1..d98d2e0 100644 --- a/src/classes/stageClasses/helpers/SupplyUnitsTracker.lua +++ b/src/classes/stageClasses/helpers/SupplyUnitsTracker.lua @@ -297,9 +297,13 @@ function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHe self._logger:debug("Unload requested for unit: " .. unitID .. " crateType: " .. crateType) local unit = DcsUtil.GetPlayerUnitByID(unitID) - if unit == nil or unit:isExist() == false then return end + if unit == nil or unit:isExist() == false then + self._logger:warn("Unload requested for non-existent unit: " .. unitID) + return + end local group = unit:getGroup() if group == nil then + self._logger:warn("Unload requested for unit with no group: " .. unit:getName()) return end @@ -329,9 +333,11 @@ function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHe y = cargoPos.z, } + self._logger:debug("Spawning crate #" .. cargoCount .. " at (" .. string.format("%.2f", cargoPos.x) .. ", " .. string.format("%.2f", cargoPos.y) .. ", " .. string.format("%.2f", cargoPos.z) .. ")") local spawned = coalition.addStaticObject(unit:getCoalition(), cargoSpawnObject) self._droppedCrates[cargoSpawnObject.name] = spawned missionCommandsHelper:updateCommandsForGroup(group:getID()) + self._logger:debug("Cargo dropped for unit: " .. unit:getName() .. " crateType: " .. crateType) end ---@return table @@ -465,6 +471,7 @@ end ---@class SupplyUnitsBoundingBox ---@field min Vec3 World space minimum ---@field max Vec3 World space maximum +---@field heading number? Object heading in radians ---@param foundObject Object ---@return SupplyUnitsBoundingBox? @@ -481,20 +488,61 @@ function SupplyUnitsTracker:GetBoundingBoxes(foundObject) local objPos = foundObject:getPoint() local box = desc.box + local heading = 0 + + -- Try to get object heading from position vector's forward direction + -- This works for units and other objects that support getPosition + pcall(function() + local objPosition = foundObject:getPosition() + if objPosition and objPosition.x then + heading = math.atan2(objPosition.x.z, objPosition.x.x) + end + end) + + -- For rotated objects, we need to rotate the bounding box + local minX = box.min.x + local maxX = box.max.x + local minZ = box.min.z + local maxZ = box.max.z + + -- If object has significant rotation, apply rotation to bbox corners + if math.abs(heading) > 0.1 then + -- Get all 4 corners of bbox in local space + local corners = { + {minX, minZ}, + {minX, maxZ}, + {maxX, minZ}, + {maxX, maxZ} + } + + -- Rotate corners and find new min/max + minX, maxX = math.huge, -math.huge + minZ, maxZ = math.huge, -math.huge + + for _, corner in ipairs(corners) do + local rotX = corner[1] * math.cos(heading) - corner[2] * math.sin(heading) + local rotZ = corner[1] * math.sin(heading) + corner[2] * math.cos(heading) + minX = math.min(minX, rotX) + maxX = math.max(maxX, rotX) + minZ = math.min(minZ, rotZ) + maxZ = math.max(maxZ, rotZ) + end + end -- Convert relative bbox to world space by adding object position ---@type SupplyUnitsBoundingBox return { min = { - x = objPos.x + box.min.x, + x = objPos.x + minX, y = objPos.y + box.min.y, - z = objPos.z + box.min.z + z = objPos.z + minZ }, max = { - x = objPos.x + box.max.x, + x = objPos.x + maxX, y = objPos.y + box.max.y, - z = objPos.z + box.max.z - } + z = objPos.z + maxZ + }, + heading = heading } end @@ -530,6 +578,10 @@ end function SupplyUnitsTracker:GetCargoPlacePosition(unit, crateTypeName) local unitPos = unit:getPosition() + + -- Get unit's heading from the forward vector (x component) + -- Heading is calculated as: atan2(forward.z, forward.x) + local unitHeading = math.atan2(unitPos.x.z, unitPos.x.x) -- Get crate bbox - relative to placement position local crateDesc = StaticObject.getDescByName(crateTypeName) --[[@as table]] @@ -566,14 +618,24 @@ function SupplyUnitsTracker:GetCargoPlacePosition(unit, crateTypeName) local found = function(foundItem, val) local bbox = self:GetBoundingBoxes(foundItem) if bbox then + self._logger:debug("Found object: " .. foundItem:getTypeName() .. " at (" .. foundItem:getPoint().x .. ", " .. foundItem:getPoint().z .. ")") table.insert(occupiedObjects, { pos = foundItem:getPoint(), bbox = bbox }) + else + self._logger:debug("Found object without bbox: " .. foundItem:getTypeName()) end end - world.searchObjects(searchVolume.id, searchVolume, found) + local searchCategories = {} + for key, value in pairs(Object.Category) do + self._logger:debug("Adding category to search: " .. tostring(value) .. " (" .. tostring(key) .. ")") + table.insert(searchCategories, value) + end + +---@diagnostic disable-next-line: param-type-mismatch + world.searchObjects(searchCategories, searchVolume, found) local safetyMargin = 3 -- Safety margin around objects @@ -591,23 +653,26 @@ function SupplyUnitsTracker:GetCargoPlacePosition(unit, crateTypeName) for angle = minAngle, maxAngle, angleStep do local radians = math.rad(angle) - -- Calculate position at this angle and distance + -- Calculate position at this angle and distance, relative to unit's heading -- Angle 0 = forward, 90 = right, 180 = rear, 270 = left - local candidateX = unitPos.p.x + distance * math.sin(radians) - local candidateY = unitPos.p.y + distance * math.cos(radians) - local candidateZ = land.getHeight({ x = candidateX, y = candidateY }) + -- Apply unit heading to make angles relative to unit orientation + local worldAngle = radians + unitHeading + + local candidateX = unitPos.p.x + distance * math.sin(worldAngle) + local candidateZ = unitPos.p.z + distance * math.cos(worldAngle) + local candidateY = land.getHeight({ x = candidateX, y = candidateZ }) -- Convert crate's relative bbox to world space at this position local crateBBoxWorldSpace = { min = { x = candidateX + crateRelativeBBox.min.x, - y = candidateZ + crateRelativeBBox.min.y, - z = candidateY + crateRelativeBBox.min.z + y = candidateY + crateRelativeBBox.min.y, + z = candidateZ + crateRelativeBBox.min.z }, max = { x = candidateX + crateRelativeBBox.max.x, - y = candidateZ + crateRelativeBBox.max.y, - z = candidateY + crateRelativeBBox.max.z + y = candidateY + crateRelativeBBox.max.y, + z = candidateZ + crateRelativeBBox.max.z } } @@ -616,12 +681,14 @@ function SupplyUnitsTracker:GetCargoPlacePosition(unit, crateTypeName) for _, obj in ipairs(occupiedObjects) do if self:CheckBBoxCollision(crateBBoxWorldSpace, obj.bbox, safetyMargin) then collides = true + self._logger:debug("Collision at angle=" .. angle .. ", distance=" .. distance) break end end if not collides then - return { x = candidateX, y = candidateZ, z = candidateY } + self._logger:debug("Valid position found at angle=" .. angle .. ", distance=" .. distance .. ", pos=(" .. string.format("%.2f", candidateX) .. ", " .. string.format("%.2f", candidateY) .. ", " .. string.format("%.2f", candidateZ) .. ")") + return { x = candidateX, y = candidateY, z = candidateZ } end end end -- 2.54.0