From 35047971473251832c948d848d9db1a4048a88b3 Mon Sep 17 00:00:00 2001 From: leka1986 <83298840+leka1986@users.noreply.github.com> Date: Mon, 5 Jan 2026 03:43:28 +0100 Subject: [PATCH 1/2] Update CTLD.lua Fixed so the Engineer can remove all the items when he is building from an airdrop. Added a function that I use, CanGetCrates, it's a check before getting it if the conditions are met. Added small fixes where calling FindCratesNearBy would remove the units when it's called by the function RemoveCratesNearBy when it was checking the surroundings and it should ignore what is inside. In the case of C-130 and the CH-47. Added how many crates are dropped when getting statics. Expanded OnAfterTroopsRTB to accept chunk, ie the number of units which is returned. --- Moose Development/Moose/Ops/CTLD.lua | 186 +++++++++++++++------------ 1 file changed, 105 insertions(+), 81 deletions(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index e60848bca..ea74bf372 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -193,6 +193,7 @@ CTLD_CARGO = { -- @param Wrapper.Unit#UNIT Unit -- @return #boolean Outcome function CTLD_CARGO:UnitCanCarry(Unit) + if not Unit then return false end if self.TypeNames == nil then return true end local typename = Unit:GetTypeName() or "none" if self.TypeNames[typename] then @@ -3137,6 +3138,17 @@ function CTLD:_AddCrateQuantityMenus(Group, Unit, parentMenu, cargoObj, stockSum return self end +--- User overrideable function to determine if a unit can get crates. + -- @param #CTLD self + -- @param Wrapper.Group#GROUP Group + -- @param Wrapper.Unit#UNIT Unit + -- @param #table Config Configuration entry for the unit. + -- @param #number quantity Number of crate sets requested. + -- @param #boolean quiet If true, do not send messages to the user. + function CTLD:CanGetUnits(Group, Unit, Config, quantity, quiet) + return true + end + --- (Internal) Spawn a “Get units” entry for a C-130J-30 at load zone. -- @param #CTLD self -- @param Wrapper.Group#GROUP Group @@ -3167,6 +3179,9 @@ function CTLD:_C130GetUnits(Group, Unit, Name) self:_SendMessage("You are not close enough to a logistics zone!",10,false,Group) return self end + if not self:CanGetUnits(Group, Unit, cfg, 1, false) then + return self + end local coord = Unit:GetCoordinate() or Group:GetCoordinate() local capabilities = self:_GetUnitCapabilities(Unit) @@ -3514,9 +3529,9 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack, quiet, suppress Cargo:RemoveStock(requestedSets) self:_RefreshCrateQuantityMenus(Group, Unit, Cargo) end - local text = string.format("Crates for %s have been positioned near you!",cratename) + local text = string.format("%d crates for %s have been positioned near you!",number,cratename) if drop then - text = string.format("Crates for %s have been dropped!",cratename) + text = string.format("%d crates for %s have been dropped!",number,cratename) self:__CratesDropped(1, Group, Unit, droppedcargo) else if not quiet then @@ -3612,7 +3627,7 @@ end function CTLD:_ListCratesNearby( _group, _unit) self:T(self.lid .. " _ListCratesNearby") local finddist = self.CrateDistance or 35 - local crates,number,loadedbygc,indexgc = self:_FindCratesNearby(_group,_unit, finddist,true,true) -- #table + local crates,number,loadedbygc,indexgc = self:_FindCratesNearby(_group,_unit, finddist,true,true,true) -- #table if number > 0 or indexgc > 0 then local text = REPORT:New("Crates Found Nearby:") text:Add("------------------------------------------------------------") @@ -3711,7 +3726,7 @@ end function CTLD:_RemoveCratesNearby(_group, _unit) self:T(self.lid.." _RemoveCratesNearby") local finddist=self.CrateDistance or 35 - local crates,number=self:_FindCratesNearby(_group,_unit,finddist,true,true) + local crates,number=self:_FindCratesNearby(_group,_unit,finddist,true,true,true) if number>0 then local removedIDs={} local text=REPORT:New("Removing Crates Found Nearby:") @@ -3824,7 +3839,7 @@ function CTLD:_FindCratesNearby( _group, _unit, _dist, _ignoreweight, ignoretype self:T(self.lid .. " Loading restricted: " .. tostring(restricted)) local staticpos = static:GetCoordinate() --or dcsunitpos local cando = cargo:UnitCanCarry(_unit) - if ignoretype == true then cando = true end + if ignoretype == true then cando = true restricted = false end self:T(self.lid .. " Unit can carry: " .. tostring(cando)) --- Testing local distance=self:_GetDistance(location,staticpos) @@ -4618,7 +4633,7 @@ function CTLD:_BuildCrates(Group, Unit,Engineering,MultiDrop) finddist=self.EngineerSearch finddist=self.EngineerSearch end - local crates,number = self:_FindCratesNearby(Group,Unit, finddist,true,true) -- #table + local crates,number = self:_FindCratesNearby(Group,Unit,finddist,true,true,not Engineering) -- #table local buildables = {} local foundbuilds = false local canbuild = false @@ -6411,9 +6426,11 @@ function CTLD:_RefreshDropCratesMenu(Group, Unit) -- @param Wrapper.Unit#UNIT Unit The calling unit. -- @param #number chunkID the Cargo ID -- @return #CTLD self -function CTLD:_UnloadSingleTroopByID(Group, Unit, chunkID) +function CTLD:_UnloadSingleTroopByID(Group, Unit, chunkID, qty) self:T(self.lid .. " _UnloadSingleTroopByID chunkID=" .. tostring(chunkID)) + qty = qty or 1 + local droppingatbase = false local inzone, zonename, zone, distance = self:IsUnitInZone(Unit, CTLD.CargoZoneType.LOAD) if not inzone then @@ -6453,77 +6470,76 @@ function CTLD:_UnloadSingleTroopByID(Group, Unit, chunkID) end -- Drop the FIRST cargo in that chunk - local foundCargo = chunk[1] - if not foundCargo then - self:_SendMessage(string.format("No troop cargo at chunk %d!", chunkID), 10, false, Group) - if not self.debug then return self end - return self - end + for n = 1, qty do + local foundCargo = chunk[1] + if not foundCargo then break end - local cType = foundCargo:GetType() - local name = foundCargo:GetName() or "none" - local tmpl = foundCargo:GetTemplates() or {} - local zoneradius = self.troopdropzoneradius or 100 - local factor = 1 - if isHerc then - factor = foundCargo:GetCratesNeeded() or 1 - zoneradius = Unit:GetVelocityMPS() or 100 - end - local zone = ZONE_GROUP:New(string.format("Unload zone-%s", unitName), Group, zoneradius * factor) - local randomcoord = zone:GetRandomCoordinate(10, 30 * factor) - local heading = Group:GetHeading() or 0 - - if grounded or hoverunload then - randomcoord = Group:GetCoordinate() - local Angle = (heading + 270) % 360 - if isHerc or isHook then - Angle = (heading + 180) % 360 - end - local offset = hoverunload and self.TroopUnloadDistHover or self.TroopUnloadDistGround + local cType = foundCargo:GetType() + local name = foundCargo:GetName() or "none" + local tmpl = foundCargo:GetTemplates() or {} + local zoneradius = self.troopdropzoneradius or 100 + local factor = 1 if isHerc then - offset = self.TroopUnloadDistGroundHerc or 25 + factor = foundCargo:GetCratesNeeded() or 1 + zoneradius = Unit:GetVelocityMPS() or 100 end - if isHook then - offset = self.TroopUnloadDistGroundHook or 15 - if hoverunload and self.TroopUnloadDistHoverHook then - offset = self.TroopUnloadDistHoverHook or 5 + local zone = ZONE_GROUP:New(string.format("Unload zone-%s", unitName), Group, zoneradius * factor) + local randomcoord = zone:GetRandomCoordinate(10, 30 * factor) + local heading = Group:GetHeading() or 0 + + if grounded or hoverunload then + randomcoord = Group:GetCoordinate() + local Angle = (heading + 270) % 360 + if isHerc or isHook then + Angle = (heading + 180) % 360 end + local offset = hoverunload and self.TroopUnloadDistHover or self.TroopUnloadDistGround + if isHerc then + offset = self.TroopUnloadDistGroundHerc or 25 + end + if isHook then + offset = self.TroopUnloadDistGroundHook or 15 + if hoverunload and self.TroopUnloadDistHoverHook then + offset = self.TroopUnloadDistHoverHook or 5 + end + end + randomcoord:Translate(offset, Angle, nil, true) end - randomcoord:Translate(offset, Angle, nil, true) - end - local tempcount = 0 - if isHook then - tempcount = self.ChinookTroopCircleRadius or 5 - end - for _, _template in pairs(tmpl) do - self.TroopCounter = self.TroopCounter + 1 - tempcount = tempcount + 1 - local alias = string.format("%s-%d", _template, math.random(1,100000)) - local rad = 2.5 + (tempcount * 2) - local Positions = self:_GetUnitPositions(randomcoord, rad, heading, _template) - 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) - end - - foundCargo:SetWasDropped(true) - if cType == CTLD_CARGO.Enum.ENGINEERS then - self.Engineers = self.Engineers + 1 - local grpname = self.DroppedTroops[self.TroopCounter]:GetName() - self.EngineersInField[self.Engineers] = CTLD_ENGINEERING:New(name, grpname) - self:_SendMessage(string.format("Dropped Engineers %s into action!", name), 10, false, Group) - else - self:_SendMessage(string.format("Dropped Troops %s into action!", name), 10, false, Group) - end + local tempcount = 0 + if isHook then + tempcount = self.ChinookTroopCircleRadius or 5 + end + for _, _template in pairs(tmpl) do + self.TroopCounter = self.TroopCounter + 1 + tempcount = tempcount + 1 + local alias = string.format("%s-%d", _template, math.random(1,100000)) + local rad = 2.5 + (tempcount * 2) + local Positions = self:_GetUnitPositions(randomcoord, rad, heading, _template) + 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) + end + + foundCargo:SetWasDropped(true) + if cType == CTLD_CARGO.Enum.ENGINEERS then + self.Engineers = self.Engineers + 1 + local grpname = self.DroppedTroops[self.TroopCounter]:GetName() + self.EngineersInField[self.Engineers] = CTLD_ENGINEERING:New(name, grpname) + self:_SendMessage(string.format("Dropped Engineers %s into action!", name), 10, false, Group) + else + self:_SendMessage(string.format("Dropped Troops %s into action!", name), 10, false, Group) + end - table.remove(chunk, 1) - if #chunk == 0 then - self.TroopsIDToChunk[chunkID] = nil + table.remove(chunk, 1) + if #chunk == 0 then + self.TroopsIDToChunk[chunkID] = nil + break + end end else @@ -6533,7 +6549,8 @@ function CTLD:_UnloadSingleTroopByID(Group, Unit, chunkID) if self.TroopsIDToChunk and self.TroopsIDToChunk[chunkID] then local chunk = self.TroopsIDToChunk[chunkID] - if #chunk > 0 then + for n = 1, qty do + if #chunk == 0 then break end local firstObj = chunk[1] local cName = firstObj:GetName() local gentroops = self.Cargo_Troops @@ -6548,9 +6565,9 @@ function CTLD:_UnloadSingleTroopByID(Group, Unit, chunkID) end firstObj:SetWasDropped(true) table.remove(chunk, 1) - if #chunk == 0 then - self.TroopsIDToChunk[chunkID] = nil - end + end + if #chunk == 0 then + self.TroopsIDToChunk[chunkID] = nil end end end @@ -6625,15 +6642,22 @@ function CTLD:_RefreshDropTroopsMenu(Group, Unit) for tName, objList in pairs(troopsByName) do table.sort(objList, function(a,b) return a:GetID() < b:GetID() end) local count = #objList + if count > 0 then + local chunkID = objList[1]:GetID() + self.TroopsIDToChunk[chunkID] = objList - local chunkID = objList[1]:GetID() - self.TroopsIDToChunk[chunkID] = objList - - local label = string.format("Drop %s (%d)", tName, count) - MENU_GROUP_COMMAND:New(theGroup, label, dropTroopsMenu, self._UnloadSingleTroopByID, self, theGroup, theUnit, chunkID) + local label = string.format("Drop %s (%d)", tName, count) + if count == 1 then + MENU_GROUP_COMMAND:New(theGroup, label, dropTroopsMenu, self._UnloadSingleTroopByID, self, theGroup, theUnit, chunkID, 1) + else + local parentMenu = MENU_GROUP:New(theGroup, label, dropTroopsMenu) + for q = 1, count do + MENU_GROUP_COMMAND:New(theGroup, string.format("Drop (%d) %s", q, tName), parentMenu, self._UnloadSingleTroopByID, self, theGroup, theUnit, chunkID, q) + end + end + end end end - --- [Internal] Function to check if a template exists in the mission. -- @param #CTLD self -- @param #table temptable Table of string names From aed2f1d8a914130180262b278826e4c0b9f81afe Mon Sep 17 00:00:00 2001 From: leka1986 <83298840+leka1986@users.noreply.github.com> Date: Mon, 5 Jan 2026 04:18:57 +0100 Subject: [PATCH 2/2] Enhance troop loading logic and messaging noticed a bug, I could load troops outside the zone when choosing amount. I could also load without the door was even checked. Combined the troop drop to one message instead of multiple messages for each dropped group. --- Moose Development/Moose/Ops/CTLD.lua | 44 ++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index ea74bf372..f9b48a3db 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -2911,6 +2911,28 @@ end -- @return #CTLD self function CTLD:_LoadTroopsQuantity(Group, Unit, Cargo, quantity) local n = math.max(1, tonumber(quantity) or 1) + + -- landed or hovering over load zone? + local grounded = not self:IsUnitInAir(Unit) + local hoverload = self:CanHoverLoad(Unit) + + -- check if we are in LOAD zone + local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) + if not inzone then + inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) + end + + if not inzone then + self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group) + if not self.debug then return self end + elseif not grounded and not hoverload then + self:_SendMessage("You need to land or hover in position to load!", 10, false, Group) + if not self.debug then return self end + elseif self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then + self:_SendMessage("You need to open the door(s) to load troops!", 10, false, Group) + if not self.debug then return self end + end + local prevSuppress = self.suppressmessages self.suppressmessages = true for i = 1, n do @@ -2955,11 +2977,10 @@ function CTLD:_AddTroopQuantityMenus(Group, Unit, parentMenu, cargoObj) if capacitySets < maxQuantity then maxQuantity = capacitySets end end for quantity = 1, maxQuantity do - local m = MENU_GROUP:New(Group, tostring(quantity), parentMenu) if quantity == 1 then - MENU_GROUP_COMMAND:New(Group, "Load", m, self._LoadTroops, self, Group, Unit, cargoObj) + MENU_GROUP_COMMAND:New(Group, tostring(quantity), parentMenu, self._LoadTroops, self, Group, Unit, cargoObj) else - MENU_GROUP_COMMAND:New(Group, "Load", m, self._LoadTroopsQuantity, self, Group, Unit, cargoObj, quantity) + MENU_GROUP_COMMAND:New(Group, tostring(quantity), parentMenu, self._LoadTroopsQuantity, self, Group, Unit, cargoObj, quantity) end end return self @@ -6469,6 +6490,9 @@ function CTLD:_UnloadSingleTroopByID(Group, Unit, chunkID, qty) return self end + local deployedTroopsByName = {} + local deployedEngineersByName = {} + -- Drop the FIRST cargo in that chunk for n = 1, qty do local foundCargo = chunk[1] @@ -6530,9 +6554,9 @@ function CTLD:_UnloadSingleTroopByID(Group, Unit, chunkID, qty) self.Engineers = self.Engineers + 1 local grpname = self.DroppedTroops[self.TroopCounter]:GetName() self.EngineersInField[self.Engineers] = CTLD_ENGINEERING:New(name, grpname) - self:_SendMessage(string.format("Dropped Engineers %s into action!", name), 10, false, Group) + deployedEngineersByName[name] = (deployedEngineersByName[name] or 0) + 1 else - self:_SendMessage(string.format("Dropped Troops %s into action!", name), 10, false, Group) + deployedTroopsByName[name] = (deployedTroopsByName[name] or 0) + 1 end table.remove(chunk, 1) @@ -6542,6 +6566,16 @@ function CTLD:_UnloadSingleTroopByID(Group, Unit, chunkID, qty) end end + local parts = {} + for nName,nCount in pairs(deployedTroopsByName) do + parts[#parts + 1] = tostring(nCount).."x Troops "..nName + end + for nName,nCount in pairs(deployedEngineersByName) do + parts[#parts + 1] = tostring(nCount).."x Engineers "..nName + end + if #parts > 0 then + self:_SendMessage("Dropped "..table.concat(parts, ", ").." into action!", 10, false, Group) + end else -- Return to base logic, remove ONLY the first cargo self:_SendMessage("Troops have returned to base!", 10, false, Group)