mirror of
https://github.com/iTracerFacer/DCS_MissionDev.git
synced 2025-12-03 04:14:46 +00:00
FAC laser code reservation and visibility.
Coalition-level Admin/Help menus. Build confirmations and cooldowns (CTLD).
This commit is contained in:
@@ -156,6 +156,11 @@ CTLD.Config = {
|
|||||||
CrateLifetime = 3600, -- seconds before crates auto-clean
|
CrateLifetime = 3600, -- seconds before crates auto-clean
|
||||||
MessageDuration = 15, -- seconds for on-screen messages
|
MessageDuration = 15, -- seconds for on-screen messages
|
||||||
Debug = false,
|
Debug = false,
|
||||||
|
-- Build safety
|
||||||
|
BuildConfirmEnabled = true, -- require a second confirmation within a short window before building
|
||||||
|
BuildConfirmWindowSeconds = 10, -- seconds allowed between first and second "Build Here" press
|
||||||
|
BuildCooldownEnabled = true, -- after a successful build, impose a cooldown before allowing another build by the same group
|
||||||
|
BuildCooldownSeconds = 60, -- seconds of cooldown after a successful build per group
|
||||||
PickupZoneSmokeColor = trigger.smokeColor.Green, -- default smoke color when spawning crates at pickup zones
|
PickupZoneSmokeColor = trigger.smokeColor.Green, -- default smoke color when spawning crates at pickup zones
|
||||||
RequirePickupZoneForCrateRequest = true, -- enforce that crate requests must be near a Supply (Pickup) Zone
|
RequirePickupZoneForCrateRequest = true, -- enforce that crate requests must be near a Supply (Pickup) Zone
|
||||||
PickupZoneMaxDistance = 10000, -- meters; nearest pickup zone must be within this distance to allow a request
|
PickupZoneMaxDistance = 10000, -- meters; nearest pickup zone must be within this distance to allow a request
|
||||||
@@ -297,6 +302,8 @@ CTLD._hoverState = {} -- [unitName] = { targetCrate=name, startTime=t }
|
|||||||
CTLD._unitLast = {} -- [unitName] = { x, z, t }
|
CTLD._unitLast = {} -- [unitName] = { x, z, t }
|
||||||
CTLD._coachState = {} -- [unitName] = { lastKeyTimes = {key->time}, lastHint = "", phase = "", lastPhaseMsg = 0, target = crateName, holdStart = nil }
|
CTLD._coachState = {} -- [unitName] = { lastKeyTimes = {key->time}, lastHint = "", phase = "", lastPhaseMsg = 0, target = crateName, holdStart = nil }
|
||||||
CTLD._msgState = { } -- messaging throttle state: [scopeKey] = { lastKeyTimes = { key -> time } }
|
CTLD._msgState = { } -- messaging throttle state: [scopeKey] = { lastKeyTimes = { key -> time } }
|
||||||
|
CTLD._buildConfirm = {} -- [groupName] = time of first build request (awaiting confirmation)
|
||||||
|
CTLD._buildCooldown = {} -- [groupName] = time of last successful build
|
||||||
|
|
||||||
-- #endregion State
|
-- #endregion State
|
||||||
|
|
||||||
@@ -727,9 +734,12 @@ end
|
|||||||
function CTLD:InitMenus()
|
function CTLD:InitMenus()
|
||||||
if self.Config.UseGroupMenus then
|
if self.Config.UseGroupMenus then
|
||||||
self:WireBirthHandler()
|
self:WireBirthHandler()
|
||||||
|
-- Always provide a coalition-level Admin/Help menu for mission makers
|
||||||
|
self:InitCoalitionAdminMenu()
|
||||||
else
|
else
|
||||||
self.MenuRoot = MENU_COALITION:New(self.Side, 'CTLD')
|
self.MenuRoot = MENU_COALITION:New(self.Side, 'CTLD')
|
||||||
self:BuildCoalitionMenus(self.MenuRoot)
|
self:BuildCoalitionMenus(self.MenuRoot)
|
||||||
|
self:InitCoalitionAdminMenu()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -911,6 +921,31 @@ function CTLD:BuildCoalitionMenus(root)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function CTLD:InitCoalitionAdminMenu()
|
||||||
|
if self.AdminMenu then return end
|
||||||
|
local root = MENU_COALITION:New(self.Side, 'CTLD Admin/Help')
|
||||||
|
MENU_COALITION_COMMAND:New(self.Side, 'Enable CTLD Debug Logging', root, function()
|
||||||
|
self.Config.Debug = true
|
||||||
|
env.info(string.format('[Moose_CTLD][%s] Debug ENABLED via Admin menu', tostring(self.Side)))
|
||||||
|
_msgCoalition(self.Side, 'CTLD Debug logging ENABLED', 8)
|
||||||
|
end)
|
||||||
|
MENU_COALITION_COMMAND:New(self.Side, 'Disable CTLD Debug Logging', root, function()
|
||||||
|
self.Config.Debug = false
|
||||||
|
env.info(string.format('[Moose_CTLD][%s] Debug DISABLED via Admin menu', tostring(self.Side)))
|
||||||
|
_msgCoalition(self.Side, 'CTLD Debug logging DISABLED', 8)
|
||||||
|
end)
|
||||||
|
MENU_COALITION_COMMAND:New(self.Side, 'Show CTLD Status (crates/zones)', root, function()
|
||||||
|
local crates = 0
|
||||||
|
for _ in pairs(CTLD._crates) do crates = crates + 1 end
|
||||||
|
local msg = string.format('CTLD Status:\nActive crates: %d\nPickup zones: %d\nDrop zones: %d\nFOB zones: %d\nBuild Confirm: %s (%ds window)\nBuild Cooldown: %s (%ds)'
|
||||||
|
, crates, #(self.PickupZones or {}), #(self.DropZones or {}), #(self.FOBZones or {})
|
||||||
|
, self.Config.BuildConfirmEnabled and 'ON' or 'OFF', self.Config.BuildConfirmWindowSeconds or 0
|
||||||
|
, self.Config.BuildCooldownEnabled and 'ON' or 'OFF', self.Config.BuildCooldownSeconds or 0)
|
||||||
|
_msgCoalition(self.Side, msg, 20)
|
||||||
|
end)
|
||||||
|
self.AdminMenu = root
|
||||||
|
end
|
||||||
-- #endregion Menus
|
-- #endregion Menus
|
||||||
|
|
||||||
-- =========================
|
-- =========================
|
||||||
@@ -1069,6 +1104,29 @@ end
|
|||||||
function CTLD:BuildAtGroup(group)
|
function CTLD:BuildAtGroup(group)
|
||||||
local unit = group:GetUnit(1)
|
local unit = group:GetUnit(1)
|
||||||
if not unit or not unit:IsAlive() then return end
|
if not unit or not unit:IsAlive() then return end
|
||||||
|
-- Build cooldown/confirmation guardrails
|
||||||
|
local now = timer.getTime()
|
||||||
|
local gname = group:GetName()
|
||||||
|
if self.Config.BuildCooldownEnabled then
|
||||||
|
local last = CTLD._buildCooldown[gname]
|
||||||
|
if last and (now - last) < (self.Config.BuildCooldownSeconds or 60) then
|
||||||
|
local rem = math.max(0, math.ceil((self.Config.BuildCooldownSeconds or 60) - (now - last)))
|
||||||
|
_msgGroup(group, string.format('Build on cooldown. Try again in %ds.', rem))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if self.Config.BuildConfirmEnabled then
|
||||||
|
local first = CTLD._buildConfirm[gname]
|
||||||
|
local win = self.Config.BuildConfirmWindowSeconds or 10
|
||||||
|
if not first or (now - first) > win then
|
||||||
|
CTLD._buildConfirm[gname] = now
|
||||||
|
_msgGroup(group, string.format('Confirm build: select "Build Here" again within %ds to proceed.', win))
|
||||||
|
return
|
||||||
|
else
|
||||||
|
-- within window; proceed and clear pending
|
||||||
|
CTLD._buildConfirm[gname] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
local p = unit:GetPointVec3()
|
local p = unit:GetPointVec3()
|
||||||
local here = { x = p.x, z = p.z }
|
local here = { x = p.x, z = p.z }
|
||||||
local radius = self.Config.BuildRadius
|
local radius = self.Config.BuildRadius
|
||||||
@@ -1091,7 +1149,6 @@ function CTLD:BuildAtGroup(group)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Include loaded crates carried by this group
|
-- Include loaded crates carried by this group
|
||||||
local gname = group:GetName()
|
|
||||||
local carried = CTLD._loadedCrates[gname]
|
local carried = CTLD._loadedCrates[gname]
|
||||||
if self.Config.BuildRequiresGroundCrates ~= true then
|
if self.Config.BuildRequiresGroundCrates ~= true then
|
||||||
if carried and carried.byKey then
|
if carried and carried.byKey then
|
||||||
@@ -1145,6 +1202,7 @@ function CTLD:BuildAtGroup(group)
|
|||||||
if g then
|
if g then
|
||||||
for reqKey,qty in pairs(cat.requires) do consumeCrates(reqKey, qty) end
|
for reqKey,qty in pairs(cat.requires) do consumeCrates(reqKey, qty) end
|
||||||
_eventSend(self, nil, self.Side, 'build_success_coalition', { build = cat.description or recipeKey, player = _playerNameFromGroup(group) })
|
_eventSend(self, nil, self.Side, 'build_success_coalition', { build = cat.description or recipeKey, player = _playerNameFromGroup(group) })
|
||||||
|
if self.Config.BuildCooldownEnabled then CTLD._buildCooldown[gname] = now end
|
||||||
return
|
return
|
||||||
else
|
else
|
||||||
_eventSend(self, group, nil, 'build_failed', { reason = 'DCS group spawn error' })
|
_eventSend(self, group, nil, 'build_failed', { reason = 'DCS group spawn error' })
|
||||||
@@ -1169,6 +1227,7 @@ function CTLD:BuildAtGroup(group)
|
|||||||
if g then
|
if g then
|
||||||
consumeCrates(key, cat.required or 1)
|
consumeCrates(key, cat.required or 1)
|
||||||
_eventSend(self, nil, self.Side, 'build_success_coalition', { build = cat.description or key, player = _playerNameFromGroup(group) })
|
_eventSend(self, nil, self.Side, 'build_success_coalition', { build = cat.description or key, player = _playerNameFromGroup(group) })
|
||||||
|
if self.Config.BuildCooldownEnabled then CTLD._buildCooldown[gname] = now end
|
||||||
return
|
return
|
||||||
else
|
else
|
||||||
_eventSend(self, group, nil, 'build_failed', { reason = 'DCS group spawn error' })
|
_eventSend(self, group, nil, 'build_failed', { reason = 'DCS group spawn error' })
|
||||||
|
|||||||
@@ -128,6 +128,12 @@ FAC._facPilotNames = {} -- dynamic add on Birth if name contains AFAC/RECON/RE
|
|||||||
FAC._reccePilotNames = {}
|
FAC._reccePilotNames = {}
|
||||||
FAC._artDirectNames = {}
|
FAC._artDirectNames = {}
|
||||||
|
|
||||||
|
-- Laser code reservation per coalition side: [side] = { [code] = unitName }
|
||||||
|
FAC._reservedCodes = {}
|
||||||
|
|
||||||
|
-- Coalition-level admin/help menu handle per side
|
||||||
|
FAC._coalitionMenus = {}
|
||||||
|
|
||||||
FAC._ArtyTasked = {} -- [groupName] = { tasked=int, timeTasked=time, tgt=Unit|nil, requestor=string|nil }
|
FAC._ArtyTasked = {} -- [groupName] = { tasked=int, timeTasked=time, tgt=Unit|nil, requestor=string|nil }
|
||||||
FAC._RECCETasked = {} -- [unitName] = 1 when busy
|
FAC._RECCETasked = {} -- [unitName] = 1 when busy
|
||||||
|
|
||||||
@@ -287,6 +293,9 @@ function FAC:New(ctld, cfg)
|
|||||||
o._schedStatus = SCHEDULER:New(nil, function() o:_checkFacStatus() end, {}, 5, 1.0)
|
o._schedStatus = SCHEDULER:New(nil, function() o:_checkFacStatus() end, {}, 5, 1.0)
|
||||||
o._schedAI = SCHEDULER:New(nil, function() o:_artyAICall() end, {}, 10, 30)
|
o._schedAI = SCHEDULER:New(nil, function() o:_artyAICall() end, {}, 10, 30)
|
||||||
|
|
||||||
|
-- Coalition-level Admin/Help menu
|
||||||
|
o:_ensureCoalitionMenu()
|
||||||
|
|
||||||
return o
|
return o
|
||||||
end
|
end
|
||||||
-- #endregion Construction
|
-- #endregion Construction
|
||||||
@@ -435,6 +444,26 @@ function FAC:_ensureMenus()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function FAC:_ensureCoalitionMenu()
|
||||||
|
-- Create a coalition-level Admin/Help menu regardless of per-group menus
|
||||||
|
if self._coalitionMenus[self.Side] then return end
|
||||||
|
local root = MENU_COALITION:New(self.Side, 'FAC Admin/Help')
|
||||||
|
MENU_COALITION_COMMAND:New(self.Side, 'Show FAC Codes In Use', root, function()
|
||||||
|
self:_showCodesCoalition()
|
||||||
|
end)
|
||||||
|
MENU_COALITION_COMMAND:New(self.Side, 'Enable FAC Debug Logging', root, function()
|
||||||
|
self.Config.Debug = true
|
||||||
|
env.info(string.format('[FAC][%s] Debug ENABLED via Admin menu', tostring(self.Side)))
|
||||||
|
trigger.action.outTextForCoalition(self.Side, 'FAC Debug logging ENABLED', 8)
|
||||||
|
end)
|
||||||
|
MENU_COALITION_COMMAND:New(self.Side, 'Disable FAC Debug Logging', root, function()
|
||||||
|
self.Config.Debug = false
|
||||||
|
env.info(string.format('[FAC][%s] Debug DISABLED via Admin menu', tostring(self.Side)))
|
||||||
|
trigger.action.outTextForCoalition(self.Side, 'FAC Debug logging DISABLED', 8)
|
||||||
|
end)
|
||||||
|
self._coalitionMenus[self.Side] = root
|
||||||
|
end
|
||||||
|
|
||||||
function FAC:_buildGroupMenus(group)
|
function FAC:_buildGroupMenus(group)
|
||||||
-- Build the entire FAC/RECCE menu tree for a MOOSE GROUP
|
-- Build the entire FAC/RECCE menu tree for a MOOSE GROUP
|
||||||
local root = MENU_GROUP:New(group, 'FAC/RECCE')
|
local root = MENU_GROUP:New(group, 'FAC/RECCE')
|
||||||
@@ -574,6 +603,18 @@ function FAC:_posString(u)
|
|||||||
local p = u:getPosition().p
|
local p = u:getPosition().p
|
||||||
local lat, lon = coord.LOtoLL(p)
|
local lat, lon = coord.LOtoLL(p)
|
||||||
local dms = _llToDMS(lat, lon)
|
local dms = _llToDMS(lat, lon)
|
||||||
|
-- Append code summary across active FACs for situational awareness
|
||||||
|
local codes = self._reservedCodes[side] or {}
|
||||||
|
local any = false
|
||||||
|
local summary = '\nCodes in use:\n'
|
||||||
|
for _,code in ipairs(self.Config.FAC_laser_codes or {}) do
|
||||||
|
local owner = codes[tostring(code)]
|
||||||
|
if owner then
|
||||||
|
any = true
|
||||||
|
summary = summary .. string.format(' %s -> %s\n', tostring(code), self:_facName(owner))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if any then msg = msg .. summary end
|
||||||
local mgrs = _mgrsToString(coord.LLtoMGRS(lat, lon))
|
local mgrs = _mgrsToString(coord.LLtoMGRS(lat, lon))
|
||||||
local altM = math.floor(p.y)
|
local altM = math.floor(p.y)
|
||||||
local altF = math.floor(p.y*3.28084)
|
local altF = math.floor(p.y*3.28084)
|
||||||
@@ -586,7 +627,11 @@ function FAC:_setOnStation(group, on)
|
|||||||
local uname = u:GetName()
|
local uname = u:GetName()
|
||||||
_dbg(self, string.format('Action:SetOnStation unit=%s on=%s', uname, tostring(on and true or false)))
|
_dbg(self, string.format('Action:SetOnStation unit=%s on=%s', uname, tostring(on and true or false)))
|
||||||
-- init defaults
|
-- init defaults
|
||||||
if not self._laserCodes[uname] then self._laserCodes[uname] = self.Config.FAC_laser_codes[1] end
|
if not self._laserCodes[uname] then
|
||||||
|
-- Assign a free code on first-time activation
|
||||||
|
local code = self:_assignFreeCode(u:GetCoalition(), uname)
|
||||||
|
self._laserCodes[uname] = code or (self.Config.FAC_laser_codes and self.Config.FAC_laser_codes[1]) or '1688'
|
||||||
|
end
|
||||||
if not self._markerType[uname] then self._markerType[uname] = self.Config.MarkerDefault end
|
if not self._markerType[uname] then self._markerType[uname] = self.Config.MarkerDefault end
|
||||||
if not self._facUnits[uname] then self._facUnits[uname] = { name = uname, side = u:GetCoalition() } end
|
if not self._facUnits[uname] then self._facUnits[uname] = { name = uname, side = u:GetCoalition() } end
|
||||||
|
|
||||||
@@ -597,6 +642,7 @@ function FAC:_setOnStation(group, on)
|
|||||||
self:_cancelLase(uname)
|
self:_cancelLase(uname)
|
||||||
self._currentTargets[uname] = nil
|
self._currentTargets[uname] = nil
|
||||||
self._facUnits[uname] = nil
|
self._facUnits[uname] = nil
|
||||||
|
self:_releaseCode(u:GetCoalition(), uname)
|
||||||
end
|
end
|
||||||
self._facOnStation[uname] = on and true or nil
|
self._facOnStation[uname] = on and true or nil
|
||||||
|
|
||||||
@@ -609,7 +655,9 @@ function FAC:_setLaserCode(group, code)
|
|||||||
local u = group:GetUnit(1); if not u or not u:IsAlive() then return end
|
local u = group:GetUnit(1); if not u or not u:IsAlive() then return end
|
||||||
local uname = u:GetName()
|
local uname = u:GetName()
|
||||||
_dbg(self, string.format('Action:SetLaserCode unit=%s code=%s', uname, tostring(code)))
|
_dbg(self, string.format('Action:SetLaserCode unit=%s code=%s', uname, tostring(code)))
|
||||||
self._laserCodes[uname] = tostring(code)
|
-- Enforce simple reservation: reassign if taken
|
||||||
|
local assigned = self:_reserveCode(u:GetCoalition(), uname, tostring(code))
|
||||||
|
self._laserCodes[uname] = assigned
|
||||||
if self._facOnStation[uname] then
|
if self._facOnStation[uname] then
|
||||||
trigger.action.outTextForCoalition(u:GetCoalition(), string.format('[FAC "%s" on-station using CODE %s]', self:_facName(uname), self._laserCodes[uname]), 10)
|
trigger.action.outTextForCoalition(u:GetCoalition(), string.format('[FAC "%s" on-station using CODE %s]', self:_facName(uname), self._laserCodes[uname]), 10)
|
||||||
end
|
end
|
||||||
@@ -821,6 +869,9 @@ end
|
|||||||
|
|
||||||
function FAC:_cleanupFac(uname)
|
function FAC:_cleanupFac(uname)
|
||||||
self:_cancelLase(uname)
|
self:_cancelLase(uname)
|
||||||
|
-- release reserved code if any
|
||||||
|
local side = (self._facUnits[uname] and self._facUnits[uname].side) or self.Side
|
||||||
|
if side then self:_releaseCode(side, uname) end
|
||||||
self._laserCodes[uname] = nil
|
self._laserCodes[uname] = nil
|
||||||
self._markerType[uname] = nil
|
self._markerType[uname] = nil
|
||||||
self._markerColor[uname] = nil
|
self._markerColor[uname] = nil
|
||||||
@@ -1208,6 +1259,61 @@ function FAC:_markPoint(group, point, label)
|
|||||||
local txt = string.format('FAC: %s at %s', label or 'Contact', _llToDMS(lat,lon))
|
local txt = string.format('FAC: %s at %s', label or 'Contact', _llToDMS(lat,lon))
|
||||||
trigger.action.markToCoalition(id, txt, p3, self.Side, true)
|
trigger.action.markToCoalition(id, txt, p3, self.Side, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- #region Code reservation helpers
|
||||||
|
function FAC:_reserveCode(side, uname, code)
|
||||||
|
self._reservedCodes[side] = self._reservedCodes[side] or {}
|
||||||
|
local pool = self._reservedCodes[side]
|
||||||
|
code = tostring(code)
|
||||||
|
if pool[code] and pool[code] ~= uname then
|
||||||
|
-- Find a free alternative from configured list
|
||||||
|
local fallback = self:_assignFreeCode(side, uname)
|
||||||
|
if fallback then
|
||||||
|
-- Inform coalition about reassignment
|
||||||
|
trigger.action.outTextForCoalition(side, string.format('FAC %s requested code %s but it is in use by %s. Assigned %s instead.', self:_facName(uname), code, self:_facName(pool[code]), fallback), 10)
|
||||||
|
return fallback
|
||||||
|
end
|
||||||
|
-- No free code, keep requested (collision allowed with notice)
|
||||||
|
trigger.action.outTextForCoalition(side, string.format('FAC %s is sharing code %s with %s (no free codes).', self:_facName(uname), code, self:_facName(pool[code])), 10)
|
||||||
|
end
|
||||||
|
pool[code] = uname
|
||||||
|
return code
|
||||||
|
end
|
||||||
|
|
||||||
|
function FAC:_assignFreeCode(side, uname)
|
||||||
|
self._reservedCodes[side] = self._reservedCodes[side] or {}
|
||||||
|
local pool = self._reservedCodes[side]
|
||||||
|
for _,c in ipairs(self.Config.FAC_laser_codes or {'1688'}) do
|
||||||
|
local key = tostring(c)
|
||||||
|
if not pool[key] or pool[key] == uname then
|
||||||
|
pool[key] = uname
|
||||||
|
return key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function FAC:_releaseCode(side, uname)
|
||||||
|
local pool = self._reservedCodes[side]
|
||||||
|
if not pool then return end
|
||||||
|
for code,owner in pairs(pool) do
|
||||||
|
if owner == uname then pool[code] = nil end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function FAC:_showCodesCoalition()
|
||||||
|
local side = self.Side
|
||||||
|
local pool = self._reservedCodes[side] or {}
|
||||||
|
local lines = {'FAC Codes In Use:\n'}
|
||||||
|
local any = false
|
||||||
|
for _,c in ipairs(self.Config.FAC_laser_codes or {'1688'}) do
|
||||||
|
local owner = pool[tostring(c)]
|
||||||
|
if owner then any = true; table.insert(lines, string.format(' %s -> %s', tostring(c), self:_facName(owner))) end
|
||||||
|
end
|
||||||
|
if not any then table.insert(lines, ' (none)') end
|
||||||
|
trigger.action.outTextForCoalition(side, table.concat(lines, '\n'), 15)
|
||||||
|
end
|
||||||
|
-- #endregion Code reservation helpers
|
||||||
-- #endregion Mark helpers
|
-- #endregion Mark helpers
|
||||||
|
|
||||||
-- #region Export
|
-- #region Export
|
||||||
|
|||||||
Reference in New Issue
Block a user