migrated repo

This commit is contained in:
2024-09-24 07:21:27 +02:00
parent a3e4b6bf70
commit f7c22d348c
15 changed files with 3792 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
{
"Lua.diagnostics.globals": [
"missionCommands",
"env",
"Airbase",
"world",
"trigger",
"StaticObject",
"Group",
"Unit",
"Object",
"coalition",
"timer",
"AI"
]
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+39
View File
@@ -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)
+2
View File
@@ -0,0 +1,2 @@
---KEEP THIS LINE
if not SpearheadConfig then SpearheadConfig = {} end
+200
View File
@@ -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
+465
View File
@@ -0,0 +1,465 @@
--[[
#### Why?
For Spearhead there's a lot of stages and states the mission can be in. <br/>
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. <br/>
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\>_\<Free form name\>
#### Config:
```
1 at x: [<activeStage>]<capStage>
n and n at x: [<activeStage>,<activeStage>]<capStage>
n till n at x: [<activeStage>-<activeStage>]<capStage>
n till n and n at x: [<activeStage>-<activeStage>,<activeStage>]<capStage>
n till n at Active: [<activeStage>-<activeStage>]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. <br/>
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. <br/>
The B units will simply be used to fill that amount if the A units can't due to RTB, Death, Rearming etc. <br/>
#### 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. <br/>
In this case the CAP manager sees that for stages 1 through 5 this configuration requires 2 groups in the active zone. <br/>
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. <br/>
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. <br/>
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
+61
View File
@@ -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
+162
View File
@@ -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_<ordernumber>_Name
Mission: MISSION_<oneOf(DEAD, STRIKE, BAI, SAM)>_Name
Random Mission: RANDOMMISSION_<tasking>_<NAME>_<number>
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_<order[number]>_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_<name>"
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_<tasking>_<NAME>_<number> (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
+1560
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -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_<name> 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_<name> 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
+589
View File
@@ -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
+22
View File
@@ -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
+396
View File
@@ -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. <br/>
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. <br/>
The order of unit detection is `CAP` > `MISSION` > `AIRBASE` > `STAGE` <br/>
This means that if a unit has a name that starts with `"CAP_"` it will not be included in a mission. <br/>
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. <br/>
They just need to be inside the zone at the start of the mission. <br/>
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_<type>_<name>` <br/>
`RANDOMMISSION_<type>_<name>_<index>` (Read RANDOMISATION below)
With: <br/>
`name` = A name that is easy to remember and type. Like a codename. Exmaples: BYRON, PLUKE, etc. <br/>
`type` = any of the below described types. Special types are marked with an *
TIP: You can click on the type to get more details
<details>
<summary>SAM*</summary>
&emsp; SAM Sites are managed a little different. SAM Sites can be used to guide players and to protect airfields. <br/>
&emsp; In the future when deepstrike missions might come into scope these SAM sites will also be more important. <br/>
&emsp; SAM sites will be activated when a zone is "Pre-Active". <br/>
&emsp; 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)<br/>
&emsp; If you want a SAM site to become active ONLY when the stage is fully active, then `DEAD` is the type for you!
&emsp; <u>Completion logic</u> <br/>
&emsp; TODO: documentation: Completion logic
</details>
<details>
<summary>DEAD</summary>
&emsp; DEAD missions will be spawned on activation of the stage. <br/>
&emsp; ALL DEAD missions will be activated right at the start of a stage. <br/>
&emsp; 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. <br/>
&emsp; <u>Completion logic</u> <br/>
&emsp; TODO: documentation: Completion logic
</details>
<details>
<summary>BAI</summary>
</details>
<details>
<summary>STRIKE</summary>
&emsp; STRIKE missions will be activated randomly until all of them are completed. <br/>
&emsp; A strike mission can be placed anywhere, even on airbases
&emsp; <u>Completion logic</u> <br/>
&emsp; TODO: documentation: Completion logic
</details>
### Randomisation
You can randomise missions. <br/>
Spearhead will pick up all mission zones that start with `"RANDOMMISSION_"` <br/>
Then it will combine each `RANDOMMISSION` in a zone with the same `<name>` and pick a random one. <br/>
It will always pick 1 and only 1.
This means you have some options for randomisation. <br/>
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
+249
View File
@@ -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