* Initial Methods for SpearheadAPI

* Added Spearhead API

* Added Spearhead API

* fixed compile for api

* Added notes for API in beta

* Added notes for API in beta

* Added Menu to all doc pages

* Added Menu to all doc pages

* added it for real

* added it for real

* fixed develop

* fixed develop

* fixed develop

* wip

* wip

* updated

* work in progress

* work in progress

* added empty docs page

* wip

* removed SAR files from persistence branch

* Added Blue Sams as seperate class

* added initial persistance

* added initial persistance

* added initial persistance

* added initial persistance

* added initial persistance

* added menu to persistence markdown

* added menu to persistence markdown

* added menu to persistence markdown

* docs update

* wip

* wip

* Refactor of Stage classes.

* wip

* wip

* wip

* reworked bluesam and stagebase

* Added waiting stage functionality

* updated docs

* wip

* wip

* wip

* updated

* wip

* wip

* wip

* refactored without issues, needs testing

---------

Co-authored-by: Dutchie <54616262+dutchie032@users.noreply.github.com>
Co-authored-by: dutchie032 <dutchie032>
Co-authored-by: ex61wi <tim.rorije@ing.com>
This commit is contained in:
2025-04-22 14:39:22 +02:00
committed by GitHub
co-authored by Dutchie dutchie032 <dutchie032> ex61wi
parent 9c6a5ed217
commit ff392e0e7e
37 changed files with 4494 additions and 1732 deletions
+21 -7
View File
@@ -15,7 +15,6 @@ jobs:
branch: main
username: dutchie032
repo: dutchie032.github.io
sourcefileA: Reference
sourcefileB: GetStarted
defaults:
run:
@@ -34,9 +33,12 @@ jobs:
run: git config --global user.name "automated action"
- name: clone
run: git clone --single-branch --branch $branch https://github.com/$username/$repo
- name: commit
run: git pull --rebase
working-directory: ./dutchie032.github.io
- name: ls
run: ls
- name: Replace strings in file
@@ -51,21 +53,32 @@ jobs:
with open(fileName, "w") as f:
f.write(newText)
with open("./spearhead/config.lua", "r") as luaFile:
with open("./spearhead/config.lua", "r") as luaFile:
luaConfig = luaFile.read()
replace("./spearhead/_docs/Reference.md", "##!config!##", luaConfig)
lastUpdateTime = datetime.datetime.now().isoformat()
replace("./spearhead/_docs/Reference.md", "##!lastupdatetime!##", lastUpdateTime)
replace("./spearhead/_docs/GetStarted.md", "##!lastupdatetime!##", lastUpdateTime)
menu = ""
with open("./spearhead/_docs/menu.html", "r") as menuFile:
menu = menuFile.read()
files = [ "Reference.md", "GetStarted.md", "SpearheadApi.md", "Persistence.md" ]
for file in files:
replace(f"./spearhead/_docs/{file}", "##!lastupdatetime!##", lastUpdateTime)
with open(f"./spearhead/_docs/{file}", "a") as f:
f.write(menu)
shell: python
- name: mkdir
run: mkdir -p ./$repo/Spearhead
- run: npm i markdown-to-html-cli -g
- run: markdown-to-html --title 'Spearhead' --dark-mode auto --keywords "dcs,spearhead,tdcs,mission,campaign,tactical dcs,tactical" --no-dark-mode --markdown-style-theme dark --source ./spearhead/_docs/$sourcefileA.md --output ./$repo/Spearhead/$sourcefileA.html
- run: markdown-to-html --title 'Spearhead' --dark-mode auto --keywords "dcs,spearhead,tdcs,mission,campaign,tactical dcs,tactical" --no-dark-mode --markdown-style-theme dark --source ./spearhead/_docs/$sourcefileB.md --output ./$repo/Spearhead/$sourcefileB.html
- run: markdown-to-html --title 'Spearhead' --dark-mode auto --keywords "dcs,spearhead,tdcs,mission,campaign,tactical dcs,tactical" --no-dark-mode --markdown-style-theme dark --source ./spearhead/_docs/Reference.md --output ./$repo/Spearhead/Reference.html
- run: markdown-to-html --title 'Spearhead' --dark-mode auto --keywords "dcs,spearhead,tdcs,mission,campaign,tactical dcs,tactical" --no-dark-mode --markdown-style-theme dark --source ./spearhead/_docs/GetStarted.md --output ./$repo/Spearhead/GetStarted.html
- run: markdown-to-html --title 'Spearhead' --dark-mode auto --keywords "dcs,spearhead,tdcs,mission,campaign,tactical dcs,tactical" --no-dark-mode --markdown-style-theme dark --source ./spearhead/_docs/SpearheadApi.md --output ./$repo/Spearhead/SpearheadApi.html
- run: markdown-to-html --title 'Spearhead' --dark-mode auto --keywords "dcs,spearhead,tdcs,mission,campaign,tactical dcs,tactical" --no-dark-mode --markdown-style-theme dark --source ./spearhead/_docs/Persistence.md --output ./$repo/Spearhead/Persistence.html
- run: python3 ./spearhead/SpearheadCompile.py "./spearhead" "./$repo/Spearhead/spearhead.lua"
if: github.ref == 'refs/heads/main'
@@ -87,4 +100,5 @@ jobs:
working-directory: ./dutchie032.github.io
- name: push
run: git push
if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main'
working-directory: ./dutchie032.github.io
+5
View File
@@ -0,0 +1,5 @@
{
"recommendations": [
"sumneko.lua"
]
}
+7 -1
View File
@@ -12,6 +12,12 @@
"coalition",
"timer",
"AI",
"net"
"net",
"lfs"
],
"cSpell.words": [
"farp",
"Sams",
"SEAD"
]
}
+1251
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -26,8 +26,17 @@ def compile(root, target):
mainFileName = os.path.join(root, "main.lua")
with open(mainFileName, 'r') as mainFile:
compiled += "do --Main\n"
string = mainFile.read()
compiled += string
compiled += "\nend --Main\n"
apiFileName = os.path.join(root, "api" , "SpearheadApi.lua")
with open(apiFileName, 'r') as apiFile:
part = "do --Spearhead API\n"
part += apiFile.read()
part += "\nend --Spearhead API\n"
compiled += part
with open(target, "w") as targetFile:
targetFile.write(compiled)
+77
View File
@@ -0,0 +1,77 @@
# Spearhead Persistence
> **_NOTE:_** Persistence is only released in the Beta branch at the moment.
Spearhead comes with a custom Persistence option. <br/>
It will even save burned out vehicles to give players a consistent battlefield even after the restart. <br/>
Most is pretty straight forward, however, some zones are somewhat special. <br/>
Underneath you'll see all special zones listed.
## Settings
To see the full config check: [Reference](./Reference.html#Configuration)
## Feedback
Since this feature is still very much in development, please let any issues know as soon as possible and as consice as possible so a fix can be made quickly.
Currently implemented is local file storage. <br/>
If enough interest is expressed cloud based persistence would be possible. <br/>
```lua
... Rest of config
Persistence = {
--- io and lfs cannot be sanitized in the MissionScripting.lua
--- enables or disables the persistence logic in spearhead
enabled = false,
--- sets the directory where the persistence file is stored
--- if nil then lfs.writedir() will be used.
--- which will
directory = nil ,
--- the filename of the persistence file. Should end with .json for convention, but any text extension should do.
fileName = "Spearhead_Persistence.json"
}
... Rest of config
```
## Basic behavior
While playing the mission Spearhead is keeping track of all units killed. <br/>
These units are stored in memory internally and written to file. <br/>
This happens every 2 minutes AND during "onMissionStop" event to make sure the mission is as up to date as possible without having to call IO methods on each event.<br/>
## Misc units
Miscelaneous units will follow basic behaviour. <br/>
These are units that are part of a stage, but are not in a mission or airbase. <br/>
These units will be replaced by a static "DEAD" unit after a mission restart at the location it was killed. <br/>
Due to blue units spawning afterwards it's generally best to not have these units move through or over areas where BLUESAMS and Airbase units will spawn after a stage completion.
## Missions
Missions follow the same logic as Misc Units. <br/>
## Blue SAMs
For Blue SAMs, due to placements easily overlapping between red and blue units within a BLUESAM trigger zone red units that overlap with blue units will be deleted.
<br/>
This will ensure that the blue units are placed as needed. <br/>
## Airbases
Airbase units will also be checked for overlap. As the blue units will be spawned after the RED unit. <br/>
Units that were alive when the stage was completed will be removed. Units that died will have corpses spawned. <br/>
## Warehouses
Currently warehouses are not implemented and therefore warehouses are not persisted. <br/>
When supply missions and logistics get implemented warehouses will be persisted as well. <br/>
+23 -47
View File
@@ -19,51 +19,6 @@ If you however want to go right to the Get Started guide click here: [here](./Ge
You can always come back later here. <br/>
The Get Started guide will not show all details and reasoning. <br/>
## Release Notes
<details>
<summary>01-10-2024 Initial Version </summary>
The initial version with basic functionality
</details>
## Feature / TODO list
- [ ] CAP Manager
- [x] RTB flow. Flying out before primary flies back.
- [ ] Out of Missile flow
- [ ] Stage
- [ ] Pre Activate
- [x] All SAM Sites
- [x] All Red airbase units
- [ ] OCA Missions
- [ ] Activate
- [ ] Activate all DEAD sites. (No surprise pop ups)
- [ ] Activate other random mission till 10 max
- [ ] Completion Logic
- [ ] ?? Custom Conditions ??
- [ ] ?? Airbases as final mission ??
- [ ] Required Mission types
- [ ] Missions
- [x] BAI
- [x] DEAD
- [x] STRIKE
- [ ] INTERCEPT
- [ ] LOGISTIC
- [ ] EXTRACTION | MEDEVAC
- [ ] More?
- [ ] Warehouses
- [ ] Logistics [OPTIONAL]
- [ ]
- [ ] Team creation
- [ ] Airbases
- [ ] Farps
- [ ] Carrier/Fleet routes
- [ ] Fleet tracks
- [ ] Persistance
## Configuration
Spearhead will always try to be as configurable as possible. <br/>
@@ -77,6 +32,14 @@ Overwrite the values in your own script (before spearhead runs) or <a download="
##!config!##
```
## The Spearhead API
> **_NOTE:_** The Spearhead.API space is only release in the Beta branch at the moment.
You can not only use Spearhead, but you can also interact with it via your own scripts.
See all the reference, explanation here: [SpearheadAPI](./SpearheadApi.html)
## All Trigger zone name options:
<details>
@@ -84,6 +47,21 @@ Overwrite the values in your own script (before spearhead runs) or <a download="
A mission stage that encapsulates multiple missions, airbases and other objects.
</details>
<details>
<summary>WAITINGSTAGE_</summary>
A stage that functions as a logic delay or timer. <br/>
With this you can add a wait before or in between stages.
Naming Convention: `WAITINGSTAGE_<order>_<amount of seconds>`
Example: `WAITINGSTAGE_2_180`
A waiting stage can be part of the same stage order as other stages. <br/>
This will first activate the DELAYSTAGE and only when the DELAYSTAGE is completed will the other stages be activated. <br/>
This is so you can easily add delays later in your mission development, without havint to rename all subsequent stages.
</details>
<details>
<summary>MISSION_</summary>
</details>
@@ -106,8 +84,6 @@ Overwrite the values in your own script (before spearhead runs) or <a download="
A stage is a logical part of a mission. It's isn't anything special per se, but everything revolves around stages in Spearhead. <br/>
Everything is tied to at a stage. <br/>
## Mission
A mission is a completable objective with a state and a continuous check to see if itself is completed. <br/>
+37
View File
@@ -0,0 +1,37 @@
# Spearhead.API
> **_NOTE:_** The Spearhead.API space is only release in the Beta branch at the moment.
the `Spearhead.API` space is specifically created to make sure mission makers can interact with the framework.
Simply alter logic, get the current state in Spearhead, and give the whole ME more control.
eg. Late activate the entire framework by calling `Spearhead.API.Stages.changeStage(1)` later or on demand and setting the starting config stage to -1 in the Spearhead configuration file
## Stages
```lua
---Changes the active stage of spearhead.
--- All other stages will change based on the normal logic. (CAP, BLUE etc.)
---@param stageNumber number the stage number you want changed
---@return boolean success indicator of success
---@return string message error message
Spearhead.API.Stages.changeStage = function(stageNumber) end
---Returns the current stange number
---Returns nil when the stagenumber was not set before ever, which means Spearhead was not started.
---@return number | nil
Spearhead.API.Stages.getCurrentStage = function() end
---returns whether a stage (by index) is complete.
---@param stageNumber number
---@return boolean | nil
---@return string
Spearhead.API.Stages.isStageComplete = function(stageNumber) end
```
+38
View File
@@ -0,0 +1,38 @@
<div id="menu">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<div style="position:fixed;
left:80%;
top:80%;
height:15%;
width:15%;
padding:10px;
background-color: #00053B;
border-radius: 8px;">
<div style="margin-bottom: 8px;">
<a href="/Spearhead/Reference.html" style="color: white; font-size: 18px; ">
<i class="bi bi-file-earmark-richtext"></i>
&ensp;Reference
</a>
</div>
<div style="margin-bottom: 8px;">
<a href="/Spearhead/GetStarted.html" style="color: white; font-size: 18px;">
<i class="bi bi-rocket-takeoff"></i>
&ensp;Get Started
</a>
</div>
<div style="margin-bottom: 8px;">
<a href="/Spearhead/Persistence.html" style="color: white; font-size: 18px;">
<i class="bi bi-database-check"></i>
&ensp;Persistence
</a>
</div>
<div style="margin-bottom: 8px;">
<a href="/Spearhead/SpearheadApi.html" style="color: white; font-size: 18px;">
<i class="bi bi-gear"></i>
&ensp;API
</a>
</div>
</div>
</div>
Binary file not shown.
Binary file not shown.
+55
View File
@@ -0,0 +1,55 @@
local SpearheadAPI = {}
do
SpearheadAPI.Stages = {}
--- Changes the active stage of spearhead.
--- All other stages will change based on the normal logic. (CAP, BLUE etc.)
--- @param stageNumber number the stage number you want changed
--- @return boolean success indicator of success
--- @return string message error message
SpearheadAPI.Stages.changeStage = function(stageNumber)
if type(stageNumber) ~= "number" then
return false, "stageNumber " .. stageNumber .. " is not a valid number"
end
Spearhead.Events.PublishStageNumberChanged(stageNumber)
return true, ""
end
---Returns the current stange number
---Returns nil when the stagenumber was not set before ever, which means Spearhead was not started.
---@return number | nil
SpearheadAPI.Stages.getCurrentStage = function()
return Spearhead.StageNumber or nil
end
---returns whether a stage (by index) is complete.
---@param stageNumber number
---@return boolean | nil
---@return string
SpearheadAPI.isStageComplete = function(stageNumber)
if type(stageNumber) ~= "number" then
return false, "stageNumber " .. stageNumber .. " is not a valid number"
end
local isComplete = Spearhead.internal.GlobalStageManager.isStageComplete(stageNumber)
if isComplete == nil then
return nil, "no stage found with number " .. stageNumber
end
return isComplete, ""
end
end
if Spearhead == nil then Spearhead = {} end
Spearhead.API = SpearheadAPI
+74
View File
@@ -0,0 +1,74 @@
---@class AirGroup
---@field groupName string
---@field groupState GroupState
local AirGroup = {}
---@alias GroupState
---| "UnSpawned"
---| "ReadOnTheRamp
---| "InTransit"
---| "OnStation"
---| "RtbInTen"
---| "Rtb"
---| "Dead"
---| "Rearming"
---@alias AirGroupType
---| "CAP"
---| "CAS"
---| "SEAD"
---| "INTERCEPT"
---| ""
---@class GroupNameData
---@field type AirGroupType
---@field isBackup boolean
---@field zonesConfig table<string, string>
local function parseGroupName(groupName)
local split_string = Spearhead.Util.split_string(groupName, "_")
local partCount = Spearhead.Util.tableLength(split_string)
if partCount >= 3 then
---@type boolean
local isBackup = false
do -- config
end
else
Spearhead.AddMissionEditorWarning("CAP Group with name: " .. groupName .. "should have at least 3 parts, but has " .. partCount)
return nil
end
end
---comment
---@generic T: AirGroup
---@param o T
---@param groupName string
---@return T
function AirGroup.New(o, groupName)
AirGroup.__index = AirGroup
local o = o or {}
local self = setmetatable(o, AirGroup)
self.groupName = groupName
self.groupState = "UnSpawned"
return self
end
+1 -1
View File
@@ -48,7 +48,7 @@ function CapConfig:new()
local deathDelay = tonumber(SpearheadConfig.CapConfig.deathDelay) or 1800
---@return number
o.getDeathDelay = function(self) return deathDelay end
o.logLevel = Spearhead.LoggerTemplate.LogLevelOptions.INFO
o.logLevel = "INFO"
return o;
end
+25 -31
View File
@@ -1,44 +1,38 @@
--- @class StageConfig
--- @field isEnabled boolean
--- @field isDrawStagesEnabled boolean
--- @field isAutoStages boolean
--- @field startingStage integer
--- @field maxMissionsPerStage integer
--- @field logLevel LogLevel
local StageConfig = {};
---comment
---@return StageConfig
function StageConfig:new()
local o = {}
setmetatable(o, { __index = self })
if SpearheadConfig == nil then SpearheadConfig = {} end
if SpearheadConfig.StageConfig == nil then SpearheadConfig.StageConfig = {} end
local enabled = SpearheadConfig.StageConfig.enabled
if enabled == nil then enabled = true end
---@return boolean
o.isEnabled = function(self) return enabled == true end
---@type StageConfig
local o = {
isEnabled = SpearheadConfig.StageConfig.enabled or true,
isDrawStagesEnabled = SpearheadConfig.StageConfig.drawStages or true,
isAutoStages = SpearheadConfig.StageConfig.autoStages or true,
startingStage = SpearheadConfig.StageConfig.startingStage or 1,
maxMissionsPerStage = SpearheadConfig.StageConfig.maxMissionStage or 10,
logLevel = "INFO"
}
local drawStages = SpearheadConfig.StageConfig.drawStages
if drawStages == nil then drawStages = true end
---@return boolean
o.isDrawStagesEnabled = function(self) return drawStages == true end
local autoStages = SpearheadConfig.StageConfig.autoStages or true
if autoStages == nil then autoStages = true end
---@return boolean
o.isAutoStages = function(self) return autoStages end
local maxMissionsPerStage = SpearheadConfig.StageConfig.maxMissionStage
o.getMaxMissionsPerStage = function(self) return maxMissionsPerStage end
o.logLevel = Spearhead.LoggerTemplate.LogLevelOptions.INFO
local startingStage = SpearheadConfig.StageConfig.startingStage or 1
o.getStartingStage = function(self) return startingStage end
o.toString = function()
return Spearhead.Util.toString({
maxMissionsPerStage = maxMissionsPerStage,
enabled = enabled,
drawStages = drawStages,
autoStages = autoStages
})
if SpearheadConfig.StageConfig.debugEnabled == true then
o.logLevel = "DEBUG"
end
setmetatable(o, { __index = self })
return o;
end
+39
View File
@@ -0,0 +1,39 @@
do -- mission aliases
--- @alias MissionPriority
--- | "primary"
--- | "secondary"
--- @alias missionType
--- | "nil"
--- | "STRIKE"
--- | "BAI"
--- | "DEAD"
--- | "SAM"
--- @alias MissionState
--- | "NEW"
--- | "ACTIVE"
--- | "COMPLETED"
--- @alias LogLevel
--- | "DEBUG"
--- | "INFO"
--- | "WARN"
--- | "ERROR"
--- | "NONE"
---@class Array<T>: { [integer]: T }
--- @class Position
--- @field x number x position (Top-Down on Map)
--- @field y number y altitude
--- @field z number z position (Left-Right on Map)
end
+1 -1
View File
@@ -6,7 +6,7 @@ local fleetGroups = {}
GlobalFleetManager.start = function(database)
local logger = Spearhead.LoggerTemplate:new("CARRIERFLEET", Spearhead.LoggerTemplate.LogLevelOptions.INFO)
local logger = Spearhead.LoggerTemplate:new("CARRIERFLEET", "INFO")
local all_groups = Spearhead.DcsUtil.getAllGroupNames()
for _, groupName in pairs(all_groups) do
+204
View File
@@ -0,0 +1,204 @@
local Persistence = {}
do
---@class PersistentData
---@field dead_units table<string, DeathState>
---@field activeStage integer|nil
---@class DeathState
---@field isDead boolean
---@field pos Position
---@field heading number
---@field type string
---@field country_id integer
local persistanceWriteIntervalSeconds = 15
local enabled = false
---@type PersistentData
local tables = {
dead_units = {},
activeStage = nil
}
local logger = {}
if SpearheadConfig == nil then SpearheadConfig = {} end
if SpearheadConfig.Persistence == nil then SpearheadConfig.Persistence = {} end
local path = nil
local updateRequired = false
local createFileIfNotExists = function()
if not path then return end
local f = io.open(path, "r")
if f == nil then
f = io.open(path, "w+")
if f == nil then
logger:error("Could not create a file")
else
f:write("{}")
f:close()
end
else
f:close()
end
end
local loadTablesFromFile = function()
if not path then return end
logger:info("Loading data from persistance file...")
local f = io.open(path, "r")
if f == nil then
return
end
local json = f:read("*a")
local lua = net.json2lua(json)
if lua.activeStage then
logger:info("Found active stage from save: " .. lua.activeStage)
tables.activeStage = lua.activeStage
end
if lua.dead_units then
logger:debug("Found saved dead units")
for name, deadState in pairs(lua.dead_units) do
logger:debug("Found saved dead unit: " .. name)
if type(deadState) == "table" then
tables.dead_units[name] = {
isDead = deadState.isDead == true,
pos = deadState.pos,
heading = deadState.heading,
type = deadState.type,
country_id = deadState.country_id
}
end
end
end
f:close()
end
local writeToFile = function()
if not path then return end
local f = io.open(path, "w+")
if f == nil then
error("Could not open file for writing")
return
end
local jsonString = net.lua2json(tables)
f:write(jsonString)
if f ~= nil then
f:close()
end
end
local UpdateContinuous = function(null, time)
if updateRequired then
local status, result = pcall(writeToFile)
if status == false then
env.error("[Spearhead][Persistence] Could not write state to file: " .. result)
end
end
return time + persistanceWriteIntervalSeconds
end
Persistence.UpdateNow = function()
if enabled == true then
writeToFile()
end
end
Persistence.isEnabled = function()
return enabled
end
---comment
---@param persistenceLogger Logger
Persistence.Init = function(persistenceLogger)
logger = persistenceLogger
logger:info("Initiating Persistence Manager")
if lfs == nil or io == nil then
env.error("[Spearhead][Persistence] lfs and io seem to be sanitized. Persistence is skipped and disabled")
return
end
path = (SpearheadConfig.Persistence.directory or (lfs.writedir() .. "\\Data" )) .. "\\" .. (SpearheadConfig.Persistence.fileName or "Spearhead_Persistence.json")
createFileIfNotExists()
loadTablesFromFile()
timer.scheduleFunction(UpdateContinuous, nil, timer.getTime() + 120)
enabled = true
end
---Sets the stage in the persistence table
---@param stageNumber number
Persistence.SetActiveStage = function(stageNumber)
tables.activeStage = stageNumber
updateRequired = true
end
---Get the active stage as in the persistance file
---@return integer|nil
Persistence.GetActiveStage = function()
if tables.activeStage then
return tables.activeStage
end
return nil
end
---Check if the unit was dead during the last save. Nil if persitance not enabled
---@param unitName string name
---@return DeathState|nil { isDead, pos = {x,y,z}, heading, type, country_id }
Persistence.UnitDeadState = function(unitName)
if Persistence.isEnabled() == false then
return nil
end
local entry = tables.dead_units[unitName]
if entry then
return entry
else
return { isDead = false }
end
end
---Pass the unit to be saved as "dead"
---@param name string
---@param position Position { x, y ,z }
---@param heading number
---@param type string
---@param country_id number
Persistence.UnitKilled = function (name, position, heading, type, country_id)
if enabled == false then return end
tables.dead_units[name] = {
isDead = true,
pos = position,
heading = heading,
type = type,
country_id = country_id,
isCleaned = false
}
updateRequired = true
end
end
if Spearhead == nil then Spearhead = {} end
if Spearhead.classes == nil then Spearhead.classes = {} end
if Spearhead.classes.persistence == nil then Spearhead.classes.persistence = {} end
Spearhead.classes.persistence.Persistence = Persistence
+96 -46
View File
@@ -78,8 +78,14 @@ do -- INIT UTIL
---comment
---@param str string
---@param findable string
---@param ignoreCase boolean?
---@return boolean
UTIL.startswith = function(str, findable)
UTIL.startswith = function(str, findable, ignoreCase)
if ignoreCase == true then
return string.lower(str):find('^' .. string.lower(findable)) ~= nil
end
return str:find('^' .. findable) ~= nil
end
@@ -114,6 +120,14 @@ do -- INIT UTIL
end
end
---comment
---@param a table DCS Point vector {x, z , y}
---@param b table DCS Point vector {x, z , y}
---@return number
function UTIL.VectorDistance(a, b)
return math.sqrt((b.x - a.x) ^ 2 + (b.z - a.z) ^ 2)
end
---comment
---@param polygon table of pairs { x, z }
---@param x number X location
@@ -452,8 +466,8 @@ do -- INIT DCS_UTIL
return StaticObject.getByName(groupName) ~= nil
end
---comment
---@param groupName string destroy the given group
---destroy the given group
---@param groupName string
function DCS_UTIL.DestroyGroup(groupName)
if DCS_UTIL.IsGroupStatic(groupName) then
local object = StaticObject.getByName(groupName)
@@ -468,6 +482,22 @@ do -- INIT DCS_UTIL
end
end
---destroy the given unit
function DCS_UTIL.DestroyUnit(unitName)
if DCS_UTIL.IsGroupStatic(unitName) == true then
local object = StaticObject.getByName(unitName)
if object ~= nil then
object:destroy()
end
else
local unit = Unit.getByName(unitName)
if unit and unit:isExist() then
unit:destroy()
end
end
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
@@ -604,7 +634,7 @@ do -- INIT DCS_UTIL
--- 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
---@param zone_name string zone name
---@return boolean result
function DCS_UTIL.isPositionInZone(x, z, zone_name)
local zone = DCS_UTIL.__trigger_zones[zone_name]
@@ -786,6 +816,37 @@ do -- INIT DCS_UTIL
return result
end
---Spawn an corpse
---@param countryId number countryId
---@param unitType string
---@param location table { z, y, z}
---@param heading number
function DCS_UTIL.SpawnCorpse(countryId, unitName, unitType, location, heading)
local name = "dead_" .. unitName
local staticObj = {
["heading"] = heading,
--["shape_name"] = "stolovaya",
["type"] = unitType,
["name"] = name,
["y"] = location.z,
["x"] = location.x,
["dead"] = true,
}
coalition.addStaticObject(countryId, staticObj)
end
function DCS_UTIL.CleanCorpse(unitName)
local unitName = "dead_" .. unitName
local object = StaticObject.getByName(unitName)
if object then
object:destroy()
end
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
@@ -793,19 +854,20 @@ do -- INIT DCS_UTIL
---@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
---@return boolean? isStatic whether the group is a static or not
function DCS_UTIL.SpawnGroupTemplate(groupName, location, route, uncontrolled)
if groupName == nil then
return
return nil, nil
end
local template = DCS_UTIL.GetMizGroupOrDefault(groupName, nil)
if template == nil then
return nil
return nil, 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)
return coalition.addStaticObject(template.country_id, spawn_template), true
else
local spawn_template = template.group_template
if location ~= nil then
@@ -833,7 +895,7 @@ do -- INIT DCS_UTIL
spawn_template.uncontrolled = uncontrolled
end
local new_group = coalition.addGroup(template.country_id, template.category, spawn_template)
return new_group
return new_group, false
end
end
@@ -843,11 +905,14 @@ do -- INIT DCS_UTIL
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
return true
if group then
for _, unit in pairs(group:getUnits()) do
if unit and unit:isExist() == true and unit:inAir() == true and unit:getFuel() < bingoSetting then
return true
end
end
end
return false
end
@@ -857,22 +922,25 @@ 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)
--- @class Logger
--- @field debug fun(self:Logger, text:string)
--- @field info fun(self:Logger, text:string)
--- @field warn fun(self:Logger, text:string)
--- @field error fun(self:Logger, text:string)
---comment
---@param logger_name any
---@param logLevel LogLevel
---@return Logger
function LOGGER:new(logger_name, logLevel)
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
o.LogLevel = logLevel or "INFO"
---comment
---@param self table self logger
@@ -883,13 +951,9 @@ do
end
message = UTIL.toString(message)
if self.LogLevel <= LOGGER.LogLevelOptions.INFO then
if self.LogLevel == "INFO" or self.LogLevel == "DEBUG" then
env.info("[" .. PreFix .. "]" .. "[" .. self.LoggerName .. "] " .. message)
end
if self.LiveLoggingLevel <= LOGGER.LogLevelOptions.INFO then
trigger.action.outText(message, 20)
end
end
---comment
@@ -900,12 +964,8 @@ do
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)
if self.LogLevel == "INFO" or self.LogLevel == "DEBUG" or self.LogLevel == "WARN" then
env.info("[" .. PreFix .. "]" .. "[" .. self.LoggerName .. "] " .. message)
end
end
@@ -919,12 +979,8 @@ do
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)
if self.LogLevel == "INFO" or self.LogLevel == "DEBUG" or self.LogLevel == "WARN" or self.logLevel == "ERROR" then
env.info("[" .. PreFix .. "]" .. "[" .. self.LoggerName .. "] " .. message)
end
end
@@ -937,13 +993,9 @@ do
end
message = UTIL.toString(message)
if self.LogLevel <= LOGGER.LogLevelOptions.DEBUG then
if self.LogLevel == "DEBUG" then
env.info("[" .. PreFix .. "]" .. "[" .. self.LoggerName .. "][DEBUG] " .. message)
end
if self.LiveLoggingLevel <= LOGGER.LogLevelOptions.DEBUG then
trigger.action.outText(message, 20)
end
end
@@ -957,15 +1009,13 @@ function Spearhead.AddMissionEditorWarning(warningMessage)
table.insert(Spearhead.MissionEditingWarnings, warningMessage or "skip")
end
missionCommands.addSubMenu("Missions")
local loadDone = false
Spearhead.LoadingDone = function()
if loadDone == true then
return
end
local warningLogger = Spearhead.LoggerTemplate:new("MISSIONPARSER", Spearhead.LoggerTemplate.LogLevelOptions.INFO, 4)
local warningLogger = Spearhead.LoggerTemplate:new("MISSIONPARSER", "INFO")
if Spearhead.Util.tableLength(Spearhead.MissionEditingWarnings) > 0 then
for key, message in pairs(Spearhead.MissionEditingWarnings) do
warningLogger:warn(message)
+600 -596
View File
File diff suppressed because it is too large Load Diff
+74 -57
View File
@@ -1,26 +1,39 @@
local SpearheadEvents = {}
do
---@type Logger
local logger = nil
---@param logLevel LogLevel
SpearheadEvents.Init = function(logLevel)
logger = Spearhead.LoggerTemplate:new("Events", logLevel)
end
local warn = function(text)
if logger then
logger:warn(text)
end
end
local logError = function(text)
if logger then logger:error(text) end
end
local logDebug = function(text)
if logger then logger:debug(text) end
end
---@class OnStageChangedListener
---@field OnStageNumberChanged fun(self:OnStageChangedListener, number:integer)
do -- STAGE NUMBER CHANGED
local OnStageNumberChangedListeners = {}
local OnStageNumberChangedHandlers = {}
local warn = function(text)
env.warn("[Spearhead][Events] " .. (text or "nil"))
end
local error = function(text)
env.error("[Spearhead][Events] " .. (text or "nil"))
end
---Add a stage zone number changed listener
---@param listener table object with function OnStageNumberChanged(self, number)
---@param listener OnStageChangedListener object with function OnStageNumberChanged(self, number)
SpearheadEvents.AddStageNumberChangedListener = function(listener)
if type(listener) ~= "table" then
warn("Event listener not of type table, did you mean to use handler?")
return
end
table.insert(OnStageNumberChangedListeners, listener)
end
@@ -36,21 +49,27 @@ do
---@param newStageNumber number
SpearheadEvents.PublishStageNumberChanged = function(newStageNumber)
pcall(function ()
Spearhead.classes.persistence.Persistence.SetActiveStage(newStageNumber)
end)
for _, callable in pairs(OnStageNumberChangedListeners) do
local succ, err = pcall(function()
callable:OnStageNumberChanged(newStageNumber)
end)
if err then
error(err)
logError(err)
end
end
for _, callable in pairs(OnStageNumberChangedHandlers) do
local succ, err = pcall(callable, newStageNumber)
if err then
error(err)
logError(err)
end
end
Spearhead.StageNumber = newStageNumber
end
end
@@ -72,11 +91,15 @@ do
table.insert(onLandEventListeners[unitName], landListener)
end
---@class OnUnitLostListener
---@field OnUnitLost fun(self:OnUnitLostListener, unit:table)
---@type table<string,Array<OnUnitLostListener>>
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)
---@param unitLostListener OnUnitLostListener
SpearheadEvents.addOnUnitLostEventListener = function(unitName, unitLostListener)
if type(unitLostListener) ~= "table" then
warn("Unit lost Event listener not of type table/object")
@@ -119,7 +142,7 @@ do
callable:OnGroupRTB(groupName)
end)
if err then
error(err)
logError(err)
end
end
end
@@ -154,7 +177,7 @@ do
callable:OnGroupRTBInTen(groupName)
end)
if err then
error(err)
logError(err)
end
end
end
@@ -190,7 +213,7 @@ do
callable:OnGroupOnStation(groupName)
end)
if err then
error(err)
logError(err)
end
end
end
@@ -198,39 +221,6 @@ do
end
end
do --COMMANDS
do -- status updates
local onStatusRequestReceivedListeners = {}
---comment
---@param listener table object with OnStatusRequestReceived(self, groupId)
SpearheadEvents.AddOnStatusRequestReceivedListener = function(listener)
if type(listener) ~= "table" then
warn("Unit lost Event listener not of type table/object")
return
end
table.insert(onStatusRequestReceivedListeners, listener)
end
local triggerStatusRequestReceived = function(groupId)
for _, callable in pairs(onStatusRequestReceivedListeners) do
local succ, err = pcall(function()
callable:OnStatusRequestReceived(groupId)
end)
end
end
SpearheadEvents.AddCommandsToGroup = function(groupId)
local base = "MISSIONS"
if groupId then
missionCommands.addCommandForGroup(groupId, "Stage Status", nil, triggerStatusRequestReceived,
groupId)
end
end
end
end
do -- PLAYER ENTER UNIT
local playerEnterUnitListeners = {}
---comment
@@ -252,7 +242,7 @@ do
callable:OnPlayerEntersUnit(unit)
end)
if err then
error(err)
logError(err)
end
end
end
@@ -260,6 +250,20 @@ do
end
end
do -- Ejection events
local unitEjectListeners = {}
SpearheadEvents.AddOnUnitEjectedListener = function(listener)
if type(listener) ~= "table" then
warn("Unit lost Event listener not of type table/object")
return
end
table.insert(unitEjectListeners, listener)
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
@@ -273,7 +277,7 @@ do
callable:OnUnitLanded(unit, airbase)
end)
if err then
error(err)
logError(err)
end
end
end
@@ -285,6 +289,11 @@ do
event.id == world.event.S_EVENT_EJECTION or
event.id == world.event.S_EVENT_UNIT_LOST then
local object = event.initiator
if object and object.getName then
logDebug("Receiving death event from: " .. object:getName())
end
if object and object.getName and OnUnitLostListeners[object:getName()] then
for _, callable in pairs(OnUnitLostListeners[object:getName()]) do
local succ, err = pcall(function()
@@ -292,12 +301,20 @@ do
end)
if err then
error(err)
logError(err)
end
end
end
end
if event.id == world.event.S_EVENT_EJECTION then
end
if event.id == world.event.S_EVENT_MISSION_END then
Spearhead.classes.persistence.Persistence.UpdateNow()
end
local AI_GROUPS = {}
local function CheckAndTriggerSpawnAsync(unit, time)
+195 -21
View File
@@ -1,47 +1,221 @@
local StagesByName = {}
---@type table<string, Array<Stage>>
local StagesByIndex = {}
---@type table<string, Array<Stage>>
local SideStageByIndex = {}
---@type table<string, Array<WaitingStage>>
local WaitingStagesByIndex = {}
local currentStage = -99
GlobalStageManager = {}
---comment
---@param database Database
---@param stageConfig StageConfig
---@return nil
function GlobalStageManager:NewAndStart(database, stageConfig)
local logger = Spearhead.LoggerTemplate:new("StageManager", stageConfig.logLevel)
logger:info("Using Stage Log Level: " .. stageConfig.logLevel)
local o = {}
setmetatable(o, { __index = self })
o.logger = logger
o.onStageCompleted = function(self, stage)
local stageNumber = tostring(stage.stageNumber)
local anyActive = false
for _, stage in pairs(StagesByIndex[stageNumber] or {}) do
if stage.isActive then anyActive = true end
end
if anyActive == false and stageConfig:isAutoStages() == true then
Spearhead.Events.PublishStageNumberChanged(tonumber(stageNumber) + 1)
end
if stageConfig.isAutoStages ~= true then
logger:warn("Spearhead will not automatically progress stages due to the given settings. If you manually have implemented this, please ignore this message")
end
---@type OnStageChangedListener
local OnStageNumberChangedListener = {
OnStageNumberChanged = function (self, number)
currentStage = number
end
}
Spearhead.Events.AddStageNumberChangedListener(OnStageNumberChangedListener)
---@type StageCompleteListener
local OnStageCompleteListener = {
OnStageComplete = function(self, stage)
logger:debug("Receiving stage complete event from: " .. stage.zoneName)
local anyIncomplete = false
logger:debug("Checking stages for index: " .. tostring(currentStage))
for index, stage in pairs(StagesByIndex[tostring(currentStage)]) do
if stage:IsComplete() == false then
anyIncomplete = true
logger:debug("Need to wait for Stage " .. stage.zoneName .. " to be completed")
else
logger:debug("Stage verified to be completed: " .. stage.zoneName)
end
end
if anyIncomplete == false and stageConfig.isAutoStages == true then
-- CHECK WAITING STAGES
local nextStage = currentStage + 1
if WaitingStagesByIndex[tostring(nextStage)] then
for _, waitingStage in pairs(WaitingStagesByIndex[tostring(nextStage)]) do
if waitingStage:IsActive() == false then
waitingStage:ActivateStage()
end
end
end
local anyWaiting = false
if WaitingStagesByIndex[tostring(nextStage)] then
for _, waitingStage in pairs(WaitingStagesByIndex[tostring(nextStage)]) do
if waitingStage:IsComplete() == false then
anyWaiting = true
end
end
end
if anyWaiting == false then
logger:debug("Setting next stage to: " .. tostring(currentStage + 1))
Spearhead.Events.PublishStageNumberChanged(currentStage + 1)
end
end
end
}
for _, stageName in pairs(database:getStagezoneNames()) do
local stagelogger = Spearhead.LoggerTemplate:new(stageName, stageConfig.logLevel)
local stage = Spearhead.internal.Stage:new(stageName, database, stagelogger, stageConfig)
if Spearhead.Util.startswith(stageName, "missionstage", true) then
local valid = true
local split = Spearhead.Util.split_string(stageName, "_")
if Spearhead.Util.tableLength(split) < 2 then
Spearhead.AddMissionEditorWarning("Stage zone with name " .. stageName .. " does not have a order number or valid format")
valid = false
end
if stage then
stage:AddStageCompleteListener(o);
StagesByName[stageName] = stage
local indexString = tostring(stage.stageNumber)
if StagesByIndex[indexString] == nil then StagesByIndex[indexString] = {} end
table.insert(StagesByIndex[indexString], stage)
logger:info("Initiated " .. Spearhead.Util.tableLength(StagesByName) .. " airbases for cap")
if Spearhead.Util.tableLength(split) < 3 then
Spearhead.AddMissionEditorWarning("Stage zone with name " .. stageName .. " does not have a stage name")
valid = false
end
local orderNumber = nil
local isSideStage = false
if valid == true then
local orderNumberString = string.lower(split[2])
if Spearhead.Util.startswith(orderNumberString, "x") == true then
isSideStage = true
local orderNumberString = string.gsub(orderNumberString, "x", "")
orderNumber = tonumber(orderNumberString)
else
orderNumber = tonumber(split[2])
end
if orderNumber == nil then
Spearhead.AddMissionEditorWarning("Stage zone with name " .. stageName .. " does not have a valid order number : " .. split[2])
valid = false
end
end
local stageDisplayName = split[3]
local stagelogger = Spearhead.LoggerTemplate:new(stageName, stageConfig.logLevel)
if valid == true and orderNumber then
---@type StageInitData
local initData = {
stageDisplayName = stageDisplayName,
stageNumber = orderNumber,
stageZoneName = stageName,
}
if isSideStage == true then
local stage = Spearhead.classes.stageClasses.Stages.ExtraStage.New(database, stageConfig, stagelogger, initData)
stage:AddStageCompleteListener(OnStageCompleteListener)
if SideStageByIndex[tostring(orderNumber)] == nil then SideStageByIndex[tostring(orderNumber)] = {} end
table.insert(SideStageByIndex[tostring(orderNumber)], stage)
else
local stage = Spearhead.classes.stageClasses.Stages.PrimaryStage.New(database, stageConfig, stagelogger, initData)
stage:AddStageCompleteListener(OnStageCompleteListener)
if StagesByIndex[tostring(orderNumber)] == nil then StagesByIndex[tostring(orderNumber)] = {} end
table.insert(StagesByIndex[tostring(orderNumber)], stage)
end
end
end
if Spearhead.Util.startswith(stageName, "waitingstage", true) then
local valid = true
local split = Spearhead.Util.split_string(stageName, "_")
if Spearhead.Util.tableLength(split) < 3 then
Spearhead.AddMissionEditorWarning("Stage zone with name " .. stageName .. " does not have a order number or valid format")
valid = false
end
if valid == true then
local stageIndexString = split[2]
local stageIndex = tonumber(stageIndexString)
if not stageIndex then
Spearhead.AddMissionEditorWarning("Stage zone with name " .. stageName .. " does not have a valid order number")
valid = false
end
local waitingSecondsString = split[3]
local waitingSeconds = tonumber(waitingSecondsString)
if not waitingSeconds then
Spearhead.AddMissionEditorWarning("Waiting Stage zone with name " .. stageName .. " does not have a valid amount of seconds parameter")
valid = false
end
if valid == true then
local stagelogger = Spearhead.LoggerTemplate:new(stageName, stageConfig.logLevel)
---@type WaitingStageInitData
local initData = {
stageDisplayName = "Waiting Stage " .. stageIndex,
stageNumber = stageIndex or -99,
stageZoneName = stageName,
waitingSeconds = waitingSeconds --[[@as integer]]
}
local waitingStage = Spearhead.classes.stageClasses.Stages.WaitingStage.New(database, stageConfig, stagelogger, initData)
if WaitingStagesByIndex[tostring(stageIndex)] == nil then
WaitingStagesByIndex[tostring(stageIndex)] = {}
end
table.insert(WaitingStagesByIndex[tostring(stageIndex)], waitingStage)
waitingStage:AddStageCompleteListener(OnStageCompleteListener)
end
end
end
end
return o
end
---comment
---@param stageNumber number
---@return boolean | nil
GlobalStageManager.isStageComplete = function (stageNumber)
local stageIndex = tostring(stageNumber)
if StagesByIndex[stageIndex] == nil then return nil end
for _, stage in ipairs(StagesByIndex[stageIndex]) do
if stage:IsComplete() == false then
return false
end
end
return true
end
if not Spearhead.internal then Spearhead.internal = {} end
Spearhead.internal.GlobalStageManager = GlobalStageManager
@@ -0,0 +1,115 @@
---@class SpearheadGroup : OnUnitLostListener
---@field groupName string
---@field private isStatic boolean
---@field private isSpawned boolean
local SpearheadGroup = {}
function SpearheadGroup.New(groupName)
SpearheadGroup.__index = SpearheadGroup
local o = {}
local self = setmetatable(o, SpearheadGroup)
self.isStatic = Spearhead.DcsUtil.IsGroupStatic(groupName) == true
self.groupName = groupName
self.isSpawned = false
return self
end
function SpearheadGroup:SpawnCorpsesOnly()
if self.isSpawned == true then return end
local group = Spearhead.DcsUtil.SpawnGroupTemplate(self.groupName)
if group then
for _, unit in pairs(group:getUnits()) do
local deathState = Spearhead.classes.persistence.Persistence.UnitDeadState(unit:getName())
if deathState and deathState.isDead == true then
Spearhead.DcsUtil.DestroyUnit(self.groupName, unit:getName())
Spearhead.DcsUtil.SpawnCorpse(deathState.country_id, unit:getName(), deathState.type, deathState.pos, deathState.heading)
end
end
end
self.isSpawned = true
end
function SpearheadGroup:Spawn()
if self.isSpawned == true then return end
local group = Spearhead.DcsUtil.SpawnGroupTemplate(self.groupName)
if group then
for _, unit in pairs(group:getUnits()) do
local deathState = Spearhead.classes.persistence.Persistence.UnitDeadState(unit:getName())
if deathState and deathState.isDead == true then
Spearhead.DcsUtil.DestroyUnit(self.groupName, unit:getName())
Spearhead.DcsUtil.SpawnCorpse(deathState.country_id, unit:getName(), deathState.type, deathState.pos, deathState.heading)
else
Spearhead.Events.addOnUnitLostEventListener(unit:getName(), self)
end
Spearhead.Events.addOnUnitLostEventListener(unit:getName(), self)
end
end
self.isSpawned = true
end
function SpearheadGroup:Destroy()
self.isSpawned = false
Spearhead.DcsUtil.DestroyGroup(self.groupName)
end
---comment
---@return integer
function SpearheadGroup:GetCoalition()
if self.isStatic == true then
local object = StaticObject.getByName(self.groupName)
return object:getCoalition()
else
local group = Group.getByName(self.groupName)
return group:getCoalition()
end
end
function SpearheadGroup:OnUnitLost(object)
local name = object:getName()
local pos = object:getPoint()
local type = object:getDesc().typeName
local position = object:getPosition()
local heading = math.atan2(position.x.z, position.x.x)
local country_id = object:getCountry()
Spearhead.classes.persistence.Persistence.UnitKilled(name, pos, heading, type, country_id)
end
---comment
---@return table result list of objects
function SpearheadGroup:GetUnits()
local result = {}
if self.isStatic == true then
local staticObject = StaticObject.getByName(self.groupName)
if staticObject then
table.insert(result, staticObject)
end
else
local group = Group.getByName(self.groupName)
for _, unit in pairs(group:getUnits()) do
table.insert(result, unit)
end
end
return result
end
if not Spearhead.classes then Spearhead.classes = {} end
if not Spearhead.classes.stageClasses then Spearhead.classes.stageClasses = {} end
if not Spearhead.classes.stageClasses.Groups then Spearhead.classes.stageClasses.Groups = {} end
Spearhead.classes.stageClasses.Groups.SpearheadGroup = SpearheadGroup
-401
View File
@@ -1,401 +0,0 @@
--- 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.missionTypeDisplayName = Mission.MissionType.toString(o.missionType)
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.groupNamesPerUnit = {}
o.groupUnitAliveDict = {}
o.targetAliveStates = {}
o.hasSpecificTargets = false
local CheckStateAsync = function (self, time)
self:CheckAndUpdateSelf()
return nil
end
o.GetState = function(self)
return self.missionState
end
o.OnUnitLost = function(self, object)
--[[
OnUnit lost event
]]--
self.logger:debug("Getting on unit lost event")
local category = Object.getCategory(object)
if category == Object.Category.UNIT then
local unitName = object:getName()
self.logger:debug("UnitName:" .. unitName)
local groupName = self.groupNamesPerUnit[unitName]
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
self.logger:debug("Name " .. name)
if self.targetAliveStates[name][name] then
self.targetAliveStates[name][name] = false
end
end
timer.scheduleFunction(CheckStateAsync, self, timer.getTime() + 1)
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
---comment
---@param self table
---@param checkUnitHealth boolean?
o.CheckAndUpdateSelf = function(self, checkUnitHealth)
if not checkUnitHealth then checkUnitHealth = false end
if checkUnitHealth == true then
local function unitAliveState(unitName)
local unit = Unit.getByName(unitName)
return unit ~= nil and unit:isExist() == true and unit:getLife() > 0.1
end
for groupName, unitNameDict in pairs(self.groupUnitAliveDict) do
for unitName, isAlive in pairs(unitNameDict) do
if isAlive == true then
self.groupUnitAliveDict[groupName][unitName] = unitAliveState(unitName)
end
end
end
for groupName, unitNameDict in pairs(self.targetAliveStates) do
for unitName, isAlive in pairs(unitNameDict) do
if isAlive == true then
self.targetAliveStates[groupName][unitName] = unitAliveState(unitName)
end
end
end
end
if self.missionState == Mission.MissionState.COMPLETED then
return
end
if self.hasSpecificTargets == true then
local specificTargetsAlive = false
for groupName, unitNameDict in pairs(self.targetAliveStates) do
for unitName, isAlive in pairs(unitNameDict) do
if isAlive == true then
specificTargetsAlive = true
end
end
end
if specificTargetsAlive == false then
self.missionState = Mission.MissionState.COMPLETED
end
else
local function CountAliveGroups()
self.logger:debug(self.groupUnitAliveDict)
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 = aliveGroups + 1
end
end
return aliveGroups
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
self.logger:debug("Mission complete " .. self.name)
trigger.action.outText("Mission " .. self.name .. " (" .. self.code .. ") was completed succesfully!", 20)
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
local ToStateString = function(self)
if self.hasSpecificTargets then
local dead = 0
local total = 0
for _, group in pairs(self.targetAliveStates) do
for _, isAlive in pairs(group) do
total = total + 1
if isAlive == false then
dead = dead + 1
end
end
end
local completionPercentage = math.floor((dead / total) * 100)
return "Targets Destroyed: " .. completionPercentage .. "%"
else
local dead = 0
local total = 0
for _, group in pairs(self.groupUnitAliveDict) do
for _, isAlive in pairs(group) do
total = total + 1
if isAlive == false then
dead = dead + 1
end
end
end
local completionPercentage = math.floor((dead / total) * 100)
return "Targets Destroyed: " .. completionPercentage .. "%"
end
end
o.ShowBriefing = function(self, groupId)
local stateString = ToStateString(self)
if self.missionbriefing == nil then self.missionbriefing = "No briefing available" end
local text = "Mission [" .. self.code .. "] ".. self.name .. "\n \n" .. self.missionbriefing .. " \n \n" .. stateString
trigger.action.outTextForGroup(groupId, 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
Spearhead.Events.addOnUnitLostEventListener(group_name, self)
if Spearhead.Util.startswith(group_name, "TGT_") == true then
self.targetAliveStates[group_name][group_name] = true
self.hasSpecificTargets = 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()
self.groupNamesPerUnit[unitName] = group_name
Spearhead.Events.addOnUnitLostEventListener(unitName, self)
if isGroupTarget == true or Spearhead.Util.startswith(unitName, "TGT_") == true then
self.targetAliveStates[group_name][unitName] = true
self.hasSpecificTargets = true
end
if self.missionType == MissionType.BAI then
if Spearhead.DcsUtil.IsGroupStatic(group_name) ~= true then
self.groupUnitAliveDict[group_name][unitName] = true
end
elseif self.missionType == MissionType.DEAD or self.missionType == MissionType.SAM then
local desc = unit:getDesc()
local attributes = desc.attributes
if attributes["SAM"] == true or attributes["SAM TR"] or attributes["AAA"] then
self.targetAliveStates[group_name][unitName] = true
self.hasSpecificTargets = true
end
else
self.groupUnitAliveDict[group_name][unitName] = true
end
end
end
Spearhead.DcsUtil.DestroyGroup(group_name)
end
end
Init(o)
return o;
end
end
if not Spearhead.internal then Spearhead.internal = {} end
Spearhead.internal.Mission = Mission
+404
View File
@@ -0,0 +1,404 @@
---@class Mission : OnUnitLostListener
---@field name string
---@field missionType missionType
---@field code string
---@field priority MissionPriority
---@field private _state MissionState
---@field private _zoneName string
---@field private _database Database
---@field private _logger Logger
---@field private _missionBriefing string
---@field private _missionGroups MissionGroups
---@field private _completeListeners Array<MissionCompleteListener>
local Mission = {}
--- @class MissionCompleteListener
--- @field OnMissionComplete fun(self: any, mission:Mission)
--- @class MissionGroups
--- @field hasTargets boolean
--- @field groups Array<SpearheadGroup>
--- @field unitsAlive table<string, table<string, boolean>>
--- @field targetsAlive table<string, table<string, boolean>>
--- @field groupNamesPerunit table<string,string>
MINIMAL_UNITS_ALIVE_RATIO = 0.21
---comment
---@param zoneName string
---@param priority MissionPriority
---@param database Database
---@param logger Logger
---@return Mission?
function Mission.New(zoneName, priority, database, logger)
local function ParseZoneName(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
---@type missionType
local parsedType = "nil"
local inputType = string.lower(split_name[2])
if inputType == "dead" then parsedType = "DEAD" end
if inputType == "strike" then parsedType = "STRIKE" end
if inputType == "bai" then parsedType = "BAI" end
if inputType == "sam" then parsedType = "SAM" end
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 = ParseZoneName(zoneName)
if parsed == nil then return end
Mission.__index = Mission
local o = {}
local self = setmetatable(o, Mission)
self._zoneName = zoneName
self.name = parsed.missionName
self.missionType = parsed.type
self.code = tostring(database:GetNewMissionCode())
self.priority = priority
self._state = "NEW"
self._logger = logger
self._database = database
self._completeListeners = {}
self._missionBriefing = database:getMissionBriefingForMissionZone(zoneName)
self._missionGroups = {
groups = {},
unitsAlive = {},
targetsAlive = {},
hasTargets = false,
groupNamesPerunit = {}
}
local SpearheadGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup
local groupNames = database:getGroupsForMissionZone(zoneName)
for _, groupName in pairs(groupNames) do
local spearheadGroup = SpearheadGroup.New(groupName)
table.insert(self._missionGroups.groups, spearheadGroup)
local isGroupTarget =Spearhead.Util.startswith(string.lower(groupName), "tgt_")
for _, unit in pairs(spearheadGroup:GetUnits())do
local unitName = unit:getName()
local isUnitTarget = Spearhead.Util.startswith(string.lower(unitName), "tgt_")
if self._missionGroups.unitsAlive[groupName] == nil then
self._missionGroups.unitsAlive[groupName] = {}
end
self._missionGroups.unitsAlive[groupName][unitName] = true
self._missionGroups.groupNamesPerunit[unitName] = groupName
if isGroupTarget == true or isUnitTarget == true then
self._missionGroups.hasTargets = true
if self._missionGroups.targetsAlive[groupName] == nil then
self._missionGroups.targetsAlive[groupName] = {}
end
self._missionGroups.targetsAlive[groupName][unitName] = true
end
Spearhead.Events.addOnUnitLostEventListener(unitName, self)
end
Spearhead.DcsUtil.DestroyGroup(groupName)
end
return self
end
---comment
---@return MissionState
function Mission:GetState()
return self._state
end
function Mission:SpawnPersistedState()
for _, group in pairs(self._missionGroups.groups) do
group:SpawnCorpsesOnly()
end
end
function Mission:SpawnActive()
self._logger:info("Activating " .. self.name)
self._state = "ACTIVE"
for _, group in pairs(self._missionGroups.groups) do
group:Spawn()
end
Spearhead.classes.stageClasses.helpers.MissionCommandsHelper.AddMissionToCommands(self)
self:StartCheckingContinuous()
end
---@private
function Mission:StartCheckingContinuous()
---comment
---@param mission Mission
---@param time any
---@return unknown
local Check = function (mission, time)
mission:UpdateState(true, true)
if mission:GetState() == "COMPLETED" then
return nil
end
return time + 30
end
timer.scheduleFunction(Check, self, timer.getTime() + 30)
end
---@private
---@return string?
function Mission:ToStateString()
if self._missionGroups.hasTargets == true then
local dead = 0
local total = 0
if self._missionGroups.targetsAlive then
for _, group in pairs(self._missionGroups.targetsAlive) do
for _, isAlive in pairs(group) do
total = total + 1
if isAlive == false then
dead = dead + 1
end
end
end
end
if total > 0 then
local completionPercentage = math.floor((dead / total) * 100)
return "Targets Destroyed: " .. completionPercentage .. "%"
end
else
local dead = 0
local total = 0
if self._missionGroups.unitsAlive then
for _, group in pairs(self._missionGroups.unitsAlive) do
for _, isAlive in pairs(group) do
total = total + 1
if isAlive == false then
dead = dead + 1
end
end
end
end
if total > 0 then
local completionPercentage = math.floor((dead / total) * 100)
return "Units Destroyed: " .. completionPercentage .. "%"
end
end
end
---comment
---@param groupId integer
function Mission:ShowBriefing(groupId)
local stateString = self:ToStateString()
if self._missionBriefing == nil or self._missionBriefing == "" then self._missionBriefing = "No briefing available" end
local text = "Mission [" .. self.code .. "] ".. self.name .. "\n \n" .. self._missionBriefing .. " \n \n" .. stateString
trigger.action.outTextForGroup(groupId, text, 30);
end
---@param checkHealth boolean
---@param messageIfDone boolean
function Mission:UpdateState(checkHealth, messageIfDone)
if checkHealth == nil then checkHealth = false end
if messageIfDone == false then messageIfDone = true end
if self._state == "COMPLETED" then
return
end
if checkHealth == true then
local function unitAliveState(unitName)
local staticObject = StaticObject.getByName(unitName)
if staticObject then
if staticObject:isExist() == true then
local life0 = staticObject:getDesc().life
if staticObject:getLife() / life0 < 0.3 then
self._logger:debug("exploding unit")
trigger.action.explosion(staticObject:getPoint(), 100)
return false
end
return true
else
return false
end
else
local unit = Unit.getByName(unitName)
local alive = unit ~= nil and unit:isExist() == true
if alive == true then
if unit:getLife() / unit:getLife0() < 0.2 then
self._logger:debug("exploding unit")
trigger.action.explosion(unit:getPoint(), 100)
return false
end
return true
else
return false
end
end
end
for groupName, unitNameDict in pairs(self._missionGroups.unitsAlive) do
for unitName, isAlive in pairs(unitNameDict) do
if isAlive == true then
self._missionGroups.unitsAlive[groupName][unitName] = unitAliveState(unitName)
end
end
end
for groupName, unitNameDict in pairs(self._missionGroups.targetsAlive) do
for unitName, isAlive in pairs(unitNameDict) do
if isAlive == true then
self._missionGroups.targetsAlive[groupName][unitName] = unitAliveState(unitName)
end
end
end
end
if self._missionGroups.hasTargets == true then
local anyTargetAlive = function()
for _, units in pairs(self._missionGroups.targetsAlive) do
for _, isAlive in pairs(units) do
if isAlive == true then
return true
end
end
end
return false
end
if anyTargetAlive() ~= true then
self._state = "COMPLETED"
end
else
local function CountAliveGroups()
local aliveGroups = 0
for _, group in pairs(self._missionGroups.unitsAlive) 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 = aliveGroups + 1
end
end
return aliveGroups
end
if CountAliveGroups() == 0 then
self._state = "COMPLETED"
end
end
---comment
---@param mission Mission
local NotifyMissionComplete = function(mission)
mission:NotifyMissionComplete()
return nil
end
if self._state == "COMPLETED" then
timer.scheduleFunction(NotifyMissionComplete, self, timer.getTime() + 3)
end
end
---private usage advised
function Mission:NotifyMissionComplete()
Spearhead.classes.stageClasses.helpers.MissionCommandsHelper.RemoveMissionToCommands(self)
self._logger:info("Mission Completed: " .. self._zoneName)
trigger.action.outText("Mission " .. self.name .. " [" .. self.code .. "] was completed succesfully" , 20)
for _, listener in pairs(self._completeListeners) do
pcall(function()
listener:OnMissionComplete(self)
end)
end
end
---@param listener MissionCompleteListener Object that implements "OnMissionComplete(self, mission)"
function Mission:AddMissionCompleteListener(listener)
if type(listener) ~= "table" then
return
end
table.insert(self._completeListeners, listener)
end
function Mission:OnUnitLost(object)
--[[
OnUnit lost event
]]--
self._logger:debug("Getting on unit lost event")
local category = Object.getCategory(object)
if category == Object.Category.UNIT then
local unitName = object:getName()
self._logger:debug("UnitName:" .. unitName)
local groupName = self._missionGroups.groupNamesPerunit[unitName]
self._missionGroups.unitsAlive[groupName][unitName] = false
if self._missionGroups.targetsAlive[groupName] and self._missionGroups.targetsAlive[groupName][unitName] then
self._missionGroups.targetsAlive[groupName][unitName] = false
end
elseif category == Object.Category.STATIC then
local name = object:getName()
self._missionGroups.unitsAlive[name][name] = false
self._logger:debug("Name " .. name)
if self._missionGroups.targetsAlive[name] and self._missionGroups.targetsAlive[name][name] then
self._missionGroups.targetsAlive[name][name] = false
end
end
self:UpdateState(false, true)
end
if not Spearhead.classes then Spearhead.classes = {} end
if not Spearhead.classes.stageClasses then Spearhead.classes.stageClasses = {} end
if not Spearhead.classes.stageClasses.Missions then Spearhead.classes.stageClasses.Missions = {} end
Spearhead.classes.stageClasses.Missions.Mission = Mission
@@ -0,0 +1,91 @@
---@class BlueSam
---@field Activate fun(self: BlueSam)
---@field private _database Database
---@field private _logger Logger
---@field private _zoneName string
---@field private _blueGroups Array<SpearheadGroup>
---@field private _cleanupUnits table<string, boolean>
local BlueSam = {}
function BlueSam.New(database, logger, zoneName)
BlueSam.__index = BlueSam
local self = setmetatable({}, BlueSam)
self._database = database
self._logger = logger
self._zoneName = zoneName
self._blueGroups = {}
self._cleanupUnits = {}
do
local groups = database:getBlueSamGroupsInZone(zoneName)
local blueUnitsPos = {}
local redUnitsPos = {}
for _, groupName in pairs(groups) do
local SpearheadGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName)
if SpearheadGroup then
if SpearheadGroup:GetCoalition() == 2 then
table.insert(self._blueGroups, SpearheadGroup)
end
for _, unit in pairs(SpearheadGroup:GetUnits()) do
if SpearheadGroup:GetCoalition() == 1 then
table.insert(blueUnitsPos, unit:getPoint())
elseif SpearheadGroup:GetCoalition() == 2 then
table.insert(redUnitsPos, unit:getPoint())
end
end
end
SpearheadGroup:Destroy()
end
do -- check cleanup requirements
-- Checks is any of the units are withing range (5m) of another unit.
-- If so, make sure to add them to the cleanup list.
local cleanup_distance = 5
for blueUnitName, blueUnitPos in pairs(blueUnitsPos) do
for redUnitName, redUnitPos in pairs(redUnitsPos) do
local distance = Spearhead.Util.VectorDistance(blueUnitPos, redUnitPos)
env.info("distance: " .. tostring(distance))
if distance <= cleanup_distance then
self._cleanupUnits[redUnitName] = true
end
end
end
end
end
return self
end
function BlueSam:Activate()
for unitName, needsCleanup in pairs(self._cleanupUnits) do
if needsCleanup == true then
Spearhead.DcsUtil.DestroyUnit(unitName)
else
local deathState = Spearhead.classes.persistence.Persistence.UnitDeadState(unitName)
if deathState and deathState.isDead == true then
Spearhead.DcsUtil.SpawnCorpse(deathState.country_id, unitName, deathState.type, deathState.pos, deathState.heading)
end
end
end
for _, group in pairs(self._blueGroups) do
group:Spawn()
end
end
if Spearhead == nil then Spearhead = {} end
if Spearhead.classes == nil then Spearhead.classes = {} end
if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end
if Spearhead.classes.stageClasses.SpecialZones == nil then Spearhead.classes.stageClasses.SpecialZones = {} end
Spearhead.classes.stageClasses.Missions.SpecialZones = BlueSam
@@ -0,0 +1,159 @@
---@class StageBase
---@field private _database Database
---@field private _logger Logger
---@field private _red_groups Array<SpearheadGroup>
---@field private _blue_groups Array<SpearheadGroup>
---@field private _cleanup_units table<string, boolean>
---@field private _airbase table?
---@field private _initialSide number?
local StageBase = {}
---comment
---@param databaseManager table
---@param logger table
---@param airbaseId integer
---@return StageBase
function StageBase.New(databaseManager, logger, airbaseId)
StageBase.__index = StageBase
local self = setmetatable({}, StageBase)
self._database = databaseManager
self._logger = logger
self._red_groups = {}
self._blue_groups = {}
self._cleanup_units = {}
self._airbase = Spearhead.DcsUtil.getAirbaseById(airbaseId)
self._initialSide = Spearhead.DcsUtil.getStartingCoalition(airbaseId)
do --init
local redUnitsPos = {}
local blueUnitsPos = {}
do -- fill tables
local redGroups = databaseManager:getRedGroupsAtAirbase(airbaseId)
if redGroups then
for _, groupName in pairs(redGroups) do
local shGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName)
table.insert(self._red_groups, shGroup)
for _, unit in shGroup:GetUnits() do
redUnitsPos[unit:getName()] = unit:getPoint()
end
shGroup:Destroy()
end
end
local blueGroups = databaseManager:getBlueGroupsAtAirbase(airbaseId)
if blueGroups then
for _, groupName in pairs(blueGroups) do
local shGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup.New(groupName)
table.insert(self._blue_groups, shGroup)
for _, unit in shGroup:GetUnits() do
blueUnitsPos[unit:getName()] = unit:getPoint()
end
shGroup:Destroy()
end
end
end
do -- check cleanup requirements
-- Checks is any of the units are withing range (5m) of another unit.
-- If so, make sure to add them to the cleanup list.
local cleanup_distance = 5
for blueUnitName, blueUnitPos in pairs(blueUnitsPos) do
for redUnitName, redUnitPos in pairs(redUnitsPos) do
local distance = Spearhead.Util.VectorDistance(blueUnitPos, redUnitPos)
env.info("distance: " .. tostring(distance))
if distance <= cleanup_distance then
self._cleanup_units[redUnitName] = true
end
end
end
end
end
return self
end
---@private
function StageBase:SpawnRedUnits()
---comment
---@param groups Array<SpearheadGroup>
local spawnAsync = function(groups)
for _, group in pairs(groups) do
group:Spawn()
end
return nil
end
timer.scheduleFunction(spawnAsync, self._red_groups, timer.getTime() + 3)
end
---@private
function StageBase:CleanRedUnits()
for _, value in pairs(self._red_groups) do
value:SpawnCorpsesOnly()
end
for _, unitName in pairs(self._cleanup_units) do
Spearhead.DcsUtil.DestroyUnit(unitName)
Spearhead.DcsUtil.CleanCorpse(unitName)
end
end
---@private
function StageBase:SpawnBlueUnits()
---comment
---@param groups Array<SpearheadGroup>
local spawnAsync = function(groups)
for _, group in pairs(groups) do
group:Spawn()
end
return nil
end
timer.scheduleFunction(spawnAsync, self._blue_groups, timer.getTime() + 3)
end
function StageBase:ActivateRedStage()
if self._initialSide == 2 and self._airbase then
self._airbase:setCoalition(1)
self._airbase:autoCapture(false)
end
self:SpawnRedUnits()
end
function StageBase:ActivateBlueStage()
if self._initialSide == 2 and self._airbase then
self._airbase:setCoalition(2)
end
self:CleanRedUnits()
self:SpawnBlueUnits()
end
if Spearhead == nil then Spearhead = {} end
if Spearhead.classes == nil then Spearhead.classes = {} end
if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end
if Spearhead.classes.stageClasses.SpecialZones == nil then Spearhead.classes.stageClasses.SpecialZones = {} end
Spearhead.classes.stageClasses.SpecialZones.StageBase = StageBase
-497
View File
@@ -1,497 +0,0 @@
local Stage = {}
do --init STAGE DIRECTOR
local stageDrawingId = 0
---comment
---@param stagezone_name string
---@param database table
---@param logger table
---@return table?
function Stage:new(stagezone_name, database, logger, stageConfig)
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.isActive = false
o.database = database
o.logger = logger
o.db = {}
o.db.missionsByCode = {}
o.db.missions = {}
o.db.sams = {}
o.db.redAirbasegroups = {}
o.db.blueAirbasegroups = {}
o.db.blueSamGroups = {}
o.db.airbaseIds = {}
o.db.farps = {}
o.activeStage = -99
o.preActivated = false
o.stageConfig = stageConfig or {}
o.stageDrawingId = stageDrawingId + 1
stageDrawingId = stageDrawingId + 1
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, logger)
if mission then
o.db.missionsByCode[mission.code] = mission
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, logger)
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
o.db.missionsByCode[mission.code] = mission
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
table.insert(o.db.redAirbasegroups, groupName)
Spearhead.DcsUtil.DestroyGroup(groupName)
end
for _, groupName in pairs(database:getBlueGroupsAtAirbase(airbaseId)) do
table.insert(o.db.blueAirbasegroups, groupName)
Spearhead.DcsUtil.DestroyGroup(groupName)
end
end
end
for _, samZoneName in pairs(database:getBlueSamsInStage(o.zoneName)) do
for _, samGroup in pairs(database:getBlueSamGroupsInZone(samZoneName)) do
table.insert(o.db.blueSamGroups, samGroup)
Spearhead.DcsUtil.DestroyGroup(samGroup)
end
end
local miscGroups = database:getMiscGroupsAtStage(o.zoneName)
for _, groupName in pairs(miscGroups) do
Spearhead.DcsUtil.DestroyGroup(groupName)
end
local farps = database:getFarpZonesInStage(o.zoneName)
if farps ~= nil and type(farps) == "table" then o.db.farps = farps end
end
o.StageCompleteListeners = {}
---comment
---@param self table
---@param StageCompleteListener table an Object with function onStageCompleted(stage)
o.AddStageCompleteListener = function(self, StageCompleteListener)
if type(StageCompleteListener) ~= "table" then
return
end
table.insert(self.StageCompleteListeners, StageCompleteListener)
end
local triggerStageCompleteListeners = function(self)
self.isActive = false
for _, callable in pairs(self.StageCompleteListeners) do
local succ, err = pcall( function()
callable:onStageCompleted(self)
end)
if err then
self.logger:warn("Error in misstion complete listener:" .. err)
end
end
end
o.IsComplete = function(self)
for i, mission in pairs(self.db.missions) do
local state = mission:GetState()
if state == Spearhead.internal.Mission.MissionState.ACTIVE or state == Spearhead.internal.Mission.MissionState.NEW then
return false
end
end
return true
end
local CheckContinuousAsync = function(self, time)
self.logger:info("Checking stage completion for stage: " .. self.zoneName)
if self.activeStage == self.stageNumber then
return nil -- stop looping if this stage is not even active
end
if self:IsComplete() == true then
triggerStageCompleteListeners(self)
return nil
end
return time + 60
end
---Activates all SAMS, Airbase units etc all at once.
---@param self table
o.PreActivate = function(self)
if self.preActivated == false then
self.preActivated = true
for key, mission in pairs(self.db.sams) do
if mission and mission.Activate then
mission:Activate()
end
end
self.logger:debug("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
if self.activeStage == self.stageNumber then
for _, mission in pairs(self.db.sams) do
self:AddCommmandsForMissionToAllPlayers(mission)
end
end
end
local activateMissionsIfApplicableAsync = function(self)
self:ActivateMissionsIfApplicable(self)
end
o.MarkStage = function(self, blue)
local fillColor = {1, 0, 0, 0.1}
local line ={ 1, 0,0, 1 }
if blue == true then
fillColor = {0, 0, 1, 0.1}
line ={ 0, 0,1, 1 }
end
local zone = Spearhead.DcsUtil.getZoneByName(self.zoneName)
if zone and self.stageConfig:isDrawStagesEnabled() == true then
self.logger:debug("drawing stage")
if zone.zone_type == Spearhead.DcsUtil.ZoneType.Cilinder then
trigger.action.circleToAll(-1, self.stageDrawingId, {x = zone.x, y = 0 , z = zone.z}, zone.radius, {0,0,0,0}, {0,0,0,0},4, true)
else
--trigger.action.circleToAll(-1, self.stageDrawingId, {x = zone.x, y = 0 , z = zone.z}, zone.radius, { 1, 0,0, 1 }, {1,0,0,1},4, true)
trigger.action.quadToAll( -1, self.stageDrawingId, zone.verts[1], zone.verts[2], zone.verts[3], zone.verts[4], {0,0,0,0}, {0,0,0,0}, 4, true)
end
trigger.action.setMarkupColorFill(self.stageDrawingId, fillColor)
trigger.action.setMarkupColor(self.stageDrawingId, line)
end
end
o.ActivateStage = function(self)
self.isActive = true;
pcall(function()
self:MarkStage()
end)
self:PreActivate()
local miscGroups = self.database:getMiscGroupsAtStage(self.zoneName)
self.logger:debug("Activating Misc groups for zone: " .. Spearhead.Util.tableLength(miscGroups))
for _, groupName in pairs(miscGroups) do
Spearhead.DcsUtil.SpawnGroupTemplate(groupName)
end
for _, mission in pairs(self.db.missions) do
if mission.missionType == Spearhead.internal.Mission.MissionType.DEAD then
mission:Activate()
self:AddCommmandsForMissionToAllPlayers(mission)
end
end
timer.scheduleFunction(activateMissionsIfApplicableAsync, self, timer.getTime() + 5)
timer.scheduleFunction(CheckContinuousAsync, self, timer.getTime() + 60)
end
o.ActivateMissionsIfApplicable = function (self)
local activeCount = 0
local availableMissions = {}
for _, mission in pairs(self.db.missionsByCode) do
local state = mission:GetState()
if state == Spearhead.internal.Mission.MissionState.ACTIVE then
activeCount = activeCount + 1
end
if state == Spearhead.internal.Mission.MissionState.NEW then
table.insert(availableMissions, mission)
end
end
local max = self.stageConfig:getMaxMissionsPerStage() or 10
local availableMissionsCount = Spearhead.Util.tableLength(availableMissions)
if activeCount < max and availableMissionsCount > 0 then
for i = activeCount+1, max do
if availableMissionsCount == 0 then
i = max+1 --exits this loop
else
local index = math.random(1, availableMissionsCount)
local mission = table.remove(availableMissions, index)
if mission then
mission:Activate()
self:AddCommmandsForMissionToAllPlayers(mission)
activeCount = activeCount + 1;
end
availableMissionsCount = availableMissionsCount - 1
end
end
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)
pcall(function()
self:MarkStage(true)
end)
for _, blueSamGroupName in pairs(self.db.blueSamGroups) do
Spearhead.DcsUtil.SpawnGroupTemplate(blueSamGroupName)
end
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
return nil
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)
end
o.OnStatusRequestReceived = function(self, groupId)
if self.activeStage ~= self.stageNumber then
return
end
trigger.action.outTextForGroup(groupId, "Status Update incoming... ", 3)
local text = "Mission Status: \n"
local totalmissions = 0
local completedMissions = 0
for _, mission in pairs(self.db.missionsByCode) do
totalmissions = totalmissions + 1
if mission.missionState == Spearhead.internal.Mission.MissionState.ACTIVE then
text = text .. "\n [" .. mission.code .. "] " .. mission.name ..
" (" .. mission.missionTypeDisplayName .. ") \n"
end
if mission.missionState == Spearhead.internal.Mission.MissionState.COMPLETED then
completedMissions = completedMissions + 1
end
end
local completionPercentage = math.floor((completedMissions / totalmissions) * 100)
text = text .. " \n Missions Complete: " .. completionPercentage .. "%"
self.logger:debug(text)
trigger.action.outTextForGroup(groupId, text, 20)
end
o.OnStageNumberChanged = function (self, number)
if self.activeStage == number then --only activate once for a stage
return
end
local previousActive = self.activeStage
self.activeStage = number
if Spearhead.capInfo.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then
self:PreActivate()
end
if number == self.stageNumber then
self:ActivateStage()
end
if previousActive <= self.stageNumber then
if number > self.stageNumber then
self:ActivateBlueStage()
self:RemoveAllMissionCommands()
end
end
end
--- input = { self, groupId, missionCode }
local ShowBriefingClicked = function (input)
local self = input.self
local groupId = input.groupId
local missionCode = input.missionCode
local mission = self.db.missionsByCode[missionCode]
if mission then
mission:ShowBriefing(groupId)
end
end
o.RemoveMissionCommands = function (self, mission)
self.logger:debug("Removing commands for: " .. mission.name)
local folderName = mission.name .. "(" .. mission.missionTypeDisplayName .. ")"
for i = 0, 2 do
local players = coalition.getPlayers(i)
for _, playerUnit in pairs(players) do
local groupId = playerUnit:getGroup():getID()
missionCommands.removeItemForGroup(groupId, { "Missions", folderName })
end
end
end
o.RemoveAllMissionCommands = function (self)
for _, mission in pairs(self.db.missionsByCode) do
self:RemoveMissionCommands(mission)
end
end
o.AddCommandsForMissionToGroup = function (self, groupId, mission)
local folderName = mission.name .. "(" .. mission.missionTypeDisplayName .. ")"
missionCommands.addSubMenuForGroup(groupId, folderName, { "Missions"} )
missionCommands.addCommandForGroup(groupId, "Show Briefing", { "Missions", folderName }, ShowBriefingClicked, { self = self, groupId = groupId, missionCode = mission.code })
end
o.AddCommmandsForMissionToAllPlayers = function(self, mission)
for i = 0, 2 do
local players = coalition.getPlayers(i)
for _, playerUnit in pairs(players) do
local groupId = playerUnit:getGroup():getID()
self:AddCommandsForMissionToGroup(groupId, mission)
end
end
end
o.OnPlayerEntersUnit = function (self, unit)
if self.activeStage == self.stageNumber then
local groupId = unit:getGroup():getID()
for _, mission in pairs(self.db.missionsByCode) do
if mission.missionState == Spearhead.internal.Mission.MissionState.ACTIVE then
self:AddCommandsForMissionToGroup(groupId, mission)
end
end
end
end
local removeMissionCommandsDelayed = function(input)
local self = input.self
local mission = input.mission
self:RemoveMissionCommands(mission)
end
o.OnMissionComplete = function(self, mission)
timer.scheduleFunction(removeMissionCommandsDelayed, { self = self, mission = mission}, timer.getTime() + 20)
if(self:IsComplete()) then
timer.scheduleFunction(triggerStageCompleteListeners, self, timer.getTime() + 15)
else
timer.scheduleFunction(activateMissionsIfApplicableAsync, self, timer.getTime() + 10)
end
end
for _, mission in pairs(o.db.missionsByCode) do
mission:AddMissionCompleteListener(o)
end
Spearhead.Events.AddOnPlayerEnterUnitListener(o)
Spearhead.Events.AddOnStatusRequestReceivedListener(o)
Spearhead.Events.AddStageNumberChangedListener(o)
return o
end
end
if not Spearhead.internal then Spearhead.internal = {} end
Spearhead.internal.Stage = Stage
@@ -0,0 +1,438 @@
---@alias StageColor
---| "RED"
---| "BLUE"
---| "GRAY"
--- @class StageData
--- @field missionsByCode table<string, Mission>
--- @field missions Array<Mission>
--- @field sams Array<Mission>
--- @field blueSams Array<BlueSam>
--- @field airbases Array<StageBase>
--- @field miscGroups Array<SpearheadGroup>
--- @field maxMissions integer
--- @class StageInitData
--- @field stageZoneName string
--- @field stageNumber integer
--- @field stageDisplayName string
--- @class StageCompleteListener
--- @field OnStageComplete fun(self:StageCompleteListener, stage:Stage)
--- @class Stage : MissionCompleteListener, OnStageChangedListener
--- @field zoneName string
--- @field stageName string
--- @field stageNumber number
--- @field protected _isActive boolean
--- @field protected _isComplete boolean
--- @field protected _missionPriority MissionPriority
--- @field protected _database Database
--- @field protected _db StageData
--- @field protected _logger Logger
--- @field protected _preActivated boolean
--- @field protected _activeStage integer
--- @field protected _stageConfig StageConfig
--- @field protected _stageDrawingId integer
--- @field protected _spawnedGroups Array<string>
--- @field protected _stageCompleteListeners Array<StageCompleteListener>
--- @field protected CheckContinuousAsync fun(self:Stage, time:number) : number?
--- @field protected OnPostStageComplete fun(self:Stage)?
--- @field protected OnPostBlueActivated fun(self:Stage)?
local Stage = {}
local stageDrawingId = 100
---comment
---@param database Database
---@param stageConfig StageConfig
---@param logger any
---@param initData StageInitData
---@param missionPriority MissionPriority
---@return Stage
function Stage.New(database, stageConfig, logger, initData, missionPriority)
local SpearheadGroup = Spearhead.classes.stageClasses.Groups.SpearheadGroup
Stage.__index = Stage
local o = {}
local self = setmetatable(o, Stage)
self.zoneName = initData.stageZoneName
self.stageNumber = initData.stageNumber
self._isActive = false
self._isComplete = false
self.stageName = initData.stageDisplayName
self.OnPostStageComplete = nil
self.OnPostBlueActivated = nil
self._database = database
self._logger = logger
self._db = {
missionsByCode = {},
missions = {},
sams ={},
blueSams = {},
airbases ={},
miscGroups = {},
maxMissions = stageConfig.maxMissionsPerStage
}
self._activeStage = -99
self._preActivated = false
self._stageConfig = stageConfig or {}
self._stageDrawingId = stageDrawingId + 1
self._spawnedGroups = {}
self._missionPriority = missionPriority
self._stageCompleteListeners = {}
stageDrawingId = stageDrawingId + 1
self._logger:info("Initiating new Stage with name: " .. self.zoneName)
---comment
---@param self Stage
---@param time number?
self.CheckContinuousAsync = function (self, time)
self:CheckAndUpdateSelf()
if self:IsComplete() == true then
self:NotifyComplete()
return nil
end
return time + 20
end
do -- load tables
local missionZones = database:getMissionsForStage(self.zoneName)
for _, missionZone in pairs(missionZones) do
local mission = Spearhead.classes.stageClasses.Missions.Mission.New(missionZone, self._missionPriority, database, logger)
if mission then
self._db.missionsByCode[mission.code] = mission
if mission.missionType == "SAM" then
table.insert(self._db.sams, mission)
else
table.insert(self._db.missions, mission)
end
end
end
local randomMissionNames = database:getRandomMissionsForStage(self.zoneName)
local randomMissionByName = {}
for _, missionZoneName in pairs(randomMissionNames) do
local mission = Spearhead.classes.stageClasses.Missions.Mission.New(missionZoneName, "primary", database, logger)
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
self._db.missionsByCode[mission.code] = mission
if mission.missionType == "SAM" then
table.insert(self._db.sams, mission)
else
table.insert(self._db.missions, mission)
end
end
end
for _, mission in pairs(self._db.missionsByCode) do
mission:AddMissionCompleteListener(self)
end
local airbaseIds = database:getAirbaseIdsInStage(self.zoneName)
if airbaseIds ~= nil and type(airbaseIds) == "table" then
for _, airbaseId in pairs(airbaseIds) do
local airbase = Spearhead.classes.stageClasses.SpecialZones.StageBase.New(database, logger, airbaseId)
table.insert(self._db.airbases, airbase)
end
end
for _, samZoneName in pairs(database:getBlueSamsInStage(self.zoneName)) do
local blueSam = Spearhead.classes.stageClasses.SpecialZones.BlueSam:new(database, logger, samZoneName)
table.insert(self._db.blueSams, blueSam)
end
local miscGroups = database:getMiscGroupsAtStage(self.zoneName)
for _, groupName in pairs(miscGroups) do
local miscGroup = SpearheadGroup.New(groupName)
table.insert(self._db.miscGroups, miscGroup)
Spearhead.DcsUtil.DestroyGroup(groupName)
end
end
Spearhead.Events.AddStageNumberChangedListener(self)
return self
end
---@return boolean
function Stage:IsComplete()
if self._isComplete == true then return true end
for i, mission in pairs(self._db.sams) do
local state = mission:GetState()
if state == "ACTIVE" or state == "NEW" then
return false
end
end
for i, mission in pairs(self._db.missions) do
local state = mission:GetState()
if state == "ACTIVE" or state == "NEW" then
return false
end
end
self._isComplete = true
return true
end
---@return boolean
function Stage:IsActive()
return self._isActive == true
end
---comment
function Stage:CheckAndUpdateSelf()
self._logger:debug("Checking on Stage: " .. self.zoneName)
local activeCount = 0
local dbTables = self:GetStageTables()
local availableMissions = {}
for _, mission in pairs(dbTables.missionsByCode) do
local state = mission:GetState()
if state == "ACTIVE" then
activeCount = activeCount + 1
end
if state == "NEW" then
table.insert(availableMissions, mission)
end
end
local max = dbTables.maxMissions
local availableMissionsCount = Spearhead.Util.tableLength(availableMissions)
if activeCount < max and availableMissionsCount > 0 then
for i = activeCount+1, max do
if availableMissionsCount == 0 then
i = max+1 --exits this loop
else
local index = math.random(1, availableMissionsCount)
---@type Mission
local mission = table.remove(availableMissions, index)
if mission then
mission:SpawnActive()
activeCount = activeCount + 1;
end
availableMissionsCount = availableMissionsCount - 1
end
end
end
end
---private use only
function Stage:NotifyComplete()
self._logger:info("Stage complete: " .. self.stageName)
for _, listener in pairs(self._stageCompleteListeners) do
pcall(function()
listener:OnStageComplete(self)
end)
end
if self.OnPostStageComplete then
timer.scheduleFunction(self.OnPostStageComplete, self, timer.getTime() + 3)
end
end
---@param listener StageCompleteListener
function Stage:AddStageCompleteListener(listener)
table.insert(self._stageCompleteListeners, listener)
end
---Activates all SAMS, Airbase units etc all at once.
function Stage:PreActivate()
if self._preActivated == false then
self._preActivated = true
for key, mission in pairs(self._db.sams) do
if mission then
mission:SpawnActive()
end
end
for _, airbase in pairs(self._db.airbases) do
airbase:ActivateRedStage()
end
end
end
---@param stageColor StageColor
function Stage:MarkStage(stageColor)
local fillColor = {1, 0, 0, 0.1}
local line ={ 1, 0,0, 1 }
if stageColor == "RED" then
fillColor = {1, 0, 0, 0.1}
line ={ 1, 0,0, 1 }
elseif stageColor =="BLUE" then
fillColor = {0, 0, 1, 0.1}
line ={ 0, 0,1, 1 }
elseif stageColor == "GRAY" then
fillColor = {80/255, 80/255, 80/255, 0.15}
line ={ 80/255, 80/255,80/255, 1 }
end
local zone = Spearhead.DcsUtil.getZoneByName(self.zoneName)
if zone and self._stageConfig.isDrawStagesEnabled == true then
self._logger:debug("drawing stage: " .. self.zoneName)
if zone.zone_type == Spearhead.DcsUtil.ZoneType.Cilinder then
trigger.action.circleToAll(-1, self._stageDrawingId, {x = zone.x, y = 0 , z = zone.z}, zone.radius, {0,0,0,0}, {0,0,0,0},4, true)
else
--trigger.action.circleToAll(-1, self.stageDrawingId, {x = zone.x, y = 0 , z = zone.z}, zone.radius, { 1, 0,0, 1 }, {1,0,0,1},4, true)
trigger.action.quadToAll( -1, self._stageDrawingId, zone.verts[1], zone.verts[2], zone.verts[3], zone.verts[4], {0,0,0,0}, {0,0,0,0}, 4, true)
end
trigger.action.setMarkupColorFill(self._stageDrawingId, fillColor)
trigger.action.setMarkupColor(self._stageDrawingId, line)
end
end
function Stage:ActivateStage()
self._isActive = true;
pcall(function()
self:MarkStage("RED")
end)
self:PreActivate()
self._logger:debug("Activating Misc groups for zone. Count: " .. Spearhead.Util.tableLength(self._db.miscGroups))
for _, miscGroup in pairs(self._db.miscGroups) do
miscGroup:Spawn()
end
for _, mission in pairs(self._db.missions) do
if mission.missionType == "DEAD" then
mission:SpawnActive()
end
end
timer.scheduleFunction(self.CheckContinuousAsync, self, timer.getTime() + 3)
end
---Private usage only
---@return StageData
function Stage:GetStageTables()
return self._db
end
---comment
---@param self Stage
---@param number integer
function Stage:OnStageNumberChanged(number)
if self._activeStage == number then --only activate once for a stage
return
end
local previousActive = self._activeStage
self._activeStage = number
if Spearhead.capInfo.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then
self:PreActivate()
end
if number == self.stageNumber then
self:ActivateStage()
end
if previousActive <= self.stageNumber then
if number > self.stageNumber then
self:ActivateBlueStage()
end
end
end
function Stage:GetBriefing()
return "Briefing For "
end
---@param self Stage
---@param mission Mission
Stage.OnMissionComplete = function(self, mission)
self:CheckAndUpdateSelf()
end
---private use only
function Stage:ActivateBlueGroups()
for _, blueSam in pairs(self._db.blueSams) do
blueSam:Activate()
end
for _, airbase in pairs(self._db.airbases) do
airbase:ActivateBlueStage()
end
if self.OnPostBlueActivated then
pcall(function()
self:OnPostBlueActivated()
end)
end
end
function Stage:ActivateBlueStage()
self._logger:debug("Setting stage '" .. Spearhead.Util.toString(self.zoneName) .. "' to blue")
for _, mission in pairs(self._db.missions) do
mission:SpawnPersistedState()
end
for _, mission in pairs(self._db.sams) do
mission:SpawnPersistedState()
end
for _, miscGroup in pairs(self._db.miscGroups) do
miscGroup:Spawn()
end
---@param self Stage
local ActivateBlueAsync = function(self)
pcall(function()
self:MarkStage("BLUE")
end)
self:ActivateBlueGroups()
return nil
end
timer.scheduleFunction(ActivateBlueAsync, self, timer.getTime() + 3)
end
if not Spearhead.classes then Spearhead.classes = {} end
if not Spearhead.classes.stageClasses then Spearhead.classes.stageClasses = {} end
if not Spearhead.classes.stageClasses.Stages then Spearhead.classes.stageClasses.Stages = {} end
if not Spearhead.classes.stageClasses.Stages.BaseStage then Spearhead.classes.stageClasses.Stages.BaseStage = {} end
Spearhead.classes.stageClasses.Stages.BaseStage.Stage = Stage
@@ -0,0 +1,58 @@
---@class ExtraStage : Stage
local ExtraStage = {}
---comment
---@param database Database
---@param stageConfig StageConfig
---@param logger any
---@param initData StageInitData
---@return ExtraStage
function ExtraStage.New(database, stageConfig, logger, initData)
-- "Import"
local Stage = Spearhead.classes.stageClasses.Stages.BaseStage.Stage
setmetatable(ExtraStage, Stage)
ExtraStage.__index = ExtraStage
local self = Stage.New(database, stageConfig, logger, initData, "secondary") --[[@as ExtraStage]]
setmetatable(self, ExtraStage)
self.OnPostBlueActivated = function (selfStage)
selfStage:MarkStage("GRAY")
end
self.OnPostStageComplete = function (selfStage)
self:ActivateBlueStage()
end
return self
end
---comment
---@param self Stage
---@param number integer
function ExtraStage:OnStageNumberChanged(number)
self._activeStage = number
if Spearhead.capInfo.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then
self:PreActivate()
end
if number == self.stageNumber then
self:ActivateStage()
end
if self._isComplete == true then
self:ActivateBlueStage()
end
end
if not Spearhead.classes then Spearhead.classes = {} end
if not Spearhead.classes.stageClasses then Spearhead.classes.stageClasses = {} end
if not Spearhead.classes.stageClasses.Stages then Spearhead.classes.stageClasses.Stages = {} end
Spearhead.classes.stageClasses.Stages.ExtraStage = ExtraStage
@@ -0,0 +1,28 @@
---@class PrimaryStage : Stage
local PrimaryStage = {}
---comment
---@param database Database
---@param stageConfig StageConfig
---@param logger any
---@param initData StageInitData
---@return PrimaryStage
function PrimaryStage.New(database, stageConfig, logger, initData)
-- "Import"
local Stage = Spearhead.classes.stageClasses.Stages.BaseStage.Stage
setmetatable(PrimaryStage, Stage)
PrimaryStage.__index = PrimaryStage
setmetatable(PrimaryStage, {__index = Stage})
local o = Stage.New(database, stageConfig, logger, initData, "primary") --[[@as PrimaryStage]]
return o
end
if not Spearhead.classes then Spearhead.classes = {} end
if not Spearhead.classes.stageClasses then Spearhead.classes.stageClasses = {} end
if not Spearhead.classes.stageClasses.Stages then Spearhead.classes.stageClasses.Stages = {} end
Spearhead.classes.stageClasses.Stages.PrimaryStage = PrimaryStage
@@ -0,0 +1,77 @@
---@class WaitingStage : Stage
---@field private _waitTimeSeconds integer
---@field private _startTime number
local WaitingStage = {}
---@class WaitingStageInitData : StageInitData
---@field waitingSeconds integer
local WaitingStageInitData = {}
---comment
---@param database Database
---@param stageConfig StageConfig
---@param logger any
---@param initData WaitingStageInitData
---@return WaitingStage
function WaitingStage.New(database, stageConfig, logger, initData)
-- "Import"
local Stage = Spearhead.classes.stageClasses.Stages.BaseStage.Stage
setmetatable(WaitingStage, Stage)
WaitingStage.__index = WaitingStage
setmetatable(WaitingStage, {__index = Stage})
local self = Stage.New(database, stageConfig, logger, initData, "primary") --[[@as WaitingStage]]
setmetatable(self, WaitingStage)
self._waitTimeSeconds = 5
if initData.waitingSeconds and initData.waitingSeconds > 5 then self._waitTimeSeconds = initData.waitingSeconds end
self._startTime = nil
self.CheckContinuousAsync = function (self, time)
if self:IsComplete() then
self:NotifyComplete()
return nil
end
return time + 2
end
return self
end
function WaitingStage:ActivateStage()
self._logger:info("Starting Waiting Stage '" .. self.zoneName .. "' which will complete in about " .. self._waitTimeSeconds .. " seconds")
self._isActive = true
self._startTime = timer.getTime()
timer.scheduleFunction(self.CheckContinuousAsync, self, self._startTime + self._waitTimeSeconds)
end
function WaitingStage:IsComplete()
if timer.getTime() > (self._startTime + self._waitTimeSeconds) then return true end
return false
end
function WaitingStage:OnStageNumberChanged()
self._logger:debug("Waiting Stage OnStageNumberChanged override")
end
function WaitingStage:MarkStage(stageColor)
self._logger:debug("Waiting Stage MarkStage override")
end
function WaitingStage:GetExpectedTime()
return self._startTime + self._waitTimeSeconds
end
if not Spearhead.classes then Spearhead.classes = {} end
if not Spearhead.classes.stageClasses then Spearhead.classes.stageClasses = {} end
if not Spearhead.classes.stageClasses.Stages then Spearhead.classes.stageClasses.Stages = {} end
Spearhead.classes.stageClasses.Stages.WaitingStage = WaitingStage
@@ -0,0 +1,135 @@
local MissionCommandsHelper = {}
do
---@type table<string, Mission>
local missionsByCode = {}
---@type table<string, boolean>
local enabledByCode = {}
local updateNeeded = false
---Add a mission to the F10 commands menu
---@param mission Mission
MissionCommandsHelper.AddMissionToCommands = function (mission)
missionsByCode[tostring(mission.code)] = mission
enabledByCode[tostring(mission.code)] = true
updateNeeded = true
end
---Removes a mission from the F10 commands menu
---@param mission Mission
MissionCommandsHelper.RemoveMissionToCommands = function (mission)
enabledByCode[tostring(mission.code)] = false
updateNeeded = true
end
local folderNames = {
primary = "Primary Missions",
secondary = "Secondary Missions"
}
---Add Base Folder
---@param groupId integer
local addMissionFolders = function(groupId)
missionCommands.addSubMenuForGroup(groupId, folderNames.primary)
missionCommands.addSubMenuForGroup(groupId, folderNames.secondary)
end
---Add Mission Folder
---@param groupId integer
local removeMissionFolders = function(groupId)
missionCommands.removeItemForGroup(groupId , { folderNames.primary } )
missionCommands.removeItemForGroup(groupId , { folderNames.secondary } )
end
local missionBriefingRequested = function(args)
---@type Mission
local mission = args.mission
local groupID = args.groupId
mission:ShowBriefing(groupID)
end
---comment
---@param groupId integer
---@param mission Mission
local addMissionCommands = function(groupId, mission)
local path = nil
if mission.priority == "primary" then
path = { [1] = folderNames.primary }
elseif mission.priority == "secondary" then
path = { [1] = folderNames.secondary }
end
if path then
local missionFolderName = "[" .. mission.code .. "]" .. mission.name
missionCommands.addSubMenuForGroup(groupId, missionFolderName, path)
table.insert(path, missionFolderName)
missionCommands.addCommandForGroup(groupId, "Briefing" , path , missionBriefingRequested, { groupId = groupId, mission = mission })
end
end
local updateCommandsForGroup = function(group)
local groupID = group:getID()
-- Cleanup mission folder
removeMissionFolders(groupID)
-- Add mission folders
addMissionFolders(groupID)
for code, enabled in pairs(enabledByCode) do
if enabled == true then
local mission = missionsByCode[code]
if mission then
addMissionCommands(groupID, mission)
end
end
end
end
local UpdateContinuous = function(none, time)
if updateNeeded == false then
return time + 15
end
for _, unit in pairs(Spearhead.DcsUtil.getAllPlayerUnits()) do
if unit and unit:isExist() then
local group = unit:getGroup()
if group then
updateCommandsForGroup(group)
end
end
end
updateNeeded = false
return time + 15
end
timer.scheduleFunction(UpdateContinuous, {}, timer.getTime() + 10)
do -- Player enter unit listener
local onPlayerEnterUnit = function(unit)
if unit then
local group = unit:getGroup()
if group then updateCommandsForGroup(group) end
end
end
local OnPlayerEnterUnitListener = {
OnPlayerEntersUnit = function (self, unit)
onPlayerEnterUnit(unit)
end,
}
Spearhead.Events.AddOnPlayerEnterUnitListener(OnPlayerEnterUnitListener)
end
end
if Spearhead == nil then Spearhead = {} end
if Spearhead.classes == nil then Spearhead.classes = {} end
if Spearhead.classes.stageClasses == nil then Spearhead.classes.stageClasses = {} end
if Spearhead.classes.stageClasses.helpers == nil then Spearhead.classes.stageClasses.helpers = {} end
Spearhead.classes.stageClasses.helpers.MissionCommandsHelper = MissionCommandsHelper
+20 -3
View File
@@ -32,7 +32,6 @@ SpearheadConfig = {
-- unit: seconds
deathDelay = 1800, -- default 1800
},
StageConfig = {
-- management of stages and its missions.
@@ -40,7 +39,7 @@ SpearheadConfig = {
enabled = true, -- default true
--Will draw the active and the next stage
drawStages = false, -- default true
drawStages = true, -- default true
--AutoStages will continue to the next stage automatically on completion of the missions within the stage.
-- If you want to make it so the next stage triggers only when you want to disable it here and manually implement the actions needed.
@@ -53,7 +52,25 @@ SpearheadConfig = {
maxMissionStage = 10,
--Stage starting number
startingStage = 1
startingStage = 1,
---DEBUG logging. Consider keeping this disabled
debugEnabled = false
},
Persistence = {
--- io and lfs cannot be sanitized in the MissionScripting.lua
--- enables or disables the persistence logic in spearhead
enabled = false,
--- sets the directory where the persistence file is stored
--- if nil then lfs.writedir() will be used.
--- which will
directory = nil ,
--- the filename of the persistence file. Should end with .json for convention, but any text extension should do.
fileName = "Spearhead_Persistence.json"
}
}
+36 -17
View File
@@ -1,28 +1,47 @@
local basePath = "C:\\Repos\\DCS\\Spearhead\\classes\\"
assert(loadfile(basePath .. "config.lua"))()
--assert(loadfile("C:\\Repos\\DCS\\Spearhead\\dev\\dev_classes.lua"))()
assert(loadfile(basePath .. "spearhead_base.lua"))()
assert(loadfile(basePath .. "spearhead_routeutil.lua"))()
assert(loadfile(basePath .. "spearhead_events.lua"))()
assert(loadfile(basePath .. "spearhead_db.lua"))()
local basePath = "C:\\Repos\\DCS\\Spearhead\\"
local classPath = basePath .. "classes\\"
assert(loadfile(basePath .. "fleetClasses\\FleetGroup.lua"))()
assert(loadfile(basePath .. "fleetClasses\\GlobalFleetManager.lua"))()
assert(loadfile(classPath .. "spearhead_base.lua"))()
assert(loadfile(classPath .. "spearhead_routeutil.lua"))()
assert(loadfile(classPath .. "spearhead_events.lua"))()
assert(loadfile(classPath .. "spearhead_db.lua"))()
assert(loadfile(basePath .. "configuration\\CapConfig.lua"))()
assert(loadfile(basePath .. "configuration\\StageConfig.lua"))()
assert(loadfile(classPath .. "fleetClasses\\FleetGroup.lua"))()
assert(loadfile(classPath .. "fleetClasses\\GlobalFleetManager.lua"))()
assert(loadfile(basePath .. "stageClasses\\GlobalStageManager.lua"))()
assert(loadfile(basePath .. "stageClasses\\Mission.lua"))()
assert(loadfile(basePath .. "stageClasses\\Stage.lua"))()
assert(loadfile(classPath .. "configuration\\CapConfig.lua"))()
assert(loadfile(classPath .. "configuration\\StageConfig.lua"))()
assert(loadfile(basePath .. "capClasses\\CapGroup.lua"))()
assert(loadfile(basePath .. "capClasses\\GlobalCapManager.lua"))()
assert(loadfile(basePath .. "capClasses\\CapAirbase.lua"))()
assert(loadfile(classPath .. "stageClasses\\GlobalStageManager.lua"))()
assert(loadfile(classPath .. "stageClasses\\Missions\\Mission.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\BaseStage\\Stage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\PrimaryStage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\ExtraStage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\WaitingStage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Groups\\SpearheadGroup.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\MissionCommandsHelper.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\StageBase.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\BlueSam.lua"))()
assert(loadfile(classPath .. "capClasses\\CapGroup.lua"))()
assert(loadfile(classPath .. "capClasses\\GlobalCapManager.lua"))()
assert(loadfile(classPath .. "capClasses\\CapAirbase.lua"))()
assert(loadfile(classPath .. "persistence\\Persistence.lua"))()
-- Startup:
assert(loadfile(basePath .. "main.lua"))()
assert(loadfile(basePath .. "main.lua"))()
+76
View File
@@ -0,0 +1,76 @@
SpearheadConfig = {
CapConfig = {
--quickly enable of disable the entire CAP Logic
--(you can also just rename all units to not be named "CAP_")
enabled = true, -- default true
--min ground speed for CAP aircraft during patrol
-- unit: knots
minSpeed = 400, -- default 400
--max speed for CAP aircraft during patrol
-- unit: knots
maxSpeed = 500, -- default 500
--minAlt for aircraft on patrol
-- unit: feet
minAlt = 18000, -- default 18000
--maxAlt for aircraft on patrol
-- unit: feet
maxAlt = 28000, -- default 28000
--Delay for aircraft from touchdown to off the chocks.
-- unit: seconds
rearmDelay = 600, -- default 600
--Delay for aircraft from death to takeoff.
--When the seconds remaining is the same at the rearmDelay it will be spawned on the ramp and follow the rearm logic.
-- !! Can not be lower than rearmDelay
-- unit: seconds
deathDelay = 1800, -- default 1800
},
StageConfig = {
-- management of stages and its missions.
-- This is not related to CAP managers which will continue to work even if stage management is disabled
enabled = true, -- default true
--Will draw the active and the next stage
drawStages = true, -- default true
--AutoStages will continue to the next stage automatically on completion of the missions within the stage.
-- If you want to make it so the next stage triggers only when you want to disable it here and manually implement the actions needed.
--[[
TODO: Add manual stage transition documentation
]]
autoStages = true, --default true
--Maximum missions per stage (includes all types of missions)
maxMissionStage = 10,
--Stage starting number
startingStage = 1,
---DEBUG logging. Consider keeping this disabled
debugEnabled = true
},
Persistence = {
--- io and lfs cannot be sanitized in the MissionScripting.lua
--- enables or disables the persistence logic in spearhead
enabled = false,
--- sets the directory where the persistence file is stored
--- if nil then lfs.writedir() will be used.
--- which will
directory = nil ,
--- the filename of the persistence file. Should end with .json for convention, but any text extension should do.
fileName = "Spearhead_Persistence.json"
}
}
+25 -6
View File
@@ -1,21 +1,38 @@
--Single player purpose
local debug = false
local id = net.get_my_player_id()
if id == 0 then
debug = true
end
local dbLogger = Spearhead.LoggerTemplate:new("database", Spearhead.LoggerTemplate.LogLevelOptions.INFO)
local standardLogger = Spearhead.LoggerTemplate:new("", Spearhead.LoggerTemplate.LogLevelOptions.INFO)
local startTime = timer.getTime() * 1000
Spearhead.Events.Init("DEBUG")
local dbLogger = Spearhead.LoggerTemplate:new("database", "INFO")
local standardLogger = Spearhead.LoggerTemplate:new("", "INFO")
local databaseManager = Spearhead.DB:new(dbLogger, debug)
local capConfig = Spearhead.internal.configuration.CapConfig:new();
local stageConfig = Spearhead.internal.configuration.StageConfig:new();
standardLogger:info("Using StageConfig: ".. stageConfig:toString())
local startingStage = stageConfig.startingStage or 1
if SpearheadConfig and SpearheadConfig.Persistence and SpearheadConfig.Persistence.enabled == true then
standardLogger:info("Persistence enabled")
local persistenceLogger = Spearhead.LoggerTemplate:new("Persistence", "INFO")
Spearhead.classes.persistence.Persistence.Init(persistenceLogger)
local persistanceStage = Spearhead.classes.persistence.Persistence.GetActiveStage()
if persistanceStage then
standardLogger:info("Persistance activated and using persistant active stage: " .. persistanceStage)
startingStage = persistanceStage
end
else
standardLogger:info("Persistence disabled")
end
Spearhead.internal.GlobalCapManager.start(databaseManager, capConfig, stageConfig)
Spearhead.internal.GlobalStageManager:NewAndStart(databaseManager, stageConfig)
@@ -26,11 +43,14 @@ local SetStageDelayed = function(number, time)
return nil
end
local startingStage = stageConfig:getStartingStage() or 1
timer.scheduleFunction(SetStageDelayed, startingStage, timer.getTime() + 3)
env.info(startTime .. "ms / " .. timer.getTime() * 1000 .. "ms")
local duration = (timer.getTime() * 1000) - startTime
standardLogger:info("Spearhead Initialisation duration: " .. tostring(duration) .. "ms")
Spearhead.LoadingDone()
--Check lines of code in directory per file:
-- Get-ChildItem . -Include *.lua -Recurse | foreach {""+(Get-Content $_).Count + " => " + $_.name }; && GCI . -Include *.lua* -Recurse | foreach{(GC $_).Count} | measure-object -sum | % Sum
-- find . -name '*.lua' | xargs wc -l
@@ -53,4 +73,3 @@ Spearhead.LoadingDone()
-- trigger.action.lineToAll(-1 , 56+i , { x= a.x, y = 0, z = a.z } , { x = b.x, y = 0, z = b.z } , color , 1, true)
-- end