diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..ca275db
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,16 @@
+{
+ "Lua.diagnostics.globals": [
+ "missionCommands",
+ "env",
+ "Airbase",
+ "world",
+ "trigger",
+ "StaticObject",
+ "Group",
+ "Unit",
+ "Object",
+ "coalition",
+ "timer",
+ "AI"
+ ]
+}
\ No newline at end of file
diff --git a/SPEARHEADDEV.miz b/SPEARHEADDEV.miz
new file mode 100644
index 0000000..0d09e53
Binary files /dev/null and b/SPEARHEADDEV.miz differ
diff --git a/SPEARHEADDEV.zip b/SPEARHEADDEV.zip
new file mode 100644
index 0000000..a0f304f
Binary files /dev/null and b/SPEARHEADDEV.zip differ
diff --git a/SpearheadCompile.py b/SpearheadCompile.py
new file mode 100644
index 0000000..86f2317
--- /dev/null
+++ b/SpearheadCompile.py
@@ -0,0 +1,39 @@
+import os
+import sys
+import glob
+
+Order = [
+ "spearhead_defaults.lua",
+ "spearhead_base.lua",
+ "spearhead_events.lua",
+ "spearhead_dcsbase.lua",
+ "spearhead_routes.lua",
+ "spearhead_mission.lua",
+ "spearhead_db.lua",
+ "spearhead_stage.lua",
+ "spearhead_cap.lua",
+ "spearhead.lua"
+]
+
+def compile(root, target):
+ compiled = ""
+ for filename in Order:
+ path = os.path.join(root, filename)
+ with open(path,'r') as file:
+ fileContents = file.read()
+ compiled += fileContents
+ print(path)
+
+ with open(target, "w") as targetFile:
+ targetFile.write(compiled)
+
+
+if __name__ == "__main__" :
+ args = sys.argv[1:]
+ root = args[0]
+ target = args[1]
+
+ print(f"Source: {root}")
+ print(f"target: {target}")
+
+ compile(root, target)
\ No newline at end of file
diff --git a/SpearheadConfig.lua b/SpearheadConfig.lua
new file mode 100644
index 0000000..b2771f1
--- /dev/null
+++ b/SpearheadConfig.lua
@@ -0,0 +1,2 @@
+---KEEP THIS LINE
+if not SpearheadConfig then SpearheadConfig = {} end
diff --git a/capClasses/CapAirbase.lua b/capClasses/CapAirbase.lua
new file mode 100644
index 0000000..7bb616f
--- /dev/null
+++ b/capClasses/CapAirbase.lua
@@ -0,0 +1,200 @@
+
+local CapBase = {}
+
+---comment
+---@param airbaseId number
+---@param database table
+---@param logger table
+---@param capConfig table
+---@param stageConfig table
+---@return table
+function CapBase:new(airbaseId, database, logger, capConfig, stageConfig)
+ local o = {}
+ setmetatable(o, { __index = self })
+
+ o.groupNames = database:getCapGroupsAtAirbase(airbaseId)
+ o.database = database
+ o.airbaseId = airbaseId
+ o.logger = logger
+ o.activeStage = 0
+ o.capConfig = capConfig
+ o.spawned = false
+
+ if capConfig == nil then
+ capConfig = {}
+ table.insert(Spearhead.MissionEditingWarnings,"CapConfig is nil")
+ else
+ if capConfig.minSpeed == nil then Spearhead.MissionEditingWarnings("CapConfig.minSpeed is nil") end
+ if capConfig.maxSpeed == nil then Spearhead.MissionEditingWarnings("CapConfig.maxSpeed is nil") end
+ if capConfig.minAlt == nil then Spearhead.MissionEditingWarnings("CapConfig.minAlt is nil") end
+ if capConfig.maxAlt == nil then Spearhead.MissionEditingWarnings("CapConfig.maxAlt is nil") end
+ if capConfig.minDurationOnStation == nil then Spearhead.MissionEditingWarnings("CapConfig.minDurationOnStation is nil") end
+ if capConfig.maxDurationOnStation == nil then Spearhead.MissionEditingWarnings("CapConfig.maxDurationOnStation is nil") end
+ if capConfig.rearmDelay == nil then Spearhead.MissionEditingWarnings("CapConfig.rearmDelay is nil") end
+ if capConfig.deathDelay == nil then Spearhead.MissionEditingWarnings("CapConfig.deathDelay is nil") end
+ end
+
+ o.activeCapStages = (stageConfig or {}).capActiveStages or 10
+
+ o.lastStatesByName = {}
+ o.groupsByName = {}
+ o.PrimaryGroups = {}
+ o.BackupGroups = {}
+
+ local CheckReschedulingAsync = function(self, time)
+ self:CheckAndScheduleCAP()
+ end
+
+ o.OnGroupStateUpdated = function (self, capGroup)
+ --[[
+ There is no update needed for INTRANSIT, ONSTATION or REARMING as the PREVIOUS state already was checked and nothing changes in the actual overal state.
+ ]]--
+ if capGroup.state == Spearhead.internal.CapGroup.GroupState.INTRANSIT
+ or capGroup.state == Spearhead.internal.CapGroup.GroupState.ONSTATION
+ or capGroup.state == Spearhead.internal.CapGroup.GroupState.REARMING
+ then
+ return
+ end
+ timer.scheduleFunction(CheckReschedulingAsync, self, timer.getTime() + 1)
+ end
+
+ for key, name in pairs(o.groupNames) do
+ local capGroup = Spearhead.internal.CapGroup:new(name, airbaseId, logger, database, capConfig)
+ if capGroup then
+ o.groupsByName[name] = capGroup
+
+ if capGroup.isBackup ==true then
+ table.insert(o.BackupGroups, capGroup)
+ else
+ table.insert(o.PrimaryGroups, capGroup)
+ end
+
+ capGroup:AddOnStateUpdatedListener(o)
+ end
+ end
+
+ o.SpawnIfApplicable = function(self)
+ self.logger:debug("Check spawns for airbase " .. self.airbaseId )
+ for groupName, capGroup in pairs(self.groupsByName) do
+
+ local activeStage = tostring(self.activeStage)
+ local targetStage = capGroup:GetTargetZone(activeStage)
+
+ if targetStage ~= nil and capGroup.state == Spearhead.internal.CapGroup.GroupState.UNSPAWNED then
+ capGroup:SpawnOnTheRamp()
+ self.spawned = true
+ end
+ end
+ end
+
+ o.CheckAndScheduleCAP = function (self)
+
+ self.logger:debug("Check taskings for airbase " .. self.airbaseId )
+
+ local countPerStage = {}
+ local requiredPerStage = {}
+
+ --Count back up groups that are active or reassign to the new zone if that's needed
+ for _, backupGroup in pairs(self.BackupGroups) do
+ if backupGroup.state == Spearhead.internal.CapGroup.GroupState.INTRANSIT or backupGroup.state == Spearhead.internal.CapGroup.GroupState.ONSTATION then
+ local supposedTargetStage = backupGroup:GetTargetZone(self.activeStage)
+ if supposedTargetStage then
+ if supposedTargetStage ~= backupGroup.assignedStageNumber then
+ backupGroup:SendToStage(supposedTargetStage)
+ end
+
+ if countPerStage[supposedTargetStage] == nil then
+ countPerStage[supposedTargetStage] = 0
+ end
+ countPerStage[supposedTargetStage] = countPerStage[supposedTargetStage] + 1
+ else
+ backupGroup:SendRTB()
+ end
+ elseif backupGroup.state == Spearhead.internal.CapGroup.GroupState.RTBINTEN and backupGroup:GetTargetZone(self.activeStage) ~= backupGroup.assignedStageNumber then
+ backupGroup:SendRTB()
+ end
+ end
+
+ --Schedule or reassign primary units if applicable
+ for _, primaryGroup in pairs(self.PrimaryGroups) do
+ local supposedTargetStage = primaryGroup:GetTargetZone(self.activeStage)
+ if supposedTargetStage then
+ if requiredPerStage[supposedTargetStage] == nil then
+ requiredPerStage[supposedTargetStage] = 0
+ end
+
+ if countPerStage[supposedTargetStage] == nil
+ then
+ countPerStage[supposedTargetStage] = 0
+ end
+
+ requiredPerStage[supposedTargetStage] = requiredPerStage[supposedTargetStage] + 1
+
+ if primaryGroup.state == Spearhead.internal.CapGroup.GroupState.READYONRAMP then
+ if countPerStage[supposedTargetStage] < requiredPerStage[supposedTargetStage] then
+ primaryGroup:SendToStage(supposedTargetStage)
+ countPerStage[supposedTargetStage] = countPerStage[supposedTargetStage] + 1
+ end
+ elseif primaryGroup.state == Spearhead.internal.CapGroup.GroupState.INTRANSIT or primaryGroup.state == Spearhead.internal.CapGroup.GroupState.ONSTATION then
+ if supposedTargetStage ~= primaryGroup.assignedStageNumber then
+ if countPerStage[supposedTargetStage] < requiredPerStage[supposedTargetStage] then
+ primaryGroup:SendToStage(supposedTargetStage)
+ else
+ countPerStage[supposedTargetStage] = countPerStage[supposedTargetStage] + 1
+ primaryGroup:SendRTB()
+ end
+ end
+ countPerStage[supposedTargetStage] = countPerStage[supposedTargetStage] + 1
+ elseif primaryGroup.state == Spearhead.internal.CapGroup.GroupState.RTBINTEN and primaryGroup:GetTargetZone(self.activeStage) ~= primaryGroup.assignedStageNumber then
+ primaryGroup:SendRTB()
+ end
+ else
+ if primaryGroup.state == Spearhead.internal.CapGroup.GroupState.INTRANSIT or primaryGroup.state == Spearhead.internal.CapGroup.GroupState.ONSTATION then
+ primaryGroup:SendRTB()
+ end
+ end
+ end
+
+ for _, backupGroup in pairs(self.BackupGroups) do
+ if backupGroup.state == Spearhead.internal.CapGroup.GroupState.READYONRAMP then
+ local supposedTargetStage = backupGroup:GetTargetZone(self.activeStage)
+ if supposedTargetStage then
+ if countPerStage[supposedTargetStage] == nil then
+ countPerStage[supposedTargetStage] = 0
+ end
+
+ if countPerStage[supposedTargetStage] < requiredPerStage[supposedTargetStage] then
+ backupGroup:SendToStage(supposedTargetStage)
+ countPerStage[supposedTargetStage] = countPerStage[supposedTargetStage] + 1
+ end
+ end
+ end
+ end
+ end
+
+ o.OnStageNumberChanged = function (self, number)
+ self.activeStage = number
+ self:SpawnIfApplicable()
+ timer.scheduleFunction(CheckReschedulingAsync, self, timer.getTime() + 5)
+ end
+
+ ---Check if any CAP is active when a certain stage is active
+ ---@param self table
+ ---@param stageNumber number
+ ---@return boolean
+ o.IsBaseActiveWhenStageIsActive = function (self, stageNumber)
+ for _, group in pairs(self.PrimaryGroups) do
+ local target = group:GetTargetZone(stageNumber)
+ if target ~= nil then
+ return true
+ end
+ end
+ return false
+ end
+
+ Spearhead.Events.AddStageNumberChangedListener(o)
+ return o
+end
+
+if not Spearhead.internal then Spearhead.internal = {} end
+Spearhead.internal.CapAirbase = CapBase
\ No newline at end of file
diff --git a/capClasses/CapGroup.lua b/capClasses/CapGroup.lua
new file mode 100644
index 0000000..751db8a
--- /dev/null
+++ b/capClasses/CapGroup.lua
@@ -0,0 +1,465 @@
+--[[
+
+#### Why?
+For Spearhead there's a lot of stages and states the mission can be in.
+To make sure CAP units will be at the place where the mission maker expects them to be there's a naming convention that should help.
+You as a mission maker will have full controll over where they are supposed to be, the script will take care of getting them there.
+The CAP manager's goal is to provide dedicated aircraft scheduling that doesn't reset every stage reset.
+
+Naming: CAP\_\<"A" | B"\>\_\
+#### Config:
+```
+1 at x: []
+n and n at x: [,]
+n till n at x: [-]
+n till n and n at x: [-,]
+n till n at Active: [-]A
+
+divider: |
+
+examples:
+
+CAP_A[1-4,6]7|[5,7]8_SomeName => Will fly CAP at stage 7 when stages 1 through 4 and 6 are active and will fly CAP at 8 when 5 and 7 are active
+CAP_A[2-5]5|[6]6_SomeName => Will fly CAP at stage 5 when stages 2 through 5 active and will fly CAP at 6 when 6 is active
+CAP_A[1-5]A|[6]7_SomeName => Will fly CAP at the ACTIVE stage if Stages 1-5 are active. Basically following the active stages. Then when 6 is active it will fly in 7
+
+CAP_B[1-5]A|[6]7_SomeName => Will fly BACKUP CAP for the active zones 1 through 5 and back up for 7 when 6 is active.
+
+```
+
+### How many? And how to add backups?
+
+To fascilitate a nice flow of the mission and also make sure it doesn't oversaturate the zones with aircraft the script works with a Active/Backup system in the naming.
+This really doesn't mean much per se once the mission runs, but most importantly is that the A units define how many groups there should be max in a zone at a time.
+The B units will simply be used to fill that amount if the A units can't due to RTB, Death, Rearming etc.
+
+#### Example
+
+Take the units:
+```
+CAP_A[1-5]A_SomeName1
+CAP_A[1-5]A_SomeName2
+CAP_B[1-3,5]A_SomeName
+```
+`CAP_A...` units are primary units where the `CAP_B...` units are the backups.
+In this case the CAP manager sees that for stages 1 through 5 this configuration requires 2 groups in the active zone.
+If one of those 2 groups dies or is going back to base the B group will be used to top up the CAP units at that zone.
+After scheduling the B units the A units that are back at base ready on the ramp will also not be scheduled until the CAP units that are active in the zone (inlcuding B units) drop below the required CAP unit (of 2 in this example)
+
+In this example there is no Backup unit for zone 4. This might quiet down the CAP a little as the Active groups will have to rearm and refuel without there being any backup.
+
+### What the cap manager does:
+- Spawn aircraft on the ramp (or despawn when they are not needed anymore for culling)
+- Send out aircraft based on where they are supposed to be
+- Send Aircraft RTB after X time.
+ RTB in this sense means back to its base of origin. Not the closest friendly base like DCS does.
+- Simulates Rearming and then sending them out when needed.
+- Delays aircraft for X amount of time before spawning and rearming after a groups demise.
+- Aircraft are spawned on the ramp so OCA does have effect. (Be sure to also take a look at the Airbase and SAM spawning for defences)
+
+### Future Ideas
+
+- Aircraft rearm hubs with finite spawns on other airbases that get replenished by aircraft flying in.
+
+
+]] --
+
+local CapHelper = {}
+do
+ ---comment
+ ---@param groupName string
+ ---@return table?
+ CapHelper.ParseGroupName = function(groupName)
+ local split_string = Spearhead.Util.split_string(groupName, "_")
+ local partCount = Spearhead.Util.tableLength(split_string)
+ if partCount >= 3 then
+ local result = {}
+ result.zonesConfig = {}
+
+ -- CAP_[1-5]5|[6]6|[7]7_Sukhoi
+ -- CAP_[1-5,7]A|[6]7_Sukhoi
+
+ local configPart = split_string[2]
+ local first = configPart:sub(1, 1)
+ if first == "A" then
+ result.isBackup = false
+ configPart = string.sub(configPart, 2, #configPart)
+ elseif first == "B" then
+ configPart = string.sub(configPart, 2, #configPart)
+ result.isBackup = true
+ elseif first == "[" then
+ result.isBackup = false
+ else
+ table.insert(Spearhead.MissionEditingWarnings, "Could not parse the CAP config for group: " .. groupName)
+ return nil
+ end
+
+ local subsplit = Spearhead.Util.split_string(configPart, "|")
+ if subsplit then
+ for key, value in pairs(subsplit) do
+ local keySplit = Spearhead.Util.split_string(value, "]")
+ local targetZone = keySplit[2]
+ local allActives = string.sub(keySplit[1], 2, #keySplit[1])
+ local commaSeperated = Spearhead.Util.split_string(allActives, ",")
+ for _, value in pairs(commaSeperated) do
+ local dashSeperated = Spearhead.Util.split_string(value, "-")
+ if Spearhead.Util.tableLength(dashSeperated) > 1 then
+ local from = tonumber(dashSeperated[1])
+ local till = tonumber(dashSeperated[2])
+
+ for i = from, till do
+ if targetZone == "A" then
+ result.zonesConfig[tostring(i)] = tostring(i)
+ else
+ result.zonesConfig[tostring(i)] = tostring(targetZone)
+ end
+ end
+ else
+ if targetZone == "A" then
+ result.zonesConfig[tostring(dashSeperated[1])] = tostring(dashSeperated[1])
+ else
+ result.zonesConfig[tostring(dashSeperated[1])] = tostring(targetZone)
+ end
+ end
+ end
+ end
+ end
+ return result
+ else
+ table.insert(Spearhead.MissionEditingWarnings,
+ "CAP Group with name: " .. groupName .. "should have at least 3 parts, but has " .. partCount)
+ return nil
+ end
+ end
+end
+
+---comment
+---@param input table { groupName, task, logger }
+---@param time number
+---@return nil
+local function setTaskAsync(input, time)
+ local task = input.task
+ local groupName = input.groupName
+ local group = Group.getByName(groupName)
+
+ if task then
+ group:getController():setTask(task)
+ if input.logger ~= nil then
+ input.logger:debug("task set succesfully to group " .. groupName)
+ end
+ end
+ return nil
+end
+
+local CapGroup = {}
+
+CapGroup.GroupState = {
+ UNSPAWNED = 0,
+ READYONRAMP = 1,
+ INTRANSIT = 2,
+ ONSTATION = 3,
+ RTBINTEN = 4,
+ RTB = 5,
+ DEAD = 6,
+ REARMING = 7,
+ DESPAWNED = 8
+}
+
+local function SetReadyOnRampAsync(self, time)
+ self:SetState(CapGroup.GroupState.READYONRAMP)
+end
+
+---comment
+---@param groupName string
+---@param airbaseId number
+---@param logger table logger dependency injection
+---@param database table database dependency injection
+---@param capConfig table config dependency injection
+---@return table? o
+function CapGroup:new(groupName, airbaseId, logger, database, capConfig)
+ local o = {}
+ setmetatable(o, { __index = self })
+
+ local RESPAWN_AFTER_TOUCHDOWN_SECONDS = 180
+
+ -- initials
+ o.groupName = groupName
+ o.airbaseId = airbaseId
+ o.logger = logger
+ o.database = database
+
+ local parsed = CapHelper.ParseGroupName(groupName)
+ if parsed == nil then return nil end
+ o.capZonesConfig = parsed.zonesConfig
+ o.isBackup = parsed.isBackup
+
+ --vars
+ o.assignedStageName = nil
+ o.assignedStageNumber = nil
+
+ o.state = CapGroup.GroupState.UNSPAWNED
+ o.aliveUnits = {}
+ o.landedUnits = {}
+ o.unitCount = 0
+ o.onStationSince = 0
+ o.currentCapTaskingDuration = 0
+
+ --config
+ o.capConfig = {}
+
+ if not capConfig then capConfig = {} end
+ o.capConfig.maxDeviationRange = capConfig.maxDeviationRange
+ o.capConfig.minSpeed = (capConfig.minSpeed or 400) * 0.514444
+ o.capConfig.maxSpeed = (capConfig.maxSpeed or 500) * 0.514444
+ o.capConfig.minAlt = (capConfig.minAlt or 8000) * 0.3048
+ o.capConfig.maxAlt = (capConfig.maxAlt or 27000) * 0.3048
+ o.capConfig.minDurationOnStation = capConfig.minDurationOnStation or 600
+ o.capConfig.maxDurationOnstation = capConfig.maxDurationOnStation or 1800
+ o.capConfig.rearmDelay = capConfig.rearmDelay or 300
+
+ ---comment
+ ---@param self table
+ ---@param currentActive number
+ ---@return table
+ o.GetTargetZone = function (self, currentActive)
+ return self.capZonesConfig[tostring(currentActive)]
+ end
+
+ o.SetState = function(self, state)
+ self.state = state
+ self:PublishUnitUpdatedEvent()
+ end
+
+ o.StartRearm = function(self)
+ self:SpawnOnTheRamp()
+ self:SetState(CapGroup.GroupState.REARMING)
+ timer.scheduleFunction(SetReadyOnRampAsync, self, timer.getTime() + self.capConfig.rearmDelay - RESPAWN_AFTER_TOUCHDOWN_SECONDS)
+ end
+
+ o.SpawnOnTheRamp = function(self)
+ self.aliveUnits = {}
+ self.landedUnits = {}
+ self.onStationSince = 0
+
+ local group = Spearhead.DcsUtil.SpawnGroupTemplate(self.groupName, nil, nil, true)
+ if group then
+ self.unitCount = group:getInitialSize()
+
+ if self.state == CapGroup.GroupState.UNSPAWNED then
+ self:SetState(CapGroup.GroupState.READYONRAMP)
+ end
+
+ for _, unit in pairs(group:getUnits()) do
+ local name = unit:getName()
+ self.aliveUnits[name] = true
+ self.landedUnits[name] = false
+ end
+ end
+ end
+
+ o.Despawn = function(self)
+ Spearhead.DcsUtil.DestroyGroup(self.groupName)
+ end
+
+ o.SendRTB = function(self)
+ local group = Group.getByName(self.groupName)
+ if group and group:isExist() then
+ local speed = math.random(self.capConfig.minSpeed, self.capConfig.maxSpeed)
+ local rtbTask, errormessage = Spearhead.RouteUtil.CreateRTBMission(self.groupName, self.airbaseId, speed)
+ if rtbTask then
+ timer.scheduleFunction(setTaskAsync, { task = rtbTask, groupName = self.groupName, logger = self.logger }, timer.getTime() + 3)
+ else
+ self.logger:error("No RTB task could be created for group: " .. self.groupName .. " due to " .. errormessage)
+ end
+ end
+ end
+
+ ---Starts and send this group to perform CAP at a stage
+ ---@param self any
+ ---@param stageZoneNumber string
+ o.SendToStage = function(self, stageZoneNumber)
+ if self.state == CapGroup.GroupState.DEAD or self.state == CapGroup.GroupState.RTB then
+ return --Can't task a unit that's dead or RTB
+ end
+
+ local stageZoneName = self.database:getStageZoneByStageNumber(stageZoneNumber)
+ self.assignedStageNumber = stageZoneNumber
+ self.assignedStageName = stageZoneName
+ local group = Group.getByName(self.groupName)
+ if group and group:isExist() then
+ local zone = Spearhead.DcsUtil.getZoneByName(stageZoneName)
+ if zone then
+ self.logger:debug("Sending group out " .. self.groupName)
+ local controller = group:getController()
+ local capPoints = database:getCapRouteInZone(stageZoneName, self.airbaseId) or { point1 = { x = zone.x, z = zone.z }, point2 = nil }
+
+ local altitude = math.random(self.capConfig.minAlt, self.capConfig.maxAlt)
+ local speed = math.random(self.capConfig.minSpeed, self.capConfig.maxSpeed)
+ local attackHelos = false
+ local deviationDistance = self.capConfig.maxDeviationRange
+ local capTask
+ if self.state == CapGroup.GroupState.ONRAMP or self.onStationSince == 0 then
+ controller:setCommand({
+ id = 'Start',
+ params = {}
+ })
+ local duration = math.random(self.capConfig.minDurationOnStation, self.capConfig
+ .maxDurationOnstation)
+ self.logger:debug("random schedule min: " ..
+ tostring(self.capConfig.minDurationOnStation or "nil") ..
+ " max: " .. tostring(self.capConfig.maxDurationOnstation or "nil") .. " actual " .. duration)
+ self.currentCapTaskingDuration = duration
+
+
+ capTask = Spearhead.RouteUtil.createCapMission(self.groupName, self.airbaseId, capPoints.point1, capPoints.point2, altitude, speed, duration, attackHelos, deviationDistance)
+ else
+ local duration = self.currentCapTaskingDuration - (timer.getTime() - o.onStationSince)
+ capTask = Spearhead.RouteUtil.createCapMission(self.groupName, self.airbaseId, capPoints.point1, capPoints.point2, altitude, speed, duration, attackHelos, deviationDistance)
+ end
+
+ if capTask then
+ timer.scheduleFunction(setTaskAsync,
+ { task = capTask, groupName = self.groupName, logger = self.logger }, timer.getTime() + 3)
+ end
+ self:SetState(CapGroup.GroupState.INTRANSIT)
+ end
+ end
+ end
+
+ ---Starts and send a unit to another airbase
+ ---@param self table
+ ---@param airdomeId any
+ o.SendToAirbase = function(self, airdomeId)
+ self.airbaseId = airdomeId
+ local speed = math.random(self.capConfig.minSpeed, self.capConfig.maxSpeed)
+ local rtbTask = Spearhead.RouteUtil.CreateRTBMission(self.groupName, airdomeId, speed)
+ local group = Group.getByName(self.groupName)
+ local controller = group:getController()
+ controller:setCommand({
+ id = 'Start',
+ params = {}
+ })
+ timer.scheduleFunction(setTaskAsync, { task = rtbTask, groupName = self.groupName, logger = self.logger },
+ timer.getTime() + 5)
+ end
+
+ o.OnGroupRTB = function(self, groupName)
+ if groupName == self.groupName then
+ self.logger:debug("Setting group " ..
+ groupName ..
+ " to state RTB after a total of " ..
+ timer.getTime() - self.onStationSince .. "s of the " .. self.currentCapTaskingDuration .. "s")
+ self:SetState(CapGroup.GroupState.RTB)
+ end
+ end
+
+ o.OnGroupRTBInTen = function(self, groupName)
+ if groupName == self.groupName then
+ self:SetState(CapGroup.GroupState.RTBINTEN)
+ end
+ end
+
+ o.OnGroupOnStation = function(self, groupName)
+ if groupName == self.groupName then
+ self.onStationSince = timer.getTime()
+ self.logger:debug("Setting group " .. groupName .. " to state Onstation")
+ self:SetState(CapGroup.GroupState.ONSTATION)
+ end
+ end
+
+ ---comment
+ ---@param self table
+ ---@param proActive boolean Will check all units in group for aliveness
+ o.UpdateState = function(self, proActive)
+ local landed = false
+ local landedCount = 0
+ for name, landedBool in pairs(self.landedUnits) do
+ if landedBool == true then
+ landedCount = landedCount + 1
+ landed = true
+ end
+ end
+
+ local deadCount = 0
+ for name, isAlive in pairs(self.aliveUnits) do
+ if isAlive == false then
+ deadCount = deadCount + 1
+ end
+ end
+
+ local function DelayedStartRearm(input, time)
+ local capGroup = input.self
+ capGroup:StartRearm()
+ end
+
+ if landedCount + deadCount == self.unitCount then
+ if landed then
+ timer.scheduleFunction(DelayedStartRearm, { self = self }, timer.getTime() + RESPAWN_AFTER_TOUCHDOWN_SECONDS)
+ else
+ local delay = self.capConfig.deathDelay - self.capConfig.rearmDelay + RESPAWN_AFTER_TOUCHDOWN_SECONDS
+ timer.scheduleFunction(DelayedStartRearm, { self = self }, timer.getTime() + delay)
+ end
+ end
+ end
+
+ o.eventListeners = {}
+ ---comment
+ ---@param self table
+ ---@param listener table object with function OnGroupStateUpdated(capGroupTable)
+ o.AddOnStateUpdatedListener = function(self, listener)
+ if type(listener) ~= "table" then
+ self.logger:error("Listener not of type table for AddOnStateUpdatedListener")
+ return
+ end
+
+ if listener.OnGroupStateUpdated == nil then
+ self.logger:error("Listener does not implement OnGroupStateUpdated")
+ return
+ end
+ table.insert(self.eventListeners, listener)
+ end
+
+ o.PublishUnitUpdatedEvent = function(self)
+ for _, callable in pairs(self.eventListeners) do
+ local _, error = pcall(function()
+ callable:OnGroupStateUpdated(self)
+ end)
+ if error then
+ self.logger:error(error)
+ end
+ end
+ end
+
+ o.OnUnitLanded = function(self, initiatorUnit, airbase)
+ if airbase then
+ local airdomeId = airbase:getID()
+ self.airbaseId = airdomeId
+ end
+ local name = initiatorUnit:getName()
+ self.logger:debug("Received unit land event for unit " .. name .. " of group " .. self.groupName)
+
+ self.landedUnits[name] = true
+ self:UpdateState(false)
+ end
+
+ o.OnUnitLost = function(self, initiatorUnit)
+ self.logger:debug("Received unit lost event for group " .. self.groupName)
+ if initiatorUnit then
+ self.aliveUnits[initiatorUnit:getName()] = false
+ end
+ self:UpdateState(false)
+ end
+
+ Spearhead.Events.addOnGroupRTBListener(o.groupName, o)
+ Spearhead.Events.addOnGroupRTBInTenListener(o.groupName, o)
+ Spearhead.Events.addOnGroupOnStationListener(o.groupName, o)
+ local units = Group.getByName(groupName):getUnits()
+ for key, unit in pairs(units) do
+ Spearhead.Events.addOnUnitLandEventListener(unit:getName(), o)
+ Spearhead.Events.addOnUnitLostEventListener(unit:getName(), o)
+ end
+ return o
+end
+
+if not Spearhead.internal then Spearhead.internal = {} end
+Spearhead.internal.CapGroup = CapGroup
diff --git a/capClasses/GlobalCapManager.lua b/capClasses/GlobalCapManager.lua
new file mode 100644
index 0000000..500671a
--- /dev/null
+++ b/capClasses/GlobalCapManager.lua
@@ -0,0 +1,61 @@
+local GlobalCapManager = {}
+do
+ local airbasesPerStage = {}
+ local allAirbasesByName = {}
+ local activeAirbasesPerActiveStage = {}
+ local unitsPerzonePerStage = {}
+
+ local initiated = false
+
+ function GlobalCapManager.start(database, capConfig, stageConfig)
+ if initiated == true then return end
+
+ local logger = Spearhead.LoggerTemplate:new("AirbaseManager", Spearhead.config.logLevel)
+
+ local zones = database:getStagezoneNames()
+ if zones then
+ for key, stageName in pairs(zones) do
+ if airbasesPerStage[stageName] == nil then
+ airbasesPerStage[stageName] = {}
+ end
+
+ local airbaseIds = database:getAirbaseIdsInStage(stageName)
+ if airbaseIds then
+ for _, id in pairs(airbaseIds) do
+ local airbaseName = Spearhead.DcsUtil.getAirbaseName(id)
+ if airbaseName then
+ local airbaseSpecificLogger = Spearhead.LoggerTemplate:new("CAP_" .. airbaseName, Spearhead.config.logLevel)
+ local airbase = Spearhead.internal.CapAirbase:new(id, database, airbaseSpecificLogger, capConfig, stageConfig)
+ if airbase then
+ table.insert(airbasesPerStage[stageName], airbase)
+ allAirbasesByName[airbaseName] = airbase
+ end
+ end
+ end
+ end
+ end
+ end
+
+ logger:info("Initiated " .. Spearhead.Util.tableLength(allAirbasesByName) .. " airbases for cap")
+ initiated = true
+
+ local InfoFunctions = {}
+
+ ---returns if there is CAP active
+ ---@param zoneName any
+ ---@param activeZoneNumber number
+ ---@return boolean
+ InfoFunctions.IsCapActiveWhenZoneIsActive = function(zoneName, activeZoneNumber)
+ for _, airbase in pairs(airbasesPerStage[zoneName]) do
+ if airbase:IsBaseActiveWhenStageIsActive(activeZoneNumber) == true then
+ return true
+ end
+ end
+ return false
+ end
+
+ Spearhead.capInfo = InfoFunctions
+ end
+end
+
+Spearhead.internal.GlobalCapManager = GlobalCapManager
diff --git a/spearhead.lua b/spearhead.lua
new file mode 100644
index 0000000..16d2ff6
--- /dev/null
+++ b/spearhead.lua
@@ -0,0 +1,162 @@
+--[[
+ The Mission Manager creates a way to create missions in a stage like manner with missions without having to worry about monitoring and triggering of said missions.
+
+ The Mission manager assumes players are BLUE and are fighting RED. (Which countries and spawns, that's up to you)
+
+ Mission Naming TriggerZone:
+ Stage: MISSIONSTAGE__Name
+ Mission: MISSION__Name
+ Random Mission: RANDOMMISSION___
+
+ IMPORTANT NOTES
+ - DO NOT put mission zones inside of mission zones.
+ - DO NOT let stage zones overlap (or without there being anything indide of said overlap, this includes airbases)
+
+
+ Stage Naming: MISSIONSTAGE__Name
+ NOTE: Multiple stages can have an order number.
+ This gives you the opportunity to add multiple zones in a stage.
+ All stages need to be completed for the next stage order to start.
+
+ Player SPAWNS
+ Airbases
+ It is assumed all player spawns are Dynamic slots.
+ These work relatively nicely nowadays and with a dynamic mission it can provide the best experience.
+ FARPS
+ Need to be in TriggerZone with name convention: "FARP_"
+ will be removed at start and activated inside of the active stage only, so be wary of where you place them.
+
+ Mission Types and their logic:
+ Random Missions:
+ To maximise replayability randomisation is directly supported.
+ There is 2 ways. First you can randomise the units inside of the mission zone the second way is to randomise the mission zone altogether.
+
+ 1. Randomising the units in the mission.
+ You can use the "Chance" function for groups to spawn or not spawn groups inside of a mission zone.
+ The framework will only take control over the units after the initial spawn and will therefore not spawn units that did not get spawned on initial creation.
+ NOTE: This however gives the least predictable outcome and can easily lead to empty missions.
+
+ 2. Randomised mission zones
+ The best way to randomise it to create X amount of trigger zones with the same mission and let the framework pick 1 on initialisation.
+ Naming convention: RANDOMMISSION___ (eg. RANDOMMISSION_BAI_BYRON_1 and RANDOMMISSION_BAI_BYRON_2)
+ RANDOMMISSION: Recogniser
+ tasking: the tasking just like any other mission
+ NAME: Codename of the mission. (Use single word only for commands later on)
+ number: can be any number. Only intention it to make it unique for the editor to not freak out.
+
+ The framework will recognise that RANDOMMISSION_BAI_BYRON_1 and RANDOMMISSION_BAI_BYRON_2 compete against each other and will select a random one and add that to the stage.
+ After that a random mission will act just like any other mission.
+
+ TIP: If you want a mission that doesn't always spawn: You can do something like the following example:
+ - RANDOMMISSION_BAI_BYRON_1 => The mission you want to spawn about 1 every 4 times with the units and description
+ - RANDOMMISSION_BAI_BYRON_2 => Empty trigger zone
+ - RANDOMMISSION_BAI_BYRON_3 => Empty trigger zone
+ - RANDOMMISSION_BAI_BYRON_4 => Empty trigger zone
+
+ Special Types:
+ SAM
+ All SAM missions will be spawned during the stage, so there's no random pop-ups
+ SAM missions will be have slightly different ways of briefing and will be shown in the overview of a stage as "known air defenses".
+ SAMS however do not count towards the completion of the zone and will be despawned once all other missions are done.
+ SAM missions of the NEXT 2 stages (by order) will also be spawned.
+ This makes it so you can create defenses of airfields where CAP units are spawned and add long range defenses without having to make the stage huge.
+ Eg. If MISSIONSTAGE_1_Name is active then all stages with numbers 2 and 3 will also have active SAMS.
+
+ SAM vs DEAD
+ Generally best practive: Use SAM missions for long range sams that need to be active for longer.
+ Use DEAD for shorter range popup sams like moving SHORADS.
+
+
+ AIRBASES
+ Airbases have a special logic to them. This is to make sure that it's manageable which bases are used by friendly forces after pushing along.
+ Capturable bases can be selected and units on airbases are managed.
+
+ Logic is based on the starting coalition of the base.
+ RED
+ The base will be used for CAP of the enemy.
+ On Capture the base will turn NEUTRAL.
+ Units inside of the airport circle will despawn on the Stage completion.
+ NEUTRAL
+ Nothing will be done. It will not be used and units around the airport will not be specifically managed.
+ BLUE
+ Airbase will be set to "RED" on intialisation.
+ All blue units will be despawned and red units spawned on activation of the zone.
+ When the zone is captured by blue (by finishing the missions), all red units will be removed and all blue units inside it will spawn.
+
+ Airbase Units
+ All units inside of the circle of the airbase (shown in the me) and not in a mission zone will be regarded as a airbase unit and spawned when the airbase becomes active.
+
+ Missions on airbases.
+ Missions at airbases are perfectly possible. Any unit that is part of that mission will not be regarded as an "Airbase unit"
+
+ AWACS
+ ENEMY
+ An enemy awacs will be spawned in the active stage + 2 unless it's disabled with Config.DisableAwacs.
+ TODO: AWACS logic
+ FRIENDLY
+ Friendly AWACS will be spawned at the ACTIVE stage - 2. Which means that at the start there will be no awacs.
+ There is one awacs spawned per stage with a delay of 15 minutes delay for respawn per default.
+ If there is enough blue fighters a red fighter group will be spawned randomly to try and intercept the AWACS.
+ A message will pop up and players are expected to defend it.
+ This can be disabled wth Config.DisableAwacsInterceptTask
+
+
+ SCRIPTERS
+ This area of the documentation is for mission makers that want to hook into the framework from their own scripts.
+ This script will expose flags of it's state, but there are no public methods to alter the framework (at this time).
+
+ FLAGS:
+ TODO: Expose flags for stage and other metrics
+]] --
+
+--[[
+ TODOLIST:
+ - FARPS and Airbases V
+ - RANDOM missions
+
+ - CAP Manager
+ - Mission Activation
+ - OPTIONAL Drawings
+
+]] --
+
+
+local dbLogger = Spearhead.LoggerTemplate:new("database", Spearhead.config.logLevel)
+local databaseManager = Spearhead.DB:new(dbLogger)
+
+local capConfig = {
+ maxDeviationRange = 32186, --20NM -- sets max deviation before flight starts pulling back,
+ minSpeed = 400,
+ maxSpeed = 500,
+ minAlt = 18000,
+ maxAlt = 28000,
+ minDurationOnStation = 1800,
+ maxDurationOnStation = 2700,
+ rearmDelay = 600,
+ deathDelay = 1800
+}
+
+local stageConfig = {
+ preActivatedStages = 3,
+ capActiveStages = 4
+}
+
+Spearhead.internal.GlobalCapManager.start(databaseManager, capConfig, stageConfig)
+Spearhead.internal.GlobalStageManager.start(databaseManager)
+
+local activateStage = function (number)
+ local succ, err = pcall( function ()
+ Spearhead.Events.PublishStageNumberChanged(number)
+ end)
+ env.error(err)
+end
+
+missionCommands.addCommand("stage1", {}, activateStage, 1)
+missionCommands.addCommand("stage2", {}, activateStage, 2)
+missionCommands.addCommand("stage3", {}, activateStage, 3)
+missionCommands.addCommand("stage4", {}, activateStage, 4)
+
+Spearhead.LoadingDone()
+--Check lines of code in directory per file:
+-- Get-ChildItem . -Include *.lua -Recurse | foreach {""+(Get-Content $_).Count + " => " + $_.name }
+-- find . -name '*.lua' | xargs wc -l
\ No newline at end of file
diff --git a/spearhead_base.lua b/spearhead_base.lua
new file mode 100644
index 0000000..c2872f5
--- /dev/null
+++ b/spearhead_base.lua
@@ -0,0 +1,1560 @@
+--1
+
+--- DEFAULT Values
+if Spearhead == nil then Spearhead = {} end
+
+Spearhead.config = {}
+Spearhead.config.logLevel = 0
+
+--Set SpearheadConfig if nil without values
+if not SpearheadConfig then SpearheadConfig = {} end
+
+if not SpearheadConfig.CAP then SpearheadConfig.CAP = {} end
+Spearhead.config.CAP = {
+
+ --- Enables or disables the cap altogether.
+ enable = SpearheadConfig.CAP.enable or true,
+
+ --- Sets the AI cap units
+ engageHelos = SpearheadConfig.CAP.engageHelos or false,
+
+ --- sets the tech type. { 0 = custom, 1 = Modern , 2 = WW2 }
+ technologyType = SpearheadConfig.CAP.technologyType or 1,
+
+ bingoRatio = SpearheadConfig.CAP.bingoRatio or 0.20,
+
+}
+
+if not SpearheadConfig.Stages then SpearheadConfig.Stages = {} end
+
+Spearhead.config.Stages = {
+ preActivatedStages = SpearheadConfig.Stages.maxPreActivated or 3,
+ capActiveStages = SpearheadConfig.Stages.capActiveStages or 4,
+}
+
+local UTIL = {}
+do -- INIT UTIL
+ ---splits a string in sub parts by seperator
+ ---@param input string
+ ---@param seperator string
+ ---@return table result list of strings
+ function UTIL.split_string(input, seperator)
+ if seperator == nil then
+ seperator = " "
+ end
+
+ local result = {}
+ if input == nil then
+ return result
+ end
+
+ for str in string.gmatch(input, "([^" .. seperator .. "]+)") do
+ table.insert(result, str)
+ end
+ return result
+ end
+
+ ---comment
+ ---@param table any
+ ---@return number
+ function UTIL.tableLength(table)
+ if table == nil then return 0 end
+
+ local count = 0
+ for _ in pairs(table) do count = count + 1 end
+ return count
+ end
+
+ ---Gets a random from the list
+ ---@param list table
+ function UTIL.randomFromList(list)
+ local max = #list
+
+ if max == 0 or max == nil then
+ return nil
+ end
+
+ local random = math.random(0, max)
+ if random == 0 then random = 1 end
+
+ return list[random]
+ end
+
+ local function table_print(tt, indent, done)
+ done = done or {}
+ indent = indent or 0
+ if type(tt) == "table" then
+ local sb = {}
+ for key, value in pairs(tt) do
+ table.insert(sb, string.rep(" ", indent)) -- indent it
+ if type(value) == "table" and not done[value] then
+ done[value] = true
+ table.insert(sb, key .. " = {\n");
+ table.insert(sb, table_print(value, indent + 2, done))
+ table.insert(sb, string.rep(" ", indent)) -- indent it
+ table.insert(sb, "}\n");
+ elseif "number" == type(key) then
+ table.insert(sb, string.format("\"%s\"\n", tostring(value)))
+ else
+ table.insert(sb, string.format(
+ "%s = \"%s\"\n", tostring(key), tostring(value)))
+ end
+ end
+ return table.concat(sb)
+ else
+ return tt .. "\n"
+ end
+ end
+
+ ---comment
+ ---@param str string
+ ---@param findable string
+ ---@return boolean
+ UTIL.startswith = function(str, findable)
+ return str:find('^' .. findable) ~= nil
+ end
+
+ ---comment
+ ---@param str string
+ ---@param findableTable table
+ ---@return boolean
+ UTIL.startswithAny = function(str, findableTable)
+ for key, value in pairs(findableTable) do
+ if type(value) == "string" and UTIL.startswith(str, value) then return true end
+ end
+ return false
+ end
+
+ function UTIL.toString(something)
+ if something == nil then
+ return "nil"
+ elseif "table" == type(something) then
+ return table_print(something)
+ elseif "string" == type(something) then
+ return something
+ else
+ return tostring(something)
+ end
+ end
+end
+Spearhead.Util = UTIL
+
+---DCS UTIL Takes inspiration from MIST but only takes the things it needs, changes for DCS updates and different vision for advanced mission scripting stuff.
+---It also adds functions that make the other TDCS scripts easier without taking too much "control" away like MOOSE can sometimes.
+local DCS_UTIL = {}
+do -- INIT DCS_UTIL
+ do -- local databases
+ --[[
+ groupdata = {
+ category,
+ country_id,
+ group_template
+ }
+ ]] --
+ DCS_UTIL.__miz_groups = {}
+ DCS_UTIL.__groupNames = {}
+ DCS_UTIL.__blueGroupNames = {}
+ DCS_UTIL.__redGroupNames = {}
+ --[[
+ zone = {
+ name,
+
+ zone_type,
+ x,
+ z,
+ radius
+ verts,
+
+ }
+ ]] --
+ DCS_UTIL.__trigger_zones = {}
+ end
+
+ DCS_UTIL.Coalition =
+ {
+ NEUTRAL = 0,
+ RED = 1,
+ BLUE = 2
+ }
+
+ DCS_UTIL.ZoneType = {
+ Cilinder = 0,
+ Polygon = 2
+ }
+
+ DCS_UTIL.GroupCategory = {
+ AIRPLANE = 0,
+ HELICOPTER = 1,
+ GROUND = 2,
+ SHIP = 3,
+ TRAIN = 4,
+ STATIC = 5 --CUSTOM CATEGORY
+ }
+
+ DCS_UTIL.__airbaseNamesById = {}
+
+ DCS_UTIL.__airportsStartingCoalition = {}
+ DCS_UTIL.__warehouseStartingCoalition = {}
+ function DCS_UTIL.__INIT()
+ do -- INITS ALL TABLES WITH DATA THAT's from the MIZ environment
+ do -- init group tables
+ for coalition_name, coalition_data in pairs(env.mission.coalition) do
+ local coalition_nr = DCS_UTIL.stringToCoalition(coalition_name)
+ if coalition_data.country then
+ for country_index, country_data in pairs(coalition_data.country) do
+ for category_name, categorydata in pairs(country_data) do
+ local category_id = DCS_UTIL.stringToGroupCategory(category_name)
+ if category_id ~= nil and type(categorydata) == "table" and categorydata.group ~= nil and type(categorydata.group) == "table" then
+ for group_index, group in pairs(categorydata.group) do
+ local name = group.name
+ if category_id == DCS_UTIL.GroupCategory.STATIC then
+ local unit = group.units[1]
+ name = unit.name
+ local staticObj = {
+ heading = unit.heading,
+ name = unit.name,
+ x = unit.x,
+ y = unit.y,
+ type = unit.type,
+ dead = group.dead
+ }
+
+ if string.lower(unit.category) == "planes" then
+ staticObj.livery_id = unit.livery_id
+ end
+
+ group = staticObj
+ end
+
+ table.insert(DCS_UTIL.__groupNames, name)
+ DCS_UTIL.__miz_groups[name] =
+ {
+ category = category_id,
+ country_id = country_data.id,
+ group_template = group
+ }
+
+ if coalition_nr == 1 then
+ table.insert(DCS_UTIL.__redGroupNames, name)
+ elseif coalition_nr == 2 then
+ table.insert(DCS_UTIL.__blueGroupNames, name)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ do --init trigger zones
+ for i, trigger_zone in pairs(env.mission.triggers.zones) do
+ local verticies = {}
+ if trigger_zone.verticies and type(trigger_zone.verticies) == "table" then
+ for ii, vert in pairs(trigger_zone.verticies) do
+ table.insert(verticies, { x = vert.x, z = vert.y })
+ end
+ end
+
+ --see if this works. Swap 3 and 4 to make sure points are ordered and edges can be created
+ local p4 = verticies[4]
+ local p3 = verticies[3]
+ verticies[3] = p4
+ verticies[4] = p3
+
+ local zone = {
+ name = trigger_zone.name,
+ zone_type = trigger_zone.type,
+ x = trigger_zone.x,
+ z = trigger_zone.y,
+ radius = trigger_zone.radius,
+ verts = verticies
+ }
+
+ DCS_UTIL.__trigger_zones[zone.name] = zone
+ end
+ end
+
+ do -- init airports and warehouses
+ if env.warehouses.airports then
+ for warehouse_id, value in pairs(env.warehouses.airports) do
+ if warehouse_id ~= nil then
+ warehouse_id = tostring(warehouse_id) or "nil"
+ local coalitionNumber = DCS_UTIL.stringToCoalition(value.coalition)
+ DCS_UTIL.__airportsStartingCoalition[warehouse_id] = coalitionNumber
+ end
+ end
+ end
+
+ if env.warehouses.warehouses then
+ DCS_UTIL.__warehouseStartingCoalition[-1] = "placeholder"
+ for warehouse_id, value in pairs(env.warehouses.warehouses) do
+ if warehouse_id ~= nil then
+ warehouse_id = tostring(warehouse_id) or "nil"
+ local coalitionNumber = DCS_UTIL.stringToCoalition(value.coalition)
+ DCS_UTIL.__warehouseStartingCoalition[warehouse_id] = coalitionNumber
+ end
+ end
+ end
+ end
+
+ do -- fill airbaseNames
+ local airbases = world.getAirbases()
+ if airbases then
+ for _, airbase in pairs(airbases) do
+ local name = airbase:getName()
+ local id = tostring(airbase:getID())
+
+ if name and id then
+ DCS_UTIL.__airbaseNamesById[id] = name
+ end
+ end
+ end
+ end
+ end
+ end
+
+ ---maps the coalition name to the DCS coalition integer
+ ---@param input string the name
+ ---@return integer
+ function DCS_UTIL.stringToCoalition(input)
+ --[[
+ coalition.side = {
+ NEUTRAL = 0
+ RED = 1
+ BLUE = 2
+ }
+ ]] --
+ local input = string.lower(input)
+ if input == 'neutrals' or input == "neutral" or input == "0" then
+ return DCS_UTIL.Coalition.NEUTRAL
+ end
+
+ if input == 'red' or input == "1" then
+ return DCS_UTIL.Coalition.RED
+ end
+
+ if input == 'blue' or input == "2" then
+ return DCS_UTIL.Coalition.BLUE
+ end
+
+ return -1
+ end
+
+ ---checks if the groupname is a static group
+ ---@param groupName any
+ function DCS_UTIL.IsGroupStatic(groupName)
+ return DCS_UTIL.__miz_groups[groupName].category == 5;
+ end
+
+ ---comment
+ ---@param groupName string destroy the given group
+ function DCS_UTIL.DestroyGroup(groupName)
+ if DCS_UTIL.IsGroupStatic(groupName) then
+ local object = StaticObject.getByName(groupName)
+ if object ~= nil then
+ object:destroy()
+ end
+ else
+ local group = Group.getByName(groupName)
+ if group and group:isExist() then
+ group:destroy()
+ end
+ end
+ end
+
+ ---comment
+ ---@param polygon table of pairs { x, z }
+ ---@param x number X location
+ ---@param z number Y location
+ ---@return boolean
+ function DCS_UTIL.IsPointInPolygon(polygon, x, z)
+ local function isInComplexPolygon(polygon, x, z)
+ local function getEdges(poly)
+ local moved = {}
+ moved[#poly] = poly[1]
+ for i = 2, #poly do
+ moved[i - 1] = poly[i]
+ end
+
+ local result = {}
+ for i = 1, #poly do
+ local point1 = moved[i]
+ local point2 = poly[i]
+ local edge = { x1 = point1.x, z1 = point1.z, x2 = point2.x, z2 = point2.z }
+ table.insert(result, edge)
+ end
+ return result
+ end
+
+ local edges = getEdges(polygon)
+ local count = 0;
+ for _, edge in pairs(edges) do
+ if (x < edge.x1) ~= (x < edge.x2) and z < edge.z1 + ((x - edge.x1) / (edge.x2 - edge.x1)) * (edge.z2 - edge.z1) then
+ count = count + 1
+ -- if (yp < y1) != (yp < y2) and xp < x1 + ((yp-y1)/(y2-y1))*(x2-x1) then
+ -- count = count + 1
+ end
+ end
+ return count % 2 == 1
+ end
+ return isInComplexPolygon(polygon, x, z)
+ end
+
+ --- takes a list of units and returns all the units that are in any of the zones
+ ---@param unit_names table unit names
+ ---@param zone_names table zone names
+ ---@return table unit list of objects { unit = UNIT, zone_name = zoneName}
+ function DCS_UTIL.getUnitsInZones(unit_names, zone_names)
+ local units = {}
+ local zones = {}
+
+ for k = 1, #unit_names do
+ local unit = Unit.getByName(unit_names[k]) or StaticObject.getByName(unit_names[k])
+ if unit and unit:isExist() == true then
+ units[#units + 1] = unit
+ end
+ end
+
+ for index, zone_name in pairs(zone_names) do
+ local zone = DCS_UTIL.__trigger_zones[zone_name]
+ if zone then
+ zones[#zones + 1] = zone
+ end
+ end
+
+ local in_zone_units = {}
+ for units_ind = 1, #units do
+ local lUnit = units[units_ind]
+ local unit_pos = lUnit:getPosition().p
+ local lCat = Object.getCategory(lUnit)
+ for zone_name, zone in pairs(zones) do
+ if unit_pos and ((lCat == 1 and lUnit:isActive() == true) or lCat ~= 1) then -- it is a unit and is active or it is not a unit
+ if zone.zone_type == DCS_UTIL.ZoneType.Polygon and zone.verts then
+ if DCS_UTIL.IsPointInPolygon(zone.verts, unit_pos.x, unit_pos.z) == true then
+ in_zone_units[#in_zone_units + 1] = { unit = lUnit, zone_name = zone.name }
+ end
+ else
+ if (((unit_pos.x - zone.x) ^ 2 + (unit_pos.z - zone.z) ^ 2) ^ 0.5 <= zone.radius) then
+ in_zone_units[#in_zone_units + 1] = { unit = lUnit, zone_name = zone.name }
+ end
+ end
+ end
+ end
+ end
+ return in_zone_units
+ end
+
+ --- takes a list of groups and returns all the group leaders that are in any of the zones
+ ---@param group_names table unit names
+ ---@param zone_name string zone names
+ ---@return table groupnames list of group names
+ function DCS_UTIL.getGroupsInZone(group_names, zone_name)
+ local zone = DCS_UTIL.__trigger_zones[zone_name]
+ if zone == nil then
+ return {}
+ end
+
+ -- MAP Just for mapping sake
+ local custom_zone = {
+ x = zone.x,
+ z = zone.z,
+ zone_type = zone.zone_type,
+ radius = zone.radius,
+ verts = zone.verts
+ }
+
+ return DCS_UTIL.areGroupsInCustomZone(group_names, custom_zone)
+ end
+
+ --- takes a x, y poistion and checks if it is inside any of the zones
+ ---@param group_names table North South position
+ ---@param zone table { x, z, zonetype, radius, verts }
+ ---@return table groupnames list of groups that are in the zone
+ function DCS_UTIL.areGroupsInCustomZone(group_names, zone)
+ local units = {}
+ if Spearhead.Util.tableLength(group_names) < 1 then return {} end
+
+ for k = 1, #group_names do
+ local entry = nil
+ local group = Group.getByName(group_names[k])
+ if group ~= nil then
+ entry = { unit = group:getUnit(1), groupname = group_names[k] }
+ else
+ entry = { unit = StaticObject.getByName(group_names[k]), groupname = group_names[k] }
+ end
+
+ if entry and entry.unit and entry.unit:isExist() == true then
+ units[#units + 1] = entry
+ end
+ end
+
+ local result_groups = {}
+ for _, entry in pairs(units) do
+ local pos = entry.unit:getPoint()
+ if zone.zone_type == DCS_UTIL.ZoneType.Polygon and zone.verts then
+ if DCS_UTIL.IsPointInPolygon(zone.verts, pos.x, pos.z) == true then
+ table.insert(result_groups, entry.groupname)
+ end
+ else
+ if (((pos.x - zone.x) ^ 2 + (pos.z - zone.z) ^ 2) ^ 0.5 <= zone.radius) then
+ table.insert(result_groups, entry.groupname)
+ end
+ end
+ end
+ return result_groups
+ end
+
+ --- takes a x, y poistion and checks if it is inside any of the zones
+ ---@param x number North South position
+ ---@param z number West East position
+ ---@param zone_names table zone names
+ ---@return table zones list of objects { zone_name = zoneName}
+ function DCS_UTIL.isPositionInZones(x, z, zone_names)
+ local zones = {}
+ for index, zone_name in pairs(zone_names) do
+ local zone = DCS_UTIL.__trigger_zones[zone_name]
+ if zone then
+ zones[#zones + 1] = zone
+ end
+ end
+
+ local result_zones = {}
+ for zone_name, zone in pairs(zones) do
+ if zone.zone_type == DCS_UTIL.ZoneType.Polygon and zone.verts then
+ if DCS_UTIL.IsPointInPolygon(zone.verts, x, z) == true then
+ result_zones[#result_zones + 1] = zone.name
+ end
+ else
+ if (((x - zone.x) ^ 2 + (z - zone.z) ^ 2) ^ 0.5 <= zone.radius) then
+ result_zones[#result_zones + 1] = zone.name
+ end
+ end
+ end
+ return result_zones
+ end
+
+ --- takes a x, y poistion and checks if it is inside any of the zones
+ ---@param x number North South position
+ ---@param z number West East position
+ ---@param zone_name table zone names
+ ---@return boolean result
+ function DCS_UTIL.isPositionInZone(x, z, zone_name)
+ local zone = DCS_UTIL.__trigger_zones[zone_name]
+ if zone.zone_type == DCS_UTIL.ZoneType.Polygon and zone.verts then
+ if DCS_UTIL.IsPointInPolygon(zone.verts, x, z) == true then
+ return true
+ end
+ else
+ if (((x - zone.x) ^ 2 + (z - zone.z) ^ 2) ^ 0.5 <= zone.radius) then
+ return true
+ end
+ end
+ return false
+ end
+
+ --- takes a x, y poistion and checks if it is inside any of the zones
+ ---@param zone_name string
+ ---@param parent_zone_name string
+ ---@return boolean result
+ function DCS_UTIL.isZoneInZone(zone_name, parent_zone_name)
+ local zoneA = DCS_UTIL.__trigger_zones[zone_name]
+ local zoneB = DCS_UTIL.__trigger_zones[parent_zone_name]
+
+ if zoneB.zone_type == DCS_UTIL.ZoneType.Polygon and zoneB.verts then
+ if DCS_UTIL.IsPointInPolygon(zoneB.verts, zoneA.x, zoneA.z) == true then
+ return true
+ end
+ else
+ if (((zoneA.x - zoneB.x) ^ 2 + (zoneA.z - zoneB.z) ^ 2) ^ 0.5 <= zoneB.radius) then
+ return true
+ end
+ end
+ return false
+ end
+
+ --- takes a x, y poistion and checks if it is inside any of the zones
+ ---@param x number North South position
+ ---@param z number West East position
+ ---@param zone table { x, z, zonetype, radius }
+ ---@return boolean result
+ function DCS_UTIL.isPositionInCustomZone(x, z, zone)
+ if zone.zone_type == DCS_UTIL.ZoneType.Polygon and zone.verts then
+ if DCS_UTIL.IsPointInPolygon(zone.verts, x, z) == true then
+ return true
+ end
+ else
+ if (((x - zone.x) ^ 2 + (z - zone.z) ^ 2) ^ 0.5 <= zone.radius) then
+ return true
+ end
+ end
+ return false
+ end
+
+ ---comment
+ ---@param zone_name any
+ ---@return table? zone { name,b zone_type, x, z, radius, verts }
+ function DCS_UTIL.getZoneByName(zone_name)
+ if zone_name == nil then return nil end
+ return DCS_UTIL.__trigger_zones[zone_name]
+ end
+
+ ---maps the category name to the DCS group category
+ ---@param input string the name
+ ---@return integer?
+ function DCS_UTIL.stringToGroupCategory(input)
+ input = string.lower(input)
+ if input == 'airplane' or input == 'plane' then
+ return DCS_UTIL.GroupCategory.AIRPLANE
+ end
+ if input == 'helicopter' then
+ return DCS_UTIL.GroupCategory.HELICOPTER
+ end
+ if input == 'ground' or input == 'vehicle' then
+ return DCS_UTIL.GroupCategory.GROUND
+ end
+ if input == 'ship' then
+ return DCS_UTIL.GroupCategory.SHIP
+ end
+ if input == 'train' then
+ return DCS_UTIL.GroupCategory.TRAIN
+ end
+ if input == "static" then
+ return DCS_UTIL.GroupCategory.STATIC
+ end
+ return nil;
+ end
+
+ --- get the group config as per start of the mission
+ --- group = {
+ --- category,
+ --- country_id,
+ --- group_template
+ --- }
+ ---@param groupname string groupName you're looking for
+ function DCS_UTIL.GetMizGroupOrDefault(groupname, default)
+ local group = DCS_UTIL.__miz_groups[groupname]
+ if group == nil then
+ return default
+ end
+ return group
+ end
+
+ ---comment Get all group names. Can be a LOT
+ ---Includes statics
+ ---@return table groups
+ function DCS_UTIL.getAllGroupNames()
+ return DCS_UTIL.__groupNames
+ end
+
+ ---comment Get all BLUE group names. Can be a LOT
+ ---Includes statics
+ ---@return table groups
+ function DCS_UTIL.getAllBlueGroupNames()
+ return DCS_UTIL.__blueGroupNames
+ end
+
+ ---comment Get all RED group names. Can be a LOT
+ ---Includes statics
+ ---@return table groups
+ function DCS_UTIL.getAllRedGroupNames()
+ return DCS_UTIL.__redGroupNames
+ end
+
+ ---comment Get all units that are players
+ ---@return table units
+ function DCS_UTIL.getAllPlayerUnits()
+ local units = {}
+ for key, value in pairs({ 1, 2, 3 }) do
+ local players = coalition.getPlayers(value)
+ for key, unit in pairs(players) do
+ units[#units + 1] = unit
+ end
+ end
+ return units
+ end
+
+ ---get base name from ID
+ ---@param baseId number
+ ---@return string? name
+ function DCS_UTIL.getAirbaseName(baseId)
+ local stringified = tostring(baseId)
+ return DCS_UTIL.__airbaseNamesById[stringified]
+ end
+
+ ---get base from id
+ ---@param baseId number
+ ---@return table? table
+ function DCS_UTIL.getAirbaseById(baseId)
+ local name = DCS_UTIL.getAirbaseName(baseId)
+ if name == nil then return nil end
+ return Airbase.getByName(name)
+ end
+
+ ---Get the starting coalition of a farp or airbase
+ ---@return number? coalition
+ function DCS_UTIL.getStartingCoalition(baseId)
+ if baseId == nil then
+ return nil
+ end
+
+ --STRING based dictionary otherwise it'll be a string/collapsed array
+ baseId = tostring(baseId) or "nil"
+
+ local result = DCS_UTIL.__airportsStartingCoalition[baseId]
+ if result == nil then
+ result = DCS_UTIL.__warehouseStartingCoalition[baseId]
+ end
+ return result
+ end
+
+ ---Gets all groups that have players
+ ---@return table groups
+ function DCS_UTIL.getAllPlayerGroups()
+ local groupNames = {}
+ local result = {}
+ for key, value in pairs({ 1, 2, 3 }) do
+ local players = coalition.getPlayers(value)
+ for key, unit in pairs(players) do
+ local group = unit:getGroup()
+ if group ~= nil then
+ local name = group:getName()
+ if name ~= nil then
+ if groupNames[name] ~= nil then
+ groupNames[name] = 1
+ table.insert(result, group)
+ end
+ end
+ end
+ end
+ end
+ return result
+ end
+
+ --- spawns the units as specified in the mission file itself
+ --- location and route can be nil and will then use default route
+ ---@param groupName string
+ ---@param location table? vector 3 data. { x , z, alt }
+ ---@param route table? route of the group. If nil wil be the default route.
+ ---@param uncontrolled boolean? Sets the group to be uncontrolled on spawn
+ ---@return table? new_group the Group class that was spawned
+ function DCS_UTIL.SpawnGroupTemplate(groupName, location, route, uncontrolled)
+ if groupName == nil then
+ return
+ end
+
+ local template = DCS_UTIL.GetMizGroupOrDefault(groupName, nil)
+ if template == nil then
+ return nil
+ end
+ if template.category == DCS_UTIL.GroupCategory.STATIC then
+ --TODO: Implement location and route stuff
+ local spawn_template = template.group_template
+ coalition.addStaticObject(template.country_id, spawn_template)
+ else
+ local spawn_template = template.group_template
+ if location ~= nil then
+ local x_offset
+ if location.x ~= nil then x_offset = spawn_template.x - location.x end
+
+ local y_offset
+ if location.z ~= nil then y_offset = spawn_template.y - location.z end
+
+ spawn_template.x = location.x
+ spawn_template.y = location.z
+
+ for i, unit in pairs(spawn_template.units) do
+ unit.x = unit.x - x_offset
+ unit.y = unit.y - y_offset
+ unit.alt = location.alt
+ end
+ end
+
+ if route ~= nil then
+ spawn_template.route = route
+ end
+
+ if uncontrolled ~= nil then
+ spawn_template.uncontrolled = uncontrolled
+ end
+ local new_group = coalition.addGroup(template.country_id, template.category, spawn_template)
+ return new_group
+ end
+ end
+
+ function DCS_UTIL.IsBingoFuel(groupName, offset)
+ if offset == nil then offset = 0 end
+ local bingoSetting = 0.20
+ if Spearhead.config and Spearhead.config.CAP then
+ bingoSetting = Spearhead.config.CAP.bingoRatio or 0.20
+ end
+
+ bingoSetting = bingoSetting + offset
+
+ local group = Group.getByName(groupName)
+ for _, unit in pairs(group:getUnits()) do
+ if unit and unit:isExist() == true and unit:inAir() == true and unit:getFuel() < bingoSetting then
+ if Spearhead.config.logLevel == Spearhead.LoggerTemplate.LogLevelOptions.DEBUG then
+ env.info("[Spearhead][DcsUtil][DEBUG] " .. groupName .. " is bingo fuel: " .. unit:getFuel())
+ end
+ return true
+ end
+ end
+ return false
+ end
+
+ DCS_UTIL.__INIT();
+end
+Spearhead.DcsUtil = DCS_UTIL
+
+local LOGGER = {}
+do
+ LOGGER.LogLevelOptions = {
+ DEBUG = 0,
+ INFO = 1,
+ WARN = 2,
+ ERROR = 3,
+ NONE = 4
+ }
+
+ local PreFix = "Spearhead"
+
+ function LOGGER:new(logger_name, logLevel, liveLoggingLevel)
+ local o = {}
+ setmetatable(o, { __index = self })
+ o.LoggerName = logger_name or "(loggername not set)"
+ o.LogLevel = logLevel or LOGGER.LogLevelOptions.INFO
+ o.LiveLoggingLevel = liveLoggingLevel or LOGGER.LogLevelOptions.NONE
+
+ ---comment
+ ---@param self table self logger
+ ---@param message any the message
+ o.info = function(self, message)
+ if message == nil then
+ return
+ end
+ message = UTIL.toString(message)
+
+ if self.LogLevel <= LOGGER.LogLevelOptions.INFO then
+ env.info("[" .. PreFix .. "]" .. "[" .. self.LoggerName .. "] " .. message)
+ end
+
+ if self.LiveLoggingLevel <= LOGGER.LogLevelOptions.INFO then
+ trigger.action.outText(message, 20)
+ end
+ end
+
+ ---comment
+ ---@param message string
+ o.warn = function(self, message)
+ if message == nil then
+ return
+ end
+ message = UTIL.toString(message)
+
+ if self.LogLevel <= LOGGER.LogLevelOptions.WARN then
+ env.warning("[" .. PreFix .. "]" .. "[" .. self.LoggerName .. "] " .. message)
+ end
+
+ if self.LiveLoggingLevel <= LOGGER.LogLevelOptions.WARN then
+ trigger.action.outText(message, 20)
+ end
+ end
+
+ ---comment
+ ---@param self table -- logger
+ ---@param message any -- the message
+ o.error = function(self, message)
+ if message == nil then
+ return
+ end
+
+ message = UTIL.toString(message)
+
+ if self.LogLevel <= LOGGER.LogLevelOptions.ERROR then
+ env.error("[" .. PreFix .. "]" .. "[" .. self.LoggerName .. "] " .. message)
+ end
+
+ if self.LiveLoggingLevel <= LOGGER.LogLevelOptions.ERROR then
+ trigger.action.outText(message, 20)
+ end
+ end
+
+ ---write debug
+ ---@param self table
+ ---@param message any the message
+ o.debug = function(self, message)
+ if message == nil then
+ return
+ end
+
+ message = UTIL.toString(message)
+ if self.LogLevel <= LOGGER.LogLevelOptions.DEBUG then
+ env.info("[" .. PreFix .. "]" .. "[" .. self.LoggerName .. "][DEBUG] " .. message)
+ end
+
+ if self.LiveLoggingLevel <= LOGGER.LogLevelOptions.DEBUG then
+ trigger.action.outText(message, 20)
+ end
+ end
+
+
+ return o
+ end
+end
+Spearhead.LoggerTemplate = LOGGER
+
+local ROUTE_UTIL = {}
+do --setup route util
+ ---comment
+ ---@param attackHelos boolean
+ ---@return table
+ local function GetCAPTargetTypes(attackHelos)
+ local targetTypes = {
+ [1] = "Fighters",
+ [2] = "Multirole fighters",
+ [3] = "Bombers",
+ }
+
+ if attackHelos then
+ targetTypes[4] = "Helicopters"
+ end
+
+ return targetTypes
+ end
+
+ ---comment
+ ---@param airdromeId number
+ ---@param basePoint table { x, z, y } (y == alt)
+ ---@param speed number the speed
+ ---@return table task
+ local RtbTask = function(airdromeId, basePoint, speed)
+ if basePoint == nil then
+ basePoint = Spearhead.Util.getAirbaseById(airdromeId):getPoint()
+ end
+
+ return {
+ alt = basePoint.y,
+ action = "Landing",
+ alt_type = "BARO",
+ speed = speed,
+ ETA = 0,
+ ETA_locked = false,
+ x = basePoint.x,
+ y = basePoint.z,
+ speed_locked = true,
+ formation_template = "",
+ airdromeId = airdromeId,
+ type = "Land",
+ task = {
+ id = "ComboTask",
+ params = {
+ tasks = {}
+ }
+ }
+ }
+ end
+
+ ---comment
+ ---@param groupName string
+ ---@param position table { x, y}
+ ---@param altitude number
+ ---@param speed number
+ ---@param duration number
+ ---@param engageHelos boolean
+ ---@param pattern string ["Race-Track"|"Circle"]
+ ---@return table
+ local CapTask = function(groupName, position, altitude, speed, duration, engageHelos, deviationdistance, pattern)
+ local durationBefore10 = duration - 600
+ if durationBefore10 < 0 then durationBefore10 = 0 end
+ local durationAfter10 = 600
+ if duration < 600 then
+ durationAfter10 = duration
+ end
+
+ return {
+ alt = altitude,
+ action = "Turning Point",
+ alt_type = "BARO",
+ speed = speed,
+ ETA = 0,
+ ETA_locked = false,
+ x = position.x,
+ y = position.z,
+ speed_locked = true,
+ formation_template = "",
+ task = {
+ id = "ComboTask",
+ params = {
+ tasks = {
+ [1] = {
+ number = 1,
+ auto = false,
+ id = "WrappedAction",
+ enabled = "true",
+ params = {
+ action = {
+ id = "Script",
+ params = {
+ command = "pcall(Spearhead.Events.PublishOnStation, \"" .. groupName .. "\")"
+ }
+ }
+ }
+ },
+ [2] = {
+ id = 'EngageTargets',
+ params = {
+ maxDist = deviationdistance,
+ maxDistEnabled = deviationdistance >= 0, -- required to check maxDist
+ targetTypes = GetCAPTargetTypes(engageHelos),
+ priority = 0
+ }
+ },
+ [3] = {
+ number = 3,
+ auto = false,
+ id = "ControlledTask",
+ enabled = true,
+ params = {
+ task = {
+ id = "Orbit",
+ params = {
+ altitude = altitude,
+ pattern = pattern,
+ speed = speed,
+ }
+ },
+ stopCondition = {
+ duration = durationBefore10,
+ condition = "return Spearhead.DcsUtil.IsBingoFuel(\"" .. groupName .. "\", 0.10)",
+ }
+ }
+ },
+ [4] = {
+ number = 4,
+ auto = false,
+ id = "WrappedAction",
+ enabled = "true",
+ params = {
+ action = {
+ id = "Script",
+ params = {
+ command = "pcall(Spearhead.Events.PublishRTBInTen, \"" .. groupName .. "\")"
+ }
+ }
+ }
+ },
+ [5] = {
+ number = 5,
+ auto = false,
+ id = "ControlledTask",
+ enabled = true,
+ params = {
+ task = {
+ id = "Orbit",
+ params = {
+ altitude = altitude,
+ pattern = pattern,
+ speed = speed,
+ }
+ },
+ stopCondition = {
+ duration = durationAfter10,
+ condition = "return Spearhead.DcsUtil.IsBingoFuel(\"" .. groupName .. "\")",
+ }
+ }
+ },
+ [6] = {
+ number = 6,
+ auto = false,
+ id = "WrappedAction",
+ enabled = "true",
+ params = {
+ action = {
+ id = "Script",
+ params = {
+ command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ end
+
+ ---comment
+ ---@param position table { x, y}
+ ---@param altitude number
+ ---@param speed number
+ ---@param childTasks table
+ ---@return table
+ local FlyToPointTask = function(position, altitude, speed, childTasks)
+ return {
+ alt = altitude,
+ action = "Turning Point",
+ alt_type = "BARO",
+ speed = speed,
+ ETA = 0,
+ ETA_locked = false,
+ x = position.x,
+ y = position.z,
+ speed_locked = true,
+ formation_template = "",
+ task = {
+ id = "ComboTask",
+ params = {
+ tasks = childTasks or {}
+ }
+ }
+ }
+ end
+
+ ---comment
+ ---@param groupName string groupName you're creating this route for
+ ---@param airdromeId number airdromeId
+ ---@param capPoint table { x, z }
+ ---@param altitude number
+ ---@param speed number
+ ---@param durationOnStation number
+ ---@param attackHelos boolean
+ ---@param deviationDistance number
+ ---@return table route
+ ROUTE_UTIL.createCapMission = function(groupName, airdromeId, capPoint, racetrackSecondPoint, altitude, speed,
+ durationOnStation, attackHelos, deviationDistance)
+ local baseName = Spearhead.DcsUtil.getAirbaseName(airdromeId)
+ if baseName == nil then
+ return {}
+ end
+
+ durationOnStation = durationOnStation or 1800
+ altitude = altitude or 3000
+ speed = speed or 130
+ attackHelos = attackHelos or false
+ deviationDistance = deviationDistance or 32186
+
+ local base = Airbase.getByName(baseName)
+ if base == nil then
+ return {}
+ end
+
+ local additionalFlyOverTasks = {
+ {
+ enabled = true,
+ auto = false,
+ id = "WrappedAction",
+ number = 1,
+ params = {
+ action = {
+ id = "Option",
+ params = {
+ variantIndex = 2,
+ name = AI.Option.Air.id.FORMATION,
+ formationIndex = 2,
+ value = 131074
+ }
+ }
+ }
+ }
+ }
+
+ local orbitType = "Circle"
+ if racetrackSecondPoint then orbitType = "Race-Track" end
+
+ local basePoint = base:getPoint()
+ local points = {}
+ if racetrackSecondPoint == nil then
+ points = {
+ [1] = FlyToPointTask(capPoint, altitude, speed, additionalFlyOverTasks),
+ [2] = CapTask(groupName, capPoint, altitude, speed, durationOnStation, attackHelos, deviationDistance,
+ orbitType),
+ [3] = RtbTask(airdromeId, basePoint, speed)
+ }
+ else
+ points = {
+ [1] = FlyToPointTask(capPoint, altitude, speed, additionalFlyOverTasks),
+ [2] = CapTask(groupName, capPoint, altitude, speed, durationOnStation, attackHelos, deviationDistance,
+ orbitType),
+ [3] = FlyToPointTask(racetrackSecondPoint, altitude, speed, {}),
+ [4] = RtbTask(airdromeId, basePoint, speed)
+ }
+ end
+
+ return {
+ id = 'Mission',
+ params = {
+ airborne = true,
+ route = {
+ points = points
+ }
+ }
+ }
+ end
+
+ ---Creates an RTB task. The first point is to trigger the TDCS OnRTB Event, the second task will be the actual RTB point
+ ---If any of the values are not met it will return nil
+ ---@param groupName string
+ ---@param airdromeId number
+ ---@param speed number
+ ---@return table?, string ComboTask
+ ROUTE_UTIL.CreateRTBMission = function(groupName, airdromeId, speed)
+ --[[
+ TODO: Test the creation and pubishing of event and the timing of said event
+ ]] --
+
+ local base = Spearhead.DcsUtil.getAirbaseById(airdromeId)
+ if base == nil then
+ return nil, "No airbase found for ID " .. tostring(airdromeId)
+ end
+
+ local group = Group.getByName(groupName)
+ local pos;
+ local i = 1
+ local units = group:getUnits()
+ while pos == nil and i <= Spearhead.Util.tableLength(units) do
+ local unit = units[i]
+ if unit and unit:isExist() == true and unit:inAir() == true then
+ pos = unit:getPoint()
+ end
+ i = i + 1
+ end
+
+ speed = speed or 130
+ if pos == nil then
+ return nil, "Could not find any unit in the air to set the RTB task"
+ end
+
+ local additionalFlyOverTasks = {
+ {
+ enabled = true,
+ auto = false,
+ id = "WrappedAction",
+ number = 1,
+ params = {
+ action = {
+ id = "Option",
+ params = {
+ variantIndex = 2,
+ name = AI.Option.Air.id.FORMATION,
+ formationIndex = 2,
+ value = 131074
+ }
+ }
+ }
+ }
+ }
+
+ return {
+ id = "Mission",
+ params = {
+ airborne = true, -- RTB mission generally are given to airborne units
+ route = {
+ points = {
+
+ [1] = {
+ alt = pos.y,
+ action = "Turning Point",
+ alt_type = "BARO",
+ speed = speed,
+ ETA = 0,
+ ETA_locked = false,
+ x = pos.x,
+ y = pos.z,
+ speed_locked = true,
+ formation_template = "",
+ task = {
+ id = "ComboTask",
+ params = {
+ tasks = {
+ [1] = {
+ number = 1,
+ auto = false,
+ id = "WrappedAction",
+ enabled = "true",
+ params = {
+ action = {
+ id = "Script",
+ params = {
+ command = "pcall(Spearhead.Events.PublishRTB, \"" ..
+ groupName .. "\")"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ [2] = FlyToPointTask(base:getPoint(), 600, speed, additionalFlyOverTasks),
+ [3] = RtbTask(airdromeId, base:getPoint(), speed)
+ }
+ }
+ }
+ }, ""
+ end
+end
+
+Spearhead.RouteUtil = ROUTE_UTIL
+
+local SpearheadEvents = {}
+do
+ local SpearheadLogger = Spearhead.LoggerTemplate:new("Spearhead Events", Spearhead.config.logLevel)
+
+ do -- STAGE NUMBER CHANGED
+ local OnStageNumberChangedListeners = {}
+ local OnStageNumberChangedHandlers = {}
+
+ ---Add a stage zone number changed listener
+ ---@param listener table object with function OnStageNumberChanged(self, number)
+ SpearheadEvents.AddStageNumberChangedListener = function(listener)
+ if type(listener) ~= "table" then
+ SpearheadLogger:warn("Event listener not of type table, did you mean to use handler?")
+ return
+ end
+ table.insert(OnStageNumberChangedListeners, listener)
+ end
+
+ ---Add a stage zone number changed listener
+ ---@param handler function function(number)
+ SpearheadEvents.AddStageNumberChangedHandler = function(handler)
+ if type(handler) ~= "function" then
+ SpearheadLogger:warn("Event handler not of type function, did you mean to use listener?")
+ return
+ end
+ table.insert(OnStageNumberChangedHandlers, handler)
+ end
+
+ ---@param newStageNumber number
+ SpearheadEvents.PublishStageNumberChanged = function(newStageNumber)
+ for _, callable in pairs(OnStageNumberChangedListeners) do
+ local succ, err = pcall(function()
+ callable:OnStageNumberChanged(newStageNumber)
+ end)
+ if err then
+ SpearheadLogger:error(err)
+ end
+ end
+
+ for _, callable in pairs(OnStageNumberChangedHandlers) do
+ local succ, err = pcall(callable, newStageNumber)
+ if err then
+ SpearheadLogger:error(err)
+ end
+ end
+ end
+ end
+
+ local onLandEventListeners = {}
+ ---Add an event listener to a specific unit
+ ---@param unitName string to call when the unit lands
+ ---@param landListener table table with function OnUnitLanded(self, initiatorUnit, airbase)
+ SpearheadEvents.addOnUnitLandEventListener = function(unitName, landListener)
+ if type(landListener) ~= "table" then
+ SpearheadLogger:warn("Event handler not of type table/object")
+ return
+ end
+
+ SpearheadLogger:debug("Added Land event handler for unit: " .. unitName)
+
+ if onLandEventListeners[unitName] == nil then
+ onLandEventListeners[unitName] = {}
+ end
+ table.insert(onLandEventListeners[unitName], landListener)
+ end
+
+ local OnUnitLostListeners = {}
+ ---This listener gets fired for any event that can indicate a loss of a unit.
+ ---Such as: Eject, Crash, Dead, Unit_Lost,
+ ---@param unitName any
+ ---@param unitLostListener table Object with function: OnUnitLost(initiatorUnit)
+ SpearheadEvents.addOnUnitLostEventListener = function(unitName, unitLostListener)
+ if type(unitLostListener) ~= "table" then
+ SpearheadLogger:warn("Unit lost Event listener not of type table/object")
+ return
+ end
+
+ if OnUnitLostListeners[unitName] == nil then
+ OnUnitLostListeners[unitName] = {}
+ end
+
+ table.insert(OnUnitLostListeners[unitName], unitLostListener)
+ end
+
+ do -- ON RTB
+ local OnGroupRTBListeners = {}
+ ---Adds a function to the events listener that triggers when a group publishes themselves RTB.
+ ---This is only available when a ROUTE is created via the Spearhead.RouteUtil
+ ---@param groupName string the groupname to expect
+ ---@param handlingObject table object with OnGroupRTB(self, groupName)
+ SpearheadEvents.addOnGroupRTBListener = function(groupName, handlingObject)
+ if type(handlingObject) ~= "table" then
+ SpearheadLogger:warn("Event handler not of type table/object")
+ return
+ end
+
+ if OnGroupRTBListeners[groupName] == nil then
+ OnGroupRTBListeners[groupName] = {}
+ end
+
+ table.insert(OnGroupRTBListeners[groupName], handlingObject)
+ end
+
+ ---Publish the Group to RTB
+ ---@param groupName string
+ SpearheadEvents.PublishRTB = function(groupName)
+ SpearheadLogger:debug("Publishing RTB event for group " .. groupName)
+ if groupName ~= nil then
+ if OnGroupRTBListeners[groupName] then
+ for _, callable in pairs(OnGroupRTBListeners[groupName]) do
+ local succ, err = pcall(function()
+ callable:OnGroupRTB(groupName)
+ end)
+ if err then
+ SpearheadLogger:error(err)
+ end
+ end
+ end
+ end
+ end
+
+ local OnGroupRTBInTenListeners = {}
+ ---Adds a function to the events listener that triggers when a group publishes themselves RTB.
+ ---This is only available when a ROUTE is created via the Spearhead.RouteUtil
+ ---@param groupName string the groupname to expect
+ ---@param handlingObject table object with OnGroupRTBInTen(self, groupName)
+ SpearheadEvents.addOnGroupRTBInTenListener = function(groupName, handlingObject)
+ if type(handlingObject) ~= "table" then
+ SpearheadLogger:warn("Event handler not of type table/object")
+ return
+ end
+
+ if OnGroupRTBInTenListeners[groupName] == nil then
+ OnGroupRTBInTenListeners[groupName] = {}
+ end
+
+ table.insert(OnGroupRTBInTenListeners[groupName], handlingObject)
+ end
+
+ ---Publish the Group is RTB
+ ---@param groupName string
+ SpearheadEvents.PublishRTBInTen = function(groupName)
+ SpearheadLogger:debug("Publishing RTB in TEN event for group " .. groupName)
+ if groupName ~= nil then
+ if OnGroupRTBInTenListeners[groupName] then
+ for _, callable in pairs(OnGroupRTBInTenListeners[groupName]) do
+ local succ, err = pcall(function()
+ callable:OnGroupRTBInTen(groupName)
+ end)
+ if err then
+ SpearheadLogger:error(err)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ do -- ON Station
+ local OnGroupOnStationListeners = {}
+ ---Adds a function to the events listener that triggers when a group publishes themselves RTB.
+ ---This is only available when a ROUTE is created via the Spearhead.RouteUtil
+ ---@param groupName string the groupname to expect
+ SpearheadEvents.addOnGroupOnStationListener = function(groupName, handlingObject)
+ if type(handlingObject) ~= "table" then
+ SpearheadLogger:warn("Event handler not of type table/object")
+ return
+ end
+
+ if OnGroupOnStationListeners[groupName] == nil then
+ OnGroupOnStationListeners[groupName] = {}
+ end
+
+ table.insert(OnGroupOnStationListeners[groupName], handlingObject)
+ end
+
+ ---Publish the Group to RTB
+ ---@param groupName string
+ SpearheadEvents.PublishOnStation = function(groupName)
+ SpearheadLogger:debug("Publishing onStation event for group " .. groupName)
+ if groupName ~= nil then
+ if OnGroupOnStationListeners[groupName] then
+ for _, callable in pairs(OnGroupOnStationListeners[groupName]) do
+ local succ, err = pcall(function()
+ callable:OnGroupOnStation(groupName)
+ end)
+ if err then
+ SpearheadLogger:error(err)
+ end
+ end
+ end
+ end
+ end
+ end
+
+
+ local e = {}
+ function e:onEvent(event)
+ if event.id == world.event.S_EVENT_LAND or event.id == world.event.S_EVENT_RUNWAY_TOUCH then
+ local unit = event.initiator
+ local airbase = event.place
+ if unit ~= nil then
+ local name = unit:getName()
+ if onLandEventListeners[name] then
+ for _, callable in pairs(onLandEventListeners[name]) do
+ local succ, err = pcall(function()
+ callable:OnUnitLanded(unit, airbase)
+ end)
+ if err then
+ SpearheadLogger:error(err)
+ end
+ end
+ end
+ end
+ end
+
+ if event.id == world.event.S_EVENT_DEAD or
+ event.id == world.event.S_EVENT_CRASH or
+ event.id == world.event.S_EVENT_EJECTION or
+ event.id == world.event.S_EVENT_UNIT_LOST then
+ local object = event.initiator
+ if object and OnUnitLostListeners[object:getName()] then
+ for _, callable in pairs(OnUnitLostListeners[object:getName()]) do
+ local succ, err = pcall(function()
+ callable:OnUnitLost(object)
+ end)
+
+ if err then
+ SpearheadLogger:error(err)
+ end
+ end
+ end
+ end
+ end
+
+ world.addEventHandler(e)
+end
+Spearhead.Events = SpearheadEvents
+
+Spearhead.MissionEditingWarnings = {}
+function Spearhead.AddMissionEditorWarning(warningMessage)
+ table.insert(Spearhead.MissionEditingWarnings, warningMessage or "skip")
+end
+
+local loadDone = false
+Spearhead.LoadingDone = function()
+ if loadDone == true then
+ return
+ end
+
+ local warningLogger = Spearhead.LoggerTemplate:new("MISSIONPARSER", Spearhead.LoggerTemplate.LogLevelOptions.INFO, 4)
+ if Spearhead.Util.tableLength(Spearhead.MissionEditingWarnings) > 0 then
+ for key, message in pairs(Spearhead.MissionEditingWarnings) do
+ warningLogger:warn(message)
+ end
+ else
+ warningLogger:info("No issues detected")
+ end
+
+ loadDone = true
+end
diff --git a/spearhead_cap.lua b/spearhead_cap.lua
new file mode 100644
index 0000000..eac0cf9
--- /dev/null
+++ b/spearhead_cap.lua
@@ -0,0 +1,31 @@
+--[[
+
+ CAP Standby Timing
+ This works with 3 timing
+
+
+
+ MONITORING AND PREDICTIONS
+ TODO: Make sure that before the mission is started the mission maker has a way to see how many groups(units) are scheduled for each zone.
+
+
+ CAP Naming
+ CAP_1_1_ means the required cap state is 1 and it's the first unit. This means these units will be the first units to be dispatched.
+
+ CAP_1-2_1_ means the unit cap state is 1 AND 2 and it will spawn a unique
+
+
+]]--
+
+--[[
+ TODO: Spawn groups that are in state 1 or 2 at position and then give them waypoints when tasked.
+]]--
+
+local StageCapManager = {}
+do
+
+
+
+end
+
+Spearhead.StageCapManager = StageCapManager
\ No newline at end of file
diff --git a/spearhead_db.lua b/spearhead_db.lua
new file mode 100644
index 0000000..91923d3
--- /dev/null
+++ b/spearhead_db.lua
@@ -0,0 +1,589 @@
+-- 3
+
+local SpearheadDB = {}
+do -- DB
+
+ local singleton = nil
+
+ ---comment
+ ---@param Logger table
+ ---@return table
+ function SpearheadDB:new(Logger)
+ if singleton ~= nil then
+ Logger:info("Returning an already initiated instance of SpearheadDB")
+ return singleton
+ end
+
+ local o = {}
+ setmetatable(o, { __index = self })
+
+ o.Logger = Logger
+ o.tables = {}
+ do --INIT ALL TABLES
+ Logger:debug("Initiating tables")
+
+ o.tables.all_zones = {}
+ o.tables.stage_zones = {}
+ o.tables.mission_zones = {}
+ o.tables.random_mission_zones = {}
+ o.tables.farp_zones = {}
+ o.tables.cap_route_zones = {}
+
+ o.tables.stage_zonesByNumer = {}
+
+ do -- INIT ZONE TABLES
+ if env.mission.triggers and env.mission.triggers.zones then
+ for zone_ind, zone_data in pairs(env.mission.triggers.zones) do
+ local zone_name = zone_data.name
+ local split_string = Spearhead.Util.split_string(zone_name, "_")
+ table.insert(o.tables.all_zones, zone_name)
+
+ if string.lower(split_string[1]) == "missionstage" then
+ table.insert(o.tables.stage_zones, zone_name)
+ if split_string[2] then
+ local stringified = tostring(split_string[2]) or "unknown"
+ if o.tables.stage_zonesByNumer[stringified] == nil then
+ o.tables.stage_zonesByNumer[stringified] = zone_name
+ else
+ table.insert(Spearhead.MissionEditingWarnings, "Duplicate Stage Order number found. This zone will work, but will not be part of the CAP script")
+ end
+
+ end
+ end
+
+ if string.lower(split_string[1]) == "mission" then
+ table.insert(o.tables.mission_zones, zone_name)
+ end
+
+ if string.lower(split_string[1]) == "randommission" then
+ table.insert(o.tables.random_mission_zones, zone_name)
+ end
+
+ if string.lower(split_string[1]) == "farp" then
+ table.insert(o.tables.farp_zones, zone_name)
+ end
+
+ if string.lower(split_string[1]) == "caproute" then
+ table.insert(o.tables.cap_route_zones, zone_name)
+ end
+ end
+ end
+ end
+
+ Logger:debug("initiated zone tables, continuing with descriptions")
+ o.tables.descriptions = {}
+ do --load markers
+ if env.mission.drawings and env.mission.drawings.layers then
+ for i, layer in pairs(env.mission.drawings.layers) do
+ if string.lower(layer.name) == "author" then
+ for key, layer_object in pairs(layer.objects) do
+ local inZone = Spearhead.DcsUtil.isPositionInZones(layer_object.mapX, layer_object.mapY, o.tables.mission_zones)
+ if Spearhead.Util.tableLength(inZone) >= 1 then
+ local name = inZone[1]
+ if name ~= nil then
+ o.tables.descriptions[name] = layer_object.text
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ o.tables.missionZonesPerStage = {}
+ for key, missionZone in pairs(o.tables.mission_zones) do
+ local found = false
+ local i = 1
+ while found == false and i <= Spearhead.Util.tableLength(o.tables.stage_zones) do
+ local stageZone = o.tables.stage_zones[i]
+ if Spearhead.DcsUtil.isZoneInZone(missionZone, stageZone) == true then
+ if o.tables.missionZonesPerStage[stageZone] == nil then
+ o.tables.missionZonesPerStage[stageZone] = {}
+ end
+ table.insert(o.tables.missionZonesPerStage[stageZone], missionZone)
+ end
+ i = i + 1
+ end
+ end
+
+ o.tables.randomMissionZonesPerStage = {}
+ for key, missionZone in pairs(o.tables.random_mission_zones) do
+ local found = false
+ local i = 1
+ while found == false and i <= Spearhead.Util.tableLength(o.tables.stage_zones) do
+ local stageZone = o.tables.stage_zones[i]
+ if Spearhead.DcsUtil.isZoneInZone(missionZone, stageZone) == true then
+ if o.tables.randomMissionZonesPerStage[stageZone] == nil then
+ o.tables.randomMissionZonesPerStage[stageZone] = {}
+ end
+ table.insert(o.tables.randomMissionZonesPerStage[stageZone], missionZone)
+ end
+ i = i + 1
+ end
+ end
+
+ local isAirbaseInZone = {}
+ o.tables.airbasesPerStage = {}
+ o.tables.farpIdsInFarpZones = {}
+ local airbases = world.getAirbases()
+ for _, airbase in pairs(airbases) do
+ local baseId = airbase:getID()
+ local point = airbase:getPoint()
+ local found = false
+ for _, zoneName in pairs(o.tables.stage_zones) do
+ if found == false then
+ if Spearhead.DcsUtil.isPositionInZone(point.x, point.z, zoneName) == true then
+ found = true
+ local baseIdString = tostring(baseId) or "nil"
+ isAirbaseInZone[baseIdString] = true
+
+ if airbase:getDesc().category == 0 then
+ --Airbase
+ if Spearhead.DcsUtil.getStartingCoalition(baseId) == 2 then
+ airbase:setCoalition(1)
+ airbase:autoCapture(false)
+ end
+
+ if o.tables.airbasesPerStage[zoneName] == nil then
+ o.tables.airbasesPerStage[zoneName] = {}
+ end
+
+ table.insert(o.tables.airbasesPerStage[zoneName], baseId)
+ else
+ -- farp
+ local i = 1
+ local farpFound = false
+ while farpFound == false and i <= Spearhead.Util.tableLength(o.tables.farp_zones) do
+ local farpZoneName = o.tables.farp_zones[i]
+ if Spearhead.DcsUtil.isPositionInZone(point.x, point.z, farpZoneName) == true then
+ farpFound = true
+
+ if o.tables.farpIdsInFarpZones[farpZoneName] == nil then
+ o.tables.farpIdsInFarpZones[farpZoneName] = {}
+ end
+
+ airbase:setCoalition(1)
+ airbase:autoCapture(false)
+ table.insert(o.tables.farpIdsInFarpZones[farpZoneName], baseIdString)
+ end
+ i = i +1
+ end
+ end
+ end
+ end
+ end
+ end
+
+
+ o.tables.farpZonesPerStage = {}
+ for _, farpZoneName in pairs(o.tables.farp_zones) do
+ local findFirst = function (farpZoneName)
+ for _, stage_zone in pairs(o.tables.stage_zones) do
+ if Spearhead.DcsUtil.isZoneInZone(farpZoneName, stage_zone) then
+ return stage_zone
+ end
+ end
+ return nil
+ end
+
+ local found = findFirst(farpZoneName)
+ if found then
+
+ if o.tables.farpZonesPerStage[found] == nil then
+ o.tables.farpZonesPerStage[found] = {}
+ end
+
+ table.insert(o.tables.farpZonesPerStage[found], farpZoneName)
+ end
+ end
+
+
+ local is_group_taken = {}
+ do
+ local all_groups = Spearhead.DcsUtil.getAllGroupNames()
+ for _, value in pairs(all_groups) do
+ is_group_taken[value] = false
+ end
+ end
+
+ local getAvailableGroups = function()
+ local result = {}
+ for name, value in pairs(is_group_taken) do
+ if value == false then
+ table.insert(result, name)
+ end
+ end
+ return result
+ end
+
+ local getAvailableCAPGroups = function()
+ local result = {}
+ for name, value in pairs(is_group_taken) do
+ if value == false and Spearhead.Util.startswith(name, "CAP") then
+ table.insert(result, name)
+ end
+ end
+ return result
+ end
+
+ --- airbaseId <> groupname[]
+ o.tables.capGroupsOnAirbase = {}
+ local loadCapUnits = function()
+ local all_groups = getAvailableCAPGroups()
+ Logger:debug(all_groups)
+ local airbases = world.getAirbases()
+ for _, airbase in pairs(airbases) do
+ local baseId = airbase:getID()
+ local point = airbase:getPoint()
+
+ o.tables.capGroupsOnAirbase[baseId] = {}
+ local groups = Spearhead.DcsUtil.areGroupsInCustomZone(all_groups,
+ { x = point.x, z = point.z, radius = 6600 })
+ for _, groupName in pairs(groups) do
+ is_group_taken[groupName] = true
+ table.insert(o.tables.capGroupsOnAirbase[baseId], groupName)
+ end
+ end
+ end
+
+ --- missionZoneName <> groupname[]
+ o.tables.groupsInMissionZone = {}
+ local loadMissionzoneUnits = function()
+ local all_groups = getAvailableGroups()
+ for _, missionZoneName in pairs(o.tables.mission_zones) do
+ o.tables.groupsInMissionZone[missionZoneName] = {}
+ local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, missionZoneName)
+ for _, groupName in pairs(groups) do
+ is_group_taken[groupName] = true
+ table.insert(o.tables.groupsInMissionZone[missionZoneName], groupName)
+ end
+ end
+ end
+
+ --- missionZoneName <> groupname[]
+ o.tables.groupsInRandomMissions = {}
+ local loadRandomMissionzoneUnits = function()
+ local all_groups = getAvailableGroups()
+ for _, missionZoneName in pairs(o.tables.random_mission_zones) do
+ o.tables.groupsInRandomMissions[missionZoneName] = {}
+ local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, missionZoneName)
+ for _, groupName in pairs(groups) do
+ is_group_taken[groupName] = true
+ table.insert(o.tables.groupsInRandomMissions[missionZoneName], groupName)
+ end
+ end
+ end
+
+ --- farpZoneName <> groupname[]
+ o.tables.groupsInFarpZone = {}
+ local loadFarpGroups = function()
+ local all_groups = getAvailableGroups()
+ for _, farpZone in pairs(o.tables.farp_zones) do
+ o.tables.groupsInFarpZone[farpZone] = {}
+ local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, farpZone)
+ for _, groupName in pairs(groups) do
+ is_group_taken[groupName] = true
+ table.insert(o.tables.groupsInFarpZone[farpZone], groupName)
+ end
+ end
+ end
+
+ --- farpZoneName <> groupname[]
+ o.tables.redAirbaseGroupsPerAirbase = {}
+ o.tables.blueAirbaseGroupsPerAirbase = {}
+ local loadAirbaseGroups = function()
+ local all_groups = getAvailableGroups()
+ local airbases = world.getAirbases()
+ for _, airbase in pairs(airbases) do
+ local baseId = tostring(airbase:getID())
+ local point = airbase:getPoint()
+
+ if isAirbaseInZone[tostring(baseId) or "something" ] == true then
+ o.tables.redAirbaseGroupsPerAirbase[baseId] = {}
+ o.tables.blueAirbaseGroupsPerAirbase[baseId] = {}
+ local groups = Spearhead.DcsUtil.areGroupsInCustomZone(all_groups, { x = point.x, z = point.z, radius = 6600 })
+ for _, groupName in pairs(groups) do
+
+ if Spearhead.DcsUtil.IsGroupStatic(groupName) == true then
+ local object = StaticObject.getByName(groupName)
+ if object then
+ if object:getCoalition() == coalition.side.RED then
+ table.insert(o.tables.redAirbaseGroupsPerAirbase[baseId], groupName)
+ elseif object:getCoalition() == coalition.side.BLUE then
+ table.insert(o.tables.blueAirbaseGroupsPerAirbase[baseId], groupName)
+ end
+ end
+ else
+ local group = Group.getByName(groupName)
+ if group then
+ if group:getCoalition() == coalition.side.RED then
+ table.insert(o.tables.redAirbaseGroupsPerAirbase[baseId], groupName)
+ is_group_taken[groupName] = true
+ elseif group:getCoalition() == coalition.side.BLUE then
+ table.insert(o.tables.blueAirbaseGroupsPerAirbase[baseId], groupName)
+ is_group_taken[groupName] = true
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ o.tables.miscGroupsInStage = {}
+ local loadMiscGroupsInStage = function ()
+ local all_groups = getAvailableGroups()
+ for _, stage_zone in pairs(o.tables.stage_zones) do
+ o.tables.miscGroupsInStage[stage_zone] = {}
+ local groups = Spearhead.DcsUtil.getGroupsInZone(all_groups, stage_zone)
+ for _, groupName in pairs(groups) do
+
+ if Spearhead.DcsUtil.IsGroupStatic(groupName) == true then
+ local object = StaticObject.getByName(groupName)
+ if object and object:getCoalition() ~= coalition.side.NEUTRAL then
+ is_group_taken[groupName] = true
+ table.insert(o.tables.miscGroupsInStage[stage_zone], groupName)
+ end
+ else
+ local group = Group.getByName(groupName)
+ if group and group:getCoalition() ~= coalition.side.NEUTRAL then
+ is_group_taken[groupName] = true
+ table.insert(o.tables.miscGroupsInStage[stage_zone], groupName)
+ end
+ end
+ end
+ end
+ end
+
+ loadCapUnits()
+ loadMissionzoneUnits()
+ loadRandomMissionzoneUnits()
+ loadFarpGroups()
+ loadAirbaseGroups()
+ loadMiscGroupsInStage()
+
+ local cleanup = function () --CLean up all groups that are now managed inside zones by spearhead
+
+ local count = 0
+ for name, taken in pairs(is_group_taken) do
+ if taken == true then
+ Spearhead.DcsUtil.DestroyGroup(name)
+ count = count + 1
+ end
+ end
+ Logger:info("Destroyed " .. count .. " units that are now managed in zones by Spearhead")
+ end
+ cleanup()
+
+ --- key: zoneName value: { current, routes = [ { point1, point2 } ] }
+ o.tables.capRoutes = {}
+ for _, zoneName in pairs(o.tables.stage_zones) do
+
+ local configValue = {
+ current = 0,
+ routes = {}
+ }
+ for _, cap_route_zone in pairs(o.tables.cap_route_zones) do
+ if Spearhead.DcsUtil.isZoneInZone(cap_route_zone, zoneName) == true then
+ local zone = Spearhead.DcsUtil.getZoneByName(cap_route_zone)
+ if zone then
+ if zone.zone_type == Spearhead.DcsUtil.ZoneType.Cilinder then
+ table.insert(configValue.routes, { point1 = { x = zone.x , z = zone.z }, point2 = nil } )
+ else
+ local function getDist(a, b)
+ return math.sqrt((b.x - a.x) ^ 2 + (b.z - a.z) ^ 2)
+ end
+
+ local biggest = nil
+ local biggestA = nil
+ local biggestB = nil
+
+ for i=1, 3 do
+ for ii = i + 1, 4 do
+
+ local a = zone.verts[i]
+ local b = zone.verts[ii]
+ local dist = getDist(a,b)
+
+ if biggest == nil or dist > biggest then
+ biggestA = a
+ biggestB = b
+ biggest = dist
+ end
+ end
+ end
+
+ if biggestA and biggestB then
+ table.insert(configValue.routes,
+ {
+ point1 = { x = biggestA.x , z = biggestA.z },
+ point2 = { x = biggestB.x , z = biggestB.z }
+ })
+ end
+ end
+ end
+ end
+ end
+ o.tables.capRoutes[zoneName] = configValue
+ end
+
+ o.tables.missionCodes = {}
+ end
+
+ o.GetDescriptionForMission = function(self, missionZoneName)
+ return self.tables.descriptions[missionZoneName]
+ end
+
+ o.getCapRouteInZone = function(self, targetZone, baseId)
+
+ local routeData = self.tables.capRoutes[targetZone]
+ if routeData then
+ local count = Spearhead.Util.tableLength(routeData.routes)
+ if count > 0 then
+ routeData.current = routeData.current + 1
+ if count < routeData.current then
+ routeData.current = 1
+ end
+
+ return routeData.routes[routeData.current]
+ end
+ end
+ do
+ local function GetClosestPointOnCircle(pC, radius, p)
+ local vX = p.x - pC.x;
+ local vY = p.z - pC.z;
+ local magV = math.sqrt(vX*vX + vY*vY);
+ local aX = pC.x + vX / magV * radius;
+ local aY = pC.z + vY / magV * radius;
+ return { x = aX, z = aY }
+ end
+ local stagezone = Spearhead.DcsUtil.getZoneByName(targetZone)
+ if stagezone then
+ local base = Spearhead.DcsUtil.getAirbaseById(baseId)
+ if base then
+ local closest = nil
+ if stagezone.zone_type == Spearhead.DcsUtil.ZoneType.Cilinder then
+ closest = GetClosestPointOnCircle({x = stagezone.x, z = stagezone.z}, stagezone.radius, base:getPoint())
+ else
+ local function getDist(a, b)
+ return math.sqrt((b.x - a.x) ^ 2 + (b.z - a.z) ^ 2)
+ end
+
+ local closestDistance = -1
+ for _, vert in pairs(stagezone.verts) do
+ local distance = getDist(vert, base:getPoint())
+ if closestDistance == -1 or distance < closestDistance then
+ closestDistance = distance
+ closest = vert
+ end
+ end
+ end
+
+ if math.random(1,2)%2 == 0 then
+ return { point1 = closest, point2 = {x = stagezone.x, z = stagezone.z} }
+ else
+ return { point1 = {x = stagezone.x, z = stagezone.z}, point2 = closest }
+ end
+ end
+ end
+ end
+
+ end
+ ---comment
+ ---@param self table
+ ---@param number number
+ ---@return string zoneName
+ o.getStageZoneByStageNumber = function (self, number)
+ local numberString = tostring(number)
+ return self.tables.stage_zonesByNumer[numberString]
+ end
+
+ ---comment
+ ---@param self table
+ ---@return table result a list of stage zone names
+ o.getStagezoneNames = function (self)
+ return self.tables.stage_zones
+ end
+
+ o.getMissionsForStage = function (self, stagename)
+ return self.tables.missionZonesPerStage[stagename] or {}
+ end
+
+ o.getRandomMissionsForStage = function(self, stagename)
+ return self.tables.randomMissionZonesPerStage[stagename] or {}
+ end
+
+ o.getGroupsForMissionZone = function (self, missionZoneName)
+ if Spearhead.Util.startswith(missionZoneName, "RANDOMMISSION") == true then
+ return self.tables.groupsInRandomMissions[missionZoneName] or {}
+ end
+ return self.tables.groupsInMissionZone[missionZoneName] or {}
+ end
+
+ o.getMissionBriefingForMissionZone = function (self, missionZoneName)
+ return self.tables.descriptions[missionZoneName] or ""
+ end
+
+ ---comment
+ ---@param self table
+ ---@param stageName string
+ ---@return table result airbase IDs. Use Spearhead.DcsUtil.getAirbaseById
+ o.getAirbaseIdsInStage = function (self, stageName)
+ return self.tables.airbasesPerStage[stageName] or {}
+ end
+
+ o.getFarpZonesInStage = function (self, stageName)
+ return self.tables.farpZonesPerStage[stageName]
+ end
+
+ ---comment
+ ---@param self table
+ ---@param airbaseId number
+ ---@return table
+ o.getCapGroupsAtAirbase = function(self, airbaseId)
+ return self.tables.capGroupsOnAirbase[airbaseId] or {}
+ end
+
+ o.getRedGroupsAtAirbase = function (self, airbaseId)
+ local baseId = tostring(airbaseId)
+ return self.tables.redAirbaseGroupsPerAirbase[baseId]
+ end
+
+ o.getBlueGroupsAtAirbase = function (self, airbaseId)
+ local baseId = tostring(airbaseId)
+ return self.tables.blueAirbaseGroupsPerAirbase[baseId]
+ end
+
+ o.GetNewMissionCode = function (self)
+ local code = nil
+ local tries = 0
+ while code == nil and tries < 10 do
+ local random = math.random(1000, 9999)
+ if self.tables.missionCodes[random] == nil then
+ code = random
+ end
+ tries = tries + 1
+ end
+ return code
+
+ --[[
+ TODO: What to do when there's no random possible
+ ]]
+ end
+
+ do -- LOG STATE
+ Logger:info("initiated the database with amount of zones: ")
+ Logger:info("Stages: " .. Spearhead.Util.tableLength(o.tables.stage_zones))
+ Logger:info("Missions: " .. Spearhead.Util.tableLength(o.tables.mission_zones))
+ Logger:info("Random Missions: " .. Spearhead.Util.tableLength(o.tables.random_mission_zones))
+ Logger:info("Farps: " .. Spearhead.Util.tableLength(o.tables.farp_zones))
+ Logger:info("Airbases: " .. Spearhead.Util.tableLength(o.tables.airbasesPerStage))
+ Logger:info("RedAirbase Groups: " .. Spearhead.Util.tableLength(o.tables.redAirbaseGroupsPerAirbase["21"]))
+ Spearhead.Util.tableLength(o.tables.airbasesPerStage)
+ end
+ singleton = o
+ return o
+ end
+end
+
+Spearhead.DB = SpearheadDB
diff --git a/stageClasses/GlobalStageManager.lua b/stageClasses/GlobalStageManager.lua
new file mode 100644
index 0000000..c513da2
--- /dev/null
+++ b/stageClasses/GlobalStageManager.lua
@@ -0,0 +1,22 @@
+
+
+local StagesByName = {}
+
+
+GlobalStageManager = {}
+GlobalStageManager.start = function (database)
+
+ for _, stageName in pairs(database:getStagezoneNames()) do
+
+ local logger = Spearhead.LoggerTemplate:new(stageName, Spearhead.config.logLevel)
+ local stage = Spearhead.internal.Stage:new(stageName, database, logger)
+
+ StagesByName[stageName] = stage
+
+ local logger = Spearhead.LoggerTemplate:new("StageManager", Spearhead.config.logLevel)
+ logger:info("Initiated " .. Spearhead.Util.tableLength(StagesByName) .. " airbases for cap")
+
+ end
+end
+
+Spearhead.internal.GlobalStageManager = GlobalStageManager
diff --git a/stageClasses/Mission.lua b/stageClasses/Mission.lua
new file mode 100644
index 0000000..db0217d
--- /dev/null
+++ b/stageClasses/Mission.lua
@@ -0,0 +1,396 @@
+--[[
+# 3. Missions
+
+A mission is a completable objective with a state and a continuous check to see if itself is completed.
+The delay between checks is quite big, but it also is checked on unit deaths and other events.
+
+### Placement
+The placement of MISSION trigger zones can be anywhere.
+The order of unit detection is `CAP` > `MISSION` > `AIRBASE` > `STAGE`
+This means that if a unit has a name that starts with `"CAP_"` it will not be included in a mission.
+But all other units in a `MISSION` trigger zone will be managed as part of that mission.
+
+Units inside a `MISSION` do not have to stay within the triggerzone.
+They just need to be inside the zone at the start of the mission.
+You can for example let a `BAI` mission drive back and forth between airbases. The `MISSIONZONE` only needs to be around the units that are part of the objective, not the waypoints.
+
+### Naming
+`MISSION__`
+`RANDOMMISSION___` (Read RANDOMISATION below)
+
+With:
+`name` = A name that is easy to remember and type. Like a codename. Exmaples: BYRON, PLUKE, etc.
+`type` = any of the below described types. Special types are marked with an *
+
+TIP: You can click on the type to get more details
+
+
+SAM*
+ SAM Sites are managed a little different. SAM Sites can be used to guide players and to protect airfields.
+ In the future when deepstrike missions might come into scope these SAM sites will also be more important.
+ SAM sites will be activated when a zone is "Pre-Active".
+ A stage is "Pre-Active" when there is a CAP base active, or there is other things to do that would need the SAM site to be live (OCA, DEEPSTRIKE, EXTRACTION \<= all feature development)
+ If you want a SAM site to become active ONLY when the stage is fully active, then `DEAD` is the type for you!
+
+ Completion logic
+ TODO: documentation: Completion logic
+
+
+
+
+DEAD
+
+ DEAD missions will be spawned on activation of the stage.
+ ALL DEAD missions will be activated right at the start of a stage.
+ This might be against the "randomisation" feel, but it is to make sure mission don't get activated randomly and players get ambushed by a random spawn.
+
+ Completion logic
+ TODO: documentation: Completion logic
+
+
+
+
+
+BAI
+
+
+
+STRIKE
+ STRIKE missions will be activated randomly until all of them are completed.
+ A strike mission can be placed anywhere, even on airbases
+
+ Completion logic
+ TODO: documentation: Completion logic
+
+
+
+### Randomisation
+
+You can randomise missions.
+Spearhead will pick up all mission zones that start with `"RANDOMMISSION_"`
+Then it will combine each `RANDOMMISSION` in a zone with the same `` and pick a random one.
+It will always pick 1 and only 1.
+
+This means you have some options for randomisation.
+For example, if you have 1 missionzone with name `RANDOMMISSION_SAM_PLUKE_1` that is filled with an SA-2 and another zone with `RANDOMMISSION_SAM_PLUKE_2` filled with an SA-3 then some runs of the mission it will spawn an SA-3 and sometimes spawn an SA-3. (works for any mission type)
+
+What you can also do is add empty `RANDOMMISSION_` zones next to the filled `RANDOMMISSION_` zone.
+For example. You have a `RANDOMMISSION_DEAD_BYRON_1` filled with an SA-19 driving around and 2 more `RANDOMMISSION_DEAD_BYRON_<2 & 3>` zones then it will have a 33% chance of being spawned.
+If a zone is empty it will not be briefed, activated or count towards completion of the `STAGE`
+
+]]
+
+--- A mission Object.
+local Mission = {}
+do -- INIT Mission Class
+
+ local MINIMAL_UNITS_ALIVE_RATIO = 0.20
+
+ local Defaults = {}
+ Defaults.MainMenu = "Missions"
+ Defaults.SelectMenuSubMenus = { Defaults.MainMenu, "Select Mission" }
+ Defaults.ShowMissionSubs = { Defaults.MainMenu }
+
+ local PlayersInMission = {}
+ local MissionType = {
+ UNKNOWN = 0,
+ STRIKE = 1,
+ BAI = 2,
+ DEAD = 3,
+ SAM = 4,
+ }
+
+ do --INIT MISSION TYPE FUNCTIONS
+ ---Parse string to mission type
+ ---@param input string
+ MissionType.Parse = function(input)
+ if input == nil then
+ return Mission.MissionType.UNKNOWN
+ end
+
+ input = string.lower(input)
+ if input == "dead" then return MissionType.DEAD end
+ if input == "strike" then return MissionType.STRIKE end
+ if input == "bai" then return MissionType.BAI end
+ if input == "sam" then return MissionType.SAM end
+ return Mission.MissionType.UNKNOWN
+ end
+
+ ---comment
+ ---@param input number missionType
+ ---@return string text
+ MissionType.toString = function(input)
+ if input == MissionType.DEAD then return "DEAD" end
+ if input == MissionType.STRIKE then return "STRIKE" end
+ if input == MissionType.BAI then return "BAI" end
+ if input == MissionType.SAM then return "SAM" end
+ return "?"
+ end
+ end
+ Mission.MissionType = MissionType
+
+ Mission.MissionState = {
+ NEW = 0,
+ ACTIVE = 1,
+ COMPLETED = 2,
+ }
+
+ ---comment
+ ---@param missionZoneName string missionZoneName
+ ---@param database table db dependency injection
+ ---@return table?
+ function Mission:new(missionZoneName, database, logger)
+ local o = {}
+ setmetatable(o, { __index = self })
+
+ local function ParseGroupName(input)
+ local split_name = Spearhead.Util.split_string(input, "_")
+ local split_length = Spearhead.Util.tableLength(split_name)
+ if Spearhead.Util.startswith(input, "RANDOMMISSION") == true and split_length < 4 then
+ Spearhead.AddMissionEditorWarning("Random Mission with zonename " .. input .. " not in right format")
+ return nil
+ elseif split_length < 3 then
+ Spearhead.AddMissionEditorWarning("Mission with zonename" .. input .. " not in right format")
+ return nil
+ end
+ local type = split_name[2]
+ local parsedType = Mission.MissionType.Parse(type)
+
+ if parsedType == nil then
+ Spearhead.AddMissionEditorWarning("Mission with zonename '" .. input .. "' has an unsupported type '" .. (type or "nil" ))
+ return nil
+ end
+ local name = split_name[3]
+ return {
+ missionName = name,
+ type = parsedType
+ }
+ end
+
+ local parsed = ParseGroupName(missionZoneName)
+ if parsed == nil then return nil end
+
+ o.missionZoneName = missionZoneName
+ o.database = database
+ o.groupNames = database:getGroupsForMissionZone(missionZoneName)
+ o.name = parsed.missionName
+ o.missionType = parsed.type
+ o.startingGroups = Spearhead.Util.tableLength(o.groupNames)
+ o.missionState = Mission.MissionState.NEW
+ o.missionbriefing = database:GetDescriptionForMission(missionZoneName)
+ o.startingUnits = 0
+ o.logger = logger
+ o.code = database:GetNewMissionCode()
+
+ o.groupUnitAliveDict = {}
+ o.targetAliveStates = {}
+ o.hasSpecificTargets = false
+
+ local CheckStateAsync = function (self, time)
+ self:CheckAndUpdateSelf()
+ return nil
+ end
+
+ o.OnUnitLost = function(self, object)
+ --[[
+ OnUnit lost event
+ ]]--
+ local category = object:getCategory()
+ if category == Object.Category.UNIT then
+ local unitName = object:getName()
+ local groupName = object:getGroup():getName()
+ self.groupUnitAliveDict[groupName][unitName] = false
+
+ if self.targetAliveStates[groupName][unitName] then
+ self.targetAliveStates[groupName][unitName] = false
+ end
+ elseif category == Object.Category.STATIC then
+ local name = object:getName()
+ self.groupUnitAliveDict[name][name] = false
+
+ if self.targetAliveStates[name][name] then
+ self.targetAliveStates[name][name] = false
+ end
+ end
+ CheckStateAsync(false)
+ end
+
+ o.MissionCompleteListeners = {}
+ ---comment
+ ---@param self table
+ ---@param listener table Object that implements "OnMissionComplete(self, mission)"
+ o.AddMissionCompleteListener = function(self, listener)
+ if type(listener) ~= "table" then
+ return
+ end
+
+ table.insert(self.MissionCompleteListeners, listener)
+ end
+
+ local TriggerMissionComplete = function(self)
+ for _, callable in pairs(self.MissionCompleteListeners) do
+ local succ, err = pcall( function()
+ callable:OnMissionComplete(self)
+ end)
+ if err then
+ self.logger:warn("Error in misstion complete listener:" .. err)
+ end
+ end
+ end
+
+
+ local StartCheckingAndUpdateSelfContinuous = function (self)
+ local CheckAndUpdate = function(self, time)
+ self:CheckAndUpdateSelf(true)
+ if self.missionState == Mission.MissionState.COMPLETED or self.missionState == Mission.MissionState.NEW then
+ return nil
+ else
+ return time + 60
+ end
+ end
+
+ timer.scheduleFunction(CheckAndUpdate, self, timer.getTime() + 300)
+ end
+
+ local CleanupDelayedAsync = function (self, time)
+ self:Cleanup()
+ return nil
+ end
+
+ ---comment
+ ---@param self table
+ ---@param checkUnitHealth boolean?
+ o.CheckAndUpdateSelf = function(self, checkUnitHealth)
+ if not checkUnitHealth then checkUnitHealth = false end
+
+ if self.missionState == Mission.MissionState.COMPLETED then
+ return
+ end
+ --[[
+ TODO: Check own state based on mission type
+ ]]--
+
+ local specificTargetsAlive = false
+ if self.hasSpecificTargets == true then
+ for groupName, unitNameDict in pairs(self.targetAliveStates) do
+ for unitName, isAlive in pairs(unitNameDict) do
+ if isAlive == true then
+ specificTargetsAlive = true
+ end
+ end
+ end
+ else
+
+ local function CountAliveGroups()
+ local aliveGroups = 0
+
+ for _, group in pairs(self.groupUnitAliveDict) do
+ local groupTotal = 0
+ local groupDeath = 0
+ for _, isAlive in pairs(group) do
+ if isAlive ~= true then
+ groupDeath = groupDeath + 1
+ end
+ groupTotal = groupTotal + 1
+ end
+
+ local aliveRatio = (groupTotal - groupDeath) / groupTotal
+ if aliveRatio >= MINIMAL_UNITS_ALIVE_RATIO then
+ aliveGroups = 1
+ end
+ end
+ end
+
+ if self.missionType == Mission.MissionType.STRIKE then --strike targets should normally have TGT targets
+ if CountAliveGroups() == 0 then
+ self.missionState = Mission.MissionState.COMPLETED
+ end
+ elseif self.missionType == Mission.MissionType.BAI then
+ if CountAliveGroups() == 0 then
+ self.missionState = Mission.MissionState.COMPLETED
+ end
+ end
+ --[[
+ TODO: Other checks for mission complete
+ ]]
+ end
+
+
+ if self.missionState == Mission.MissionState.COMPLETED then
+ TriggerMissionComplete(self)
+ --Schedule cleanup after 5 minutes of mission complete
+ timer.scheduleFunction(CleanupDelayedAsync, self, timer.getTime() + 300)
+ end
+ end
+
+ ---Activates groups for this mission
+ ---@param self table
+ o.Activate = function(self)
+ if self.missionState == Mission.MissionState.ACTIVE then
+ return
+ end
+
+ self.missionState = Mission.MissionState.ACTIVE
+ do --spawn groups
+ for key, groupname in pairs(self.groupNames) do
+ Spearhead.DcsUtil.SpawnGroupTemplate(groupname)
+ end
+ end
+
+ StartCheckingAndUpdateSelfContinuous(self)
+ end
+
+ o.ShowBriefing = function(self, unitId)
+ local text = "Mission #" .. self.code .. "\n" .. self.missionbriefing .. " \n \nState TODO"
+ trigger.action.outTextForUnit(unitId, text, 30);
+ end
+
+ o.Cleanup = function(self)
+ for key, groupName in pairs(self.groupNames) do
+ Spearhead.DcsUtil.DestroyGroup(groupName)
+ end
+ end
+
+ local Init = function(self)
+ for key, group_name in pairs(self.groupNames) do
+
+ self.groupUnitAliveDict[group_name] = {}
+ self.targetAliveStates[group_name] = {}
+
+ if Spearhead.DcsUtil.IsGroupStatic(group_name) then
+ self.startingUnits = self.startingUnits + 1
+ Spearhead.Events.addOnUnitLostEventListener(group_name, self)
+
+ self.groupUnitAliveDict[group_name][group_name] = true
+ if Spearhead.Util.startswith(group_name, "TGT_") == true then
+ self.targetAliveStates[group_name][group_name] = true
+ end
+
+ else
+ local group = Group.getByName(group_name)
+ local isGroupTarget = Spearhead.Util.startswith(group_name, "TGT_")
+
+ self.startingUnits = self.startingUnits + group:getInitialSize()
+ for _, unit in pairs(group:getUnits()) do
+ local unitName = unit:getName()
+ Spearhead.Events.addOnUnitLostEventListener(unitName, self)
+ self.groupUnitAliveDict[group_name][unitName] = true
+
+ if isGroupTarget == true or Spearhead.Util.startswith(unitName, "TGT_") == true then
+ self.targetAliveStates[group_name][unitName] = true
+ end
+ end
+ end
+ end
+
+ if Spearhead.Util.tableLength(self.targetAliveStates) > 0 then
+ self.hasSpecificTargets = true
+ end
+ end
+
+ Init(o)
+ return o;
+ end
+end
+
+Spearhead.internal.Mission = Mission
\ No newline at end of file
diff --git a/stageClasses/Stage.lua b/stageClasses/Stage.lua
new file mode 100644
index 0000000..2806503
--- /dev/null
+++ b/stageClasses/Stage.lua
@@ -0,0 +1,249 @@
+--[[
+
+# 2. Stages
+
+]]
+
+
+local Stage = {}
+do --init STAGE DIRECTOR
+
+ ---comment
+ ---@param stagezone_name string
+ ---@param database table
+ ---@param logger table
+ ---@return table?
+ function Stage:new(stagezone_name, database, logger)
+ local o = {}
+ setmetatable(o, { __index = self })
+
+ o.zoneName = stagezone_name
+
+ local split = Spearhead.Util.split_string(stagezone_name, "_")
+ if Spearhead.Util.tableLength(split) < 2 then
+ Spearhead.AddMissionEditorWarning("Stage zone with name " .. stagezone_name .. " does not have a order number or valid format")
+ return nil
+ end
+
+ local orderNumber = tonumber(split[2])
+ if orderNumber == nil then
+ Spearhead.AddMissionEditorWarning("Stage zone with name " .. stagezone_name .. " does not have a valid order number : " .. split[2])
+ return nil
+ end
+
+ o.stageNumber = orderNumber
+ o.database = database
+
+ o.db = {}
+ o.db.missions = {}
+ o.db.sams = {}
+ o.db.redAirbasegroups = {}
+ o.db.blueAirbasegroups = {}
+ o.db.airbaseIds = {}
+ o.db.farps = {}
+ o.preActivated = false
+
+ do --Init Stage
+ logger:info("Initiating new Stage with name: " .. stagezone_name)
+
+ local missionZones = database:getMissionsForStage(stagezone_name)
+ for _, missionZone in pairs(missionZones) do
+ local mission = Spearhead.internal.Mission:new(missionZone, database)
+ if mission then
+ if mission.missionType == Spearhead.internal.Mission.MissionType.SAM then
+ table.insert(o.db.sams, mission)
+ else
+ table.insert(o.db.missions, mission)
+ end
+ end
+ end
+
+ local randomMissionNames = database:getRandomMissionsForStage(stagezone_name)
+
+ local randomMissionByName = {}
+ for _, missionZoneName in pairs(randomMissionNames) do
+ local mission = Spearhead.internal.Mission:new(missionZoneName, database)
+ if mission then
+ if randomMissionByName[mission.name] == nil then
+ randomMissionByName[mission.name] = {}
+ end
+ table.insert(randomMissionByName[mission.name], mission)
+ end
+ end
+
+ for _, missions in pairs(randomMissionByName) do
+ local mission = Spearhead.Util.randomFromList(missions)
+ if mission then
+ if mission.missionType == Spearhead.internal.Mission.MissionType.SAM then
+ table.insert(o.db.sams, mission)
+ else
+ table.insert(o.db.missions, mission)
+ end
+ end
+ end
+
+ local airbaseIds = database:getAirbaseIdsInStage(o.zoneName)
+ if airbaseIds ~= nil and type(airbaseIds) == "table" then
+ o.db.airbaseIds = airbaseIds
+ for _, airbaseId in pairs(airbaseIds) do
+
+ for _, groupName in pairs(database:getRedGroupsAtAirbase(airbaseId)) do
+ logger:debug("blaat)")
+ table.insert(o.db.redAirbasegroups, groupName)
+ end
+
+ for _, groupName in pairs(database:getBlueGroupsAtAirbase(airbaseId)) do
+ table.insert(o.db.blueAirbasegroups, groupName)
+ end
+ end
+ end
+
+ local farps = database:getFarpZonesInStage(o.zoneName)
+ if farps ~= nil and type(farps) == "table" then o.db.farps = farps end
+ end
+
+ o.IsComplete = function(self)
+ for i, mission in pairs(self.db.missions) do
+ local state = mission:GetState()
+ if state == Spearhead.Mission.MissionState.ACTIVE or state == Spearhead.Mission.MissionState.NEW then
+ return false
+ end
+ end
+ return true
+ end
+
+ ---Activates all SAMS, Airbase units etc all at once.
+ ---@param self table
+ o.PreActivate = function(self)
+ if self.preActivated == true then return end
+ self.preActivated = true
+ for key, mission in pairs(self.db.sams) do
+ if mission and mission.Activate then
+ mission:Activate()
+ end
+ end
+
+ logger:debug("blaat Pre-activating stage with airbase groups amount: " .. Spearhead.Util.tableLength(self.db.redAirbasegroups))
+
+ for _ , groupName in pairs(self.db.redAirbasegroups) do
+ Spearhead.DcsUtil.SpawnGroupTemplate(groupName)
+ end
+ end
+
+ o.ActiveStage = function(self)
+ if self.preActivated == false then
+ self:PreActivate()
+ end
+
+ --[[
+ TODO: Activate Stage
+ ]]--
+
+ end
+
+ o.ActivateAllMissions = function(self)
+ for _, mission in pairs(self.db.missions) do
+ mission:Activate()
+ end
+ end
+
+ ---Cleans up all missions
+ ---@param self table
+ o.Clean = function(self)
+ for key, mission in pairs(self.db.missions) do
+ mission:Cleanup()
+ end
+
+ for key, samMission in pairs(self.db.sams) do
+ samMission:Cleanup()
+ end
+
+ for _, airbase in pairs(self.db.airbases) do
+ for _, redGroupName in pairs(airbase.redAirbaseGroupNames) do
+ Spearhead.DcsUtil.DcsUtil.DestroyGroup(redGroupName)
+ end
+ end
+
+ logger:debug("'" .. Spearhead.Util.toString(self.zoneName) .. "' cleaned")
+ end
+
+ local ActivateBlueAsync = function(self)
+ for key, airbaseId in pairs(self.db.airbaseIds) do
+ local airbase = Spearhead.DcsUtil.getAirbaseById(airbaseId)
+
+ if airbase then
+ local startingCoalition = Spearhead.DcsUtil.getStartingCoalition(airbaseId)
+ if startingCoalition == coalition.side.BLUE then
+ airbase:setCoalition(2)
+ for _, blueGroupName in pairs(self.db.blueAirbasegroups) do
+ Spearhead.DcsUtil.SpawnGroupTemplate(blueGroupName)
+ end
+ else
+ airbase:setCoalition(0)
+ end
+ end
+ end
+ end
+
+ ---Sets airfields to blue and spawns friendly farps
+ o.ActivateBlueStage = function(self)
+ logger:debug("Setting stage '" .. Spearhead.Util.toString(self.zoneName) .. "' to blue")
+
+ for _, groupName in pairs(self.db.redAirbasegroups) do
+ Spearhead.DcsUtil.DestroyGroup(groupName)
+ end
+
+ for _, mission in pairs(self.db.missions) do
+ mission:Cleanup()
+ end
+
+ for _, mission in pairs(self.db.sams) do
+ mission:Cleanup()
+ end
+
+ timer.scheduleFunction(ActivateBlueAsync, self, timer.getTime() + 3)
+
+ -- for key, farp in pairs(self.db.farps) do
+ -- if farp.helipadnames then
+ -- for _, helipadName in pairs(farp.helipadnames) do
+ -- local helipad = Airbase.getByName(helipadName)
+ -- if helipad then
+ -- logger:debug("Enabling: '" .. helipad:getName() .. "'")
+ -- helipad:setCoalition(2)
+ -- else
+ -- logger:warn(helipadName .. " not found when spawning farps")
+ -- end
+ -- end
+ -- end
+
+ -- if farp.group_names then
+ -- for _, groupName in pairs(farp.group_names) do
+ -- Spearhead.DcsUtil.SpawnGroupTemplate(groupName)
+ -- end
+ -- end
+ -- end
+ end
+
+ o.ManageStage = function(self)
+ end
+
+ o.OnStageNumberChanged = function (self, number)
+ if Spearhead.capInfo.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then
+ self:PreActivate()
+ end
+
+ if number == self.stageNumber then
+ self:ActiveStage()
+ end
+
+ if number > self.stageNumber then
+ self:ActivateBlueStage()
+ end
+ end
+
+ Spearhead.Events.AddStageNumberChangedListener(o)
+ return o
+ end
+end
+
+Spearhead.internal.Stage = Stage
\ No newline at end of file