First run of it working and spawning a crate! ha! suck on that mist

This commit is contained in:
iTracerFacer 2025-11-04 15:35:54 -06:00
parent 9affd7a583
commit b799e83283
4 changed files with 285 additions and 116 deletions

View File

@ -8,13 +8,53 @@
-- Outputs: F10 menus for helo/transport groups; crate spawning/building; troop load/unload; optional JTAC hookup (via FAC module);
-- Error modes: missing Moose -> abort; unknown crate key -> message; spawn blocked in enemy airbase; zone missing -> message.
if not _G.Moose or not _G.BASE then
env.info('[Moose_CTLD] Moose not detected (BASE class missing). Ensure Moose.lua is loaded before Moose_CTLD.lua')
if not _G.BASE then
env.info('[Moose_CTLD] Moose (BASE) not detected. Ensure Moose.lua is loaded before Moose_CTLD.lua')
end
local CTLD = {}
CTLD.__index = CTLD
-- Safe deep copy: prefer MOOSE UTILS.DeepCopy when available; fallback to Lua implementation
local function _deepcopy_fallback(obj, seen)
if type(obj) ~= 'table' then return obj end
seen = seen or {}
if seen[obj] then return seen[obj] end
local res = {}
seen[obj] = res
for k, v in pairs(obj) do
res[_deepcopy_fallback(k, seen)] = _deepcopy_fallback(v, seen)
end
local mt = getmetatable(obj)
if mt then setmetatable(res, mt) end
return res
end
local function DeepCopy(obj)
if _G.UTILS and type(UTILS.DeepCopy) == 'function' then
return UTILS.DeepCopy(obj)
end
return _deepcopy_fallback(obj)
end
-- Deep-merge src into dst (recursively). Arrays/lists in src replace dst.
local function DeepMerge(dst, src)
if type(dst) ~= 'table' or type(src) ~= 'table' then return src end
for k, v in pairs(src) do
if type(v) == 'table' then
local isArray = (rawget(v, 1) ~= nil) -- simple heuristic
if isArray then
dst[k] = DeepCopy(v)
else
dst[k] = DeepMerge(dst[k] or {}, v)
end
else
dst[k] = v
end
end
return dst
end
-- =========================
-- Defaults and State
-- =========================
@ -54,6 +94,7 @@ CTLD.Messages = {
-- Crates
crate_spawn_requested = "Request received—spawning {type} crate at {zone}.",
pickup_zone_required = "Move within {zone_dist} {zone_dist_u} of a Supply Zone to request crates.",
no_pickup_zones = "No Pickup Zones are configured for this coalition. Ask the mission maker to add supply zones or disable the pickup zone requirement.",
crate_re_marked = "Re-marking crate {id} with {mark}.",
crate_expired = "Crate {id} expired and was removed.",
crate_max_capacity = "Max load reached ({total}). Drop or build before picking up more.",
@ -290,12 +331,34 @@ end
local function _nearestZonePoint(unit, list)
if not unit or not unit:IsAlive() then return nil end
local p3 = unit:GetPointVec3()
-- Get unit position using DCS API to avoid dependency on MOOSE point methods
local uname = unit:GetName()
local du = Unit.getByName and Unit.getByName(uname) or nil
if not du or not du:getPoint() then return nil end
local up = du:getPoint()
local ux, uz = up.x, up.z
local best, bestd = nil, 1e12
for _,z in ipairs(list or {}) do
for _, z in ipairs(list or {}) do
local mz = _findZone(z)
if mz then
local d = p3:DistanceFromPoint(mz:GetPointVec3())
local zx, zz
if z and z.name and trigger and trigger.misc and trigger.misc.getZone then
local tz = trigger.misc.getZone(z.name)
if tz and tz.point then zx, zz = tz.point.x, tz.point.z end
end
if (not zx) and mz and mz.GetPointVec3 then
local zp = mz:GetPointVec3()
-- Try to read numeric fields directly to avoid method calls
if zp and type(zp) == 'table' and zp.x and zp.z then zx, zz = zp.x, zp.z end
end
if (not zx) and z and z.coord then
zx, zz = z.coord.x, z.coord.z
end
if zx and zz then
local dx = (zx - ux)
local dz = (zz - uz)
local d = math.sqrt(dx*dx + dz*dz)
if d < bestd then best, bestd = mz, d end
end
end
@ -381,9 +444,11 @@ end
local function _fmtTemplate(tpl, data)
if not tpl or tpl == '' then return '' end
return (tpl:gsub('{(%w+)}', function(k)
-- Support placeholder keys with underscores (e.g., {zone_dist_u})
return (tpl:gsub('{([%w_]+)}', function(k)
local v = data and data[k]
if v == nil then return '' end
-- If value is missing, leave placeholder intact to aid debugging
if v == nil then return '{'..k..'}' end
return tostring(v)
end))
end
@ -487,8 +552,8 @@ end
-- #region Construction
function CTLD:New(cfg)
local o = setmetatable({}, self)
o.Config = BASE:DeepCopy(CTLD.Config)
if cfg then o.Config = BASE:Inherit(o.Config, cfg) end
o.Config = DeepCopy(CTLD.Config)
if cfg then o.Config = DeepMerge(o.Config, cfg) end
o.Side = o.Config.CoalitionSide
o.MenuRoots = {}
o.MenusByGroup = {}
@ -511,6 +576,8 @@ function CTLD:New(cfg)
end
end
o:InitZones()
-- Validate configured zones and warn if missing
o:ValidateZones()
o:InitMenus()
-- Periodic cleanup for crates
@ -555,6 +622,95 @@ function CTLD:InitZones()
if mz then table.insert(self.FOBZones, mz); self._ZoneDefs.FOBZones[mz:GetName()] = z end
end
end
-- Validate configured zone names exist in the mission; warn coalition if any are missing.
function CTLD:ValidateZones()
local function zoneExistsByName(name)
if not name or name == '' then return false end
if trigger and trigger.misc and trigger.misc.getZone then
local z = trigger.misc.getZone(name)
if z then return true end
end
if ZONE and ZONE.FindByName then
local mz = ZONE:FindByName(name)
if mz then return true end
end
return false
end
local function sideToStr(s)
if coalition and coalition.side then
if s == coalition.side.BLUE then return 'BLUE' end
if s == coalition.side.RED then return 'RED' end
if s == coalition.side.NEUTRAL then return 'NEUTRAL' end
end
return tostring(s)
end
local function join(t)
local s = ''
for i,name in ipairs(t) do s = s .. (i>1 and ', ' or '') .. tostring(name) end
return s
end
local missing = { Pickup = {}, Drop = {}, FOB = {} }
local found = { Pickup = {}, Drop = {}, FOB = {} }
local coords = { Pickup = 0, Drop = 0, FOB = 0 }
for _,z in ipairs(self.Config.Zones.PickupZones or {}) do
if z.name then
if zoneExistsByName(z.name) then table.insert(found.Pickup, z.name) else table.insert(missing.Pickup, z.name) end
elseif z.coord then
coords.Pickup = coords.Pickup + 1
end
end
for _,z in ipairs(self.Config.Zones.DropZones or {}) do
if z.name then
if zoneExistsByName(z.name) then table.insert(found.Drop, z.name) else table.insert(missing.Drop, z.name) end
elseif z.coord then
coords.Drop = coords.Drop + 1
end
end
for _,z in ipairs(self.Config.Zones.FOBZones or {}) do
if z.name then
if zoneExistsByName(z.name) then table.insert(found.FOB, z.name) else table.insert(missing.FOB, z.name) end
elseif z.coord then
coords.FOB = coords.FOB + 1
end
end
-- Log a concise summary to dcs.log
local sideStr = sideToStr(self.Side)
env.info(string.format('[Moose_CTLD][ZoneValidation][%s] Pickup: configured=%d (named=%d, coord=%d) found=%d missing=%d',
sideStr,
#(self.Config.Zones.PickupZones or {}), #found.Pickup + #missing.Pickup, coords.Pickup, #found.Pickup, #missing.Pickup))
env.info(string.format('[Moose_CTLD][ZoneValidation][%s] Drop : configured=%d (named=%d, coord=%d) found=%d missing=%d',
sideStr,
#(self.Config.Zones.DropZones or {}), #found.Drop + #missing.Drop, coords.Drop, #found.Drop, #missing.Drop))
env.info(string.format('[Moose_CTLD][ZoneValidation][%s] FOB : configured=%d (named=%d, coord=%d) found=%d missing=%d',
sideStr,
#(self.Config.Zones.FOBZones or {}), #found.FOB + #missing.FOB, coords.FOB, #found.FOB, #missing.FOB))
local anyMissing = (#missing.Pickup > 0) or (#missing.Drop > 0) or (#missing.FOB > 0)
if anyMissing then
if #missing.Pickup > 0 then
local msg = 'CTLD config warning: Missing Pickup Zones: '..join(missing.Pickup)
_msgCoalition(self.Side, msg); env.info('[Moose_CTLD][ZoneValidation]['..sideStr..'] '..msg)
end
if #missing.Drop > 0 then
local msg = 'CTLD config warning: Missing Drop Zones: '..join(missing.Drop)
_msgCoalition(self.Side, msg); env.info('[Moose_CTLD][ZoneValidation]['..sideStr..'] '..msg)
end
if #missing.FOB > 0 then
local msg = 'CTLD config warning: Missing FOB Zones: '..join(missing.FOB)
_msgCoalition(self.Side, msg); env.info('[Moose_CTLD][ZoneValidation]['..sideStr..'] '..msg)
end
else
env.info(string.format('[Moose_CTLD][ZoneValidation][%s] All configured zone names resolved successfully.', sideStr))
end
self._MissingZones = missing
end
-- #endregion Construction
-- =========================
@ -729,10 +885,18 @@ function CTLD:RequestCrateForGroup(group, crateKey)
local unit = group:GetUnit(1)
if not unit or not unit:IsAlive() then return end
local zone, dist = _nearestZonePoint(unit, self.Config.Zones.PickupZones)
local hasPickupZones = (self.PickupZones and #self.PickupZones > 0) or (self.Config.Zones and self.Config.Zones.PickupZones and #self.Config.Zones.PickupZones > 0)
local spawnPoint
local maxd = (self.Config.PickupZoneMaxDistance or 10000)
-- Announce request
_eventSend(self, group, nil, 'crate_spawn_requested', { type = tostring(crateKey), zone = zone and zone:GetName() or 'nearest zone' })
local zoneName = zone and zone:GetName() or (hasPickupZones and 'nearest zone' or 'NO PICKUP ZONES CONFIGURED')
_eventSend(self, group, nil, 'crate_spawn_requested', { type = tostring(crateKey), zone = zoneName })
if not hasPickupZones and self.Config.RequirePickupZoneForCrateRequest then
_eventSend(self, group, nil, 'no_pickup_zones', {})
return
end
if zone and dist <= maxd then
spawnPoint = zone:GetPointVec3()
-- if pickup zone has smoke configured, mark it
@ -976,7 +1140,7 @@ function CTLD:DropLoadedCrates(group, howMany)
local toDrop = math.min(requested, initialTotal)
_eventSend(self, group, nil, 'drop_initiated', { count = toDrop })
-- Drop in key order
for k,count in pairs(BASE:DeepCopy(lc.byKey)) do
for k,count in pairs(DeepCopy(lc.byKey)) do
if toDrop <= 0 then break end
local dropNow = math.min(count, toDrop)
for i=1,dropNow do
@ -1258,54 +1422,61 @@ function CTLD:AutoBuildFOBCheck()
local filtered = {}
for _,c in ipairs(nearby) do if c.meta.side == self.Side then table.insert(filtered, c) end end
nearby = filtered
if #nearby == 0 then goto continue end
local counts = {}
for _,c in ipairs(nearby) do counts[c.meta.key] = (counts[c.meta.key] or 0) + 1 end
if #nearby > 0 then
local counts = {}
for _,c in ipairs(nearby) do counts[c.meta.key] = (counts[c.meta.key] or 0) + 1 end
local function consumeCrates(key, qty)
local removed = 0
for _,c in ipairs(nearby) do
if removed >= qty then break end
if c.meta.key == key then
local obj = StaticObject.getByName(c.name)
if obj then obj:destroy() end
CTLD._crates[c.name] = nil
removed = removed + 1
end
end
end
-- Prefer composite recipes
for recipeKey,cat in pairs(fobDefs) do
if type(cat.requires) == 'table' then
local ok = true
for reqKey,qty in pairs(cat.requires) do if (counts[reqKey] or 0) < qty then ok = false; break end end
if ok then
local gdata = cat.build({ x = center.x, z = center.z }, 0, cat.side or self.Side)
local g = _coalitionAddGroup(cat.side or self.Side, cat.category or Group.Category.GROUND, gdata)
if g then
for reqKey,qty in pairs(cat.requires) do consumeCrates(reqKey, qty) end
_msgCoalition(self.Side, string.format('FOB auto-built at %s', zone:GetName()))
goto continue -- move to next zone; avoid multiple builds per tick
local function consumeCrates(key, qty)
local removed = 0
for _,c in ipairs(nearby) do
if removed >= qty then break end
if c.meta.key == key then
local obj = StaticObject.getByName(c.name)
if obj then obj:destroy() end
CTLD._crates[c.name] = nil
removed = removed + 1
end
end
end
end
-- Then single-key FOB recipes
for key,cat in pairs(fobDefs) do
if not cat.requires and (counts[key] or 0) >= (cat.required or 1) then
local gdata = cat.build({ x = center.x, z = center.z }, 0, cat.side or self.Side)
local g = _coalitionAddGroup(cat.side or self.Side, cat.category or Group.Category.GROUND, gdata)
if g then
consumeCrates(key, cat.required or 1)
_msgCoalition(self.Side, string.format('FOB auto-built at %s', zone:GetName()))
goto continue
local built = false
-- Prefer composite recipes
for recipeKey,cat in pairs(fobDefs) do
if type(cat.requires) == 'table' then
local ok = true
for reqKey,qty in pairs(cat.requires) do if (counts[reqKey] or 0) < qty then ok = false; break end end
if ok then
local gdata = cat.build({ x = center.x, z = center.z }, 0, cat.side or self.Side)
local g = _coalitionAddGroup(cat.side or self.Side, cat.category or Group.Category.GROUND, gdata)
if g then
for reqKey,qty in pairs(cat.requires) do consumeCrates(reqKey, qty) end
_msgCoalition(self.Side, string.format('FOB auto-built at %s', zone:GetName()))
built = true
break -- move to next zone; avoid multiple builds per tick
end
end
end
end
end
::continue::
-- Then single-key FOB recipes
if not built then
for key,cat in pairs(fobDefs) do
if not cat.requires and (counts[key] or 0) >= (cat.required or 1) then
local gdata = cat.build({ x = center.x, z = center.z }, 0, cat.side or self.Side)
local g = _coalitionAddGroup(cat.side or self.Side, cat.category or Group.Category.GROUND, gdata)
if g then
consumeCrates(key, cat.required or 1)
_msgCoalition(self.Side, string.format('FOB auto-built at %s', zone:GetName()))
built = true
break
end
end
end
end
-- next zone iteration continues automatically
end
end
end
-- #endregion Auto-build FOB in zones
@ -1333,7 +1504,7 @@ function CTLD:AddDropZone(z)
end
function CTLD:SetAllowedAircraft(list)
self.Config.AllowedAircraft = BASE:DeepCopy(list)
self.Config.AllowedAircraft = DeepCopy(list)
end
-- #endregion Public helpers

View File

@ -24,14 +24,54 @@ Design notes
are conservative approximations to avoid silent failures.
]]
if not _G.Moose or not _G.BASE then
env.info('[Moose_CTLD_FAC] Moose not detected. Ensure Moose.lua is loaded before this script.')
if not _G.BASE then
env.info('[Moose_CTLD_FAC] Moose (BASE) not detected. Ensure Moose.lua is loaded before this script.')
end
local FAC = {}
FAC.__index = FAC
FAC.Version = '0.2.0'
-- Safe deep copy: prefer MOOSE UTILS.DeepCopy when available; fallback to Lua implementation
local function _deepcopy_fallback(obj, seen)
if type(obj) ~= 'table' then return obj end
seen = seen or {}
if seen[obj] then return seen[obj] end
local res = {}
seen[obj] = res
for k, v in pairs(obj) do
res[_deepcopy_fallback(k, seen)] = _deepcopy_fallback(v, seen)
end
local mt = getmetatable(obj)
if mt then setmetatable(res, mt) end
return res
end
local function DeepCopy(obj)
if _G.UTILS and type(UTILS.DeepCopy) == 'function' then
return UTILS.DeepCopy(obj)
end
return _deepcopy_fallback(obj)
end
-- Deep-merge src into dst (recursively). Arrays/lists in src replace dst.
local function DeepMerge(dst, src)
if type(dst) ~= 'table' or type(src) ~= 'table' then return src end
for k, v in pairs(src) do
if type(v) == 'table' then
local isArray = (rawget(v, 1) ~= nil)
if isArray then
dst[k] = DeepCopy(v)
else
dst[k] = DeepMerge(dst[k] or {}, v)
end
else
dst[k] = v
end
end
return dst
end
-- #region Config
-- Configuration for FAC behavior and UI. Adjust defaults here or pass overrides to :New().
FAC.Config = {
@ -218,8 +258,8 @@ end
function FAC:New(ctld, cfg)
local o = setmetatable({}, self)
o._ctld = ctld
o.Config = BASE:DeepCopy(FAC.Config)
if cfg then o.Config = BASE:Inherit(o.Config, cfg) end
o.Config = DeepCopy(FAC.Config)
if cfg then o.Config = DeepMerge(o.Config, cfg) end
o.Side = o.Config.CoalitionSide
o._zones = {}
@ -336,6 +376,13 @@ function FAC:RunZones(interval)
end, {}, 5, interval or 20)
end
-- Backwards-compatible Run() entry point used by init scripts
function FAC:Run()
-- Schedulers for menus/status are started in New(); here we can kick off zone scans if any zones exist.
if #self._zones > 0 then self:RunZones() end
return self
end
function FAC:_scanZone(Z)
-- Perform one detection update and mark contacts, with spatial de-duplication
Z.Detector:DetectionUpdate()

Binary file not shown.

View File

@ -12,7 +12,8 @@
-- Adjust names below if you use different zone names.
-- Create CTLD for BLUE
-- Create CTLD instances only if Moose and CTLD are available
if _MOOSE_CTLD and _G.BASE then
ctldBlue = _MOOSE_CTLD:New({
CoalitionSide = coalition.side.BLUE,
PickupZoneSmokeColor = trigger.smokeColor.Blue,
@ -21,7 +22,7 @@ ctldBlue = _MOOSE_CTLD:New({
},
Zones = {
PickupZones = { { name = 'PICKUP_BLUE_MAIN', smoke = trigger.smokeColor.Blue } },
PickupZones = { { name = 'Blue_PickupZone_1', smoke = trigger.smokeColor.Blue } },
--DropZones = { { name = 'DROP_BLUE_1' } },
-- FOBZones = { { name = 'FOB_BLUE_A' } },
},
@ -37,71 +38,19 @@ ctldRed = _MOOSE_CTLD:New({
},
Zones = {
PickupZones = { { name = 'PICKUP_RED_MAIN', smoke = trigger.smokeColor.Red } },
PickupZones = { { name = 'Red_PickupZone_1', smoke = trigger.smokeColor.Red } },
--DropZones = { { name = 'DROP_RED_1' } },
-- FOBZones = { { name = 'FOB_RED_A' } },
},
BuildRequiresGroundCrates = true,
})
else
env.info('[init_mission_dual_coalition] Moose or CTLD missing; skipping CTLD init')
end
-- If the external catalog was loaded (as _CTLD_EXTRACTED_CATALOG), both instances auto-merged it
-- thanks to Moose_CTLD.lua. If you want to load it manually or from a different path, you can do:
-- local extracted = dofile(lfs.writedir()..[[Scripts\Moose_CTLD_Pure\catalogs\CrateCatalog_CTLD_Extract.lua]])
-- ctldBlue:MergeCatalog(extracted)
-- ctldRed:MergeCatalog(extracted)
-- Optional: add a couple of small, side-specific examples if you don't use the big catalog
-- (Uncomment to add a simple MANPADS and AAA on each side)
-- ctldBlue:RegisterCrate('MANPADS', {
-- menuCategory='Infantry', description='2x crates -> MANPADS (Stinger)', dcsCargoType='uh1h_cargo', required=2,
-- side=coalition.side.BLUE, category=Group.Category.GROUND,
-- build=function(point, headingDeg)
-- local hdg = math.rad(headingDeg or 0)
-- return { visible=false, lateActivation=false, tasks={}, task='Ground Nothing', route={}, name='CTLD_BP_MANPADS_'..math.random(1,999999),
-- units={ { type='Soldier stinger', name='CTLD-Stinger-'..math.random(1,999999), x=point.x, y=point.z, heading=hdg } } }
-- end,
-- })
-- ctldBlue:RegisterCrate('AAA', {
-- menuCategory='AAA', description='3x crates -> ZU-23 site', dcsCargoType='container_cargo', required=3,
-- side=coalition.side.BLUE, category=Group.Category.GROUND,
-- build=function(point, headingDeg)
-- local hdg = math.rad(headingDeg or 0)
-- local function off(dx,dz) return { x=point.x+dx, z=point.z+dz } end
-- local units={
-- { type='ZU-23 Emplacement', name='CTLD-ZU23-'..math.random(1,999999), x=point.x, y=point.z, heading=hdg },
-- { type='Ural-375', name='CTLD-TRK-'..math.random(1,999999), x=off(15,12).x, y=off(15,12).z, heading=hdg },
-- { type='Infantry AK', name='CTLD-INF-'..math.random(1,999999), x=off(-12,-15).x, y=off(-12,-15).z, heading=hdg },
-- }
-- return { visible=false, lateActivation=false, tasks={}, task='Ground Nothing', units=units, route={}, name='CTLD_BP_AAA_'..math.random(1,999999) }
-- end,
-- })
-- ctldRed:RegisterCrate('MANPADS', {
-- menuCategory='Infantry', description='2x crates -> MANPADS (Igla)', dcsCargoType='uh1h_cargo', required=2,
-- side=coalition.side.RED, category=Group.Category.GROUND,
-- build=function(point, headingDeg)
-- local hdg = math.rad(headingDeg or 0)
-- -- Using generic infantry to avoid DCS type-name mismatches; replace with accurate manpad unit if desired
-- return { visible=false, lateActivation=false, tasks={}, task='Ground Nothing', route={}, name='CTLD_RP_MANPADS_'..math.random(1,999999),
-- units={ { type='Infantry AK', name='CTLD-Igla-'..math.random(1,999999), x=point.x, y=point.z, heading=hdg } } }
-- end,
-- })
-- ctldRed:RegisterCrate('AAA', {
-- menuCategory='AAA', description='3x crates -> ZU-23 site', dcsCargoType='container_cargo', required=3,
-- side=coalition.side.RED, category=Group.Category.GROUND,
-- build=function(point, headingDeg)
-- local hdg = math.rad(headingDeg or 0)
-- local function off(dx,dz) return { x=point.x+dx, z=point.z+dz } end
-- local units={
-- { type='ZU-23 Emplacement', name='CTLD-ZU23-'..math.random(1,999999), x=point.x, y=point.z, heading=hdg },
-- { type='Ural-375', name='CTLD-TRK-'..math.random(1,999999), x=off(15,12).x, y=off(15,12).z, heading=hdg },
-- { type='Infantry AK', name='CTLD-INF-'..math.random(1,999999), x=off(-12,-15).x, y=off(-12,-15).z, heading=hdg },
-- }
-- return { visible=false, lateActivation=false, tasks={}, task='Ground Nothing', units=units, route={}, name='CTLD_RP_AAA_'..math.random(1,999999) }
-- end,
-- })
-- Optional: FAC/RECCE for both sides (requires Moose_CTLD_FAC.lua)
if _MOOSE_CTLD_FAC then
if _MOOSE_CTLD_FAC and _G.BASE and ctldBlue and ctldRed then
facBlue = _MOOSE_CTLD_FAC:New(ctldBlue, {
CoalitionSide = coalition.side.BLUE,
Arty = { Enabled = false },
@ -115,4 +64,6 @@ if _MOOSE_CTLD_FAC then
})
-- facRed:AddRecceZone({ name = 'RECCE_RED_1' })
facRed:Run()
else
env.info('[init_mission_dual_coalition] FAC not initialized (missing Moose/CTLD/FAC or CTLD not created)')
end