diff --git a/Moose_CTLD_Pure/Moose_CTLD.lua b/Moose_CTLD_Pure/Moose_CTLD.lua index baf278c..a881cc4 100644 --- a/Moose_CTLD_Pure/Moose_CTLD.lua +++ b/Moose_CTLD_Pure/Moose_CTLD.lua @@ -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 diff --git a/Moose_CTLD_Pure/Moose_CTLD_FAC.lua b/Moose_CTLD_Pure/Moose_CTLD_FAC.lua index b2a0dff..891ca3d 100644 --- a/Moose_CTLD_Pure/Moose_CTLD_FAC.lua +++ b/Moose_CTLD_Pure/Moose_CTLD_FAC.lua @@ -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() diff --git a/Moose_CTLD_Pure/Moose_CTLD_Pure.miz b/Moose_CTLD_Pure/Moose_CTLD_Pure.miz new file mode 100644 index 0000000..6ebc2d5 Binary files /dev/null and b/Moose_CTLD_Pure/Moose_CTLD_Pure.miz differ diff --git a/Moose_CTLD_Pure/init_mission_dual_coalition.lua b/Moose_CTLD_Pure/init_mission_dual_coalition.lua index cba5a76..d28b733 100644 --- a/Moose_CTLD_Pure/init_mission_dual_coalition.lua +++ b/Moose_CTLD_Pure/init_mission_dual_coalition.lua @@ -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