Develop to Main #32

Merged
dutchie031 merged 16 commits from develop into main 2026-08-20 15:00:07 +00:00
36 changed files with 1168 additions and 540 deletions
+105
View File
@@ -0,0 +1,105 @@
name: Publish Release
on:
push:
branches:
- develop
paths:
- src/**
- .gitea/workflows/release-beta.yml
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read Version from version.txt
id: read_version
run: |
VERSION=beta
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION"
- name: Extract Release Notes
id: release_notes
run: |
VERSION=beta
RELEASE_NOTES=$(awk -v ver="Unreleased" '
/^## \[/ {
if (found) exit
if (index($0, "[" ver "]") > 0) found = 1
next
}
found { print }
' RELEASENOTES.md)
# Write multi-line output properly
echo "body<<EOF" >> "$GITHUB_OUTPUT"
echo "$RELEASE_NOTES" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Compile Spearhead
uses: https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools/github-action@action/v0
with:
source-root: ./src
output-file: ./output/spearhead.lua
- name: Remove existing tag if it exists
id: check_tag
run: |
TAG="${{ steps.read_version.outputs.tag }}"
if git tag | grep -q "^$TAG$"; then
echo "Tag exists. Removing it..."
git push origin :refs/tags/"$TAG" || true
git tag -d "$TAG" || true
fi
echo "Ready to create fresh tag: $TAG"
- name: Apply Tag to Current Commit
run: |
git config user.name "github-actions"
git config user.email "github-actions@github.com"
git tag ${{ steps.read_version.outputs.tag }}
git push origin ${{ steps.read_version.outputs.tag }}
- name: Copy Release to Versioned File
run: |
cp ./output/spearhead.lua ./output/spearhead.${{ steps.read_version.outputs.tag }}.lua
echo "Versioned file created: spearhead.${{ steps.read_version.outputs.tag }}.lua"
- name: Create Release and Upload Assets
uses: akkuman/gitea-release-action@v1
with:
tag_name: ${{ steps.read_version.outputs.tag }}
name: Release ${{ steps.read_version.outputs.tag }}
body: ${{ steps.release_notes.outputs.body }}
prerelease: true
files: |-
./output/spearhead.lua
./output/spearhead.${{ steps.read_version.outputs.tag }}.lua
#token: ${{ secrets.GITHUB_TOKEN }}
env:
NODE_OPTIONS: '--experimental-fetch'
- name: Announce Release
uses: tsickert/discord-webhook@v7.0.0
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
content: "New Beta Release"
username: "Spearhead Release Bot"
#TODO: avatar-url spearhead avatar
thread-id: 1538520488442331227
embed-title: "Spearhead Release Beta"
embed-color: 3093247
embed-description:
${{ steps.release_notes.outputs.body }}
embed-url:
https://git.dutchie031.com/Spearhead/spearhead/releases/tag/${{ steps.read_version.outputs.tag }}
+15
View File
@@ -84,3 +84,18 @@ jobs:
env: env:
NODE_OPTIONS: '--experimental-fetch' NODE_OPTIONS: '--experimental-fetch'
- name: Announce Release
uses: tsickert/discord-webhook@v7.0.0
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
content: "New Beta Release"
username: "Spearhead Release Bot"
#TODO: avatar-url spearhead avatar
thread-id: 1538520596802175037
embed-title: "Spearhead Release"
embed-color: 3093247
embed-description:
${{ steps.release_notes.outputs.body }}
embed-url:
https://git.dutchie031.com/Spearhead/spearhead/releases/tag/${{ steps.read_version.outputs.tag }}
+1
View File
@@ -4,3 +4,4 @@
/dist /dist
.vscode/settings.json .vscode/settings.json
**\settings.json
-25
View File
@@ -1,25 +0,0 @@
{
"cSpell.words": [
"farp",
"Sams",
"SEAD"
],
"Lua.diagnostics.globals": [
"lfs"
],
"maptz.regionfolder": {
"[lua]" : {
"foldEnd": "--endregion",
"foldEndRegex": "[\\s]*--endregion",
"foldStart": "--region [NAME]",
"foldStartRegex": "[\\s]*--region[\\s]*(.*)",
"disableFolding": false
}
},
"livePreview.serverRoot": "/_docs",
"Lua.workspace.library": [
"/Users/ex61wi/Developer/personal/DcsMissionScriptingTools/dutchies-dcs-scripting-tools/lua-addons",
"c:\\Users\\Tim\\.vscode\\extensions\\dutchie031.dutchies-dcs-scripting-tools-0.0.3\\lua-addons"
],
"dutchies-dcs-scripting-tools.compileAt": "onSave",
}
+27 -2
View File
@@ -7,16 +7,41 @@
### Bug Fixes ### Bug Fixes
## [0.13.0] 2026-08
## [0.12.1] 2026-07 A good first release that finally has all major bugs fixed that were caused by the migration from both the underlying script transpiler and the migration to Gitea. <br>
### Breaking Changes ### Breaking Changes
### New Features ### New Features
- Issue #26
Possibility to have Stage Overview briefings (which include current missions sorted by distance) to be shown on spawning of a player.
PR #31
### Bug Fixes ### Bug Fixes
- Fixed stage drawing to only be checked when a stage completed. Now done on stage number changed. - Issue #11
Supply crate spawning now checks for free space and will not spawn if the area is too crowded.
Additionally different units will spawn in different areas depending on loading side.
PR #17
- Fixed command wiring for supply hubs for better and more accurate detection of units spawning and entering/exiting zone.
PR #15
- Fixed custom drawings not being drawn correctly.
PR #18
- Fixed CAP Callbacks not working since the change to a transpiled script. Now a global callback circumvents this issue.
PR #18
- Fixed #9
Changed order of checking mission briefings
PR #19
- Fixed Configuration defaulting to true for all booleans in StageConfig
PR #28
- Fixed Pre-Activated stages not always drawing or pre-activating correctly.
PR #28
- Addressed #24
CAP max commit range is now configurable in the config.lua file.
Issue remains open in order to apply further fine grained tuning.
PR #29
## [0.12.0] 2026-06 ## [0.12.0] 2026-06
+7 -1
View File
@@ -6,7 +6,7 @@ SpearheadConfig = {
--- The time briefings should be displayed by default. --- The time briefings should be displayed by default.
--- Players can always "Clear Messages" through the F10 menu, so setting it to a high value can be --- Players can always "Clear Messages" through the F10 menu, so setting it to a high value can be
briefingMessageDuration = 30, --default 30 briefingMessageDuration = 60, --default 60
CapConfig = { CapConfig = {
--quickly enable of disable the entire CAP Logic --quickly enable of disable the entire CAP Logic
@@ -29,6 +29,12 @@ SpearheadConfig = {
-- unit: feet -- unit: feet
maxAlt = 28000, -- default 28000 maxAlt = 28000, -- default 28000
--The "maxDistance" that's set on the CAP tasking for the aircraft to commit to a target.
--This is the distance from the aircraft to the target that the aircraft will commit to engaging
--This is shared for CAP, Intercept and Sweep missions for now.
-- unit: nautical miles
maxCommitRange = 35, -- default 35
-- DELAYS. -- DELAYS.
-- Delays work as follow. -- Delays work as follow.
-- When an aircraft lands alive and well it will be rearmed and ready to go. -- When an aircraft lands alive and well it will be rearmed and ready to go.
+5 -5
View File
@@ -220,7 +220,7 @@ function CAP.getAsTasking(groupName, airbase, capZone, capConfig)
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishOnStation, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishOnStation, \"" .. groupName .. "\")"
} }
} }
} }
@@ -260,7 +260,7 @@ function CAP.getAsTasking(groupName, airbase, capZone, capConfig)
}, },
stopCondition = { stopCondition = {
duration = durationBefore10, duration = durationBefore10,
condition = "return Spearhead.DcsUtil.NeedsRTBInTen(\"" .. groupName .. "\", 0.10)", condition = "return GlobalCapCallBacks.NeedsRTBInTen(\"" .. groupName .. "\", 0.10)",
} }
} }
}, },
@@ -273,7 +273,7 @@ function CAP.getAsTasking(groupName, airbase, capZone, capConfig)
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishRTBInTen, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishRTBInTen, \"" .. groupName .. "\")"
} }
} }
} }
@@ -299,7 +299,7 @@ function CAP.getAsTasking(groupName, airbase, capZone, capConfig)
}, },
stopCondition = { stopCondition = {
duration = durationAfter10, duration = durationAfter10,
condition = "return Spearhead.DcsUtil.IsBingoFuel(\"" .. groupName .. "\")", condition = "return GlobalCapCallBacks.IsBingoFuel(\"" .. groupName .. "\", 0.10)",
} }
} }
}, },
@@ -312,7 +312,7 @@ function CAP.getAsTasking(groupName, airbase, capZone, capConfig)
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")"
} }
} }
} }
@@ -0,0 +1,26 @@
local DcsUtil = require("classes.util.DcsUtil")
local Events = require("classes.spearhead_events")
GlobalCapCallBacks = {}
function GlobalCapCallBacks.IsBingoFuel(groupName, fuelPercent)
return DcsUtil.IsBingoFuel(groupName, fuelPercent)
end
function GlobalCapCallBacks.NeedsRTBInTen(groupName, fuelOffset)
return DcsUtil.NeedsRTBInTen(groupName, fuelOffset)
end
function GlobalCapCallBacks.PublishRTBInTen(groupName)
return Events.PublishRTBInTen(groupName)
end
function GlobalCapCallBacks.PublishRTB(groupName)
return Events.PublishRTB(groupName)
end
function GlobalCapCallBacks.PublishOnStation(groupName)
return Events.PublishOnStation(groupName)
end
@@ -247,7 +247,7 @@ function INTERCEPT.getInterceptTaskPoint(groupName, currentPoint, targetPoint, a
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")"
} }
} }
} }
@@ -379,7 +379,7 @@ function INTERCEPT.getUnitInterceptTaskPoint(groupName, currentPoint, targetPosi
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")"
} }
} }
} }
+2 -2
View File
@@ -189,7 +189,7 @@ function SWEEP.getAsTasking(groupName, airbase, capZone, capConfig)
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishOnStation, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishOnStation, \"" .. groupName .. "\")"
} }
} }
} }
@@ -270,7 +270,7 @@ function SWEEP.getAsTasking(groupName, airbase, capZone, capConfig)
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")"
} }
} }
} }
+1 -1
View File
@@ -34,7 +34,7 @@ function CapConfig.new()
self._minDurationOnStation = 1200 self._minDurationOnStation = 1200
self._maxDurationOnStation = 2700 self._maxDurationOnStation = 2700
self._maxDeviationRange = 35 * 1852 -- in meters self._maxDeviationRange = (tonumber(SpearheadConfig.CapConfig.maxCommitRange) or 35) * 1852 -- in meters
self._rearmDelay = tonumber(SpearheadConfig.CapConfig.rearmDelay) or 600 self._rearmDelay = tonumber(SpearheadConfig.CapConfig.rearmDelay) or 600
self._repairDelay = tonumber(SpearheadConfig.CapConfig.repairDelay) or 600 self._repairDelay = tonumber(SpearheadConfig.CapConfig.repairDelay) or 600
self._deathDelay = tonumber(SpearheadConfig.CapConfig.deathDelay) or 1800 self._deathDelay = tonumber(SpearheadConfig.CapConfig.deathDelay) or 1800
+14 -2
View File
@@ -4,7 +4,8 @@
local briefingMessageTime = nil local briefingMessageTime = nil
---@class GlobalConfig ---@class GlobalConfig
---@field private _briefingTime number ; ---@field private _briefingTime number
---@field private _debugEnabled boolean
local GlobalConfig = {} local GlobalConfig = {}
GlobalConfig.__index = GlobalConfig; GlobalConfig.__index = GlobalConfig;
@@ -19,13 +20,24 @@ function GlobalConfig.New()
if SpearheadConfig.briefingMessageDuration then if SpearheadConfig.briefingMessageDuration then
self._briefingTime = SpearheadConfig.briefingMessageDuration self._briefingTime = SpearheadConfig.briefingMessageDuration
end end
if SpearheadConfig.debugEnabled ~= nil then
self._debugEnabled = SpearheadConfig.debugEnabled
else
self._debugEnabled = false
end
end end
return self return self
end end
function GlobalConfig:getBriefingTime() function GlobalConfig:getBriefingTime()
return self._briefingTime or 30 return self._briefingTime or 60
end
function GlobalConfig:isDebugEnabled()
return self._debugEnabled or false
end end
+33 -15
View File
@@ -7,29 +7,47 @@
--- @field startingStage integer --- @field startingStage integer
--- @field maxMissionsPerStage integer --- @field maxMissionsPerStage integer
--- @field AmountPreactivateStage integer --- @field AmountPreactivateStage integer
--- @field briefingOnSpawnEnabled boolean
local StageConfig = {}; local StageConfig = {};
StageConfig.__index = StageConfig
local Logger = require("classes.util.Logger")
local _logger = Logger.new("StageConfig", Logger.LogLevel)
---comment ---comment
---@return StageConfig ---@return StageConfig
function StageConfig:new() local function new()
if SpearheadConfig == nil then SpearheadConfig = {} end if SpearheadConfig == nil then
if SpearheadConfig.StageConfig == nil then SpearheadConfig.StageConfig = {} end _logger:warn("SpearheadConfig is nil, creating default SpearheadConfig")
SpearheadConfig = {}
end
---@type StageConfig if SpearheadConfig.StageConfig == nil then
local o = { _logger:warn("SpearheadConfig.StageConfig is nil, creating default StageConfig")
isEnabled = SpearheadConfig.StageConfig.enabled or true, SpearheadConfig.StageConfig = {}
isDrawStagesEnabled = SpearheadConfig.StageConfig.drawStages or true, end
isAutoStages = SpearheadConfig.StageConfig.autoStages or true,
startingStage = SpearheadConfig.StageConfig.startingStage or 1,
maxMissionsPerStage = SpearheadConfig.StageConfig.maxMissionStage or 10,
isDrawPreActivatedEnabled = SpearheadConfig.StageConfig.drawPreActivated or true,
AmountPreactivateStage = SpearheadConfig.StageConfig.preactivateStage or 1,
}
setmetatable(o, { __index = self }) local self = setmetatable({}, StageConfig)
self.isEnabled = SpearheadConfig.StageConfig.enabled ~= false
self.isDrawStagesEnabled = SpearheadConfig.StageConfig.drawStages ~= false
self.isAutoStages = SpearheadConfig.StageConfig.autoStages ~= false
self.startingStage = SpearheadConfig.StageConfig.startingStage or 1
self.maxMissionsPerStage = SpearheadConfig.StageConfig.maxMissionStage or 10
self.isDrawPreActivatedEnabled = SpearheadConfig.StageConfig.drawPreActivated ~= false
self.AmountPreactivateStage = SpearheadConfig.StageConfig.preactivateStage or 1
self.briefingOnSpawnEnabled = SpearheadConfig.StageConfig.briefingOnSpawn ~= false
return o; _logger:info("Successfully created StageConfig Object")
return self;
end
local config = new();
---@return StageConfig
function StageConfig:getInstance()
return config
end end
return StageConfig return StageConfig
+15 -12
View File
@@ -350,18 +350,6 @@ function Database.New(Logger)
end end
end end
for _, missionZone in pairs(self._tables.MissionZones) do
if self._tables.MissionZoneData[missionZone] == nil or self._tables.MissionZoneData[missionZone].description == nil then
MissionEditorWarnings.Add("Mission with zonename: " .. missionZone .. " does not have a briefing")
end
end
for _, missionZone in pairs(self._tables.RandomMissionZones) do
if self._tables.MissionZoneData[missionZone] == nil or self._tables.MissionZoneData[missionZone].description == nil then
MissionEditorWarnings.Add("Mission with zonename: " .. missionZone .. " does not have a briefing")
end
end
for _, farpZoneName in pairs(self._tables.AllFarpZones) do for _, farpZoneName in pairs(self._tables.AllFarpZones) do
for _, airbase in pairs(world.getAirbases()) do for _, airbase in pairs(world.getAirbases()) do
if airbase:getDesc().category == Airbase.Category.HELIPAD then if airbase:getDesc().category == Airbase.Category.HELIPAD then
@@ -447,6 +435,21 @@ function Database.New(Logger)
end end
end end
-- Checks for missing briefings in mission zones and random mission zones
do
for _, missionZone in pairs(self._tables.MissionZones) do
if self._tables.MissionZoneData[missionZone] == nil or self._tables.MissionZoneData[missionZone].description == nil then
MissionEditorWarnings.Add("Mission with zonename: " .. missionZone .. " does not have a briefing")
end
end
for _, missionZone in pairs(self._tables.RandomMissionZones) do
if self._tables.MissionZoneData[missionZone] == nil or self._tables.MissionZoneData[missionZone].description == nil then
MissionEditorWarnings.Add("Mission with zonename: " .. missionZone .. " does not have a briefing")
end
end
end
if missions == 0 then missions = 1 end if missions == 0 then missions = 1 end
self._logger:info("initiated the database with amount of zones: ") self._logger:info("initiated the database with amount of zones: ")
+23
View File
@@ -50,6 +50,19 @@ do
table.insert(OnStageNumberChangedHandlers, handler) table.insert(OnStageNumberChangedHandlers, handler)
end end
---@class OnStageNumberChangeCompleteListener
---@field OnStageNumberChangeComplete fun(self:OnStageNumberChangeCompleteListener, number:integer)
local OnStageNumberChangeCompleteListeners = {}
---@param listener OnStageNumberChangeCompleteListener
SpearheadEvents.AddStageNumberChangeCompleteListener = function(listener)
if type(listener) ~= "table" or type(listener.OnStageNumberChangeComplete) ~= "function" then
warn("Event handler not of type table/object with function OnStageNumberChangeComplete(self, number)")
return
end
table.insert(OnStageNumberChangeCompleteListeners, listener)
end
---@param newStageNumber number ---@param newStageNumber number
SpearheadEvents.PublishStageNumberChanged = function(newStageNumber) SpearheadEvents.PublishStageNumberChanged = function(newStageNumber)
pcall(function () pcall(function ()
@@ -71,6 +84,16 @@ do
logError(err) logError(err)
end end
end end
for _, callable in pairs(OnStageNumberChangeCompleteListeners) do
local succ, err = pcall(function()
callable:OnStageNumberChangeComplete(newStageNumber)
end)
if err then
logError(err)
end
end
Logger.new("Events", "INFO"):info("Published stage number changed to: " .. tostring(newStageNumber)) Logger.new("Events", "INFO"):info("Published stage number changed to: " .. tostring(newStageNumber))
end end
end end
+6 -6
View File
@@ -92,7 +92,7 @@ do --setup route util
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishOnStation, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishOnStation, \"" .. groupName .. "\")"
} }
} }
} }
@@ -122,7 +122,7 @@ do --setup route util
}, },
stopCondition = { stopCondition = {
duration = durationBefore10, duration = durationBefore10,
condition = "return Spearhead.DcsUtil.IsBingoFuel(\"" .. groupName .. "\", 0.10)", condition = "return GlobalCapCallBacks.IsBingoFuel(\"" .. groupName .. "\", 0.10)",
} }
} }
}, },
@@ -135,7 +135,7 @@ do --setup route util
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishRTBInTen, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishRTBInTen, \"" .. groupName .. "\")"
} }
} }
} }
@@ -156,7 +156,7 @@ do --setup route util
}, },
stopCondition = { stopCondition = {
duration = durationAfter10, duration = durationAfter10,
condition = "return Spearhead.DcsUtil.IsBingoFuel(\"" .. groupName .. "\")", condition = "return GlobalCapCallBacks.IsBingoFuel(\"" .. groupName .. "\", 0.10)",
} }
} }
}, },
@@ -169,7 +169,7 @@ do --setup route util
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")"
} }
} }
} }
@@ -375,7 +375,7 @@ do --setup route util
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" .. command = "pcall(GlobalCapCallBacks.PublishRTB, \"" ..
groupName .. "\")" groupName .. "\")"
} }
} }
@@ -1,11 +1,13 @@
local Events = require("classes.spearhead_events") local Events = require("classes.spearhead_events")
local Util = require("classes.util.Util") local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil")
local Logger = require("classes.util.Logger") local Logger = require("classes.util.Logger")
local MissionEditorWarnings = require("classes.util.MissionEditorWarnings") local MissionEditorWarnings = require("classes.util.MissionEditorWarnings")
local ExtraStage = require("classes.stageClasses.Stages.ExtraStage") local ExtraStage = require("classes.stageClasses.Stages.ExtraStage")
local PrimaryStage = require("classes.stageClasses.Stages.PrimaryStage") local PrimaryStage = require("classes.stageClasses.Stages.PrimaryStage")
local WaitingStage = require("classes.stageClasses.Stages.WaitingStage") local WaitingStage = require("classes.stageClasses.Stages.WaitingStage")
local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper")
local StagesByName = {} local StagesByName = {}
@@ -20,10 +22,11 @@ local WaitingStagesByIndex = {}
local currentStage = -99 local currentStage = -99
---@class GlobalStageManager : StageCompleteListener, OnStageChangedListener ---@class GlobalStageManager : StageCompleteListener, OnStageChangedListener, OnStageNumberChangeCompleteListener
---@field private database Database ---@field private database Database
---@field private logger Logger ---@field private logger Logger
---@field private stageConfig StageConfig ---@field private stageConfig StageConfig
---@field private _missionCommandsHelper MissionCommandsHelper
local GlobalStageManager = {} local GlobalStageManager = {}
GlobalStageManager.__index = GlobalStageManager GlobalStageManager.__index = GlobalStageManager
@@ -41,6 +44,7 @@ function GlobalStageManager.NewAndStart(database, stageConfig, logLevel, spawnMa
local self = setmetatable({}, GlobalStageManager) local self = setmetatable({}, GlobalStageManager)
self.database = database self.database = database
self.stageConfig = stageConfig self.stageConfig = stageConfig
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
self.logger = logger self.logger = logger
if stageConfig.isAutoStages ~= true then if stageConfig.isAutoStages ~= true then
@@ -214,14 +218,34 @@ function GlobalStageManager:OnStageNumberChanged(stageNumber)
self:UpdateDrawings(stageNumber) self:UpdateDrawings(stageNumber)
end end
function GlobalStageManager:OnStageNumberChangeComplete(stageNumber)
self.logger:debug("Stage number change complete to: " .. tostring(stageNumber))
local groups = {}
for _, player in pairs(DcsUtil.getAllPlayerUnits()) do
local group = player:getGroup()
if group then
groups[group:getID()] = group
end
end
for _, group in pairs(groups) do
self._missionCommandsHelper:OverviewToGroup(group:getID())
end
end
---@private ---@private
function GlobalStageManager:UpdateDrawings(stageNumber) function GlobalStageManager:UpdateDrawings(stageNumber)
self.logger:debug("Updating custom drawings for stage number: " .. tostring(stageNumber))
local drawings = self.database:getCustomDrawings() local drawings = self.database:getCustomDrawings()
for _, drawing in pairs(drawings) do for _, drawing in pairs(drawings) do
local startStage, stopStage = drawing:GetStartAndStop() local startStage, stopStage = drawing:GetStartAndStop()
if stageNumber >= startStage and stageNumber < stopStage then if stageNumber >= startStage and stageNumber < stopStage then
self.logger:debug("Drawing " .. drawing:GetName() .. " is active for stage number: " .. tostring(stageNumber))
drawing:Draw() drawing:Draw()
else else
self.logger:debug("Drawing " .. drawing:GetName() .. " is not active for stage number: " .. tostring(stageNumber))
drawing:Remove() drawing:Remove()
end end
end end
@@ -2,6 +2,8 @@ local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil") local DcsUtil = require("classes.util.DcsUtil")
local SupplyUnitsTracker = require("classes.stageClasses.helpers.SupplyUnitsTracker") local SupplyUnitsTracker = require("classes.stageClasses.helpers.SupplyUnitsTracker")
local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper") local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper")
local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
---@class SupplyHub ---@class SupplyHub
---@field private _database Database ---@field private _database Database
@@ -12,7 +14,7 @@ local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionComma
---@field private _isCommmandAdded table<string, boolean> ---@field private _isCommmandAdded table<string, boolean>
---@field private _missionCommandsHelper MissionCommandsHelper ---@field private _missionCommandsHelper MissionCommandsHelper
---@field private _inZone table<string, boolean> ---@field private _inZone table<string, boolean>
---@field private _drawID number ---@field private _customDrawing CustomDrawing?
---@field private _cargoInUnits table<table<string, number>> ---@field private _cargoInUnits table<table<string, number>>
---@field private _activeAtStart boolean ---@field private _activeAtStart boolean
---@field private _active boolean ---@field private _active boolean
@@ -30,6 +32,7 @@ function SupplyHub.new(database, logger, zoneName)
self._database = database self._database = database
self._logger = logger self._logger = logger
self._zoneName = zoneName self._zoneName = zoneName
self._customDrawing = nil
local split = Util.split_string(zoneName, "_") local split = Util.split_string(zoneName, "_")
if string.lower(split[2]) == "a" then if string.lower(split[2]) == "a" then
@@ -40,9 +43,9 @@ function SupplyHub.new(database, logger, zoneName)
self._zone = DcsUtil.getZoneByName(zoneName) self._zone = DcsUtil.getZoneByName(zoneName)
self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate(logger.LogLevel) self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate()
self._inZone = {} self._inZone = {}
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate(logger.LogLevel) self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
self._logger:debug("Creating Supply Hub zone: " .. self._zoneName) self._logger:debug("Creating Supply Hub zone: " .. self._zoneName)
@@ -72,13 +75,13 @@ function SupplyHub:Activate()
self._logger:debug("Activating Supply Hub zone: " .. self._zoneName) self._logger:debug("Activating Supply Hub zone: " .. self._zoneName)
local zone = DcsUtil.getZoneByName(self._zoneName) local zone = DcsUtil.getZoneByName(self._zoneName)
if zone and self._drawID == nil then if zone and self._customDrawing == nil then
---@type DrawColor local fillColor = DrawingHelper.ColorTableToColorString({ 0, 1, 0, 0.2 })
local fillColor = { r=0, g=1, b=0, a=0.2 } local lineColor = DrawingHelper.ColorTableToColorString({ 0, 1, 0, 1})
---@type DrawColor
local lineColor = { r=0, g=1, b=0, a=1}
local lineStyle = 1 local lineStyle = 1
self._drawID = DcsUtil.DrawZone(zone, lineColor, fillColor, lineStyle)
self._customDrawing = CustomDrawing.FromZone(zone, lineColor, fillColor, lineStyle, 6)
self._customDrawing:Draw()
end end
self._supplyUnitsTracker:RegisterHub(self) self._supplyUnitsTracker:RegisterHub(self)
@@ -11,6 +11,8 @@ local StageBase = require("classes.stageClasses.SpecialZones.StageBase")
local BlueSam = require("classes.stageClasses.SpecialZones.BlueSam") local BlueSam = require("classes.stageClasses.SpecialZones.BlueSam")
local Events = require("classes.spearhead_events") local Events = require("classes.spearhead_events")
local GlobalCapManager = require("classes.capClasses.GlobalCapManager") local GlobalCapManager = require("classes.capClasses.GlobalCapManager")
local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
---@alias StageColor ---@alias StageColor
---| "RED" ---| "RED"
@@ -43,17 +45,15 @@ local GlobalCapManager = require("classes.capClasses.GlobalCapManager")
--- @field zoneName string --- @field zoneName string
--- @field stageName string? --- @field stageName string?
--- @field stageNumber number --- @field stageNumber number
--- @field protected _currentStageState CurrentStageState
--- @field protected _missionCommandsHelper MissionCommandsHelper --- @field protected _missionCommandsHelper MissionCommandsHelper
--- @field protected _isActive boolean
--- @field protected _isComplete boolean
--- @field protected _missionPriority MissionPriority --- @field protected _missionPriority MissionPriority
--- @field protected _database Database --- @field protected _database Database
--- @field protected _db StageData --- @field protected _db StageData
--- @field protected _logger Logger --- @field protected _logger Logger
--- @field protected _preActivated boolean
--- @field protected _activeStage integer --- @field protected _activeStage integer
--- @field protected _stageConfig StageConfig --- @field protected _stageConfig StageConfig
--- @field protected _stageDrawingId integer --- @field protected _customDrawing CustomDrawing
--- @field protected _spawnedGroups Array<string> --- @field protected _spawnedGroups Array<string>
--- @field protected _stageCompleteListeners Array<StageCompleteListener> --- @field protected _stageCompleteListeners Array<StageCompleteListener>
--- @field protected CheckContinuousAsync fun(self:Stage, time:number) : number? --- @field protected CheckContinuousAsync fun(self:Stage, time:number) : number?
@@ -63,13 +63,20 @@ local Stage = {}
Stage.__index = Stage Stage.__index = Stage
---@enum CurrentStageState
Stage.CurrentStageState = {
INACTIVE = 0,
PREACTIVATED = 1,
ACTIVE = 2,
BLUE = 3
}
Stage.StageColors = { Stage.StageColors = {
INVISIBLE = { r=0, g=0, b=0, a=0 }, INVISIBLE = { 0, 0, 0, 0 },
RED_ACTIVE = { r=1, g=0, b=0, a=0.15 }, RED_ACTIVE = { 1, 0, 0, 0.20 },
RED_PREACTIVE = { r=1, g=0, b=0, a=0.10}, RED_PREACTIVE = { 1, 0, 0, 0.05},
BLUE = { r=0, g=0, b=1, a=0.10}, BLUE = { 0, 0, 1, 0.10},
GRAY = { r=80/255, g=80/255, b=80/255, a=0.10 } GRAY = { 80/255, 80/255, 80/255, 0.10 }
} }
---comment ---comment
@@ -83,12 +90,11 @@ Stage.StageColors = {
function Stage:superNew(database, stageConfig, logger, initData, missionPriority, spawnManager) function Stage:superNew(database, stageConfig, logger, initData, missionPriority, spawnManager)
logger:debug("[BaseStage] Initiating stage with name: " .. initData.stageZoneName) logger:debug("[BaseStage] Initiating stage with name: " .. initData.stageZoneName)
self._currentStageState = Stage.CurrentStageState.INACTIVE
self.zoneName = initData.stageZoneName self.zoneName = initData.stageZoneName
self.stageNumber = initData.stageNumber self.stageNumber = initData.stageNumber
self._isActive = false
self._isComplete = false
self.stageName = initData.stageDisplayName self.stageName = initData.stageDisplayName
self._currentStageState = Stage.CurrentStageState.INACTIVE
self.OnPostStageComplete = nil self.OnPostStageComplete = nil
self.OnPostBlueActivated = nil self.OnPostBlueActivated = nil
@@ -110,13 +116,19 @@ function Stage:superNew(database, stageConfig, logger, initData, missionPriority
} }
self._activeStage = -99 self._activeStage = -99
self._preActivated = false
self._stageConfig = stageConfig or {} self._stageConfig = stageConfig or {}
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate(logger.LogLevel) self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
local zone = DcsUtil.getZoneByName(self.zoneName) local zone = DcsUtil.getZoneByName(self.zoneName)
if zone then if zone then
self._stageDrawingId = DcsUtil.DrawZone(zone, Stage.StageColors.INVISIBLE, Stage.StageColors.INVISIBLE, 4)
local colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.INVISIBLE)
local fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.INVISIBLE)
local customDrawing = CustomDrawing.FromZone(zone, colorString, fillColorString, 1, 5)
if customDrawing then
self._customDrawing = customDrawing
end
end end
self._spawnedGroups = {} self._spawnedGroups = {}
@@ -253,6 +265,7 @@ function Stage:superNew(database, stageConfig, logger, initData, missionPriority
end end
end end
Events.AddStageNumberChangedListener(self) Events.AddStageNumberChangedListener(self)
return self return self
@@ -260,7 +273,7 @@ end
---@return boolean ---@return boolean
function Stage:IsComplete() function Stage:IsComplete()
if self._isComplete == true then return true end if self._currentStageState == Stage.CurrentStageState.BLUE then return true end
for i, mission in pairs(self._db.sams) do for i, mission in pairs(self._db.sams) do
local state = mission:getState() local state = mission:getState()
@@ -276,13 +289,13 @@ function Stage:IsComplete()
end end
end end
self._isComplete = true self._currentStageState = Stage.CurrentStageState.BLUE
return true return true
end end
---@return boolean ---@return boolean
function Stage:IsActive() function Stage:IsActive()
return self._isActive == true return self._currentStageState == Stage.CurrentStageState.ACTIVE
end end
---comment ---comment
@@ -367,10 +380,9 @@ function Stage:AddStageCompleteListener(listener)
end end
---Activates all SAMS, Airbase units etc all at once. ---Activates all SAMS, Airbase units etc all at once.
---@param draw boolean function Stage:PreActivate()
function Stage:PreActivate(draw) if self._currentStageState == Stage.CurrentStageState.INACTIVE then
if self._preActivated == false then self._currentStageState = Stage.CurrentStageState.PREACTIVATED
self._preActivated = true
for key, mission in pairs(self._db.sams) do for key, mission in pairs(self._db.sams) do
if mission then if mission then
mission:SpawnInactive() mission:SpawnInactive()
@@ -382,40 +394,60 @@ function Stage:PreActivate(draw)
end end
end end
if draw == true then self:MarkStage()
self:MarkStage(Stage.StageColors.RED_PREACTIVE)
end
end end
---@param stageColor DrawColor function Stage:MarkStage()
function Stage:MarkStage(stageColor)
local lineColor = { r=stageColor.r, g=stageColor.g, b=stageColor.b, a=stageColor.a }
local fillColor = { r=stageColor.r, g=stageColor.g, b=stageColor.b, a=stageColor.a }
if stageColor.a > 0 then self._logger:debug("Marking stage '" .. Util.toString(self.zoneName) .. "' with state: " .. self._currentStageState)
lineColor.a = 1
if self._customDrawing then
self._customDrawing:Remove()
end end
if stageColor == Stage.StageColors.RED_PREACTIVE then if self._stageConfig.isDrawStagesEnabled == false then return end
lineColor.a = 0 if self._stageConfig.isDrawPreActivatedEnabled == false and self._currentStageState == Stage.CurrentStageState.PREACTIVATED then
return
end end
if self._stageDrawingId and self._stageConfig.isDrawStagesEnabled == true then if self._customDrawing then
DcsUtil.SetLineColor(self._stageDrawingId, lineColor) self._customDrawing:UpdateDrawingObject(function(drawingObject)
DcsUtil.SetFillColor(self._stageDrawingId, fillColor) local drawing = drawingObject --[[@as Polygon]]
if self._currentStageState == Stage.CurrentStageState.ACTIVE then
drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_ACTIVE)
drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_ACTIVE)
drawing.style = "dot dash"
elseif self._currentStageState == Stage.CurrentStageState.PREACTIVATED then
drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_PREACTIVE)
drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_PREACTIVE)
drawing.style = "no line"
elseif self._currentStageState == Stage.CurrentStageState.BLUE then
drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.BLUE)
drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.BLUE)
drawing.style = "two dash"
else
drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.INVISIBLE)
drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.INVISIBLE)
drawing.style = "no line"
end end
return drawing
end)
self._customDrawing:Draw()
end
end end
function Stage:ActivateStage() function Stage:ActivateStage()
self._isActive = true; self._currentStageState = Stage.CurrentStageState.ACTIVE
self:PreActivate()
pcall(function() pcall(function()
self:MarkStage(Stage.StageColors.RED_ACTIVE) self:MarkStage()
end) end)
self:PreActivate(false)
self._logger:debug("Activating Misc groups for zone. Count: " .. Util.tableLength(self._db.miscGroups)) self._logger:debug("Activating Misc groups for zone. Count: " .. Util.tableLength(self._db.miscGroups))
for _, miscGroup in pairs(self._db.miscGroups) do for _, miscGroup in pairs(self._db.miscGroups) do
miscGroup:Spawn() miscGroup:Spawn()
@@ -462,9 +494,9 @@ function Stage:OnStageNumberChanged(number)
if self.stageNumber - self._activeStage == self._stageConfig.AmountPreactivateStage then if self.stageNumber - self._activeStage == self._stageConfig.AmountPreactivateStage then
self._logger:debug("Pre-activating stage: " .. self.zoneName .. " with number: " .. number) self._logger:debug("Pre-activating stage: " .. self.zoneName .. " with number: " .. number)
self:PreActivate(true) self:PreActivate()
elseif GlobalCapManager.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then elseif GlobalCapManager.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then
self:PreActivate(false) self:PreActivate()
end end
if number == self.stageNumber then if number == self.stageNumber then
@@ -559,7 +591,7 @@ end
function Stage:ActivateBlueStage() function Stage:ActivateBlueStage()
self._logger:debug("Setting stage '" .. Util.toString(self.zoneName) .. "' to blue") self._logger:debug("Setting stage '" .. Util.toString(self.zoneName) .. "' to blue")
self._currentStageState = Stage.CurrentStageState.BLUE
for _, mission in pairs(self._db.missions) do for _, mission in pairs(self._db.missions) do
mission:SpawnPersistedState() mission:SpawnPersistedState()
end end
@@ -575,7 +607,7 @@ function Stage:ActivateBlueStage()
---@param self Stage ---@param self Stage
local ActivateBlueAsync = function(self) local ActivateBlueAsync = function(self)
pcall(function() pcall(function()
self:MarkStage(Stage.StageColors.BLUE) self:MarkStage()
end) end)
self:ActivateBlueGroups() self:ActivateBlueGroups()
@@ -23,7 +23,7 @@ function ExtraStage.New(database, stageConfig, logger, initData, spawnManager)
self.OnPostBlueActivated = function (selfStage) self.OnPostBlueActivated = function (selfStage)
selfStage:MarkStage(Stage.StageColors.GRAY) selfStage:MarkStage()
end end
self.OnPostStageComplete = function (selfStage) self.OnPostStageComplete = function (selfStage)
@@ -34,7 +34,7 @@ function ExtraStage.New(database, stageConfig, logger, initData, spawnManager)
end end
---comment ---comment
---@param self Stage ---@param self ExtraStage
---@param number integer ---@param number integer
function ExtraStage:OnStageNumberChanged(number) function ExtraStage:OnStageNumberChanged(number)
@@ -47,16 +47,16 @@ function ExtraStage:OnStageNumberChanged(number)
if self.stageNumber - self._activeStage == self._stageConfig.AmountPreactivateStage then if self.stageNumber - self._activeStage == self._stageConfig.AmountPreactivateStage then
self._logger:debug("Pre-activating stage: " .. self.zoneName .. " with number: " .. number) self._logger:debug("Pre-activating stage: " .. self.zoneName .. " with number: " .. number)
self:PreActivate(true) self:PreActivate()
elseif GlobalCapManager.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then elseif GlobalCapManager.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then
self:PreActivate(false) self:PreActivate()
end end
if number == self.stageNumber then if number == self.stageNumber then
self:ActivateStage() self:ActivateStage()
end end
if self._isComplete == true then if self._currentStageState == Stage.CurrentStageState.BLUE then
self:ActivateBlueStage() self:ActivateBlueStage()
end end
@@ -1,23 +1,28 @@
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper") local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
local Util = require("classes.util.Util") local Util = require("classes.util.Util")
local Logger = require("classes.util.Logger")
local drawingLogger = Logger.new("CustomDrawing")
---@class CustomDrawing ---@class CustomDrawing
---@field private _id integer? ---@field private _id integer?
---@field private _drawingObject DrawingObject ---@field private _drawingObject DrawingObject
---@field private _startingStage number ---@field private _startingStage number
---@field private _removeAtStage number ---@field private _removeAtStage number
---@field private _logger Logger
local CustomDrawing = {} local CustomDrawing = {}
CustomDrawing.__index = CustomDrawing CustomDrawing.__index = CustomDrawing
---@param drawingObject DrawingObject ---@param drawingObject DrawingObject
---@param id integer?
---@return CustomDrawing ---@return CustomDrawing
function CustomDrawing.New(drawingObject, id) function CustomDrawing.New(drawingObject)
local self = setmetatable({}, CustomDrawing) local self = setmetatable({}, CustomDrawing)
self._drawingObject = drawingObject self._drawingObject = drawingObject
self._id = id self._id = nil -- initiate ID as nil, when drawn it will be set to the ID returned by DrawingHelper.Draw
self._logger = drawingLogger
local name = drawingObject.name local name = drawingObject.name
local split = Util.split_string(name or "", "_") local split = Util.split_string(name or "", "_")
@@ -28,6 +33,63 @@ function CustomDrawing.New(drawingObject, id)
return self return self
end end
---@param zone SpearheadTriggerZone
---@param colorString string
---@param fillColorString string
---@param lineStyle LineType
---@param lineThickness number
---@return CustomDrawing?
function CustomDrawing.FromZone(zone, colorString, fillColorString, lineStyle, lineThickness)
if zone == nil then
drawingLogger:warn("CustomDrawing.FromZone called with nil zone")
return nil
end
if zone.zone_type == "Cilinder" then
---@type Circle
local drawingObject = {
mapX = zone.location.x,
mapY = zone.location.y,
radius = zone.radius,
name = zone.name .. "_drawing",
primitiveType = "Polygon",
polygonMode = "circle",
visible = true,
style = DrawingHelper.ToLineStyleString(lineStyle),
colorString = colorString,
fillColorString = fillColorString,
thickness = lineThickness,
}
return CustomDrawing.New(drawingObject)
end
if zone.zone_type == "Polygon" then
---@type Free
local drawingObject = {
mapX = 0,
mapY = 0,
points = zone.verts,
name = zone.name .. "_drawing",
primitiveType = "Polygon",
polygonMode = "free",
visible = true,
style = DrawingHelper.ToLineStyleString(lineStyle),
colorString = colorString,
fillColorString = fillColorString,
thickness = lineThickness,
}
return CustomDrawing.New(drawingObject)
end
end
---@return string
function CustomDrawing:GetName()
return self._drawingObject.name
end
---@return number start ---@return number start
---@return number stop ---@return number stop
function CustomDrawing:GetStartAndStop() function CustomDrawing:GetStartAndStop()
@@ -35,14 +97,26 @@ function CustomDrawing:GetStartAndStop()
end end
function CustomDrawing:Draw() function CustomDrawing:Draw()
self._id = DrawingHelper.Draw(self._drawingObject) self._logger:debug("Drawing custom drawing with ID " .. tostring(self._id))
if self._id ~= nil then
DrawingHelper.Remove(self._id)
end
self._id = DrawingHelper.Draw(self._drawingObject, self._logger)
end end
function CustomDrawing:Remove() function CustomDrawing:Remove()
if self._id ~= nil then if self._id ~= nil then
self._logger:debug("Removing custom drawing with ID " .. tostring(self._id))
DrawingHelper.Remove(self._id) DrawingHelper.Remove(self._id)
self._id = nil self._id = nil
end end
end end
---@param updateFunc fun(drawingObject:DrawingObject):DrawingObject
function CustomDrawing:UpdateDrawingObject(updateFunc)
self._drawingObject = updateFunc(self._drawingObject)
end
return CustomDrawing return CustomDrawing
@@ -1,25 +1,49 @@
local Util = require("classes.util.Util")
local Logger = require("classes.util.Logger")
local GlobalConfig = require("classes.configuration.GlobalConfig")
---@type LogLevel
local level = "INFO"
if GlobalConfig:isDebugEnabled() then
level = "DEBUG"
end
local drawingLogger = Logger.new("DrawingHelper", level)
---@class DrawingHelper ---@class DrawingHelper
local DrawingHelper = {} local DrawingHelper = {}
DrawingHelper.__index = DrawingHelper DrawingHelper.__index = DrawingHelper
local customDrawingIdIncrementer = 4210 local customDrawingIdIncrementer = 4210
---@param object DrawingObject ---@param object DrawingObject
---@param logger Logger?
---@return integer? id ---@return integer? id
function DrawingHelper.Draw(object) function DrawingHelper.Draw(object, logger)
if object == nil then if object == nil then
if logger then
logger:warn("DrawingHelper.Draw called with nil object")
end
return nil return nil
end end
local id = DrawingHelper.GetAndAddId() local id = DrawingHelper.GetAndAddId()
if logger then
logger:debug("Drawing object with ID " .. tostring(id) .. " and primitive type: " .. tostring(object.primitiveType))
end
if(object.primitiveType == "Polygon") then if(object.primitiveType == "Polygon") then
DrawingHelper.DrawPolygon(object--[[@as Polygon]], id) DrawingHelper.DrawPolygon(object--[[@as Polygon]], id, logger)
elseif(object.primitiveType == "Line") then elseif(object.primitiveType == "Line") then
DrawingHelper.DrawLine(object--[[@as Line]], id) DrawingHelper.DrawLine(object--[[@as Line]], id, logger)
elseif(object.primitiveType == "TextBox") then elseif(object.primitiveType == "TextBox") then
DrawingHelper.DrawTextBox(object--[[@as TextBox]], id) DrawingHelper.DrawTextBox(object--[[@as TextBox]], id)
else
if logger then
logger:warn("Unknown primitive type: " .. tostring(object.primitiveType))
end
end end
return id return id
@@ -36,16 +60,25 @@ end
---@param points Array<Vec3> ---@param points Array<Vec3>
---@param fillColor table ---@param fillColor table
---@param lineColor table ---@param lineColor table
local function MarkupToAll(shapeID, drawID, points, fillColor, lineColor, lineStyle) ---@param lineStyle LineType
---@param lineThickness number
---@param logger Logger?
local function MarkupToAll(shapeID, drawID, points, fillColor, lineColor, lineStyle, lineThickness, logger)
if lineThickness == nil or lineThickness <= 0 then
lineStyle = 0
end
local functionString = "trigger.action.markupToAll(" .. shapeID .. ", -1, " .. drawID .. "," local functionString = "trigger.action.markupToAll(" .. shapeID .. ", -1, " .. drawID .. ","
for _, point in ipairs(points) do
for _, point in pairs(points) do
functionString = functionString .. " { x=" .. point.x .. ", y=0,z=" .. point.z .. "}," functionString = functionString .. " { x=" .. point.x .. ", y=0,z=" .. point.z .. "},"
end end
functionString = functionString .. functionString = functionString .. "{0,1,0,1}, {0,1,0,1}, " .. lineStyle .. ")"
"{ " .. lineColor[1] .. "," .. lineColor[2] .. "," .. lineColor[3] .. "," .. lineColor[4] .. "}, " ..
"{ " .. fillColor[1] .. "," .. fillColor[2] .. "," .. fillColor[3] .. "," .. fillColor[4] .. "}, " .. if logger then
lineStyle .. ")" logger:debug("Drawing complex drawing with ID " .. tostring(drawID) .. " and function string: " .. functionString)
end
---@diagnostic disable-next-line: deprecated ---@diagnostic disable-next-line: deprecated
local f, err = loadstring(functionString) local f, err = loadstring(functionString)
@@ -55,12 +88,21 @@ local function MarkupToAll(shapeID, drawID, points, fillColor, lineColor, lineS
env.error("Something failed when drawing complex drawing" .. err) env.error("Something failed when drawing complex drawing" .. err)
end end
if logger then
logger:debug("Drawing with fill color: " .. table.concat(fillColor, ",") .. " and line color: " .. table.concat(lineColor, ",") .. " and line style: " .. tostring(lineStyle) .. " and line thickness: " .. tostring(lineThickness))
end
trigger.action.setMarkupColorFill(drawID, fillColor)
trigger.action.setMarkupColor(drawID, lineColor)
trigger.action.setMarkupTypeLine(drawID, lineStyle)
end end
---@private ---@private
---@param object Polygon ---@param object Polygon
---@param id integer ---@param id integer
function DrawingHelper.DrawPolygon(object, id) ---@param logger Logger?
function DrawingHelper.DrawPolygon(object, id, logger)
if object == nil then if object == nil then
return return
end end
@@ -75,11 +117,12 @@ function DrawingHelper.DrawPolygon(object, id)
end end
---@param oval Oval ---@param oval Oval
local function DrawOval(oval) ---@param logger Logger?
local function DrawOval(oval, logger)
---@type Array<Vec3> ---@type Array<Vec3>
local points = {} local points = {}
local pointsNo = 30 local pointsNo = 30
local angleStep = (2 * math.pi) / points local angleStep = (2 * math.pi) / pointsNo
local fillColor = DrawingHelper.ColorToColorTable(oval.fillColorString) local fillColor = DrawingHelper.ColorToColorTable(oval.fillColorString)
local color = DrawingHelper.ColorToColorTable(oval.colorString) local color = DrawingHelper.ColorToColorTable(oval.colorString)
@@ -91,20 +134,36 @@ function DrawingHelper.DrawPolygon(object, id)
local y = oval.mapY + (oval.r2 * math.sin(angle)) local y = oval.mapY + (oval.r2 * math.sin(angle))
table.insert(points, { x = x, y = 0, z = y } ) table.insert(points, { x = x, y = 0, z = y } )
end end
MarkupToAll(7, id, points, fillColor, color, lineStyle) MarkupToAll(7, id, points, fillColor, color, lineStyle, oval.thickness, logger)
end end
---@param free Free ---@param free Free
local function DrawFree(free) ---@param logger Logger?
local function DrawFree(free, logger)
local fillColor = DrawingHelper.ColorToColorTable(free.fillColorString) local fillColor = DrawingHelper.ColorToColorTable(free.fillColorString)
local color = DrawingHelper.ColorToColorTable(free.colorString) local color = DrawingHelper.ColorToColorTable(free.colorString)
local lineStyle = DrawingHelper.ToLineStyleInteger(free.style) local lineStyle = DrawingHelper.ToLineStyleInteger(free.style)
local points = {} local keys = {}
for _, point in ipairs(free.points) do for k, _ in pairs(free.points) do
table.insert(points, { x = point.x, y = 0, z = point.y } ) table.insert(keys, k)
end end
MarkupToAll(7, id, points, fillColor, color, lineStyle)
table.sort(keys, function(a, b) return a < b end)
local points = {}
for _, k in ipairs(keys) do
local point = free.points[k]
local newPoint = { x = free.mapX + point.x, y = 0, z = free.mapY + point.y }
local firstPoint = points[1]
if firstPoint == nil or newPoint.x ~= firstPoint.x or newPoint.z ~= firstPoint.z then
table.insert(points, newPoint)
end
end
MarkupToAll(7, id, points, fillColor, color, lineStyle, free.thickness, logger)
end end
---@param rect Rect ---@param rect Rect
@@ -116,38 +175,49 @@ function DrawingHelper.DrawPolygon(object, id)
local pointA = { x = rect.mapX, y = 0, z = rect.mapY } local pointA = { x = rect.mapX, y = 0, z = rect.mapY }
local pointB = { x = rect.mapX + rect.width, y = 0, z = rect.mapY + rect.height } local pointB = { x = rect.mapX + rect.width, y = 0, z = rect.mapY + rect.height }
trigger.action.rectToAll(-1, id, pointA, pointB, color, fillColor, lineStyle, true) trigger.action.rectToAll(-1, id, pointA, pointB, color, fillColor, lineStyle, true)
end end
---@param arrow Arrow ---@param arrow Arrow
local function DrawArrow(arrow) ---@param logger Logger?
local function DrawArrow(arrow, logger)
local fillColor = DrawingHelper.ColorToColorTable(arrow.fillColorString) local fillColor = DrawingHelper.ColorToColorTable(arrow.fillColorString)
local color = DrawingHelper.ColorToColorTable(arrow.colorString) local color = DrawingHelper.ColorToColorTable(arrow.colorString)
local lineStyle = DrawingHelper.ToLineStyleInteger(arrow.style) local lineStyle = DrawingHelper.ToLineStyleInteger(arrow.style)
local startPoint = { x = arrow.mapX, y = 0, z = arrow.mapY } if logger then
logger:debug("Drawing arrow with start point: " .. tostring(arrow.mapX) .. ", " .. tostring(arrow.mapY) .. " and angle: " .. tostring(arrow.angle) .. " and length: " .. tostring(arrow.length))
end
local endPoint = { x = arrow.mapX, y = 0, z = arrow.mapY }
local rad = math.rad(arrow.angle or 0) local rad = math.rad(arrow.angle or 0)
local length = arrow.length or 100 local length = arrow.length or 100
local endPoint = { x = arrow.mapX + length * math.cos(rad), y = 0, z = arrow.mapY + length * math.sin(rad) } local startPoint = { x = arrow.mapX - length * math.sin(rad), y = 0, z = arrow.mapY + length * math.cos(rad) }
trigger.action.arrowToAll(-1, id, startPoint, endPoint, color, fillColor, lineStyle, true) trigger.action.arrowToAll(-1, id, startPoint, endPoint, color, fillColor, lineStyle, true)
end end
if logger then
logger:debug("Drawing polygon with ID " .. tostring(id) .. " and polygon mode: " .. tostring(object.polygonMode))
end
if object.polygonMode == "circle" then if object.polygonMode == "circle" then
DrawCircle(object--[[@as Circle]]) DrawCircle(object--[[@as Circle]])
elseif object.polygonMode == "oval" then elseif object.polygonMode == "oval" then
DrawOval(object--[[@as Oval]]) DrawOval(object--[[@as Oval]], logger)
elseif object.polygonMode == "free" then elseif object.polygonMode == "free" then
DrawFree(object--[[@as Free]]) DrawFree(object--[[@as Free]], logger)
elseif object.polygonMode == "rect" then elseif object.polygonMode == "rect" then
DrawRect(object--[[@as Rect]]) DrawRect(object--[[@as Rect]])
elseif object.polygonMode == "arrow" then elseif object.polygonMode == "arrow" then
DrawArrow(object--[[@as Arrow]]) DrawArrow(object--[[@as Arrow]], logger)
end end
end end
---@private ---@private
---@param object Line ---@param object Line
---@param id integer ---@param id integer
function DrawingHelper.DrawLine(object, id) ---@param logger Logger?
function DrawingHelper.DrawLine(object, id, logger)
---@type Array<Vec3> ---@type Array<Vec3>
local points = {} local points = {}
@@ -158,7 +228,7 @@ function DrawingHelper.DrawLine(object, id)
local color = DrawingHelper.ColorToColorTable(object.colorString) local color = DrawingHelper.ColorToColorTable(object.colorString)
local lineStyle = DrawingHelper.ToLineStyleInteger(object.style) local lineStyle = DrawingHelper.ToLineStyleInteger(object.style)
MarkupToAll(1, id, points, color, color, lineStyle) MarkupToAll(1, id, points, color, color, lineStyle, object.thickness, logger)
end end
---@private ---@private
@@ -177,20 +247,41 @@ function DrawingHelper.Remove(id)
trigger.action.removeMark(id) trigger.action.removeMark(id)
end end
---@param hexStr string ---@param hexStr string
---@return table ---@return table
function DrawingHelper.ColorToColorTable(hexStr) function DrawingHelper.ColorToColorTable(hexStr)
if hexStr == nil then
drawingLogger:warn("ColorToColorTable called with nil hexStr, returning default color {0, 0, 0, 0}")
return { 0, 0, 0, 0 }
end
hexStr = hexStr:gsub("0x", "") hexStr = hexStr:gsub("0x", "")
local a = tonumber(hexStr:sub(1, 2), 16) / 255 local r = tonumber(hexStr:sub(1, 2), 16) / 255
local r = tonumber(hexStr:sub(3, 4), 16) / 255 local g = tonumber(hexStr:sub(3, 4), 16) / 255
local g = tonumber(hexStr:sub(5, 6), 16) / 255 local b = tonumber(hexStr:sub(5, 6), 16) / 255
local b = tonumber(hexStr:sub(7, 8), 16) / 255 local a = tonumber(hexStr:sub(7, 8), 16) / 255
return { r, g , b , a } return { r, g , b , a }
end end
---@param rgba table
---@return string
function DrawingHelper.ColorTableToColorString(rgba)
if rgba == nil or #rgba < 4 or rgba[1] == nil or rgba[2] == nil or rgba[3] == nil or rgba[4] == nil then
drawingLogger:warn("ColorTableToColorString called with invalid rgba table, returning default color string '0x00000000'")
return "0x00000000"
end
local r = string.format("%02X", math.floor(rgba[1] * 255))
local g = string.format("%02X", math.floor(rgba[2] * 255))
local b = string.format("%02X", math.floor(rgba[3] * 255))
local a = string.format("%02X", math.floor(rgba[4] * 255))
return "0x" .. r .. g .. b .. a
end
---@param lineStyle string ---@param lineStyle string
function DrawingHelper.ToLineStyleInteger(lineStyle) function DrawingHelper.ToLineStyleInteger(lineStyle)
lineStyle = lineStyle:lower() lineStyle = lineStyle:lower()
@@ -213,6 +304,26 @@ function DrawingHelper.ToLineStyleInteger(lineStyle)
end end
end end
function DrawingHelper.ToLineStyleString(lineStyle)
if lineStyle == 0 then
return "no line"
elseif lineStyle == 1 then
return "solid"
elseif lineStyle == 2 then
return "dashed"
elseif lineStyle == 3 then
return "dotted"
elseif lineStyle == 4 then
return "dot dash"
elseif lineStyle == 5 then
return "long dash"
elseif lineStyle == 6 then
return "two dash"
else
return "no line"
end
end
---@class ARGB ---@class ARGB
---@field public a number ---@field public a number
---@field public r number ---@field public r number
@@ -137,10 +137,6 @@ function BattleManager:LetUnitsShoot(groups, targetGroups)
} }
} }
if debugDrawing == true then
self:DrawDebugLine(point, unit)
end
local controller = unit:getController() local controller = unit:getController()
if controller then if controller then
controller:setTask(shootTask) controller:setTask(shootTask)
@@ -245,42 +241,8 @@ function BattleManager:GetRandomPoint(origin, groupHulls)
if not hull then return nil end if not hull then return nil end
local shootPoints = Util.GetTangentHullPointsFromOrigin(hull, origin) local shootPoints = Util.GetTangentHullPointsFromOrigin(hull, origin)
if debugDrawing == true then
self:DrawDebugZone({ hull })
end
return Util.randomFromList(shootPoints) --[[@as Vec2]] return Util.randomFromList(shootPoints) --[[@as Vec2]]
end end
do --DEBUG
---@param unit Unit
---@param target Vec2
function BattleManager:DrawDebugLine(target, unit)
local color = {r = 1, g = 0, b = 0, a = 1}
if unit:getCoalition() == 2 then
color = {r = 0, g = 0, b = 1, a = 1}
end
DcsUtil.DrawLine(unit:getPoint(), {x = target.x, y = 0, z = target.y}, color, 1)
end
---@param hulls Array<Array<Vec2>>
function BattleManager:DrawDebugZone(hulls)
for _, drawHull in pairs(hulls) do
---@type SpearheadTriggerZone
local zone = {
name = "temp",
zone_type = "Polygon",
radius = 0,
verts = drawHull,
location = { x=drawHull[1].x, y=drawHull[1].y },
}
DcsUtil.DrawZone(zone, {r =0, g=0, b =1, a = 0.5} ,{r =0, g= 0, b =1, a = 0}, 1)
end
end
end --DEBUG
return BattleManager return BattleManager
@@ -1,20 +0,0 @@
---@class MaxLoadConfig
---@field maxInternalLoad number
---@type table<string, MaxLoadConfig>
local MaxLoadConfig = {
["Mi-8MT"] = {
maxInternalLoad = 4000,
},
["CH-47Fbl1"] = {
maxInternalLoad = 10000
},
["Mi-24P"] = {
maxInternalLoad = 2000
},
["UH-1H"] = {
maxInternalLoad = 2000
}
}
return MaxLoadConfig
@@ -4,6 +4,7 @@ local Logger = require("classes.util.Logger")
local SpearheadEvents = require("classes.spearhead_events") local SpearheadEvents = require("classes.spearhead_events")
local SupplyUnitsTracker = require("classes.stageClasses.helpers.SupplyUnitsTracker") local SupplyUnitsTracker = require("classes.stageClasses.helpers.SupplyUnitsTracker")
local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHelper") local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHelper")
local StageConfig = require("classes.configuration.StageConfig")
---@class MissionCommandsHelper ---@class MissionCommandsHelper
@@ -14,9 +15,9 @@ local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHel
---@field updateContinuous fun(self: MissionCommandsHelper, time: number): number @function to update commands continuously ---@field updateContinuous fun(self: MissionCommandsHelper, time: number): number @function to update commands continuously
---@field pinnedByGroup table<string, Mission> @table of pinned missions by group ID ---@field pinnedByGroup table<string, Mission> @table of pinned missions by group ID
---@field private _stageBriefings table<string, string> @table of stage briefings by stage name ---@field private _stageBriefings table<string, string> @table of stage briefings by stage name
---@field private _supplyHubGroups table<string, boolean> @table of supply hub groups by their ID
---@field private _logger Logger @logger instance for logging ---@field private _logger Logger @logger instance for logging
---@field private _supplyUnitsTracker SupplyUnitsTracker @supply units tracker instance ---@field private _supplyUnitsTracker SupplyUnitsTracker @supply units tracker instance
---@field private _stageConfig StageConfig
local MissionCommandsHelper = {} local MissionCommandsHelper = {}
MissionCommandsHelper.__index = MissionCommandsHelper MissionCommandsHelper.__index = MissionCommandsHelper
@@ -24,8 +25,8 @@ MissionCommandsHelper.__index = MissionCommandsHelper
---@param groupPos Vec2 ---@param groupPos Vec2
local function sortMissions(list, groupPos) local function sortMissions(list, groupPos)
table.sort(list, function(a, b) table.sort(list, function(a, b)
local distA = Util.VectorDistance2d(groupPos, a.location or {x=0, y=0}) local distA = Util.VectorDistance2d(groupPos, a.location or { x = 0, y = 0 })
local distB = Util.VectorDistance2d(groupPos, b.location or {x=0, y=0}) local distB = Util.VectorDistance2d(groupPos, b.location or { x = 0, y = 0 })
return distA < distB; return distA < distB;
end) end)
end end
@@ -35,12 +36,11 @@ local id = 0
local instance = nil local instance = nil
---@return MissionCommandsHelper ---@return MissionCommandsHelper
---@param logLevel string @log level for the logger function MissionCommandsHelper.getOrCreate()
function MissionCommandsHelper.getOrCreate(logLevel)
if instance == nil then if instance == nil then
instance = setmetatable({}, MissionCommandsHelper) instance = setmetatable({}, MissionCommandsHelper)
instance._logger = Logger.new("MissionCommandsHelper", logLevel) instance._logger = Logger.new("MissionCommandsHelper")
instance._logger:info("Creating MissionCommandsHelper instance") instance._logger:info("Creating MissionCommandsHelper instance")
@@ -49,10 +49,29 @@ function MissionCommandsHelper.getOrCreate(logLevel)
instance.updateNeeded = false instance.updateNeeded = false
instance.pinnedByGroup = {} instance.pinnedByGroup = {}
instance.lastUpdate = 0 instance.lastUpdate = 0
instance._supplyHubGroups = {}
instance._stageBriefings = {} instance._stageBriefings = {}
instance._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate(logLevel) instance._stageConfig = StageConfig:getInstance()
instance._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate()
instance._supplyUnitsTracker:AddOnSupplyUnitEventListener(
{
enteredSupplyHub = function(self, unit)
if unit == nil then return end
instance.updateNeeded = true
instance:updateCommandsForGroup(unit:getGroup():getID())
end,
exitedSupplyHub = function(self, unit)
instance.updateNeeded = true
instance:updateCommandsForGroup(unit:getGroup():getID())
end,
supplyUnitSpawned = function(self, unit)
instance.updateNeeded = true
instance:updateCommandsForGroup(unit:getGroup():getID())
end
}
)
---comment ---comment
---@param selfA MissionCommandsHelper ---@param selfA MissionCommandsHelper
@@ -79,7 +98,6 @@ function MissionCommandsHelper.getOrCreate(logLevel)
timer.scheduleFunction(instance.updateContinuous, instance, timer.getTime() + 5) timer.scheduleFunction(instance.updateContinuous, instance, timer.getTime() + 5)
SpearheadEvents.AddOnPlayerEnterUnitListener(instance) SpearheadEvents.AddOnPlayerEnterUnitListener(instance)
end end
return instance return instance
@@ -108,38 +126,16 @@ function MissionCommandsHelper:RemoveMissionToCommands(mission)
self.updateNeeded = true self.updateNeeded = true
end end
---@param groupID number
function MissionCommandsHelper:MarkUnitInSupplyHub(groupID)
self._logger:debug("Marking unit in supply hub: " .. tostring(groupID))
local updateNeeded = false
if self._supplyHubGroups[tostring(groupID)] ~= true then
updateNeeded = true
end
self._supplyHubGroups[tostring(groupID)] = true
if updateNeeded == true then self:updateCommandsForGroup(groupID) end
end
---@param groupID number
function MissionCommandsHelper:MarkUnitOutsideSupplyHub(groupID)
self._logger:debug("Marking unit outide supply hub: " .. tostring(groupID))
local updateNeeded = false
if self._supplyHubGroups[tostring(groupID)] == true then
updateNeeded = true
end
self._supplyHubGroups[tostring(groupID)] = false
if updateNeeded == true then self:updateCommandsForGroup(groupID) end
end
---@param unit Unit ---@param unit Unit
function MissionCommandsHelper:OnPlayerEntersUnit(unit) function MissionCommandsHelper:OnPlayerEntersUnit(unit)
if unit then if unit then
local group = unit:getGroup() local group = unit:getGroup()
if group then self:updateCommandsForGroup(group:getID()) end if group then
self:updateCommandsForGroup(group:getID())
if self._stageConfig.briefingOnSpawnEnabled == true then
self:OverviewToGroup(group:getID())
end
end
end end
end end
@@ -174,47 +170,40 @@ local pinMissionCommand = function(args)
end end
end end
---@private ---@param groupID integer
function MissionCommandsHelper:AddOverviewCommand(groupID) function MissionCommandsHelper:OverviewToGroup(groupID)
local MissionsOverviewToGroup = function (id)
local text = "Missions Overview\n\n" local text = "Missions Overview\n\n"
local group = DcsUtil.GetPlayerGroupByGroupID(id) local group = DcsUtil.GetPlayerGroupByGroupID(groupID)
---@type Vec2 ---@type Vec2
local groupPos = { x=0, y=0 } local groupPos = { x = 0, y = 0 }
if group then if group then
local pos = group:getUnit(1):getPosition().p local pos = group:getUnit(1):getPosition().p
groupPos = { x= pos.x, y=pos.z } groupPos = { x = pos.x, y = pos.z }
end end
---comment
---@param mission Mission ---@param mission Mission
---@return string ---@return string
local function formatLine(mission) local function formatLine(mission)
local distanceText = "?" local distanceText = "?"
if group then if group then
local lead = group:getUnit(1) local lead = group:getUnit(1)
if lead and lead:isExist() == true then if lead and lead:isExist() == true then
local pos = lead:getPoint() local pos = lead:getPoint()
local Vec2Pos = { x= pos.x, y=pos.z } local Vec2Pos = { x = pos.x, y = pos.z }
local distance = Util.VectorDistance2d(Vec2Pos, mission.location) / 1852 local distance = Util.VectorDistance2d(Vec2Pos, mission.location) / 1852
distanceText = string.format("~%d", math.floor(distance)) distanceText = string.format("~%d", math.floor(distance))
end end
end end
return string.format("[%s]\t%s \t%s \t%s %% \t%s nM\n", mission.code, mission.missionTypeDisplay, mission.name, mission:PercentageComplete(), distanceText) return string.format("[%s]\t%s \t%s \t%s %% \t%s nM\n", mission.code, mission.missionTypeDisplay, mission.name,
mission:PercentageComplete(), distanceText)
end end
for _, briefing in pairs(self._stageBriefings) do for _, briefing in pairs(self._stageBriefings) do
text = text .. briefing .. "\n\n" text = text .. briefing .. "\n\n"
end end
---Primary missions ---Primary missions
text = text .. "Primary Missions\n" text = text .. "Primary Missions\n"
@@ -241,7 +230,6 @@ function MissionCommandsHelper:AddOverviewCommand(groupID)
---@type Array<Mission> ---@type Array<Mission>
local secondaryMissions = {} local secondaryMissions = {}
for code, enabled in pairs(self.enabledByCode) do for code, enabled in pairs(self.enabledByCode) do
if enabled == true then if enabled == true then
local mission = self.missionsByCode[code] local mission = self.missionsByCode[code]
if mission and mission:getState() == "ACTIVE" and mission.priority == "secondary" then if mission and mission:getState() == "ACTIVE" and mission.priority == "secondary" then
@@ -256,29 +244,42 @@ function MissionCommandsHelper:AddOverviewCommand(groupID)
end end
trigger.action.outTextForGroup(id, text, 20, true) trigger.action.outTextForGroup(groupID, text, 20, true)
end
---@private
function MissionCommandsHelper:AddOverviewCommand(groupID)
---@class OverviewToGroupCommandArgs
---@field self MissionCommandsHelper @the MissionCommandsHelper instance
---@field groupId integer @the group ID of the player requesting the overview
---@param args OverviewToGroupCommandArgs
local MissionOverViewToGroup = function(args)
args.self:OverviewToGroup(args.groupId)
end end
missionCommands.removeItemForGroup(groupID, { "Overview" } ) ---@type OverviewToGroupCommandArgs
missionCommands.addCommandForGroup(groupID, "Overview", nil, MissionsOverviewToGroup, groupID) local overviewToGroupCommandArgs = { self = self, groupId = groupID }
missionCommands.removeItemForGroup(groupID, { "Overview" })
missionCommands.addCommandForGroup(groupID, "Overview", nil, MissionOverViewToGroup, overviewToGroupCommandArgs)
end end
---@private ---@private
---@param groupID number ---@param groupID number
function MissionCommandsHelper:AddPinnedMission(groupID) function MissionCommandsHelper:AddPinnedMission(groupID)
local pinndedMission = self.pinnedByGroup[tostring(groupID)] local pinndedMission = self.pinnedByGroup[tostring(groupID)]
missionCommands.removeItemForGroup(groupID, { "Pinned Mission" }) missionCommands.removeItemForGroup(groupID, { "Pinned Mission" })
if pinndedMission and self.enabledByCode[tostring(pinndedMission.code)] == true then if pinndedMission and self.enabledByCode[tostring(pinndedMission.code)] == true then
missionCommands.addCommandForGroup(groupID, "Pinned Mission", nil, missionBriefingRequested, { groupId = groupID, mission = pinndedMission }) missionCommands.addCommandForGroup(groupID, "Pinned Mission", nil, missionBriefingRequested,
{ groupId = groupID, mission = pinndedMission })
end end
end end
---@param groupID number ---@param groupID number
function MissionCommandsHelper:updateCommandsForGroup(groupID) function MissionCommandsHelper:updateCommandsForGroup(groupID)
self._logger:debug("Updating commands for group: " .. tostring(groupID)) self._logger:debug("Updating commands for group: " .. tostring(groupID))
self:AddPinnedMission(groupID) self:AddPinnedMission(groupID)
@@ -296,15 +297,14 @@ function MissionCommandsHelper:updateCommandsForGroup(groupID)
trigger.action.outTextForGroup(id, "clearing...", 1, true) trigger.action.outTextForGroup(id, "clearing...", 1, true)
end end
missionCommands.removeItemForGroup(groupID, { "Clear View" } ) missionCommands.removeItemForGroup(groupID, { "Clear View" })
missionCommands.addCommandForGroup(groupID, "Clear View", nil, clearView, groupID) missionCommands.addCommandForGroup(groupID, "Clear View", nil, clearView, groupID)
missionCommands.removeItemForGroup(groupID, { "Refresh Missions" } ) missionCommands.removeItemForGroup(groupID, { "Refresh Missions" })
missionCommands.addCommandForGroup(groupID, "Refresh Missions", nil, function(refresh_mission_id) missionCommands.addCommandForGroup(groupID, "Refresh Missions", nil, function(refresh_mission_id)
self._logger:debug("Manual refresh of missions for group: " .. tostring(refresh_mission_id)) self._logger:debug("Manual refresh of missions for group: " .. tostring(refresh_mission_id))
self:updateCommandsForGroup(refresh_mission_id) self:updateCommandsForGroup(refresh_mission_id)
end, groupID) end, groupID)
end end
local folderNames = { local folderNames = {
@@ -327,15 +327,14 @@ function MissionCommandsHelper:PinMission(mission, groupID)
end end
function MissionCommandsHelper:AddAllMissionCommandsToGroup(groupID) function MissionCommandsHelper:AddAllMissionCommandsToGroup(groupID)
local perFolder = 9 local perFolder = 9
local group = DcsUtil.GetPlayerGroupByGroupID(groupID) local group = DcsUtil.GetPlayerGroupByGroupID(groupID)
---@type Vec2 ---@type Vec2
local groupPos = { x=0, y=0 } local groupPos = { x = 0, y = 0 }
if group then if group then
local pos = group:getUnit(1):getPosition().p local pos = group:getUnit(1):getPosition().p
groupPos = { x= pos.x, y=pos.z } groupPos = { x = pos.x, y = pos.z }
end end
do --- primary missions do --- primary missions
@@ -363,7 +362,7 @@ function MissionCommandsHelper:AddAllMissionCommandsToGroup(groupID)
else else
local name = "Next Menu ..." local name = "Next Menu ..."
missionCommands.addSubMenuForGroup(groupID, name, path) missionCommands.addSubMenuForGroup(groupID, name, path)
path[#path+1] = name path[#path + 1] = name
count = 0 count = 0
end end
end end
@@ -392,7 +391,7 @@ function MissionCommandsHelper:AddAllMissionCommandsToGroup(groupID)
else else
local name = "Next Menu ..." local name = "Next Menu ..."
missionCommands.addSubMenuForGroup(groupID, name, path) missionCommands.addSubMenuForGroup(groupID, name, path)
path[#path+1] = name path[#path + 1] = name
count = 0 count = 0
end end
end end
@@ -405,28 +404,28 @@ end
---@param path Array<string> ---@param path Array<string>
---@param mission Mission ---@param mission Mission
function MissionCommandsHelper:addMissionCommands(groupId, path, mission) function MissionCommandsHelper:addMissionCommands(groupId, path, mission)
if path then if path then
local group = DcsUtil.GetPlayerGroupByGroupID(groupId) local group = DcsUtil.GetPlayerGroupByGroupID(groupId)
local distance = "[?]" local distance = "[?]"
if group then if group then
local lead = group:getUnit(1) local lead = group:getUnit(1)
if lead and lead:isExist() == true then if lead and lead:isExist() == true then
local pos = lead:getPoint() local pos = lead:getPoint()
local Vec2Pos = { x= pos.x, y=pos.z } local Vec2Pos = { x = pos.x, y = pos.z }
local dist = Util.VectorDistance2d(Vec2Pos, mission.location) / 1852 local dist = Util.VectorDistance2d(Vec2Pos, mission.location) / 1852
distance = "[" .. string.format("~%dnM", math.floor(dist)) .. "]" distance = "[" .. string.format("~%dnM", math.floor(dist)) .. "]"
end end
end end
local missionFolderName = "[" .. mission.code .. "]" .. distance .. mission.name .. "( " .. mission.missionTypeDisplay .. " )" local missionFolderName = "[" ..
mission.code .. "]" .. distance .. mission.name .. "( " .. mission.missionTypeDisplay .. " )"
missionCommands.addSubMenuForGroup(groupId, missionFolderName, path) missionCommands.addSubMenuForGroup(groupId, missionFolderName, path)
table.insert(path, missionFolderName) table.insert(path, missionFolderName)
---@type MissionBriefingRequestedArgs ---@type MissionBriefingRequestedArgs
local missionBriefingRequestedArgs = { groupId = groupId, mission = mission } local missionBriefingRequestedArgs = { groupId = groupId, mission = mission }
missionCommands.addCommandForGroup(groupId, "Briefing", path, missionBriefingRequested,missionBriefingRequestedArgs) missionCommands.addCommandForGroup(groupId, "Briefing", path, missionBriefingRequested,
missionBriefingRequestedArgs)
---@type PinMissionCommandArgs ---@type PinMissionCommandArgs
local pinMissionCommandArgs = { self = self, groupId = groupId, mission = mission } local pinMissionCommandArgs = { self = self, groupId = groupId, mission = mission }
@@ -437,8 +436,7 @@ end
---@private ---@private
---@param groupID integer ---@param groupID integer
function MissionCommandsHelper:AddSupplyHubCommandsIfApplicable(groupID) function MissionCommandsHelper:AddSupplyHubCommandsIfApplicable(groupID)
if self._supplyUnitsTracker:IsGroupLeadInSupplyHub(groupID) ~= true then return end
if self._supplyHubGroups[tostring(groupID)] ~= true then return end
self._logger:debug("Adding supply hub commands for group: " .. tostring(groupID)) self._logger:debug("Adding supply hub commands for group: " .. tostring(groupID))
@@ -468,28 +466,32 @@ function MissionCommandsHelper:AddSupplyHubCommandsIfApplicable(groupID)
local path = { [1] = folderNames.supplyHub } local path = { [1] = folderNames.supplyHub }
---@type LoadCargoCommandParams ---@type LoadCargoCommandParams
local farpParams1000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_1000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self } local farpParams1000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_1000", supplyUnitsTracker =
self._supplyUnitsTracker, commandHelper = self }
missionCommands.addCommandForGroup(groupID, "Load FARP Crate (1000)", path, loadCargoCommand, farpParams1000) missionCommands.addCommandForGroup(groupID, "Load FARP Crate (1000)", path, loadCargoCommand, farpParams1000)
---@type LoadCargoCommandParams ---@type LoadCargoCommandParams
local farpParams2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self } local farpParams2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "FARP_CRATE_2000", supplyUnitsTracker =
self._supplyUnitsTracker, commandHelper = self }
missionCommands.addCommandForGroup(groupID, "Load FARP Crate (2000)", path, loadCargoCommand, farpParams2000) missionCommands.addCommandForGroup(groupID, "Load FARP Crate (2000)", path, loadCargoCommand, farpParams2000)
---@type LoadCargoCommandParams ---@type LoadCargoCommandParams
local samParms1000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "SAM_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self } local samParms1000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "SAM_CRATE_2000", supplyUnitsTracker =
self._supplyUnitsTracker, commandHelper = self }
missionCommands.addCommandForGroup(groupID, "Load SAM Crate (1000)", path, loadCargoCommand, samParms1000) missionCommands.addCommandForGroup(groupID, "Load SAM Crate (1000)", path, loadCargoCommand, samParms1000)
---@type LoadCargoCommandParams ---@type LoadCargoCommandParams
local samParms2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "SAM_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self } local samParms2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "SAM_CRATE_2000", supplyUnitsTracker =
self._supplyUnitsTracker, commandHelper = self }
missionCommands.addCommandForGroup(groupID, "Load SAM Crate (2000)", path, loadCargoCommand, samParms2000) missionCommands.addCommandForGroup(groupID, "Load SAM Crate (2000)", path, loadCargoCommand, samParms2000)
---@type LoadCargoCommandParams ---@type LoadCargoCommandParams
local airbaseParms2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "AIRBASE_CRATE_2000", supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self } local airbaseParms2000 = { unitID = unit:getID(), groupID = group:getID(), crateType = "AIRBASE_CRATE_2000", supplyUnitsTracker =
self._supplyUnitsTracker, commandHelper = self }
missionCommands.addCommandForGroup(groupID, "Airbase Crate (2000)", path, loadCargoCommand, airbaseParms2000) missionCommands.addCommandForGroup(groupID, "Airbase Crate (2000)", path, loadCargoCommand, airbaseParms2000)
end end
function MissionCommandsHelper:AddCargoCommands(groupID) function MissionCommandsHelper:AddCargoCommands(groupID)
local group = DcsUtil.GetPlayerGroupByGroupID(groupID) local group = DcsUtil.GetPlayerGroupByGroupID(groupID)
if group == nil then return end if group == nil then return end
@@ -519,25 +521,23 @@ function MissionCommandsHelper:AddCargoCommands(groupID)
for i = 1, amount do for i = 1, amount do
local path = { [1] = folderNames.cargo } local path = { [1] = folderNames.cargo }
---@type UnloadCargoCommandParams ---@type UnloadCargoCommandParams
local params = { unitID = unit:getID(), crateType = cargoType, supplyUnitsTracker = self._supplyUnitsTracker, commandHelper = self } local params = { unitID = unit:getID(), crateType = cargoType, supplyUnitsTracker = self
missionCommands.addCommandForGroup(groupID, "Unload " .. cargoConfig.displayName, path, unloadCargoCommand, params) ._supplyUnitsTracker, commandHelper = self }
missionCommands.addCommandForGroup(groupID, "Unload " .. cargoConfig.displayName, path,
unloadCargoCommand, params)
end end
end end
end end
end end
end end
---@private ---@private
---@param groupId integer ---@param groupId integer
function MissionCommandsHelper:addMissionFolders(groupId) function MissionCommandsHelper:addMissionFolders(groupId)
missionCommands.addSubMenuForGroup(groupId, folderNames.primary) missionCommands.addSubMenuForGroup(groupId, folderNames.primary)
missionCommands.addSubMenuForGroup(groupId, folderNames.secondary) missionCommands.addSubMenuForGroup(groupId, folderNames.secondary)
if self._supplyHubGroups[tostring(groupId)] == true then if self._supplyUnitsTracker:IsGroupLeadInSupplyHub(groupId) == true then
self._logger:debug("Adding supply hub commands folder for group: " .. tostring(groupId))
missionCommands.addSubMenuForGroup(groupId, folderNames.supplyHub) missionCommands.addSubMenuForGroup(groupId, folderNames.supplyHub)
end end
@@ -0,0 +1,42 @@
---@class DropZoneSlice
---@field centerAngle number Angle in degrees (0=forward, 90=right, 180=rear, 270=left)
---@field angleWidth number Total width of the slice in degrees (e.g. 60 = ±30°)
---@field minRadius number Minimum search radius in meters (safe distance from helicopter)
---@field maxRadius number Maximum search radius in meters
---@field spacing number Distance increment when searching outward in meters
---@class SupplyLoadConfig
---@field maxInternalLoad number
---@field dropZones Array<DropZoneSlice>
---@type table<string, SupplyLoadConfig>
local SupplyLoadConfig = {
["Mi-8MT"] = {
maxInternalLoad = 4000,
dropZones = {
{ centerAngle = 180, angleWidth = 60, minRadius = 20, maxRadius = 50, spacing = 5 },
}
},
["CH-47Fbl1"] = {
maxInternalLoad = 10000,
dropZones = {
{ centerAngle = 180, angleWidth = 30, minRadius = 20, maxRadius = 75, spacing = 5 },
}
},
["Mi-24P"] = {
maxInternalLoad = 2000,
dropZones = {
{ centerAngle = 270, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 },
{ centerAngle = 90, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 },
}
},
["UH-1H"] = {
maxInternalLoad = 2000,
dropZones = {
{ centerAngle = 270, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 },
{ centerAngle = 90, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 },
}
}
}
return SupplyLoadConfig
@@ -3,7 +3,7 @@ local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil") local DcsUtil = require("classes.util.DcsUtil")
local SpearheadEvents = require("classes.spearhead_events") local SpearheadEvents = require("classes.spearhead_events")
local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHelper") local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHelper")
local MaxLoadConfig = require("classes.stageClasses.helpers.MaxLoadConfig") local SupplyLoadConfig = require("classes.stageClasses.helpers.SupplyLoadConfig")
---@class SupplyUnitEventListener ---@class SupplyUnitEventListener
---@field supplyUnitSpawned fun(self:SupplyUnitEventListener, unit:Unit) | nil ---@field supplyUnitSpawned fun(self:SupplyUnitEventListener, unit:Unit) | nil
@@ -26,13 +26,12 @@ SupplyUnitsTracker.__index = SupplyUnitsTracker
local singleton = nil local singleton = nil
---comment ---comment
---@param logLevel LogLevel
---@return SupplyUnitsTracker ---@return SupplyUnitsTracker
function SupplyUnitsTracker.getOrCreate(logLevel) function SupplyUnitsTracker.getOrCreate()
if singleton == nil then if singleton == nil then
singleton = setmetatable({}, SupplyUnitsTracker) singleton = setmetatable({}, SupplyUnitsTracker)
singleton._logger = Logger.new("SupplyUnitsTracker", logLevel) singleton._logger = Logger.new("SupplyUnitsTracker")
singleton._unitPositions = {} singleton._unitPositions = {}
singleton._cargoInUnits = {} singleton._cargoInUnits = {}
singleton._supplyUnitsByName = {} singleton._supplyUnitsByName = {}
@@ -94,7 +93,7 @@ end
---@param listener SupplyUnitEventListener ---@param listener SupplyUnitEventListener
function SupplyUnitsTracker:AddOnSupplyUnitSpawnedListener(listener) function SupplyUnitsTracker:AddOnSupplyUnitEventListener(listener)
if listener == nil then return end if listener == nil then return end
if self._supplyUnitEventsListeners == nil then if self._supplyUnitEventsListeners == nil then
@@ -104,6 +103,26 @@ function SupplyUnitsTracker:AddOnSupplyUnitSpawnedListener(listener)
table.insert(self._supplyUnitEventsListeners, listener) table.insert(self._supplyUnitEventsListeners, listener)
end end
---@param unit Unit
---@return boolean
function SupplyUnitsTracker:IsUnitInSupplyHub(unit)
if unit == nil then return false end
local unitIDStr = tostring(unit:getID())
return self._unitInSupplyHub[unitIDStr] == true
end
---@param groupID number
---@return boolean
function SupplyUnitsTracker:IsGroupLeadInSupplyHub(groupID)
local group = DcsUtil.GetPlayerGroupByGroupID(groupID)
if group == nil then return false end
local unit = group:getUnit(1)
if unit == nil then return false end
return self:IsUnitInSupplyHub(unit)
end
function SupplyUnitsTracker:Update() function SupplyUnitsTracker:Update()
local players = DcsUtil.getAllPlayerUnits() local players = DcsUtil.getAllPlayerUnits()
for _, player in pairs(players) do for _, player in pairs(players) do
@@ -211,7 +230,6 @@ function SupplyUnitsTracker:CheckUnitsInZones()
if Util.is3dPointInZone(pos, zone) then if Util.is3dPointInZone(pos, zone) then
if self._unitInSupplyHub[tostring(unit:getID())] ~= true then if self._unitInSupplyHub[tostring(unit:getID())] ~= true then
self._unitInSupplyHub[tostring(unit:getID())] = true self._unitInSupplyHub[tostring(unit:getID())] = true
for _, listener in pairs(self._supplyUnitEventsListeners) do for _, listener in pairs(self._supplyUnitEventsListeners) do
pcall(function() pcall(function()
if listener.enteredSupplyHub then if listener.enteredSupplyHub then
@@ -278,14 +296,15 @@ function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHe
self._logger:debug("Unload requested for unit: " .. unitID .. " crateType: " .. crateType) self._logger:debug("Unload requested for unit: " .. unitID .. " crateType: " .. crateType)
local unit = DcsUtil.GetPlayerUnitByID(unitID) local unit = DcsUtil.GetPlayerUnitByID(unitID)
if unit == nil or unit:isExist() == false then return end if unit == nil or unit:isExist() == false then
self._logger:warn("Unload requested for non-existent unit: " .. unitID)
return
end
local group = unit:getGroup() local group = unit:getGroup()
if group == nil then if group == nil then
self._logger:warn("Unload requested for unit with no group: " .. unit:getName())
return return
end end
self:RemoveCargoFromUnit(unitID, crateType)
self:UpdateWeightForUnit(unit)
local cargoConfig = SupplyConfigHelper.getSupplyConfig(crateType) local cargoConfig = SupplyConfigHelper.getSupplyConfig(crateType)
@@ -294,7 +313,16 @@ function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHe
return return
end end
local cargoPos = self:GetCargoPlacePosition(unit) local cargoPos = self:GetCargoPlacePosition(unit, cargoConfig.staticType)
if cargoPos == nil then
self._logger:warn("No valid position found to drop cargo for unit: " .. unit:getName())
trigger.action.outTextForUnit(unit:getID(), "No valid position to drop cargo. Unloading area is too crowded.", 10)
return
end
self:RemoveCargoFromUnit(unitID, crateType)
self:UpdateWeightForUnit(unit)
cargoCount = cargoCount + 1 cargoCount = cargoCount + 1
local cargoSpawnObject = { local cargoSpawnObject = {
@@ -304,9 +332,11 @@ function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHe
y = cargoPos.z, y = cargoPos.z,
} }
self._logger:debug("Spawning crate #" .. cargoCount .. " at (" .. string.format("%.2f", cargoPos.x) .. ", " .. string.format("%.2f", cargoPos.y) .. ", " .. string.format("%.2f", cargoPos.z) .. ")")
local spawned = coalition.addStaticObject(unit:getCoalition(), cargoSpawnObject) local spawned = coalition.addStaticObject(unit:getCoalition(), cargoSpawnObject)
self._droppedCrates[cargoSpawnObject.name] = spawned self._droppedCrates[cargoSpawnObject.name] = spawned
missionCommandsHelper:updateCommandsForGroup(group:getID()) missionCommandsHelper:updateCommandsForGroup(group:getID())
self._logger:debug("Cargo dropped for unit: " .. unit:getName() .. " crateType: " .. crateType)
end end
---@return table<string,StaticObject> ---@return table<string,StaticObject>
@@ -394,7 +424,7 @@ function SupplyUnitsTracker:TryLoadCrateInUnit(unit, crateType, commandHelper)
end end
end end
local unitConfig = MaxLoadConfig[unit:getTypeName()] local unitConfig = SupplyLoadConfig[unit:getTypeName()]
if unitConfig == nil then if unitConfig == nil then
trigger.action.outTextForUnit(unit:getID(), "Your unit type is not configured for logistics: " .. crateType, 5) trigger.action.outTextForUnit(unit:getID(), "Your unit type is not configured for logistics: " .. crateType, 5)
self._logger:error("Invalid unit type: " .. unit:getTypeName()) self._logger:error("Invalid unit type: " .. unit:getTypeName())
@@ -437,53 +467,234 @@ function SupplyUnitsTracker:UnitRequestCrateSpawn(groupID, crateType)
end end
end end
---@class SupplyUnitsBoundingBox
---@field min Vec3 World space minimum
---@field max Vec3 World space maximum
---@field heading number? Object heading in radians
---@param foundObject Object
---@return SupplyUnitsBoundingBox?
function SupplyUnitsTracker:GetBoundingBoxes(foundObject)
local desc = foundObject:getDesc()
if foundObject:getCategory() == Object.Category.SCENERY then
desc = SceneryObject.getDescByName(foundObject:getTypeName())
end
if desc == nil or desc.box == nil then
return nil
end
local objPos = foundObject:getPoint()
local box = desc.box
local heading = 0
-- Try to get object heading from position vector's forward direction
-- This works for units and other objects that support getPosition
pcall(function()
local objPosition = foundObject:getPosition()
if objPosition and objPosition.x then
heading = math.atan2(objPosition.x.z, objPosition.x.x)
end
end)
-- For rotated objects, we need to rotate the bounding box
local minX = box.min.x
local maxX = box.max.x
local minZ = box.min.z
local maxZ = box.max.z
-- If object has significant rotation, apply rotation to bbox corners
if math.abs(heading) > 0.1 then
-- Get all 4 corners of bbox in local space
local corners = {
{minX, minZ},
{minX, maxZ},
{maxX, minZ},
{maxX, maxZ}
}
-- Rotate corners and find new min/max
minX, maxX = math.huge, -math.huge
minZ, maxZ = math.huge, -math.huge
for _, corner in ipairs(corners) do
local rotX = corner[1] * math.cos(heading) - corner[2] * math.sin(heading)
local rotZ = corner[1] * math.sin(heading) + corner[2] * math.cos(heading)
minX = math.min(minX, rotX)
maxX = math.max(maxX, rotX)
minZ = math.min(minZ, rotZ)
maxZ = math.max(maxZ, rotZ)
end
end
-- Convert relative bbox to world space by adding object position
---@type SupplyUnitsBoundingBox
return {
min = {
x = objPos.x + minX,
y = objPos.y + box.min.y,
z = objPos.z + minZ
},
max = {
x = objPos.x + maxX,
y = objPos.y + box.max.y,
z = objPos.z + maxZ
},
heading = heading
}
end
---Check if two axis-aligned bounding boxes collide with safety margin
---@param crateBBox SupplyUnitsBoundingBox The crate's bbox in world space
---@param objBBox SupplyUnitsBoundingBox The existing object's bbox in world space
---@param safetyMargin number Safety margin around objects
---@return boolean True if collision detected
function SupplyUnitsTracker:CheckBBoxCollision(crateBBox, objBBox, safetyMargin)
-- Apply safety margin to object bbox
local objMin = {
x = objBBox.min.x - safetyMargin,
y = objBBox.min.y - safetyMargin,
z = objBBox.min.z - safetyMargin
}
local objMax = {
x = objBBox.max.x + safetyMargin,
y = objBBox.max.y + safetyMargin,
z = objBBox.max.z + safetyMargin
}
-- AABB collision detection
return crateBBox.min.x <= objMax.x and crateBBox.max.x >= objMin.x and
crateBBox.min.y <= objMax.y and crateBBox.max.y >= objMin.y and
crateBBox.min.z <= objMax.z and crateBBox.max.z >= objMin.z
end
---@private ---@private
---@param unit Unit ---@param unit Unit
---@return Vec3 ---@param crateTypeName string
function SupplyUnitsTracker:GetCargoPlacePosition(unit) ---@return Vec3?
function SupplyUnitsTracker:GetCargoPlacePosition(unit, crateTypeName)
local pos = unit:getPosition() local unitPos = unit:getPosition()
local preferredPos = {
x = pos.p.x - 10 * pos.x.x, -- Get unit's heading from the forward vector (x component)
y = pos.p.y - 10 * pos.x.y, -- Heading is calculated as: atan2(forward.z, forward.x)
z = pos.p.z - 10 * pos.x.z local unitHeading = math.atan2(unitPos.x.z, unitPos.x.x)
-- Get crate bbox - relative to placement position
local crateDesc = StaticObject.getDescByName(crateTypeName) --[[@as table]]
if crateDesc == nil or crateDesc.box == nil then
self._logger:error("Could not get bbox for crate type: " .. crateTypeName)
return nil
end
local crateRelativeBBox = crateDesc.box
-- Get drop zone config for this unit
local dropZones = {
{ centerAngle = 180, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 }
} }
return preferredPos if SupplyLoadConfig[unit:getTypeName()] ~= nil then
dropZones = SupplyLoadConfig[unit:getTypeName()].dropZones
end
-- Get occupied objects with their bboxes
local searchVolume = {
id = world.VolumeType.SPHERE,
params = {
point = {
x = unitPos.p.x,
y = unitPos.p.y,
z = unitPos.p.z
},
radius = 100 -- Search a large area
}
}
-- local volume = { local occupiedObjects = {}
-- id = world.VolumeType.SPHERE, local found = function(foundItem, val)
-- params = { local bbox = self:GetBoundingBoxes(foundItem)
-- point = preferredPos, if bbox then
-- radius = 10 self._logger:debug("Found object: " .. foundItem:getTypeName() .. " at (" .. foundItem:getPoint().x .. ", " .. foundItem:getPoint().z .. ")")
-- } table.insert(occupiedObjects, {
-- } pos = foundItem:getPoint(),
bbox = bbox
})
else
self._logger:debug("Found object without bbox: " .. foundItem:getTypeName())
end
end
-- local occupiedPosX = {} local searchCategories = {}
-- local occupiedPosZ = {} for key, value in pairs(Object.Category) do
self._logger:debug("Adding category to search: " .. tostring(value) .. " (" .. tostring(key) .. ")")
table.insert(searchCategories, value)
end
-- ---@param foundItem Object ---@diagnostic disable-next-line: param-type-mismatch
-- local found = function(foundItem, val) world.searchObjects(searchCategories, searchVolume, found)
-- local foundPos = foundItem:getPoint() local safetyMargin = 3 -- Safety margin around objects
-- local z = math.floor(foundPos.z) -- Search through each slice
-- for i = z - 3 , z + 3 do for _, zone in ipairs(dropZones) do
-- occupiedPosZ[i] = true -- Calculate angle range for this slice
-- end local minAngle = zone.centerAngle - (zone.angleWidth / 2)
local maxAngle = zone.centerAngle + (zone.angleWidth / 2)
-- local x = math.floor(foundPos.x) -- Search outward in rings starting from minRadius
-- for i = x - 3 , x + 3 do for distance = zone.minRadius, zone.maxRadius, zone.spacing do
-- occupiedPosX[i] = true -- Check multiple positions within the angular slice
-- end local angleStep = math.min(15, zone.angleWidth / 3) -- Divide slice into sections
-- end
-- world.searchObjects(volume.id, volume.params, found) for angle = minAngle, maxAngle, angleStep do
local radians = math.rad(angle)
-- Calculate position at this angle and distance, relative to unit's heading
-- Angle 0 = forward, 90 = right, 180 = rear, 270 = left
-- Apply unit heading to make angles relative to unit orientation
local worldAngle = radians + unitHeading
local candidateX = unitPos.p.x + distance * math.sin(worldAngle)
local candidateZ = unitPos.p.z + distance * math.cos(worldAngle)
local candidateY = land.getHeight({ x = candidateX, y = candidateZ })
-- Convert crate's relative bbox to world space at this position
local crateBBoxWorldSpace = {
min = {
x = candidateX + crateRelativeBBox.min.x,
y = candidateY + crateRelativeBBox.min.y,
z = candidateZ + crateRelativeBBox.min.z
},
max = {
x = candidateX + crateRelativeBBox.max.x,
y = candidateY + crateRelativeBBox.max.y,
z = candidateZ + crateRelativeBBox.max.z
}
}
-- Check if crate bbox collides with any existing objects
local collides = false
for _, obj in ipairs(occupiedObjects) do
if self:CheckBBoxCollision(crateBBoxWorldSpace, obj.bbox, safetyMargin) then
collides = true
self._logger:debug("Collision at angle=" .. angle .. ", distance=" .. distance)
break
end
end
if not collides then
self._logger:debug("Valid position found at angle=" .. angle .. ", distance=" .. distance .. ", pos=(" .. string.format("%.2f", candidateX) .. ", " .. string.format("%.2f", candidateY) .. ", " .. string.format("%.2f", candidateZ) .. ")")
return { x = candidateX, y = candidateY, z = candidateZ }
end
end
end
end
-- No free spot found
return nil
end end
return SupplyUnitsTracker return SupplyUnitsTracker
@@ -5,6 +5,8 @@ local SupplyUnitsTracker = require("classes.stageClasses.helpers.SupplyUnitsTrac
local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper") local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper")
local GlobalConfig = require("classes.configuration.GlobalConfig") local GlobalConfig = require("classes.configuration.GlobalConfig")
local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHelper") local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHelper")
local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
---@class BuildableMission : Mission, SupplyUnitEventListener ---@class BuildableMission : Mission, SupplyUnitEventListener
---@field private _requiredKilos number ---@field private _requiredKilos number
@@ -17,8 +19,8 @@ local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHel
---@field private _supplyUnitsTracker SupplyUnitsTracker ---@field private _supplyUnitsTracker SupplyUnitsTracker
---@field private _noLandingZone SpearheadTriggerZone? ---@field private _noLandingZone SpearheadTriggerZone?
---@field private _dropOffZone SpearheadTriggerZone? ---@field private _dropOffZone SpearheadTriggerZone?
---@field private _noLandingZoneId number ---@field private _noLandingZoneDrawing CustomDrawing?
---@field private _dropOffZoneId number ---@field private _dropOffZoneDrawing CustomDrawing?
local BuildableMission = {} local BuildableMission = {}
BuildableMission.__index = BuildableMission BuildableMission.__index = BuildableMission
@@ -76,7 +78,7 @@ function BuildableMission.new(database, logger, targetZone, noLandingZone, requi
self._onCrateDroppedOfListeners = {} self._onCrateDroppedOfListeners = {}
self._completeListeners = {} self._completeListeners = {}
self._markIDsPerGroup = {} self._markIDsPerGroup = {}
self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate(logger.LogLevel) self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate()
self._state = "NEW" self._state = "NEW"
@@ -87,7 +89,7 @@ function BuildableMission.new(database, logger, targetZone, noLandingZone, requi
self.priority = "secondary" self.priority = "secondary"
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate(self._logger.LogLevel) self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
self._crateType = requiredCrateType self._crateType = requiredCrateType
@@ -164,20 +166,20 @@ function BuildableMission:SpawnActive()
return return
end end
---@type DrawColor local lineColor = DrawingHelper.ColorTableToColorString({ 230/255, 93/255, 49/255, 1})
local lineColor = { r=230/255, g=93/255, b=49/255, a=1} local fillColor = DrawingHelper.ColorTableToColorString({ 230/255, 93/255, 49/255, 0.2})
---@type DrawColor self._noLandingZoneDrawing = CustomDrawing.FromZone(self._noLandingZone, lineColor, fillColor, 2, 6)
local fillColor = { r=230/255, g=93/255, b=49/255, a=0.2} self._noLandingZoneDrawing:Draw()
self._noLandingZoneId = DcsUtil.DrawZone(self._noLandingZone, lineColor, fillColor, 6)
if self._dropOffZone == nil then if self._dropOffZone == nil then
self._logger:error("No drop off zone found for mission: " .. self.code) self._logger:error("No drop off zone found for mission: " .. self.code)
return return
end end
local lineColor2 = { r=0, g=0, b=1, a=1} local lineColor2 = DrawingHelper.ColorTableToColorString({ 0, 0, 1, 1})
local fillColor2 = { r=0, g=0, b=1, a=0} local fillColor2 = DrawingHelper.ColorTableToColorString({ 0, 0, 1, 0})
self._dropOffZoneId = DcsUtil.DrawZone(self._dropOffZone, lineColor2, fillColor2, 6) self._dropOffZoneDrawing = CustomDrawing.FromZone(self._dropOffZone, lineColor2, fillColor2, 2, 6)
self._dropOffZoneDrawing:Draw()
---@param selfA BuildableMission ---@param selfA BuildableMission
---@param time number ---@param time number
@@ -197,7 +199,7 @@ function BuildableMission:SpawnActive()
self._state = "ACTIVE" self._state = "ACTIVE"
self._missionCommandsHelper:AddMissionToCommands(self) self._missionCommandsHelper:AddMissionToCommands(self)
self._supplyUnitsTracker:AddOnSupplyUnitSpawnedListener(self) self._supplyUnitsTracker:AddOnSupplyUnitEventListener(self)
local units = self._supplyUnitsTracker:GetUnits() local units = self._supplyUnitsTracker:GetUnits()
if units then if units then
@@ -254,8 +256,8 @@ function BuildableMission:CheckCratesInZone()
end end
if self._droppedKilos >= self._requiredKilos then if self._droppedKilos >= self._requiredKilos then
DcsUtil.RemoveMark(self._noLandingZoneId) self._dropOffZoneDrawing:Remove()
DcsUtil.RemoveMark(self._dropOffZoneId) self._noLandingZoneDrawing:Remove()
self:NotifyMissionComplete() self:NotifyMissionComplete()
self._state = "COMPLETED" self._state = "COMPLETED"
end end
@@ -1,6 +1,7 @@
local Mission = require("classes.stageClasses.missions.baseMissions.Mission") local Mission = require("classes.stageClasses.missions.baseMissions.Mission")
local Util = require("classes.util.Util") local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil") local DcsUtil = require("classes.util.DcsUtil")
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
---@class RunwayStrikeMission : Mission ---@class RunwayStrikeMission : Mission
---@field runwayBombingTracker RunwayBombingTracker ---@field runwayBombingTracker RunwayBombingTracker
@@ -154,9 +155,22 @@ function RunwayStrikeMission:Draw()
if runwaySection.drawID == nil then if runwaySection.drawID == nil then
local zone = self:SectionToSpearheadZone(runwaySection) local zone = self:SectionToSpearheadZone(runwaySection)
local color = { r=0, g=1, b=0, a=0.5 } ---@type Free
local drawObject = {
primitiveType = "Polygon",
polygonMode = "free",
mapX = 0,
mapY = 0,
points = zone.verts,
name = zone.name,
fillColorString = DrawingHelper.ColorTableToColorString(fillColor),
colorString = DrawingHelper.ColorTableToColorString(lineColor),
style = "solid",
thickness = 1,
visible = true,
}
runwaySection.drawID = DcsUtil.DrawZone(zone, color, color, 5) runwaySection.drawID = DrawingHelper.Draw(drawObject)
else else
DcsUtil.SetFillColor(runwaySection.drawID, fillColor) DcsUtil.SetFillColor(runwaySection.drawID, fillColor)
DcsUtil.SetLineColor(runwaySection.drawID, lineColor) DcsUtil.SetLineColor(runwaySection.drawID, lineColor)
@@ -350,7 +350,7 @@ function ZoneMission:SpawnPersistedState()
end end
end end
---spawns the mission, but doesn't add ---spawns the mission, but doesn't add it to the mission commands.
function ZoneMission:SpawnInactive() function ZoneMission:SpawnInactive()
self._logger:info("PreActivating " .. self.name) self._logger:info("PreActivating " .. self.name)
@@ -52,7 +52,7 @@ function Mission.newSuper(self, zoneName, missionName, missionType, missionBrief
self.location = database:GetLocationForMissionZone(zoneName) self.location = database:GetLocationForMissionZone(zoneName)
self.missionTypeDisplay = self.missionType self.missionTypeDisplay = self.missionType
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate(logger.LogLevel) self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
return true, "success" return true, "success"
end end
-69
View File
@@ -173,10 +173,6 @@ do -- INIT DCS_UTIL
verts = enlargedPoints verts = enlargedPoints
} }
if SpearheadConfig and SpearheadConfig.debugEnabled == true then
DCS_UTIL.DrawZone(triggerZone, { r = 0, g = 1, b = 0, a = 1 }, { a = 0, r = 0, g = 1, b = 0 }, 1)
end
DCS_UTIL.__airbaseZonesByName[name] = triggerZone DCS_UTIL.__airbaseZonesByName[name] = triggerZone
end end
end end
@@ -636,71 +632,6 @@ do -- INIT DCS_UTIL
---@field b number ---@field b number
---@field a number ---@field a number
local drawID = 400
---@param zone SpearheadTriggerZone
---@param lineColor DrawColor
---@param fillColor DrawColor
---@param lineStyle LineType
---@return number drawID
function DCS_UTIL.DrawZone(zone, lineColor, fillColor, lineStyle)
if lineStyle == nil then lineStyle = 4 end
drawID = drawID + 1
if zone.zone_type == "Cilinder" then
trigger.action.circleToAll(-1, drawID, { x = zone.location.x, y = 0, z = zone.location.y }, zone.radius,
{ 0, 0, 0, 0 }, { 0, 0, 0, 0 }, lineStyle, true)
else
local functionString = "trigger.action.markupToAll(7, -1, " .. drawID .. ","
for _, vecpoint in pairs(zone.verts) do
functionString = functionString .. " { x=" .. vecpoint.x .. ", y=0,z=" .. vecpoint.y .. "},"
end
functionString = functionString .. "{0,1,0,1}, {0,1,0,1}, " .. lineStyle .. ")"
---@diagnostic disable-next-line: deprecated
local f, err = loadstring(functionString)
if f then
f()
else
env.error("Something failed when drawing complex drawing" .. err)
end
end
local fillColorMapped = {
fillColor.r or 0,
fillColor.g or 0,
fillColor.b or 0,
fillColor.a or 0.5
}
local lineColorMapped = {
lineColor.r or 0,
lineColor.g or 0,
lineColor.b or 0,
lineColor.a or 1
}
trigger.action.setMarkupColorFill(drawID, fillColorMapped)
trigger.action.setMarkupColor(drawID, lineColorMapped)
return drawID
end
---@param start Vec3
---@param finish Vec3
---@param lineColor DrawColor
---@param lineStyle LineType
function DCS_UTIL.DrawLine(start, finish, lineColor, lineStyle)
if lineStyle == nil then lineStyle = 4 end
drawID = drawID + 1
local lineColorMapped = {
lineColor.r or 0,
lineColor.g or 0,
lineColor.b or 0,
lineColor.a or 1
}
trigger.action.lineToAll(-1, drawID, start, finish, lineColorMapped, lineStyle)
return drawID
end
---@param groupID number ---@param groupID number
---@param text string ---@param text string
+14 -2
View File
@@ -1,5 +1,17 @@
local Util = require("classes.util.Util") local Util = require("classes.util.Util")
local SpearheadConfig = require("classes.configuration.GlobalConfig")
---@type LogLevel
local defaultLogLevel = "INFO"
if SpearheadConfig then
if SpearheadConfig:isDebugEnabled() then
defaultLogLevel = "DEBUG"
end
end
--- @class Logger --- @class Logger
--- @field LoggerName string the name of the logger --- @field LoggerName string the name of the logger
@@ -10,13 +22,13 @@ do
---comment ---comment
---@param logger_name any ---@param logger_name any
---@param logLevel LogLevel ---@param logLevel LogLevel? override the default log level
---@return Logger ---@return Logger
function LOGGER.new(logger_name, logLevel) function LOGGER.new(logger_name, logLevel)
LOGGER.__index = LOGGER LOGGER.__index = LOGGER
local self = setmetatable({}, LOGGER) local self = setmetatable({}, LOGGER)
self.LoggerName = logger_name or "(loggername not set)" self.LoggerName = logger_name or "(loggername not set)"
self.LogLevel = logLevel or "INFO" self.LogLevel = logLevel or defaultLogLevel
return self return self
end end
+43
View File
@@ -297,6 +297,49 @@ do -- INIT UTIL
return false return false
end end
---@param points Array<Vec3>
---@return Array<Vec3>
function UTIL.getConvexHull3d(points)
if #points == 0 then
return {}
end
---comment
---@param a Vec3
---@param b Vec3
---@param c Vec3
---@return boolean
local function ccw(a, b, c)
return (b.z - a.z) * (c.x - a.x) > (b.x - a.x) * (c.z - a.z)
end
table.sort(points, function(left, right)
return left.z < right.z
end)
local hull = {}
-- lower hull
for _, point in pairs(points) do
while #hull >= 2 and not ccw(hull[#hull - 1], hull[#hull], point) do
table.remove(hull, #hull)
end
table.insert(hull, point)
end
-- upper hull
local t = #hull + 1
for i = #points, 1, -1 do
local point = points[i]
while #hull >= t and not ccw(hull[#hull - 1], hull[#hull], point) do
table.remove(hull, #hull)
end
table.insert(hull, point)
end
table.remove(hull, #hull)
return hull
end
---comment ---comment
---@param points Array<Vec2> points ---@param points Array<Vec2> points
---@return Array<Vec2> hullPoints ---@return Array<Vec2> hullPoints
+2 -24
View File
@@ -28,10 +28,10 @@ SpearheadEvents.Init(defaultLogLevel)
local dbLogger = Logger.new("database", defaultLogLevel) local dbLogger = Logger.new("database", defaultLogLevel)
local standardLogger = Logger.new("", defaultLogLevel) local standardLogger = Logger.new("", defaultLogLevel)
local databaseManager = Database.New(dbLogger) local databaseManager = Database.New(dbLogger)
MissionCommandsHelper.getOrCreate(defaultLogLevel) -- initiate MissionCommandsHelper.getOrCreate() -- initiate
local capConfig = CapConfig:new(); local capConfig = CapConfig:new();
local stageConfig = StageConfig:new(); local stageConfig = StageConfig:getInstance();
local startingStage = stageConfig.startingStage or 1 local startingStage = stageConfig.startingStage or 1
if SpearheadConfig and SpearheadConfig.Persistence and SpearheadConfig.Persistence.enabled == true then if SpearheadConfig and SpearheadConfig.Persistence and SpearheadConfig.Persistence.enabled == true then
@@ -72,25 +72,3 @@ local missionEditorWarningsLogger = Logger.new("MissionEditorWarnings", defaultL
MissionEditorWarnings.WriteAll(missionEditorWarningsLogger) MissionEditorWarnings.WriteAll(missionEditorWarningsLogger)
GlobalStageManager:printFullOverview() GlobalStageManager:printFullOverview()
--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
--- ==================== DEBUG ORDER OR ZONE VEC ===========================
-- local zone = Spearhead.DcsUtil.getZoneByName("MISSIONSTAGE_99")
-- local count = Spearhead.Util.tableLength(zone.verts)
-- for i = 1, count - 1 do
-- local a = zone.verts[i]
-- local b = zone.verts[i+1]
-- local color = {0,0,0,1}
-- color[i] = 1
-- trigger.action.textToAll(-1, 46+i , { x= a.x, y = 0, z = a.z } , color, {0,0,0}, 24 , true , "" .. i )
-- 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
+1 -1
View File
@@ -1 +1 @@
0.12.1 0.13.0