Author SHA1 Message Date
dutchie031 c3c6450b0c updated to change the release name so two releases can co-exist 2026-08-30 14:15:15 +02:00
85 changed files with 1420 additions and 6020 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

-10
View File
@@ -21,9 +21,6 @@
<main>
<div class="reference-container">
<div class="side-nav">
<div id="beta-box-div"></div>
<h4>Quick Links</h4>
<ul>
<li><a href="/pages/tutorials.html" class="side-nav-h2">Tutorials</a></li>
@@ -62,13 +59,6 @@
<footer>
<p>&copy; 2025 Spearhead Project</p>
</footer>
<script>
if (window.location.hostname.toLowerCase() == 'beta.spearhead.rocks') {
betaHtml = '<note-box type="warning" title="Beta Version">You are looking at docs that are released with the beta version.</note-box>';
}
document.getElementById('beta-box-div').innerHTML = betaHtml || '';
</script>
</body>
</html>
+1 -2
View File
@@ -19,10 +19,9 @@ class Header extends HTMLElement {
<div class="dropdown-content">
<a href="/pages/include-script.html">Include the Script</a>
<a href="/pages/first-start.html">First Start</a>
<a href="/pages/advanced/map-markings.html">Map Markings</a>
<a href="/pages/advanced/CAP.html">Advanced: CAP</a>
<a href="/pages/advanced/missions.html">Advanced: Missions</a>
<a href="/pages/advanced/lanes.html">Advanced: Lanes</a>
<a href="/pages/advanced/map-markings.html">Map Markings</a>
</div>
</div>
@@ -49,6 +49,17 @@ class DownloadScript extends HTMLElement {
return null;
}
}
async fetchReleaseAssets(url){
try {
const response = await fetch(url);
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching release assets:', error);
return null;
}
}
async connectedCallback() {
@@ -61,9 +72,10 @@ class DownloadScript extends HTMLElement {
if (latestRelease) {
version = latestRelease.tag_name;
timestamp = new Date(latestRelease.published_at).toLocaleDateString('en-CA');
const assets = latestRelease.assets;
const assets = await this.fetchReleaseAssets(latestRelease.assets_url);
if (assets && assets.length > 0) {
const spearheadAsset = assets.find(asset => asset.name.includes('spearhead.v'));
const spearheadAsset = assets.find(asset => asset.name.includes('spearhead'));
if (spearheadAsset) {
size = (spearheadAsset.size / 1024).toFixed(2) + 'kb';
href = spearheadAsset.browser_download_url;
@@ -93,7 +105,7 @@ class DownloadScript extends HTMLElement {
<p>
<span class="version-text">${version}</span> (${timestamp}) <br>
<a style="text-decoration: underline;" target="_blank" href="${href}">Download</a> (${size}) <br>
<a style="text-decoration: underline;" target="_blank" href="https://git.dutchie031.com/Spearhead/spearhead/releases">See All Versions here</a>
<a style="text-decoration: underline;" target="_blank" href="https://git.dutchie031.com/api/v1/repos/Spearhead/spearhead/releases">See All Versions here</a>
</p>
</div>
-9
View File
@@ -168,17 +168,8 @@ class Sidebar extends HTMLElement {
}
render() {
var betaHtml = '';
if (window.location.hostname.toLowerCase() == 'beta.spearhead.rocks') {
betaHtml = '<note-box type="warning" title="Beta Version">You are looking at docs that are released with the beta version.</note-box>';
}
this.innerHTML = `
<div class="side-nav">
<div style="width:100%">
${betaHtml}
</div>
<h4 class="side-nav-title"></h4>
<ul>
<!-- Navigation items will be populated automatically -->
+7 -16
View File
@@ -80,12 +80,17 @@
</tbody>
</table>
<note-box type="info" title="Combining Tasks">
Currently a group can do 1 thing. Either be Primary, Seconday, Sweep or Intercept. <br>
If you want to have different types during different stages, it's best to copy paste groups and
rename them accordingly. <br>
</note-box>
<note-box type="info" title="Airfield Defenses">
</note-box>
<h3 id="cap-zones-ref">Zones</h3>
<table class="table-no-style">
<tbody>
@@ -94,7 +99,6 @@
<td>=></td>
<td>A Cap ROUTE. The two most outer points are used to define the fly-over points.</td>
</tr>
<tr><td>______________________</td><td></td><td></td> </tr>
<tr>
<td><code-inline>INTERCEPTZONE_[ZoneID]_[NAME]</code-inline></td>
<td>=></td>
@@ -108,19 +112,6 @@
</p>
<h2 id="setup">Setting up groups</h2>
<h3 id="airbases-grouping">Airbases</h3>
<p>
Before talking about the groups, it's good to mention that groups are currently logically grouped and managed by an Airbase. <br>
Meaning, Airbase A does not know how many flights Airbase B is sending out. <br>
This might change in the future where multiple Airbases can work together.
</p>
<note-box type="info" title="Airbase Defenses">
When an Airbase is "active" and thus has aircraft being scheduled for taskings, all RED groups at that airfield will also be spawned. <br>
Additionally, all missions of type <code-inline>SAM</code-inline> will be spawned (without being available in Primary Missions). <br>
This should hopefully give you all the tools to make distance CAP airbases still feel very well defended.
</note-box>
<h3 id="types-explained">Types explained</h3>
<p>
Before we go into depth on each type it's best to first look at the types briefly and what we
@@ -149,7 +140,7 @@
The ground attack units mentioned are not in yet and thus escort groups are not either. <br>
<note-box type="warning" title="Overriding Taskings">
Each tasking has very specific waypoints, both for departure, arrival and ofcourse all point in between. <br>
Each tasking has very specific waypoints, both for departure, arrical and ofcourse all point in between. <br>
Overriding the "tasking" will break part of the cap flow as "RTBInTen" and RTB events are triggered by waypoints. <br>
Overriding these waypoints and taskings will break the management by Spearhead.
</note-box>
@@ -163,7 +154,7 @@
The way the trigger zone is mapped to a route is as follows: <br>
<br>
The 2 points that are furthest apart from each other are chosen as the "leg" of the route. <br>
The other 2 points are discarded at this time. <br>
The other 2 points are discarded. <br>
<br>
The Orbit route is then created with an "Anchored" task. With the closest point being the starting
point with an outbound leg to the furthest point. <br>
-183
View File
@@ -1,183 +0,0 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spearhead Lanes</title>
<link rel="stylesheet" href="/style/style.css">
<script src="/js/site.js"></script>
<style>
.side-nav a.active {
font-weight: bold;
color: #4fc3f7;
}
</style>
<script type="module" src="/js/components.js"></script>
</head>
<body>
<header>
<app-header></app-header>
</header>
<main>
<div class="reference-container">
<app-sidebar></app-sidebar>
<div class="content-wrapper">
<h1>Lanes</h1>
<p>
If up to now you've used mission stages like <code-inline>MISSIONSTAGE_1_STAGE</code-inline> you've been using the main lane. <br>
This is the default or "main" lane, however, if you wanted to mix things up and create a more dynamic mission, you can use additional lanes. <br>
Each lane can have its own set of mission stages, allowing for parallel or branching mission objectives.
</p>
<h2 id="how-it-works">How To</h2>
<h3 id="define-lanes">Define Lanes</h3>
<p>
Lanes are defined implicitly in the naming convention of the <code-inline>MISSIONSTAGE_</code-inline>. <br>
A lane is identified by a single character (alpha [A-Z]), which gives you 26 unique lanes. <br>
For example, <code-inline>MISSIONSTAGE_A1_[name]</code-inline> would belong to lane A. <br>
<br>
If there is no lane character specified, the stage belongs to the main lane. <br>
<br>
The 26 lane limit is only relevant if you want 26 lanes at the same time. <br>
But splitting up lanes (eg. (1-4 and 10-15) ) you can easily have more than 26 stages throughout your entire mission. <br>
<br>
Lanes don't have to start at index 1. as in the example lanes can easily start at any index. <br>
How lanes are then activated and activate other lanes is described below in the Dependencies Explained part.
</p>
<h3 id="dependencies">Dependencies Explained</h3>
<p>
When we are talking about lanes it's important to make the distinction between the main lane and additional lanes. <br>
The main lane is not only the default, but it's also the central lane that other lanes depend on. <br>
<h4>Chapters</h4>
Another term that we have used in the code is `Chapter` which refers to a grouping of mission stages within a lane. <br>
These are implied and cannot be directly controlled or altered. <br>
A chapter is a set of `MissionStage`s that are directly connected. <br>
For <code-inline>MissionStages</code-inline> <code-inline>[ A1, A2, A3, A5, A6 ]</code-inline> there are 2 chapters: <code-inline>[ A1, A2, A3 ]</code-inline> and <code-inline>[ A5, A6 ]</code-inline>. <br>
Within a chapter the stages are only directly dependent on each other. Meaning A2 will start when A1 is completed. <br>
<br>
However, the chapter start is where the inter-lane dependencies come into play. <br>
These operate a little differently. <br>
<br>
In the Visual example you can see the Stages grouped into chapters by the red boxes. <br>
<h4>Side Lane Chapter start</h4>
<p>
Side lane chapters are started when all are true:
<ul>
<li>The main lane is at or above the current stage lane number.</li>
<li>All previous chapters in the same side lane have been completed.</li>
</ul>
</p>
<h4>Main Lane Chapter start</h4>
<p>
Main lane chapters are started when all are true:
<ul>
<li>The previous chapter in the main lane has been completed.</li>
<li>All side lanes are at or above the current stage lane number or the side lane has no additional stages.</li>
</ul>
</p>
</p>
<h2 id="visual">Visual</h2>
<p>
The image below illustrates how different lanes can be used within a mission. <br>
In the example you can see how the dependency between the main lane and side lanes works. <br>
An arrow means that the completion of one stage is required before the next stage can begin.
</p>
<img src="/img/lanes.png" alt="Example of mission lanes">
<h2 id="mermaid">Mermaid</h2>
<p>
Spearhead automatically exports a spearhead diagram to the logs on starting a mission. <br>
This can be handy to visualize the logic and lanes you've created and verify your mission flow will be the way you want it. <br>
</p>
<h3 id="mermaid-log-example">Log Example</h3>
<code-block>
2026-09-01 17:53:13.213 INFO SCRIPTING (Main): [Spearhead][StageManager] ========== STAGE FLOW DIAGRAM ==========
graph TD
default_8["MAIN-2 (S8)"]
b_5["West Valley (S5)"]
default_1["MAIN-1 (S1)"]
a_5["WEST-2 (S5)"]
default_4["MAIN-2 (S4)"]
default_10["MAIN-1 (S10)"]
default_6["MAIN-2 (S6)"]
a_4["WEST-1 (S4)"]
default_3["MAIN-1 (S3)"]
default_9["MAIN-3 (S9)"]
a_7["WEST-2 (S7)"]
a_3["WEST (S3)"]
default_5["MAIN-3 (S5)"]
b_6["West Valley-1 (S6)"]
default_61["CAP-Maykop (S61)"]
default_7["MAIN-1 (S7)"]
default_0["START (S0)"]
a_6["WEST-3 (S6)"]
default_60["CAP-Nalchik (S60)"]
default_2["MAIN (S2)"]
a_2["WEST-1 (S2)"]
a_2 --> a_3
a_3 --> a_4
a_4 --> a_5
a_5 --> a_6
a_6 --> a_7
default_0 --> default_1
default_1 --> default_2
default_2 --> default_3
default_3 --> default_4
default_4 --> default_5
default_5 --> default_6
default_6 --> default_7
default_7 --> default_8
default_8 --> default_9
default_9 --> default_10
default_10 -->|chapter| default_60
default_60 --> default_61
b_5 --> b_6
default_2 -->|unlock| a_2
default_5 -->|unlock| b_5
a_7 -->|gate| default_60
b_6 -->|gate| default_60
========== END DIAGRAM ==========
</code-block>
<h3 id="mermaid-diagram-example">Diagram Example</h3>
<p>
If you want to then copy and paste everything between the <br>
<code-inline>========== STAGE FLOW DIAGRAM ==========</code-inline> and <br>
<code-inline>========== END DIAGRAM ==========</code-inline> <br>
you will be able to go to <a href="https://mermaid.live/">https://mermaid.live/</a> and paste the diagram code into the editor. <br>
It would look like this:
</p>
<img src="/img/mermaid-preview.png" alt="Example of mermaid diagram">
<h2 id="lane-limitations">Limitations</h2>
<h3 class="lane-limitations-cap">CAP</h3>
<p>
Due to the CAP naming conventions, we've opted to currently have CAP only react with the main lane. <br>
This might mean a bit less flexibility when using lanes for major mission logic, however, it's a limitation we've accepted for now. <br>
In the future we might be able to extend CAP's interaction with other lanes as well. <br>
Currently <a href="https://git.dutchie031.com/Spearhead/spearhead/issues/47">this issue</a> can be tracked here. <br>
This does not mean having both CAP and side-lanes is out of the question, since you can still have that side-lanes scouted by CAP aircraft.
It just needs a bit more pre-planning on where and how those CAP flights fly. <br>
</p>
</div>
</div>
</main>
<footer>
<p>&copy; 2025 Spearhead Project</p>
</footer>
</body>
</html>
+30 -106
View File
@@ -26,118 +26,42 @@
<div class="content-wrapper">
<h1>Advanced Tutorial: Missions</h1>
<div id="strike-region">
<h2 id="strike">Strike</h2>
<p>
Naming: <code-inline>MISSION_STRIKE_[Name]</code-inline> <br>
</p>
<h3 id="specific_targets">Specific Targets</h3>
<p>TBD</p>
<h2 id="strike">Strike</h2>
<p>
Naming: <span class="inline-lua"><span class="lua-variable">MISSION_STRIKE_[Name]</span></span> <br>
</p>
<h3 id="specific_targets">Specific Targets</h3>
<p>TBD</p>
<h3 id="scenery_targets">Scenery Targets</h3>
<p>
Bridges, buildings or other type of targets that are baked into the map are awesome targets.<br>
We wanted to make sure they were easily integrated in Spearhead.
</p>
<note-box type="info" title="Implementation details">
Spearhead detects scenery targets by the trigger zone name. A zone named
<strong>scenerytarget_[freeform]</strong> or
<strong>scenerytargets</strong> (case-insensitive) will be scanned and any scenery objects that
has the attribute "Buildings" inside the zone
will be added as mission scenery targets.
<h3 id="scenery_targets">Scenery Targets</h3>
<p>
Bridges, buildings or other type of targets that are baked into the map are awesome targets.<br>
We wanted to make sure they were easily integrated in Spearhead.
</p>
<note-box type="info" title="Implementation details">
Spearhead detects scenery targets by the trigger zone name. A zone named <strong>scenerytarget_[freeform]</strong> or
<strong>scenerytargets</strong> (case-insensitive) will be scanned and any scenery objects that has the attribute "Buildings" inside the zone
will be added as mission scenery targets.
</note-box>
<p>
Scenery targets are treated like mission targets: they are included in the mission completion calculation
(their `IsAlive()` state is checked) and their state is persisted/updated when missions spawn or resume.
</p>
<note-box type="warning" title="Changing IDs">
Due to Object IDs changing when maps are updated and edited the "Assign as..." functionality does not work for scenery targets.
Hence the need to use trigger zones to define them.
</note-box>
<p>
Scenery targets are treated like mission targets: they are included in the mission completion
calculation
(their `IsAlive()` state is checked) and their state is persisted/updated when missions spawn or
resume.
</p>
<note-box type="warning" title="Changing IDs">
Due to Object IDs changing when maps are updated and edited the "Assign as..." functionality
does not work for scenery targets.
Hence the need to use trigger zones to define them.
</note-box>
<p>Here's two images showing how a bridge is added to a mission.</p>
<img src="/img/bridge.png" style="max-height: 35remrem;" />
</div>
<div id="cas-region">
<h2 id="cas">CAS</h2>
<p>
Naming: <code-inline>MISSION_CAS_[Name]</code-inline> <br>
</p>
<h3 id="BattleManager">Battle Manager</h3>
<p>TBD</p>
</div>
<div id="dead-region">
<h2 id="dead">DEAD</h2>
<p>
Naming: <code-inline>MISSION_DEAD_[Name]</code-inline> <br>
Naming: <code-inline>MISSION_SAM_[Name]</code-inline> <br>
</p>
<h3 id="sam-vs-dead">SAM vs DEAD</h3>
<p>TBD</p>
</div>
<div id="bai-region">
<h2 id="bai">BAI</h2>
<p>
Naming: <code-inline>MISSION_BAI_[Name]</code-inline> <br>
</p>
</div>
<div id="deepstrike-region">
<h2 id="deepstrike">DEEPSTRIKE</h2>
<p>
Naming: <code-inline>MISSION_DEEPSTRIKE_[Name]</code-inline> <br>
<br>
When you, as a mission editor, want to create a mission that's further away from the current front-line you can use the Deep Strike mission type. <br>
This allows you to plan missions that require more strategic positioning and timing. <br>
</p>
<h3 id="activation">Activation</h3>
<p>
Deep Strike missions are activated when the Stage in which they are located becomes "PreActive" or "Active". <br>
"PreActive" is when either the current active StageNumber is a certain number of stages away OR the stage has "CAP" airbases active . <br>
<code-inline>SpearheadConfig.StageConfig.preactivateStage</code-inline> changes the configuration. <br>
<br>
For example: <br>
If the current active stage is <code-inline>MissionStage_2_[name]</code-inline> and preActivate config is set to 2. <br>
Then both <code-inline>MissionStage_3_[name]</code-inline> and <code-inline>MissionStage_4_[name]</code-inline> would become "PreActive".
</p>
<h3 id="primary-vs-secondary">Make Primary</h3>
<p>
By default DEEPSTRIKE missions are "Secondary" missions when spawned as a pre-activated stage.<br>
Then when the stage they are in is "Active", the mission becomes a "Primary" mission. <br>
<br>
If you want to make it a "Primary" mission from the start, you can overwrite it.<br>
Click the mission's trigger zone. Add a property named <code-inline>primary</code-inline> and set it to <code-inline>true</code-inline>.
This will make the mission "Primary" from the start. <br>
<br>
If you want it to "Secondary" even when its stage is the active stage, you can set "primary" to false. <br>
<br>
If you want to have it be "Secondary" when its stage is "Pre-Active" and "primary" when its stage is "Active" you don't have to do anything.
</p>
<h3 id="future-additions">Future</h3>
<p>
In the future we're thinking it would be cool to make the Deep Strike missions
be more integrated with the CAP airbases and/or logistics. <br>
Eg. Completing a DEEPSTRIKE mission would disable CAP for X amount of minutes after completion. <br>
This still needs to be refined, but we've already have ideas.
</p>
</div>
<p>Here's two images showing how a bridge is added to a mission.</p>
<img src="/img/bridge.png" style="max-height: 35remrem;"/>
</div>
</div>
</main>
<footer>
<p>&copy; 2025 Spearhead Project</p>
-7
View File
@@ -253,13 +253,6 @@
To make an airbase buildable add a text box inside of the trigger zone and name it: <code-inline>buildable_[freeform]</code-inline> <br/>
Then in the text box type amount of kilo's you want to be transfered before the logistisc mission is complete. <br/>
<br>
To add a custom briefing for a buildable zone, include a text box inside the trigger zone and name it: <code-inline>supplybriefing_[freeform]</code-inline>.
<code-inline>{{coords}}</code-inline> is highly recommended so the players can easily find their landing zone.<br>
Additionally, the amount of kilos required and delivered are always shown on the bottom, as well as a note to not land in the construction zone.
<br/>
<br>
The base or zone will slowly build up with each crate giving both a nice view of it happening AND it's good for performance as there's not a big addition of objects at once. <br/>
Right now a crate takes 15 seconds to unpack per 500kg. Meaning 2 crates of 1000kg will be faster than 1 crate of 2000kg. <br/>
+3 -3
View File
@@ -8,7 +8,7 @@
--color-bg-hover: rgba(120, 140, 255, 0.12); /* More visible hover */
--color-bg-note-dark: rgba(40, 35, 30, 0.85); /* Slightly darker note box */
--color-text-primary: #b4b3b3; /* Brighter text */
--color-text-primary: #a5a5a5; /* Brighter text */
--color-text-secondary: #bfc8d8; /* Lighter secondary text */
--color-text-accent: #7ecbff; /* Brighter accent */
--color-text-light: #ffffff;
@@ -262,7 +262,7 @@ header nav a:hover::after {
overflow: hidden;
top: 100%; /* Position below the dropdown */
left: 0; /* Align left edge */
min-width: 240px;
min-width: 160px;
background-color: var(--color-bg-secondary);
box-shadow: var(--shadow-medium);
@@ -336,8 +336,8 @@ main {
}
.side-nav {
width: 200px;
min-width: 200px;
max-width: 200px;
padding-left: 0;
overflow-y: auto;
border-right: none;
-140
View File
@@ -1,140 +0,0 @@
name: PR Prerequisites
on:
pull_request:
branches:
- main
- develop
env:
LUA_VERSION: "5.1"
LUA_LANGUAGE_SERVER_VERSION: "3.19.1"
jobs:
bundles_without_error:
runs-on: ubuntu-latest
name: Bundles Without Errors
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Lua
uses: leafo/gh-actions-lua@v13
with:
luaVersion: ${{ env.LUA_VERSION }}
- name: Compile Spearhead
uses: https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools/github-actions/bundle-script@bundle-script/v1
with:
source-root: ./src
output-file: ./output/spearhead.lua
- name: Verify Compiled Lua Syntax
run: lua -e "assert(loadfile('./output/spearhead.lua'))"
- name: Set up Luarocks
uses: leafo/gh-actions-luarocks@v4
- name: Install luacheck
run: luarocks install luacheck
# "lfs",
# "setmetatable", "getmetatable", "table", "string", "math", "pairs", "ipairs",
# "require", "type", "tostring", "io", "error", "pcall", "tonumber",
# "loadstring",
- name: Create LuaCheck Config
run: |
cat << EOF > .luacheckrc
stds.spearhead_lib = {
globals = {
"SpearheadConfig", "SpearheadAPI", "GlobalCapCallBacks",
}
}
stds.dcs_lib = {
globals = {
"env", "net", "trigger", "timer", "Object", "land", "coord", "coalition",
"Unit", "Group", "Airbase", "StaticObject", "SceneryObject", "world",
"country", "missionCommands", "Controller", "Weapon", "atmosphere", "country",
"lfs", "AI"
}
}
std = "lua51c+spearhead_lib+dcs_lib"
self = false
max_line_length = false
EOF
- name: Show LuaCheck Config
run: cat .luacheckrc
- name: Lint Compiled Output
run: luacheck ./output/spearhead.lua
lua_check:
runs-on: ubuntu-latest
name: Lua Check
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Lua
uses: leafo/gh-actions-lua@v13
with:
luaVersion: ${{ env.LUA_VERSION }}
- name: Install Dcs Plugins
uses: https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools/github-actions/install-lua-addon@install-lua-addon/v1
with:
destination-path: "./.vscode/lua-addons" ##same as local so .luarc.json can be the same
- name: Verify Dcs Plugins Installation
run: ls -la ./.vscode/lua-addons
- name: Install lua-language-server
run: |
mkdir -p ./luals
# For Ubuntu/Debian
wget https://github.com/LuaLS/lua-language-server/releases/download/${{ env.LUA_LANGUAGE_SERVER_VERSION }}/lua-language-server-${{ env.LUA_LANGUAGE_SERVER_VERSION }}-linux-x64.tar.gz
tar -xzf lua-language-server-*.tar.gz -C ./luals
rm lua-language-server-*.tar.gz
- name: Verify Lua Language Server Installation
run: ./luals/bin/lua-language-server --version
- name: Update Json
run: |
FULL_PATH=$(realpath ./.vscode/lua-addons)
echo "full-path: $FULL_PATH"
ls -R $FULL_PATH
echo "Updating .luarc.json with full path"
jq --arg full_path "$FULL_PATH" '.workspace.library[0] = $full_path' ./.luarc.json > ./.luarc.tmp.json
mv ./.luarc.tmp.json ./.luarc.json
- name: Show Config
run: cat ./.luarc.json
- name: Run Lua Language Server
run: |
CONFIG_FULL_PATH=$(realpath ./.luarc.json)
echo "Config full path: $CONFIG_FULL_PATH"
./luals/bin/lua-language-server \
--check ./src \
--checklevel=Warning \
--loglevel=trace \
--logpath="./luals/logs" \
--configpath="$CONFIG_FULL_PATH" \
# --develop
- name: Show Log
if: failure()
run: |
ls -R ./luals/logs
cat ./luals/logs/* || true
+2 -2
View File
@@ -1,4 +1,4 @@
name: Publish Beta Release
name: Publish Release
on:
push:
@@ -46,7 +46,7 @@ jobs:
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Compile Spearhead
uses: https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools/github-actions/bundle-script@bundle-script/v1
uses: https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools/github-action@action/v0
with:
source-root: ./src
output-file: ./output/spearhead.lua
+1 -1
View File
@@ -45,7 +45,7 @@ jobs:
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Compile Spearhead
uses: https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools/github-actions/bundle-script@bundle-script/v1
uses: https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools/github-action@action/v0
with:
source-root: ./src
output-file: ./output/spearhead.lua
-1
View File
@@ -3,6 +3,5 @@
/dist
.vscode/lua-addons
.vscode/settings.json
**\settings.json
-51
View File
@@ -1,51 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
"runtime": {
"version": "Lua 5.1"
},
"workspace": {
"checkThirdParty": false,
"maxPreload": 1600,
"preloadFileSize": 1000,
"ignoreDir": [ "config.lua" ]
},
"diagnostics": {
"enable": true,
"globals": [
"lfs"
],
"groupSeverity": {
"duplicate": "Warning",
"global":"Warning",
"luadoc": "Warning",
"redefined": "Warning",
"strong": "Warning",
"type-check": "Warning",
"unbalanced" : "Warning",
"ambiguity": "Warning",
"strict": "Warning",
"unused": "Warning"
},
"groupFileStatus": {
"ambiguity": "Any",
"await": "Any",
"duplicate": "Any",
"global": "Any",
"luadoc": "Any",
"redefined": "Any",
"strict": "Any",
"type-check": "Any",
"unbalanced": "Any",
"unused": "Any",
"strong": "Any"
},
"ignoredFiles": "Opened",
"libraryFiles": "Opened"
},
"hint": {
"enable": false
},
"completion": {
"enable": true
}
}
+4 -5
View File
@@ -1,6 +1,5 @@
{
"recommendations": [
"dutchie031.dutchies-dcs-scripting-tools",
"sumneko.lua",
]
{
"recommendations": [
"sumneko.lua"
]
}
+39 -29
View File
@@ -1,43 +1,53 @@
# Spearhead
Hello and welcome to Spearhead!
If you're a developer that wants to contribute, look at the code or understand what we're doing, feel free to read on.
## Contributing.
If you're a mission maker that wants to see the documentation and build you mission with this framework. <br>
It's best to go to our fully fledged documentation website:
- Stable/Main: https://spearhead.rocks
- Beta/Develop: https://beta.spearhead.rocks
I'm always happy to see contributions! <br/>
Especially if you've found an issue, annoyance or feature that you know how to solve, fix or create! <br/>
## Project Structure
### Keep it small.
The Spearhead project is structured in a lot of separate files. <br>
While Lua isn't specifically created for it, most of it has been written with the standards of OOP in mind. <br>
Classes, Objects and helper methods.
Keeping your contributions small and to the point helps both me and yourself. <br/>
It'll help the PR review and the speed of improvements. <br/>
If you have a big feature in mind? Go for it! <br/>
Does it require an entire refactor of parts or all of the program? Maybe make sure to do step 0 first.
You're find in the `/src` folder all the files. <br>
They are separated in a somewhat logical order, but some things might feel out of place. <br>
### Steps (for first timers)
0. Reach out (Optional)
### Bundling, Intellisense, Linting
If you want to let me know you want to do something to contribute, please do! <br/>
Maybe I'm already working on the thing you wanted to build, or someone else is. <br/>
Creating an issue is even better! <br/>
In DCS the easiest way to import a script is with 1 single file, however, that's not as "maintainable".
In order to keep it maintainable we're using a self-built tool to bundle, validate and rewrite some of the lua in order to get it into a single file.
The tool is open-source here: https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools. <br>
It has a `VSCode` extensions and two `Github Action` steps. <br>
If your favorite IDE isn't `VSCode` (I respect that), it might be very easy to create an extension for it, if it does allow that.
The `VSCode` extensions comes with Intellisense as well, in the form of a full file of DCS lua type annotations. <br>
This gives the Lua Language Server by Sumneko (which can be used in `VSCode`) all the type definitions it needs.
When writing Spearhead code it is also important to keep our own LuaLS Annotations up to date and complete. <br>
Read all about those annotations here: https://luals.github.io/wiki/annotations/
1. Fork the repository
When a pull-request to the `develop` or `main` branches is created, before it can be merged it requires to pass the linter checks.
The first linting check is done by the Lua Language Server. It checks for whitespace and formatting, but it also checks if all types, type-checking and casts are in order. It marks unused variables etc. `.luarc.json` has all the config for those checks. <br>
The second linter job is with `luacheck` which first "bundles" the script, and then runs a full linter check over the bundled script. <br>
This is to make sure that also the bundled script does not contain any errors.
By forking you can create your own working set of code. <br/>
Whether you keep that fork public or private is up to you! <br/>
## Contributing
Keep in mind, forking is alright, but it's best with the intent to contribute back. <br/>
After all, instead of having 5 slightly different versions and products, it might be nicer to have 1 much better version. <br/>
### Setting up the project
2. Create a Draft PR as soon as possible!
If you've kept on reading I assume you either want to brwose
As soon as you've found some times to create an initial version, please create a draft PR. <br/>
That way you can let me and everyone know it's being worked on and people can see what conflicts might arise with their own changes. <br/>
3. Test, test, test
With DCS and Lua there's a bunch of edge cases that are really hard to get to. <br/>
However, you kinda need to make sure most if not all cases are caught and tested. <br/>
Since automated testing for now doesn't seem feasible, please make sure to verify and test functionality after touching stuff. <br/>
4. Finalise and publish the PR.
Finalise the PR and let the maintainers know! <br/>
We can all have a look and discuss the changes. <br/>
# TODO:
- Would it be possible to generate optional markers on mission briefing locations for Tomcat / Phantom navigation? Then Jester can type it in for the pilots automatically.
+71 -87
View File
@@ -1,87 +1,71 @@
## [Unreleased]
### Breaking Changes
### New Features
- Addresses #41
Adds Stage Complete debug methods for local testing of the Stage flow.
PR #40
- Closes #35
Creating Lane option for stages.
Additionally to the primary stage lane, stages can now be created in different lanes.
These lanes can be used to create different stage progression flows and create different "side-stories".
PR #40
- Closes #37
Now enabled the mission editor to add a custom briefing to the "Buildable" missions.
PR #44
- Closes #42
DEEPSTRIKE now added as a possible feature.
This enables to have missions active in "Pre-Activated" Stages.
PR #48
### Bug Fixes
## [0.13.0] 2026-08
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.
### Breaking Changes
### 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
- 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
Migration to Gitea. <br>
First version of the Gitea repository. <br>
Everything is new, but also old.
### Breaking Changes
Nothing worth mentioning
### New Features
All and none
### Bug Fixes
No bugs fixed, they're all still there.
## [x.x.x] Year - Month Release Template
### Breaking Changes
### New Features
### Bug Fixes
## [Unreleased]
### Breaking Changes
### New Features
### Bug Fixes
## [0.13.0] 2026-08
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
### 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
- 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
Migration to Gitea. <br>
First version of the Gitea repository. <br>
Everything is new, but also old.
### Breaking Changes
Nothing worth mentioning
### New Features
All and none
### Bug Fixes
No bugs fixed, they're all still there.
## [x.x.x] Year - Month Release Template
### Breaking Changes
### New Features
### Bug Fixes
+3 -6
View File
@@ -4,9 +4,6 @@ SpearheadConfig = {
---DEBUG LOGGING
debugEnabled = false, -- default false
--- DEBUG MENU
debugMenuEnabled = false, -- default false
--- 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
briefingMessageDuration = 60, --default 60
@@ -15,7 +12,7 @@ SpearheadConfig = {
--quickly enable of disable the entire CAP Logic
--(you can also just rename all units to not be named "CAP_")
enabled = true, -- default true
--min ground speed for CAP aircraft during patrol
-- unit: knots
minSpeed = 400, -- default 400
@@ -62,7 +59,7 @@ SpearheadConfig = {
deathDelay = 1800, -- default 1800
},
StageConfig = {
-- management of stages and its missions.
-- This is not related to CAP managers which will continue to work even if stage management is disabled
enabled = true, -- default true
@@ -98,7 +95,7 @@ SpearheadConfig = {
},
Persistence = {
--- io and lfs cannot be sanitized in the MissionScripting.lua
--- enables or disables the persistence logic in spearhead
enabled = false,
+73
View File
@@ -0,0 +1,73 @@
--assert(loadfile("C:\\Repos\\DCS\\Spearhead\\dev\\dev_classes.lua"))()
local basePath = "C:\\Repos\\DCS\\Spearhead\\"
local classPath = basePath .. "classes\\"
assert(loadfile(classPath .. "_baseClasses\\Queue.lua"))()
assert(loadfile(classPath .. "spearhead_base.lua"))()
assert(loadfile(classPath .. "spearhead_routeutil.lua"))()
assert(loadfile(classPath .. "spearhead_events.lua"))()
assert(loadfile(classPath .. "spearhead_db.lua"))()
assert(loadfile(classPath .. "fleetClasses\\FleetGroup.lua"))()
assert(loadfile(classPath .. "fleetClasses\\GlobalFleetManager.lua"))()
assert(loadfile(classPath .. "helpers\\MizGroupsManager.lua"))()
assert(loadfile(classPath .. "helpers\\SpawnManager.lua"))()
assert(loadfile(classPath .. "configuration\\CapConfig.lua"))()
assert(loadfile(classPath .. "configuration\\StageConfig.lua"))()
assert(loadfile(classPath .. "stageClasses\\GlobalStageManager.lua"))()
assert(loadfile(classPath .. "stageClasses\\missions\\baseMissions\\Mission.lua"))()
assert(loadfile(classPath .. "stageClasses\\missions\\ZoneMission.lua"))()
assert(loadfile(classPath .. "stageClasses\\missions\\RunwayStrikeMission.lua"))()
assert(loadfile(classPath .. "stageClasses\\missions\\BuildableMission.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\BaseStage\\Stage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\PrimaryStage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\ExtraStage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\WaitingStage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Groups\\SpearheadGroup.lua"))()
assert(loadfile(classPath .. "stageClasses\\Groups\\SpearheadSceneryObject.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\MissionCommandsHelper.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\SupplyConfig.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\SupplyUnitsTracker.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\BattleManager.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\abstract\\BuildableZone.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\StageBase.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\BlueSam.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\FarpZone.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\SupplyHub.lua"))()
assert(loadfile(classPath .. "capClasses\\taskings\\RTB.lua"))()
assert(loadfile(classPath .. "capClasses\\taskings\\CAP.lua"))()
assert(loadfile(classPath .. "capClasses\\taskings\\SWEEP.lua"))()
assert(loadfile(classPath .. "capClasses\\taskings\\INTERCEPT.lua"))()
assert(loadfile(classPath .. "capClasses\\airGroups\\AirGroup.lua"))()
assert(loadfile(classPath .. "capClasses\\airGroups\\CapGroup.lua"))()
assert(loadfile(classPath .. "capClasses\\airGroups\\SweepGroup.lua"))()
assert(loadfile(classPath .. "capClasses\\airGroups\\InterceptGroup.lua"))()
assert(loadfile(classPath .. "capClasses\\detection\\DetectionManager.lua"))()
assert(loadfile(classPath .. "capClasses\\GlobalCapManager.lua"))()
assert(loadfile(classPath .. "capClasses\\CapAirbase.lua"))()
assert(loadfile(classPath .. "capClasses\\runwayBombing\\RunwayBombingTracker.lua"))()
assert(loadfile(classPath .. "persistence\\Persistence.lua"))()
-- Startup:
assert(loadfile(basePath .. "main.lua"))()
+79
View File
@@ -0,0 +1,79 @@
SpearheadConfig = {
debugEnabled = true,
CapConfig = {
--quickly enable of disable the entire CAP Logic
--(you can also just rename all units to not be named "CAP_")
enabled = true, -- default true
--min ground speed for CAP aircraft during patrol
-- unit: knots
minSpeed = 400, -- default 400
--max speed for CAP aircraft during patrol
-- unit: knots
maxSpeed = 500, -- default 500
--minAlt for aircraft on patrol
-- unit: feet
minAlt = 18000, -- default 18000
--maxAlt for aircraft on patrol
-- unit: feet
maxAlt = 28000, -- default 28000
--Delay for aircraft from touchdown to off the chocks.
-- unit: seconds
rearmDelay = 180, -- default 600
repairDelay = 600, -- default 600
--Delay for aircraft from death to takeoff.
--When the seconds remaining is the same at the rearmDelay it will be spawned on the ramp and follow the rearm logic.
-- !! Can not be lower than rearmDelay
-- unit: seconds
deathDelay = 1800, -- default 1800
},
StageConfig = {
-- management of stages and its missions.
-- This is not related to CAP managers which will continue to work even if stage management is disabled
enabled = true, -- default true
--Will draw the active and the next stage
drawStages = true, -- default true
drawPreActivated = true,
markLastContact = true,
--AutoStages will continue to the next stage automatically on completion of the missions within the stage.
-- If you want to make it so the next stage triggers only when you want to disable it here and manually implement the actions needed.
--[[
TODO: Add manual stage transition documentation
]]
autoStages = true, --default true
--Maximum missions per stage (includes all types of missions)
maxMissionStage = 100,
--Stage starting number
startingStage = 1,
---DEBUG logging. Consider keeping this disabled
debugEnabled = true
},
Persistence = {
--- io and lfs cannot be sanitized in the MissionScripting.lua
--- enables or disables the persistence logic in spearhead
enabled = true,
--- sets the directory where the persistence file is stored
--- if nil then lfs.writedir() will be used.
--- which will
directory = nil ,
--- the filename of the persistence file. Should end with .json for convention, but any text extension should do.
fileName = "Spearhead_Persistence_Dev"
}
}
File diff suppressed because it is too large Load Diff
+47
View File
@@ -0,0 +1,47 @@
---@class Queue
---@field private _items Array
---@field private _first number
---@field private _last number
local Queue = {}
Queue.__index = Queue
---@return Queue
function Queue.new()
local self = setmetatable({}, Queue)
self._items = {}
self._first = 1
self._last = 0
return self
end
---@return nil
function Queue:push(item)
self._last = self._last + 1
self._items[self._last] = item
end
---@return any?
function Queue:pop()
if self._first > self._last then
return nil
end
local item = self._items[self._first]
self._items[self._first] = nil
self._first = self._first + 1
return item
end
---@return Array<any>
function Queue:toList()
local items = {}
for i = self._first, self._last do
items[#items + 1] = self._items[i]
end
return items
end
return Queue
+3 -2
View File
@@ -10,6 +10,7 @@ local MissionCompleteListeners = {}
---@class SpearheadAPIInternal
---@field notifyMissionComplete fun(zone_name: string)
---@type FullSpearheadAPI
SpearheadAPI = {
Stages = {
@@ -24,12 +25,12 @@ SpearheadAPI = {
getCurrentStage = function()
return GlobalStageManager.getCurrentStage() or nil
end,
isStageComplete = function(stageNumber, stageLaneIdentifier)
isStageComplete = function(stageNumber)
if type(stageNumber) ~= "number" then
return false, "stageNumber " .. stageNumber .. " is not a valid number"
end
local isComplete = GlobalStageManager.isStageComplete(stageNumber, stageLaneIdentifier)
local isComplete = GlobalStageManager.isStageComplete(stageNumber)
if isComplete == nil then
return nil, "no stage found with number " .. stageNumber
end
+2 -1
View File
@@ -6,7 +6,7 @@ SpearheadAPI = SpearheadAPI
---@class SpearheadStagesAPI
---@field changeStage fun(stageNumber: number): boolean, string @Changes the active stage of spearhead. <br/> All other stages will change based on the normal logic. (CAP, BLUE etc.)
---@field getCurrentStage fun(): number | nil @Returns the current stange number <br/> Returns nil when the stagenumber was not set before ever, which means Spearhead was not started.
---@field isStageComplete fun(stageNumber: number, stageLaneIdentifier: string?): boolean | nil, string @returns whether a stage (by index) is complete. <br/> @param stageNumber number <br/> @param stageLaneIdentifier string? nil for default lane <br/> @return boolean | nil <br/> @return string
---@field isStageComplete fun(stageNumber: number): boolean | nil, string @returns whether a stage (by index) is complete. <br/> @param stageNumber number <br/> @return boolean | nil <br/> @return string
---@class OnMissionCompleteListener
---@field onMissionComplete fun(self: OnMissionCompleteListener, zone_name: string) @Called when a mission is completed. @return void
@@ -14,3 +14,4 @@ SpearheadAPI = SpearheadAPI
---@class MissionAPI
---@field addOnMissionCompleteListener fun(listener: OnMissionCompleteListener) @Adds a listener to the mission. <br/> @param listener OnMissionCompleteListener <br/> @return void
+22 -21
View File
@@ -2,6 +2,7 @@ local Util = require("classes.util.Util")
local CapGroup = require("classes.capClasses.airGroups.CapGroup")
local SweepGroup = require("classes.capClasses.airGroups.SweepGroup")
local InterceptGroup = require("classes.capClasses.airGroups.InterceptGroup")
local RunwayBombingTracker = require("classes.capClasses.runwayBombing.RunwayBombingTracker")
local SpearheadEvents = require("classes.spearhead_events")
local RunwayStrikeMission = require("classes.stageClasses.missions.RunwayStrikeMission")
@@ -36,11 +37,12 @@ end
---@param database Database
---@param logger table
---@param capConfig table
---@param stageConfig table
---@param runwayBombingTracker RunwayBombingTracker
---@param detectionManager DetectionManager
---@param spawnManager SpawnManager
---@return CapBase
function CapBase.new(airbaseName, database, logger, capConfig, runwayBombingTracker, detectionManager, spawnManager)
function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runwayBombingTracker, detectionManager, spawnManager)
CapBase.__index = CapBase
local self = setmetatable({}, { __index = CapBase }) --[[@as CapBase]]
@@ -59,7 +61,7 @@ function CapBase.new(airbaseName, database, logger, capConfig, runwayBombingTrac
local baseData = database:getAirbaseDataForZone(airbaseName)
if baseData and baseData.CapGroups then
for _, name in pairs(baseData.CapGroups) do
for key, name in pairs(baseData.CapGroups) do
local capGroup = CapGroup.New(name, capConfig, logger, spawnManager)
if capGroup then
self.capGroupsByName[name] = capGroup
@@ -68,7 +70,7 @@ function CapBase.new(airbaseName, database, logger, capConfig, runwayBombingTrac
end
if baseData and baseData.SweepGroups then
for _, name in pairs(baseData.SweepGroups) do
for key, name in pairs(baseData.SweepGroups) do
local sweepGroup = SweepGroup.New(name, capConfig, logger, spawnManager)
if sweepGroup then
self.sweepGroupsByName[name] = sweepGroup
@@ -77,7 +79,7 @@ function CapBase.new(airbaseName, database, logger, capConfig, runwayBombingTrac
end
if baseData and baseData.InterceptGroups then
for _, name in pairs(baseData.InterceptGroups) do
for key, name in pairs(baseData.InterceptGroups) do
local interceptGroup = InterceptGroup.New(name, capConfig, logger, detectionManager, spawnManager)
if interceptGroup then
self.interceptGroupsByName[name] = interceptGroup
@@ -95,6 +97,7 @@ function CapBase.new(airbaseName, database, logger, capConfig, runwayBombingTrac
SpearheadEvents.AddStageNumberChangedListener(self)
timer.scheduleFunction(CheckStateContinuous, self, timer.getTime() + 15)
return self
end
@@ -124,7 +127,7 @@ end
function CapBase:SpawnIfApplicable()
self.logger:debug("Check spawns for airbase " .. self.airbaseName)
for _, capGroup in pairs(self.capGroupsByName) do
for groupName, capGroup in pairs(self.capGroupsByName) do
local targetStage = capGroup:GetZoneIDWhenStageID(tostring(self.activeStage))
if targetStage ~= nil and capGroup:GetState() == "UnSpawned" then
@@ -132,7 +135,7 @@ function CapBase:SpawnIfApplicable()
end
end
for _, sweepGroup in pairs(self.sweepGroupsByName) do
for groupName, sweepGroup in pairs(self.sweepGroupsByName) do
local targetStage = sweepGroup:GetZoneIDWhenStageID(tostring(self.activeStage))
if targetStage ~= nil and sweepGroup:GetState() == "UnSpawned" then
@@ -140,7 +143,7 @@ function CapBase:SpawnIfApplicable()
end
end
for _, interceptGroup in pairs(self.interceptGroupsByName) do
for groupName, interceptGroup in pairs(self.interceptGroupsByName) do
local targetStage = interceptGroup:GetZoneIDWhenStageID(tostring(self.activeStage))
if targetStage ~= nil and interceptGroup:GetState() == "UnSpawned" then
@@ -151,9 +154,8 @@ end
function CapBase:CheckAndScheduleCAP()
self.logger:debug("Check taskings for airbase " .. self.airbaseName)
---@type table<string, number>
local countPerStage = {}
---@type table<string, number>
local requiredPerStage = {}
local airbase = Airbase.getByName(self.airbaseName)
@@ -168,7 +170,7 @@ function CapBase:CheckAndScheduleCAP()
if group:IsBackup() == true then
local state = group:GetState()
if state == "InTransit" or state == "OnStation" or state == "RtbInTen" then
local supposedTargetZoneID = group:GetZoneIDWhenStageID(activeStageID)
local currentTargetZone = group:GetCurrentTargetZoneID()
@@ -190,7 +192,7 @@ function CapBase:CheckAndScheduleCAP()
end
end
end
if countPerStage[supposedTargetZoneID] == nil then
countPerStage[supposedTargetZoneID] = 0
end
@@ -229,7 +231,7 @@ function CapBase:CheckAndScheduleCAP()
countPerStage[supposedZone] = countPerStage[supposedZone] + 1
end
elseif state == "InTransit" or state == "OnStation" then
if supposedZone ~= group:GetCurrentTargetZoneID() then
if countPerStage[supposedZone] < requiredPerStage[supposedZone] then
local triggerZone = self.database:GetCapZoneForZoneID(supposedZone)
@@ -271,7 +273,7 @@ function CapBase:CheckAndScheduleCAP()
if triggerZone then
group:SendToZone(triggerZone, supposedZone, airbase)
end
countPerStage[supposedZone] = countPerStage[supposedZone] + 1
end
end
@@ -310,7 +312,8 @@ function CapBase:CheckAndScheduleIntercept()
self.logger:debug("Check intercept taskings for airbase " .. self.airbaseName)
---@type table<string, boolean>
local interceptZoneIDs = {}
local interceptZoneIDs = {}
local airbase = Airbase.getByName(self.airbaseName)
@@ -318,7 +321,7 @@ function CapBase:CheckAndScheduleIntercept()
return nil
end
for _, group in pairs(self.interceptGroupsByName) do
for name, group in pairs(self.interceptGroupsByName) do
local targetZoneID = group:GetZoneIDWhenStageID(tostring(self.activeStage))
if targetZoneID then
interceptZoneIDs[targetZoneID] = true
@@ -354,7 +357,7 @@ function CapBase:CheckAndScheduleIntercept()
end
end
end
---RATIO. Amount of enemy fighters required per zone before another group gets added.
local ratio = 4 -- Ratio of units to intercept per zone, can be adjusted
@@ -376,6 +379,7 @@ function CapBase:CheckAndScheduleIntercept()
if total < required then
for _, group in pairs(self.interceptGroupsByName) do
if total < required then
local zoneID = group:GetZoneIDWhenStageID(tostring(self.activeStage))
if group:GetState() == "ReadyOnTheRamp" then
group:SendToInterceptUnits(targets, name, airbase)
total = total + 1
@@ -388,12 +392,9 @@ function CapBase:CheckAndScheduleIntercept()
end
end
end
end
function CapBase:OnStageNumberChanged(number, laneIdentifier)
-- only react on "main" lane changes, ignore other lanes for now
if laneIdentifier ~= nil then return end
function CapBase:OnStageNumberChanged(number)
self.activeStage = number
if self:IsBaseActiveWhenStageIsActive(number) == true then
+9 -6
View File
@@ -6,19 +6,21 @@ local CapAirbase = require("classes.capClasses.CapAirbase")
---@class GlobalCapManager
local GlobalCapManager = {}
do
---@type table<string, CapBase[]>
local airbasesPerStage = {}
---@type table<string, CapBase>
local allAirbasesByName = {}
local activeAirbasesPerActiveStage = {}
local unitsPerzonePerStage = {}
local initiated = false
---comment
---@param database Database
---@param capConfig table
---@param stageConfig StageConfig
---@param detectionManager DetectionManager
---@param logLevel LogLevel
---@param spawnManager SpawnManager
function GlobalCapManager.start(database, capConfig, detectionManager, logLevel, spawnManager)
function GlobalCapManager.start(database, capConfig, detectionManager, stageConfig, logLevel, spawnManager)
if initiated == true then return end
local logger = Logger.new("AirbaseManager", logLevel)
@@ -27,7 +29,7 @@ do
local zones = database:getStagezoneNames()
if zones then
for _, stageName in pairs(zones) do
for key, stageName in pairs(zones) do
if airbasesPerStage[stageName] == nil then
airbasesPerStage[stageName] = {}
end
@@ -37,8 +39,9 @@ do
for _, airbaseName in pairs(airbaseNames) do
if airbaseName then
local airbaseSpecificLogger = Logger.new("CAP_" .. airbaseName, logLevel)
local airbase = CapAirbase.new(airbaseName, database, airbaseSpecificLogger, capConfig, runwayBombingTracker, detectionManager, spawnManager)
local airbase = CapAirbase.new(airbaseName, database, airbaseSpecificLogger, capConfig, stageConfig, runwayBombingTracker, detectionManager, spawnManager)
if airbase then
table.insert(airbasesPerStage[stageName], airbase)
allAirbasesByName[airbaseName] = airbase
+8 -39
View File
@@ -1,11 +1,9 @@
local SpearheadEvents = require("classes.spearhead_events")
local RTBMission = require("classes.capClasses.taskings.RTB")
local Util = require("classes.util.Util")
local GlobalConfig = require("classes.configuration.GlobalConfig")
local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
local DcsUtil = require("classes.util.DcsUtil")
---@class AirGroup : OnUnitLostListener, OnGroupRTBListener, OnGroupRTBInTenListener, OnLandEventListener, OnGroupOnStationListener
---@class AirGroup : OnUnitLostListener
---@field protected _logger Logger
---@field protected _groupName string
---@field protected _groupType AirGroupType
@@ -14,12 +12,9 @@ local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelpe
---@field protected _config CapConfig
---@field protected _checkLivenessNumber number
---@field protected _spawnManager SpawnManager
---@field protected _routeDrawing CustomDrawing?
local AirGroup = {}
AirGroup.__index = AirGroup
local globalConfig = GlobalConfig.New()
---@param logger Logger
---@param groupName string
---@param groupType AirGroupType
@@ -33,7 +28,6 @@ function AirGroup:New(groupName, groupType, config, logger, spawnManager)
self._config = config
self._logger = logger
self._spawnManager = spawnManager
self._routeDrawings = {}
local group = Group.getByName(self._groupName)
if group then
@@ -80,7 +74,7 @@ function AirGroup:SetMission(mission)
end
end
local setMissionDelayed = function(data, _)
local setMissionDelayed = function(data, time)
data.self:SetMissionPrivate(data.mission)
end
@@ -98,28 +92,6 @@ function AirGroup:SetMissionPrivate(mission)
if group and mission then
group:getController():setTask(mission)
self._logger:debug("mission - Task set for group: " .. self._groupName)
if globalConfig:isDebugMenuEnabled() == true then
-- draw the mission route for debugging purposes
if self._routeDrawing then
self._routeDrawing:Remove()
end
local points = {}
if mission and mission.params and mission.params.route and mission.params.route.points then
local routePoints = mission.params.route.points --[[@as Array<Vec2> ]]
for _, wp in pairs(routePoints) do
if wp and wp.x and wp.y then
table.insert(points, { x = wp.x, y = wp.y })
end
end
end
local colorString = DrawingHelper.ColorTableToColorString({ 1, 0, 0, 1 })
self._routeDrawing = CustomDrawing.FromPoints(points, colorString, 5, 2)
self._routeDrawing:Draw()
end
end
end
@@ -199,8 +171,6 @@ function AirGroup:SpawnInternal(force, withoutLoadout)
end
---@param selfA AirGroup
---@param time number
---@return number?
local function CheckLivenessTask(selfA, time)
local interval = selfA:CheckLiveness()
if not interval then return end
@@ -266,7 +236,6 @@ function AirGroup:OnLastUnitLanded()
---@param data CheckGroupForRestartData
---@param time number
---@return number?
local checkGroupForRestart = function(data, time)
local group = Group.getByName(data.self:GetName())
if not group then
@@ -360,7 +329,7 @@ do --- RESPAWN FUNCTIONS
self:SetState("Dead")
---@param selfA AirGroup
local respawnTask = function(selfA, _)
local respawnTask = function(selfA, time)
selfA:StartRepair()
end
@@ -379,7 +348,8 @@ do --- RESPAWN FUNCTIONS
end
---comment
local rearmTask = function(_, _)
---@param selfA AirGroup
local rearmTask = function(selfA, time)
self:StartRearm()
end
@@ -404,7 +374,7 @@ do --- RESPAWN FUNCTIONS
end
---@param selfA AirGroup
local rearmTask = function(selfA, _)
local rearmTask = function(selfA, time)
selfA:MarkRearmComplete()
end
@@ -417,7 +387,6 @@ end
do --EVENT LISTENERS
---@param unit Unit
function AirGroup:OnUnitLost(unit)
if unit == nil then return end
self:CheckLiveness()
end
@@ -437,7 +406,7 @@ do --EVENT LISTENERS
end
end
function AirGroup:OnUnitLanded(_, _)
function AirGroup:OnUnitLanded(unit, airbase)
local anyInAir = false
local group = Group.getByName(self._groupName)
if group then
@@ -34,10 +34,9 @@ function CapGroup:IsBackup()
return self._isBackup
end
---@param stageID string
---@return string?
function CapGroup:GetZoneIDWhenStageID(stageID)
return self._targetZoneIdPerStage[tostring(stageID)]
return self._targetZoneIdPerStage[stageID]
end
---@return string?
@@ -104,17 +103,17 @@ function CapGroup:InitWithName(groupName)
return
end
local subsplit = Util.split_string(configPart, "|")
if subsplit then
for _, value in pairs(subsplit) do
for key, value in pairs(subsplit) do
local keySplit = Util.split_string(value, "]")
local targetZone = keySplit[2]
local allActives = string.sub(keySplit[1], 2, #keySplit[1])
local commaSeperated = Util.split_string(allActives, ",")
for _, childValue in pairs(commaSeperated) do
local dashSeperated = Util.split_string(childValue, "-")
for _, value in pairs(commaSeperated) do
local dashSeperated = Util.split_string(value, "-")
if Util.tableLength(dashSeperated) > 1 then
local from = tonumber(dashSeperated[1])
local till = tonumber(dashSeperated[2])
@@ -60,7 +60,6 @@ end
---comment
---@param units Array<string>
---@param zoneName string
---@param homeAirbase Airbase
function InterceptGroup:SendToInterceptUnits(units, zoneName, homeAirbase)
@@ -69,10 +68,9 @@ function InterceptGroup:SendToInterceptUnits(units, zoneName, homeAirbase)
self:SetTargetUnits(units)
end
---@param stageID string
---@return string?
function InterceptGroup:GetZoneIDWhenStageID(stageID)
return self._targetZoneIdPerStage[tostring(stageID)]
return self._targetZoneIdPerStage[stageID]
end
---@return string?
@@ -93,9 +91,6 @@ function InterceptGroup:SetTargetUnits(unitNames)
end)
end
---@param selfA InterceptGroup
---@param time number
---@return number?
local updateContinous = function(selfA, time)
local next = selfA:UpdateTask()
if next then
@@ -107,7 +102,6 @@ function InterceptGroup:SetTargetUnits(unitNames)
self._updateTaskID = timer.scheduleFunction(updateContinous, self, timer.getTime() + 30)
end
---@param unit Unit
function InterceptGroup:RemoveTargetUnit(unit)
if not unit then return end
@@ -196,7 +190,7 @@ function InterceptGroup:UpdateTask()
self:SendRTB(self._airbase)
return nil -- Return to base if bingo fuel
end
if selfDetected and self:IsInAir() == true then
if self._currentTargetName and self._currentTargetName == closestUnit:getName() then
local distance = Util.VectorDistance3d(closestUnit:getPoint(), groupPoint)
@@ -219,6 +213,8 @@ function InterceptGroup:UpdateTask()
self._currentTargetName = nil
end
local speed = self._config:getMaxSpeed()
local interceptPoint = self:GetInterceptPoint(groupPoint, speed, closestUnit)
@@ -226,8 +222,7 @@ function InterceptGroup:UpdateTask()
return 30 -- Return to base if no intercept point could be calculated
end
---@type table
local mission
local mission = nil
if self:IsInAir() == true then
-- If the group is in the air, create an intercept mission
mission = INTERCEPT.getMissionFromInAir(
@@ -328,13 +323,13 @@ function InterceptGroup:InitWithName(groupName)
configPart = string.sub(configPart, 2, #configPart)
local subsplit = Util.split_string(configPart, "|")
if subsplit then
for _, value in pairs(subsplit) do
for key, value in pairs(subsplit) do
local keySplit = Util.split_string(value, "]")
local targetZone = keySplit[2]
local allActives = string.sub(keySplit[1], 2, #keySplit[1])
local commaSeperated = Util.split_string(allActives, ",")
for _, childValue in pairs(commaSeperated) do
local dashSeperated = Util.split_string(childValue, "-")
for _, value in pairs(commaSeperated) do
local dashSeperated = Util.split_string(value, "-")
if Util.tableLength(dashSeperated) > 1 then
local from = tonumber(dashSeperated[1])
local till = tonumber(dashSeperated[2])
@@ -4,7 +4,7 @@ local Util = require("classes.util.Util")
local MissionEditorWarner = require("classes.util.MissionEditorWarnings")
---@class SweepGroup : AirGroup
---@field _targetZoneIdPerStage table<string, string>
---@field _targetZoneIdPerStage table<number, string>
---@field _currentTargetZoneID string?
local SweepGroup = {}
SweepGroup.__index = SweepGroup
@@ -26,10 +26,9 @@ function SweepGroup.New(groupName, config, logger, spawnManager)
return self
end
---@param stageID string
---@return string?
function SweepGroup:GetZoneIDWhenStageID(stageID)
return self._targetZoneIdPerStage[tostring(stageID)]
return self._targetZoneIdPerStage[stageID]
end
---@return string?
@@ -42,7 +41,7 @@ end
---@field self SweepGroup
---@param params SetTaskParams
local setMissionDelayedTask = function(params, _)
local setMissionDelayedTask = function(params, time)
params.self:SetMissionPrivate(params.task)
end
@@ -51,6 +50,8 @@ function SweepGroup:SendToZone(zone, targetZoneID, airbase)
self._currentTargetZoneID = targetZoneID
local group = Group.getByName(self._groupName)
local mission = SWEEP.getAsMissionFromAirbase(self._groupName, airbase, zone, self._config)
if mission then
---@type SetTaskParams
@@ -78,16 +79,16 @@ function SweepGroup:InitWithName(groupName)
local configPart = split_string[2]
configPart = string.sub(configPart, 2, #configPart)
local subsplit = Util.split_string(configPart, "|")
if subsplit then
for _, value in pairs(subsplit) do
for key, value in pairs(subsplit) do
local keySplit = Util.split_string(value, "]")
local targetZone = keySplit[2]
local allActives = string.sub(keySplit[1], 2, #keySplit[1])
local commaSeperated = Util.split_string(allActives, ",")
for _, childValue in pairs(commaSeperated) do
local dashSeperated = Util.split_string(childValue, "-")
for _, value in pairs(commaSeperated) do
local dashSeperated = Util.split_string(value, "-")
if Util.tableLength(dashSeperated) > 1 then
local from = tonumber(dashSeperated[1])
local till = tonumber(dashSeperated[2])
@@ -8,7 +8,6 @@ DetectionManager.__index = DetectionManager
---@param logger Logger
---@return DetectionManager
function DetectionManager.New(logger)
local self = setmetatable({}, DetectionManager)
@@ -25,7 +24,6 @@ function DetectionManager.New(logger)
---@param selfA DetectionManager
---@param time number
---@return number?
local updateDetectingUnitsTask = function(selfA, time)
selfA:UpdateDetectingUnits()
return time + 120
@@ -34,19 +32,18 @@ function DetectionManager.New(logger)
---@param selfA DetectionManager
---@param time number
---@return number?
local updateDetected = function(selfA, time)
selfA:UpdateDetectedUnits()
return time + 10
end
timer.scheduleFunction(updateDetected, self, timer.getTime() + 130)
return self
end
---@param unitName string
---@param coalitionSide CoalitionSide
---@return boolean
function DetectionManager:IsUnitDetectedBy(unitName, coalitionSide)
local coalitionString = tostring(coalitionSide)
if not self._detectedUnits[coalitionString] then
@@ -60,8 +57,7 @@ function DetectionManager:IsUnitDetectedBy(unitName, coalitionSide)
return timer.getTime() - self._detectedUnits[coalitionString][unitName] < 20
end
---@param coalitionSide CoalitionSide
---@return Array<string> detectedUnits
---@return Array<string>
function DetectionManager:GetDetectedUnitsBy(coalitionSide)
local coalitionString = tostring(coalitionSide)
if not self._detectedUnits[coalitionString] then
@@ -21,7 +21,7 @@ end
---comment
---@param weapon Weapon
function RunwayBombingTracker:OnWeaponFired(_, weapon, _)
function RunwayBombingTracker:OnWeaponFired(unit, weapon, target)
if weapon == nil then
return
@@ -30,7 +30,7 @@ function RunwayBombingTracker:OnWeaponFired(_, weapon, _)
local desc = weapon:getDesc()
local isTrackable = desc.category == Weapon.Category.BOMB or (desc.category == Weapon.Category.MISSILE and desc.missileCategory == Weapon.MissileCategory.CRUISE)
if isTrackable == true then
---@type WeaponTrackingArgs
local weaponTrackingArgs = {
weapon = weapon,
@@ -56,14 +56,12 @@ end
---@field self RunwayBombingTracker
---@private
---@param time number
---@param weaponTrackingArgs WeaponTrackingArgs
---@return number?
function RunwayBombingTracker.trackWeaponTask(weaponTrackingArgs, time)
local weapon = weaponTrackingArgs.weapon
local self = weaponTrackingArgs.self
if not weapon or weapon:isExist() == false then return nil end
local pos = weapon:getPoint()
@@ -85,7 +83,11 @@ function RunwayBombingTracker.trackWeaponTask(weaponTrackingArgs, time)
x = pos.x + velocity.x * nextInterval,
y = pos.z + velocity.z * nextInterval
}
self:OnWeaponImpact(weapon:getDesc(), impactPoint)
return nil
end
@@ -104,10 +106,11 @@ function RunwayBombingTracker:OnWeaponImpact(weaponDesc, impactPoint)
self._logger:debug("RunwayBombingTracker:OnWeaponImpact")
local warhead = weaponDesc.warhead --[[@as table]]
local explosiveMass = (warhead.explosiveMass or warhead.shapedExplosiveMass) --[[@as number]]
local warhead = weaponDesc.warhead
local explosiveMass = warhead.explosiveMass or warhead.shapedExplosiveMass
for runway, strikeMission in pairs(self.trackedRunways) do
for _, strikeMission in pairs(self.trackedRunways) do
local zone= strikeMission:GetRunwayZone()
if Util.is3dPointInZone({ x = impactPoint.x, z = impactPoint.y, y = 0 }, zone) then
+3 -8
View File
@@ -7,7 +7,6 @@ local CAP = {}
---@param attackHelos boolean
---@return table
local function GetCAPTargetTypes(attackHelos)
---@type Array<string>
local targetTypes = {
[1] = "Planes",
}
@@ -34,6 +33,9 @@ local function GetCAPPointFromTriggerZone(airBase, capZone)
local furthestA = nil
local furthestB = nil
local furthestFromBase = nil
local furthestFromBaseDistance = 0
local furthestDistance = 0
for indexA, pointA in ipairs(capZone.verts) do
@@ -54,10 +56,7 @@ local function GetCAPPointFromTriggerZone(airBase, capZone)
---@type Vec2
local baseVec2 = { x = baseVec3.x, y = baseVec3.z }
---@type Vec2
local pointA = capZone.verts[furthestA]
---@type Vec2
local pointB = capZone.verts[furthestB]
local furthest = pointA
local closest = pointB
@@ -88,7 +87,6 @@ end
---@param airbase Airbase
---@param capZone SpearheadTriggerZone
---@param capConfig CapConfig
---@return table
local GetOutboundTask = function(airbase, capZone, capConfig)
local airbaseVec3 = airbase:getPoint()
local airbaseVec2 = { x = airbaseVec3.x, y = airbaseVec3.z }
@@ -130,7 +128,6 @@ end
---@param airbase Airbase
---@param capZone SpearheadTriggerZone
---@param capConfig CapConfig
---@return table
function CAP.getAsMissionFromAirbase(groupName, airbase, capZone, capConfig)
local points = {
[1] = GetOutboundTask(airbase, capZone, capConfig),
@@ -158,7 +155,6 @@ end
---@param airbase Airbase
---@param capZone SpearheadTriggerZone
---@param capConfig CapConfig
---@return table
function CAP.getAsMission(groupName, airbase, capZone, capConfig)
local points = {
[1] = CAP.getAsTasking(groupName, airbase, capZone, capConfig),
@@ -185,7 +181,6 @@ end
---@param airbase Airbase
---@param capZone SpearheadTriggerZone
---@param capConfig CapConfig
---@return table
function CAP.getAsTasking(groupName, airbase, capZone, capConfig)
local duration = math.random(capConfig:getMinDurationOnStation(), capConfig:getMaxDurationOnStation()) or 1500
@@ -11,7 +11,6 @@ local INTERCEPT = {}
---@param speed number?
---@param alt number?
---@param config CapConfig
---@return table
function INTERCEPT.getMissionFromAirbase(groupName, interceptPoint, airbase, config, speed, alt)
local airbaseVec3 = airbase:getPoint()
@@ -39,7 +38,7 @@ function INTERCEPT.getMissionFromAirbase(groupName, interceptPoint, airbase, con
id = 'Mission',
params = {
airborne = true,
route = {
route = {
points = points
}
}
@@ -93,7 +92,7 @@ end
---@param currentPoint Vec2
---@param targetPosition Vec2
---@param targetUnit Unit
---@param airbase Airbase
---@param airbase any
---@param config any
---@param speed number?
---@param alt number?
@@ -160,10 +159,10 @@ function INTERCEPT.getInterceptTaskPoint(groupName, currentPoint, targetPoint, a
params = {
tasks = {
id = 'EngageTargetsInZone',
params = {
params = {
point = targetPoint,
zoneRadius = 10 * 1852, -- 10 NM, point will be updated, so target should be in this zone.
targetTypes = {
targetTypes = {
[1] = "Planes",
},
priority = 0
@@ -189,10 +188,10 @@ function INTERCEPT.getInterceptTaskPoint(groupName, currentPoint, targetPoint, a
params = {
tasks = {
id = 'EngageTargetsInZone',
params = {
params = {
point = targetPoint,
zoneRadius = 10 * 1852, -- 10 NM, point will be updated, so target should be in this zone.
targetTypes = {
targetTypes = {
[1] = "Planes",
},
priority = 0
+3 -8
View File
@@ -7,7 +7,6 @@ local RTB = {}
---@param airbase Airbase
---@param missionPoint Vec2
---@param capConfig CapConfig
---@return table
function RTB.getAsMission(airbase, missionPoint, capConfig)
return {
id = "Mission",
@@ -48,7 +47,7 @@ local getRunwayIntoWindCourseRad = function(airbase)
else
rad = 0 - rad
end
local runwayVec = {x = math.cos(rad), z = math.sin(rad), y = 0}
local alignment = Util.vectorAlignment(windVec, runwayVec)
@@ -61,14 +60,14 @@ local getRunwayIntoWindCourseRad = function(airbase)
do --inverse
local degree = math.deg(runway.course)
degree = (degree + 180) % 360
local rad = math.rad(degree)
if rad < 0 then
rad = math.abs(rad)
else
rad = 0 - rad
end
local runwayVec = {x = math.cos(rad), z = math.sin(rad), y = 0}
local alignment = Util.vectorAlignment(windVec, runwayVec)
@@ -114,7 +113,6 @@ end
---comment
---@param airbase Airbase
---@param missionPoint Vec2
---@return table
---@param capConfig CapConfig
function RTB.getApproachPoint(airbase, missionPoint, capConfig)
@@ -149,7 +147,6 @@ function RTB.getApproachPoint(airbase, missionPoint, capConfig)
end
---@param airbase Airbase
---@return table
function RTB.getInitialPoint(airbase)
local point = calcInitialPoint(airbase)
return {
@@ -173,8 +170,6 @@ function RTB.getInitialPoint(airbase)
}
end
---@param airbase Airbase
---@return table
function RTB.getLandingPoint(airbase)
local basePoint = airbase:getPoint()
return {
@@ -7,7 +7,6 @@ local SWEEP = {}
---@param attackHelos boolean
---@return table
local function GetCAPTargetTypes(attackHelos)
---@type Array<string>
local targetTypes = {
[1] = "Planes",
}
@@ -50,9 +49,7 @@ local function GetCAPPointFromTriggerZone(airBase, capZone)
---@type Vec2
local baseVec2 = { x = baseVec3.x, y = baseVec3.z }
---@type Vec2
local pointA = capZone.verts[furthestA]
---@type Vec2
local pointB = capZone.verts[furthestB]
local furthest = pointA
local closest = pointB
@@ -83,7 +80,6 @@ end
---@param airbase Airbase
---@param capZone SpearheadTriggerZone
---@param capConfig CapConfig
---@return table
local GetOutboundTask = function(airbase, capZone, capConfig)
local airbaseVec3 = airbase:getPoint()
local airbaseVec2 = { x = airbaseVec3.x, y = airbaseVec3.z }
@@ -125,7 +121,6 @@ end
---@param airbase Airbase
---@param capZone SpearheadTriggerZone
---@param capConfig CapConfig
---@return table
function SWEEP.getAsMissionFromAirbase(groupName, airbase, capZone, capConfig)
local pointA, pointB, pointC = SWEEP.getAsTasking(groupName, airbase, capZone, capConfig)
-4
View File
@@ -21,10 +21,6 @@ function CapConfig.new()
if SpearheadConfig == nil then SpearheadConfig = {} end
if SpearheadConfig.CapConfig == nil then SpearheadConfig.CapConfig = {} end
if type(SpearheadConfig.CapConfig.enabled) ~= "boolean" then
SpearheadConfig.CapConfig.enabled = true
end
local enabled = SpearheadConfig.CapConfig.enabled
if enabled == nil then enabled = true end
self._isEnabled = enabled
+4 -7
View File
@@ -1,8 +1,11 @@
local briefingMessageTime = nil
---@class GlobalConfig
---@field private _briefingTime number
---@field private _debugEnabled boolean
---@field private _debugMenuEnabled boolean
local GlobalConfig = {}
GlobalConfig.__index = GlobalConfig;
@@ -24,8 +27,6 @@ function GlobalConfig.New()
self._debugEnabled = false
end
self._debugMenuEnabled = SpearheadConfig.debugMenuEnabled == true
end
return self
@@ -39,10 +40,6 @@ function GlobalConfig:isDebugEnabled()
return self._debugEnabled or false
end
---@return boolean
function GlobalConfig:isDebugMenuEnabled()
return self._debugMenuEnabled or false
end
return GlobalConfig
@@ -1,31 +0,0 @@
---@class PersistenceConfig
---@field private _enabled boolean
---@field private directory string?
---@field private fileName string?
local PersistenceConfig = {}
function PersistenceConfig.new()
local self = setmetatable({}, { __index = PersistenceConfig })
if not SpearheadConfig then SpearheadConfig = {} end
if not SpearheadConfig.Persistence then SpearheadConfig.Persistence = {} end
self._enabled = SpearheadConfig.Persistence.enabled == true
self.directory = SpearheadConfig.Persistence.directory
self.fileName = SpearheadConfig.Persistence.fileName
return self
end
function PersistenceConfig:isEnabled()
return self._enabled == true
end
function PersistenceConfig:getDirectory()
return self.directory
end
function PersistenceConfig:getFileName()
return self.fileName
end
return PersistenceConfig
+1 -1
View File
@@ -18,7 +18,7 @@ local _logger = Logger.new("StageConfig", Logger.LogLevel)
---@return StageConfig
local function new()
if SpearheadConfig == nil then
if SpearheadConfig == nil then
_logger:warn("SpearheadConfig is nil, creating default SpearheadConfig")
SpearheadConfig = {}
end
-79
View File
@@ -1,79 +0,0 @@
local Logger = require("classes.util.Logger")
local StageRepository = require("classes.stageClasses.StageRepository")
---@class DebugMenu
---@field private _logger Logger
---@field private _stageRepository StageRepository
local DebugMenu = {}
DebugMenu.__index = DebugMenu
function DebugMenu.new()
local self = setmetatable({}, DebugMenu)
self._logger = Logger.new("DebugMenu")
self._stageRepository = StageRepository.getInstance()
return self
end
local menuName = "Spearhead Debug"
local debugMenuPath = { [1] = menuName }
function DebugMenu:RegisterMenus()
missionCommands.addSubMenu(menuName, {})
local refresh = function(params)
local selfA = params.self --[[@as DebugMenu]]
selfA:RefreshMenu()
end
missionCommands.addCommand("Refresh Menu", debugMenuPath, refresh, { self = self })
self:AddStageOptions()
end
function DebugMenu:RefreshMenu()
missionCommands.removeItem(debugMenuPath)
self:RegisterMenus()
end
function DebugMenu:AddStageOptions()
local stageMenuTable = missionCommands.addSubMenu("Stages", debugMenuPath)
local stageLanes = self._stageRepository:getAllStageLanes()
for _, stageLane in pairs(stageLanes) do
local stages = stageLane:GetStagesAtIndex(stageLane:GetActiveStageIndex())
if stages then
for _, stage in pairs(stages) do
local stageLaneId = stageLane:GetStageLaneIdentifier() or ""
local stageIndex = stage:GetStageIndex()
local stageName = stage:GetStageName()
local stageMenuName = stageLaneId .. stageIndex .. "_" .. stageName
local currentStageMenuTable = missionCommands.addSubMenu(stageMenuName, stageMenuTable)
---@class CompleteStageParams
---@field self DebugMenu
---@field stage Stage
---@param params CompleteStageParams
local completeStage = function(params)
local stageA = params.stage
local missions = stageA:GetMissions()
for _, mission in pairs(missions) do
mission:ForceMissionComplete()
end
end
---@type CompleteStageParams
local params = { self = self, stage = stage }
missionCommands.addCommand("Complete Stage", currentStageMenuTable, completeStage, params)
end
else
self._logger:info("No stages found for lane: " .. (stageLane:GetStageLaneIdentifier() or "default") .. " at index: " .. (stageLane:GetActiveStageIndex() or "nil"))
end
end
end
return DebugMenu
+19
View File
@@ -0,0 +1,19 @@
do -- mission aliases
--- @alias LogLevel
--- | "DEBUG"
--- | "INFO"
--- | "WARN"
--- | "ERROR"
--- | "NONE"
---@class Array<T>: { [integer]: T }
end
---@class KeyValuePair
---@field key string
---@field value any
+41 -55
View File
@@ -4,43 +4,37 @@ local MissionEditorWarning = require("classes.util.MissionEditorWarnings")
local RouteUtil = require("classes.spearhead_routeutil")
local SpearheadEvents = require("classes.spearhead_events")
---@class FleetGroup : OnStageChangedListener
---@field private fleetGroupName string
---@field private logger Logger
---@field private fleetNameIdentifier string
---@field private targetZonePerStage table<string, string>
---@field private currentTargetZone string
---@field private pointsPerZone table<string, {pointA: Vec3, pointB: Vec3}>
---@class FleetGroup
local FleetGroup = {}
FleetGroup.__index = FleetGroup
---comment
---@param fleetGroupName string
---@param database Database
---@param logger Logger
---@return FleetGroup?
function FleetGroup.new(fleetGroupName, database, logger)
---@return nil
function FleetGroup:new(fleetGroupName, database, logger)
local o = {}
local self = setmetatable({}, FleetGroup)
setmetatable(o, { __index = self })
self.fleetGroupName = fleetGroupName
self.logger = logger
o.fleetGroupName = fleetGroupName
o.logger = logger
local split_name = Util.split_string(fleetGroupName, "_")
if Util.tableLength(split_name) < 2 then
MissionEditorWarning.Add("CARRIERGROUP should have at least 2 parts. CARRIERGROUP_<fleetname>")
return nil
end
self.fleetNameIdentifier = split_name[2]
o.fleetNameIdentifier = split_name[2]
self.targetZonePerStage = {}
self.currentTargetZone = nil
self.pointsPerZone = {}
o.targetZonePerStage = {}
o.currentTargetZone = nil
o.pointsPerZone = {}
do --INIT
local carrierRouteZones = database:getCarrierRouteZones()
for _, zoneName in pairs(carrierRouteZones) do
if Util.strContains(string.lower(zoneName), "_".. string.lower(self.fleetNameIdentifier) .. "_" ) == true then
if Util.strContains(string.lower(zoneName), "_".. string.lower(o.fleetNameIdentifier) .. "_" ) == true then
local zone = DcsUtil.getZoneByName(zoneName)
if zone and zone.zone_type == DcsUtil.ZoneType.Polygon then
local split_string = Util.split_string(zoneName, "_")
@@ -49,18 +43,18 @@ function FleetGroup.new(fleetGroupName, database, logger)
"CARRIERROUTE should at least have 3 parts. Check the documentation for: " .. zoneName)
else
---@param zoneA SpearheadTriggerZone
---@param zone SpearheadTriggerZone
---@return Vec2, Vec2
local function GetTwoFurthestPoints(zoneA)
local function GetTwoFurthestPoints(zone)
local biggest = nil
local biggestA = zoneA.verts[1]
local biggestB = zoneA.verts[2]
local biggestA = zone.verts[1]
local biggestB = zone.verts[2]
for i = 1, 3 do
for ii = i + 1, 4 do
local a = zoneA.verts[i]
local b = zoneA.verts[ii]
local a = zone.verts[i]
local b = zone.verts[ii]
local dist = Util.VectorDistance2d(a, b)
if biggest == nil or dist > biggest then
@@ -78,7 +72,7 @@ function FleetGroup.new(fleetGroupName, database, logger)
return nil, nil
end
if Util.startsWith(namePart, "%[") == true then
if Util.startswith(namePart, "%[") == true then
namePart = Util.split_string(namePart, "[")[1]
end
@@ -106,15 +100,9 @@ function FleetGroup.new(fleetGroupName, database, logger)
local first, second = getMinMaxStage(split_string[3])
if first ~= nil and second ~= nil then
for i = first, second do
self.targetZonePerStage[tostring(i)] = zoneName
o.targetZonePerStage[tostring(i)] = zoneName
end
---@type {pointA: Vec3, pointB: Vec3}
local entry = {
pointA = {x = pointA.x, z = pointA.y, y = 0},
pointB = {x = pointB.x, z = pointB.y, y = 0}
}
self.pointsPerZone[zoneName] = entry
o.pointsPerZone[zoneName] = { pointA = { x = pointA.x, z = pointA.y, y = 0 }, pointB = { x = pointB.x, z = pointB.y, y = 0} }
else
MissionEditorWarning.Add("CARRIERROUTE zone stage numbers not in the format _[<number>-<number>]: " .. zoneName)
end
@@ -126,32 +114,30 @@ function FleetGroup.new(fleetGroupName, database, logger)
end
end
SpearheadEvents.AddStageNumberChangedListener(self)
return self
end
local SetTaskAsync = function(input, time)
local targetZone = input.targetZone
local task = input.task
local groupName = input.groupName
local logger = input.logger
local SetTaskAsync = function(input, _)
local targetZone = input.targetZone --[[@as string]]
local task = input.task --[[@as table]]
local groupName = input.groupName --[[@as string]]
local l_logger = input.logger --[[@as Logger]]
local group = Group.getByName(groupName)
if group then
l_logger:info("Sending " .. groupName .. " to " .. targetZone)
group:getController():setTask(task)
local group = Group.getByName(groupName)
if group then
logger:info("Sending " .. groupName .. " to " .. targetZone)
group:getController():setTask(task)
end
end
end
function FleetGroup:OnStageNumberChanged(number, laneIdentifier)
-- only react on "main" lane changes, ignore other lanes for now
if laneIdentifier ~= nil then return end
local targetZone = self.targetZonePerStage[tostring(number)]
if targetZone and targetZone ~= self.currentTargetZone then
local points = self.pointsPerZone[targetZone]
local task = RouteUtil.CreateCarrierRacetrack(points.pointA, points.pointB)
timer.scheduleFunction(SetTaskAsync, { task = task, targetZone = targetZone, groupName = self.fleetGroupName, logger = self.logger }, timer.getTime() + 5)
o.OnStageNumberChanged = function(self, number)
local targetZone = self.targetZonePerStage[tostring(number)]
if targetZone and targetZone ~= self.currentTargetZone then
local points = self.pointsPerZone[targetZone]
local task = RouteUtil.CreateCarrierRacetrack(points.pointA, points.pointB)
timer.scheduleFunction(SetTaskAsync, { task = task, targetZone = targetZone, groupName = self.fleetGroupName, logger = self.logger }, timer.getTime() + 5)
end
end
SpearheadEvents.AddStageNumberChangedListener(o)
return o
end
return FleetGroup
@@ -15,9 +15,9 @@ GlobalFleetManager.start = function(database)
local all_groups = MizGroupsManager.getAllGroupNames()
for _, groupName in pairs(all_groups) do
if Util.startsWith(string.lower(groupName), "carriergroup" ) == true then
if Util.startswith(string.lower(groupName), "carriergroup" ) == true then
logger:info("Registering " .. groupName .. " as a managed fleet")
local carrierGroup = FleetGroup.new(groupName, database, logger)
local carrierGroup = FleetGroup:new(groupName, database, logger)
table.insert(fleetGroups, carrierGroup)
end
end
+9 -31
View File
@@ -15,45 +15,24 @@ MizGroupsManager._groupNames = {}
MizGroupsManager._spawnTemplateData = {}
do --init
---@param coalition_name string
---@return Coalition
local getCoalitionData = function(coalition_name)
return env.mission.coalition[coalition_name]
end
---@param country Country
---@param key string
---@return Groups?
local getGroupsData = function(country, key)
if key == "planes" then return country.plane end
if key == "helicopters" then return country.helicopter end
if key == "ground" then return country.vehicle end
if key == "ships" then return country.ship end
if key == "statics" then return country.static end
return nil
end
for _, coalition_name in ipairs({"neutral", "blue", "red"}) do
local coalition_data = getCoalitionData(coalition_name)
for coalition_name, coalition_data in pairs(env.mission.coalition) do
local coalition_nr = DcsUtil.stringToCoalition(coalition_name)
if coalition_data.country then
for _, country_data in pairs(coalition_data.country) do
for _, category_name in pairs(DcsUtil.getAllGroupCategoryNames()) do
for country_index, country_data in pairs(coalition_data.country) do
for category_name, categorydata in pairs(country_data) do
local category_id = DcsUtil.stringToGroupCategory(category_name)
local groups_data = getGroupsData(country_data, category_name)
if category_id ~= nil and type(groups_data) == "table" and groups_data.group ~= nil and type(groups_data.group) == "table" then
for _, group in pairs(groups_data.group) do
local name = group.name --[[@as string]]
if category_id ~= nil and type(categorydata) == "table" and categorydata.group ~= nil and type(categorydata.group) == "table" then
for group_index, group in pairs(categorydata.group) do
local name = group.name
local skippable = false
local isStatic = false
if category_id == DcsUtil.GroupCategory.STATIC then
isStatic = true
local unit = group.units[1] --[[@as table]]
local unit = group.units[1]
if unit and unit.category == "Heliports" then
skippable = true
elseif unit and unit.name then
name = unit.name --[[@as string]]
name = unit.name
else
env.error("Group " .. name .. " has no units, skipping it.")
skippable = true
@@ -93,7 +72,6 @@ function MizGroupsManager.IsGroupStatic(groupName)
return nil
end
---@param groupName string
---@return SpawnData?
function MizGroupsManager.getSpawnTemplateData(groupName)
return MizGroupsManager._spawnTemplateData[groupName]
+25 -19
View File
@@ -16,11 +16,11 @@ SpawnManager.__index = SpawnManager
---@field uncontrolled boolean? Sets the group to be uncontrolled on spawn
---@param logger Logger
---@return SpawnManager
function SpawnManager.new(logger)
local self = setmetatable({}, SpawnManager)
self._logger = logger
self._persistedUnits = {} -- Stores units that should be persisted by name
return self
@@ -33,7 +33,7 @@ end
---@return StaticObject|Group|nil spawned
---@return boolean isStatic
function SpawnManager:SpawnGroup(groupName, overrides, isGroupPersistant)
local spawnData = MizGroupsManager.getSpawnTemplateData(groupName)
if spawnData == nil then
@@ -44,13 +44,13 @@ function SpawnManager:SpawnGroup(groupName, overrides, isGroupPersistant)
if spawnData.isStatic == true then
return self:SpawnStaticInternal(groupName, spawnData, overrides, isGroupPersistant), true
else
return self:SpawnGroupInternal(spawnData, overrides, isGroupPersistant), false
return self:SpawnGroupInternal(groupName, spawnData, overrides, isGroupPersistant), false
end
end
function SpawnManager:DestroyGroup(groupName)
if groupName == nil then return end
if self:IsGroupStatic(groupName) == true then
local object = StaticObject.getByName(groupName)
if object ~= nil then
@@ -103,20 +103,20 @@ function SpawnManager:OnUnitLost(object)
end
end
---@param groupName string
function SpawnManager:SpawnCorpsesOnly(groupName)
if groupName == nil then return end
end
do --- privates
---@private
---@param groupName string
---@param spawnData SpawnData
---@param override SpawnOverrides?
---@param isPersistent boolean?
---@return Group|nil
function SpawnManager:SpawnGroupInternal(spawnData, override, isPersistent)
function SpawnManager:SpawnGroupInternal(groupName, spawnData, override, isPersistent)
if not spawnData then return end
local country = spawnData.country
@@ -125,31 +125,37 @@ do --- privates
end
local spawnTemplate = Util.deepCopyTable(spawnData.groupTemplate)
---@type Array<string>
local removeableUnitNames = {}
--[[
TODO: Spawn units at "current"/LastKnown position with them going to the next waypoint.
ONLY when perstable data is found.
]]
if spawnTemplate and spawnTemplate["units"] then
local units = spawnTemplate["units"] --[[@as Array<table>]]
for _, unit in pairs(units) do
local name = unit["name"] --[[@as string]]
for _, unit in pairs(spawnTemplate["units"]) do
local name = unit["name"]
SpearheadEvents.addOnUnitLostEventListener(name, self)
local state = Persistence.UnitState(name)
if state then
if state.isDead == true then
removeableUnitNames[#removeableUnitNames+1] = name
end
end
if override and override.emptyLoadouts == true then
if unit["payload"] and unit["payload"]["pylons"] then
local payload = unit["payload"] --[[@as table]]
payload["pylons"] = {} --[[@as table]]
unit["payload"]["pylons"] = {}
end
end
if unit["parking"] then
unit["parking_landing"] = unit["parking"] --[[@as number]]
unit["parking_landing"] = unit["parking"]
end
if unit["parking_id"] then
unit["parking_landing_id"] = unit["parking_id"] --[[@as number]]
unit["parking_landing_id"] = unit["parking_id"]
end
end
@@ -163,6 +169,7 @@ do --- privates
local group = coalition.addGroup(country, spawnData.category, spawnTemplate)
for _, unit in pairs(group:getUnits()) do
self:CheckUnitAndReplaceIfPersistentDead(unit)
if isPersistent == true then
@@ -174,6 +181,7 @@ do --- privates
return group
end
return nil
end
@@ -203,11 +211,9 @@ do --- privates
spawnTemplate["x"] = persistentState.pos.x
spawnTemplate["y"] = persistentState.pos.z
if spawnTemplate["units"] and spawnTemplate["units"][1] then
local units = spawnTemplate["units"] --[[@as Array<table>]]
local firstUnit = units[1] --[[@as table]]
firstUnit["x"] = persistentState.pos.x --[[@as number]]
firstUnit["y"] = persistentState.pos.z --[[@as number]]
firstUnit["heading"] = persistentState.heading or 0 --[[@as number]]
spawnTemplate["units"][1]["x"] = persistentState.pos.x
spawnTemplate["units"][1]["y"] = persistentState.pos.z
spawnTemplate["units"][1]["heading"] = persistentState.heading or 0
end
end
end
+24 -47
View File
@@ -13,8 +13,7 @@ do
---@field unitsStates table<string, UnitState>
---@field random_missions table<string, MissionState>
---@field deliveredKilos table<string, number>
---@field activeStage number? required for backwards compatibility
---@field activeStageInStageLane table<string, number>
---@field activeStage integer|nil
---@class UnitState
---@field isDead boolean
@@ -34,8 +33,7 @@ do
unitsStates = {},
random_missions = {},
deliveredKilos = {},
activeStage = nil, -- Backwards compatibility for previous version without lanes
activeStageInStageLane = {}
activeStage = nil
}
local logger = {}
@@ -89,7 +87,7 @@ do
else
logger:error("Could not load persistence file, using default tables")
end
if tables.version == nil then tables.version = version end
end
@@ -112,9 +110,9 @@ do
logger:info("Wrote persistence data to file")
end
local UpdateContinuous = function(_, time)
local UpdateContinuous = function(null, time)
env.info("[Spearhead][Persistence] Checking up on persistence state...")
if Persistence._updateRequired == true then
if Persistence._updateRequired == true then
local status, result = pcall(writeToFile)
if status == false then
env.error("[Spearhead][Persistence] Could not write state to file: " .. result)
@@ -135,22 +133,20 @@ do
end
local warnForNonPersistenceContinous = function(_, time)
local warnForNonPersistenceContinous = function(null, time)
trigger.action.outText("Persistence was enabeld, however, io and lfs are not available and no persistence will be done. Make sure to either disable persistence or fix the issues before continuing.", 10)
return time + 9
end
---@param dir string @BaseDirectory
---@param startsWith string
---@param default string?
---@return string?
---@return string
local getLastFileOrDefault = function(dir, startsWith, default)
local latestFile, lastNumber = default, 0
for file in lfs.dir(dir) do
local split = Util.split_string(file, ".")
local doesStartWith = Util.startsWith(file, startsWith, true)
local doesStartWith = Util.startswith(file, startsWith, true)
if split and #split > 0 and split[#split] == EXTENSION and doesStartWith == true then
local numberString = split[#split-1]
local number = tonumber(numberString)
@@ -183,14 +179,11 @@ do
local fileName = "Spearhead_Persistence.0.spearhead"
if SpearheadConfig and SpearheadConfig.Persistence then
if SpearheadConfig.Persistence.fileName then
if type(SpearheadConfig.Persistence.fileName) ~= "string" then
SpearheadConfig.Persistence.fileName = "Spearhead_Persistence.0.spearhead"
end
local userFileName = SpearheadConfig.Persistence.fileName
local split = Util.split_string(userFileName, ".")
if not split or #split < 3 then
split = split or {}
if not split and #split < 3 then
split[#split+1] = "0"
split[#split+1] = "spearhead"
end
@@ -199,14 +192,11 @@ do
split[#split+1] = "0"
split[#split+1] = "spearhead"
end
fileName = table.concat(split, ".")
end
if SpearheadConfig.Persistence.directory then
if type(SpearheadConfig.Persistence.directory) ~= "string" then
SpearheadConfig.Persistence.directory = lfs.writedir() .. "\\Data"
end
if SpearheadConfig.Persistence.directory ~= nil then
dir = SpearheadConfig.Persistence.directory
end
end
@@ -216,10 +206,6 @@ do
local lastFile = getLastFileOrDefault(dir--[[@as string]], matchingPart, fileName)
if lastFile == nil then
lastFile = fileName
end
local fileSplit = Util.split_string(lastFile, ".")
fileSplit[#fileSplit-1] = tostring(tonumber(fileSplit[#fileSplit-1]) + 1)
fileName = table.concat(fileSplit, ".")
@@ -230,12 +216,12 @@ do
logger:info("No previous persistence file found, using default: " .. fileName)
end
logger:info("New Persistence file name: " .. tostring(fileName))
local lastPath = dir .. "\\" .. lastFile
local path = dir .. "\\" .. fileName
Persistence._path = path
createFileIfNotExists(path)
loadTablesFromFile(lastPath)
timer.scheduleFunction(UpdateContinuous, nil, timer.getTime() + 120)
@@ -244,29 +230,12 @@ do
end
---Sets the stage in the persistence table
---@param stageLane string?
---@param stageNumber number
Persistence.SetActiveStage = function(stageLane, stageNumber)
stageLane = stageLane or "nil"
tables.activeStageInStageLane[stageLane] = stageNumber
Persistence.SetActiveStage = function(stageNumber)
tables.activeStage = stageNumber
Persistence._updateRequired = true
end
---Get the active stage as in the persistance file
---@param stageLane string?
---@return integer|nil
Persistence.GetActiveStage = function(stageLane)
stageLane = stageLane or "nil"
-- backwards compatibility for previous versions without stage lanes
if stageLane == "nil" and tables.activeStageInStageLane[stageLane] == nil then
tables.activeStageInStageLane[stageLane] = tables.activeStage
tables.activeStage = nil -- Clear the old activeStage so it's not used or confused later
end
return tables.activeStageInStageLane[stageLane]
end
---comment
---@param missionName string
---@param pickedZone string
@@ -286,6 +255,14 @@ do
return tables.random_missions[string.lower(missionName)]
end
---Get the active stage as in the persistance file
---@return integer|nil
Persistence.GetActiveStage = function()
if tables.activeStage then
return tables.activeStage
end
return nil
end
---comment
---@param zoneName string
+41 -112
View File
@@ -2,7 +2,7 @@ local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil")
local MissionEditorWarnings = require("classes.util.MissionEditorWarnings")
local MizGroupsManager = require("classes.helpers.MizGroupsManager")
local StageDrawing = require("classes.stageClasses.drawings.StageDrawing")
local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
---@class DatabaseTables
@@ -27,7 +27,7 @@ local StageDrawing = require("classes.stageClasses.drawings.StageDrawing")
---@field MissionZoneData table<string, MissionZoneData>
---@field FarpZoneData table<string,FarpZoneData>
---@field missionCodes table<string, boolean>
---@field StageDrawings Array<StageDrawing>
---@field CustomDrawings Array<CustomDrawing>
---@class CapRoute
---@field zones Array<SpearheadTriggerZone>
@@ -58,7 +58,6 @@ local StageDrawing = require("classes.stageClasses.drawings.StageDrawing")
---@class BlueSamData
---@field groups Array<string>
---@field buildingKilos number?
---@field briefing string?
---@class MissionZoneData
---@field ZoneName string
@@ -69,14 +68,12 @@ local StageDrawing = require("classes.stageClasses.drawings.StageDrawing")
---@field descriptionLocation Vec2?
---@field dependsOn Array<string>
---@field completeAt number?
---@field primaryOverwrite boolean?
---@class FarpZoneData
---@field groups Array<string>
---@field padNames Array<string>
---@field buildingKilos number?
---@field supplyHubNames Array<string>
---@field briefing string?
---@class Database
---@field private _tables DatabaseTables
@@ -110,7 +107,7 @@ function Database.New(Logger)
FarpZoneData = {},
missionCodes = {},
SupplyHubZones = {},
StageDrawings = {}
CustomDrawings = {}
}
Database.__index = Database
@@ -122,7 +119,7 @@ function Database.New(Logger)
self._logger:debug("Initiating tables")
do -- INIT ZONE TABLES
for _, zone_data in pairs(DcsUtil.__trigger_zones) do
for zone_ind, zone_data in pairs(DcsUtil.__trigger_zones) do
local zone_name = zone_data.name
---@type Vec2
@@ -209,18 +206,15 @@ function Database.New(Logger)
self._logger:debug("initiated zone tables, continuing with descriptions")
do --load markers
if env.mission.drawings and env.mission.drawings.layers then
for _, layer in pairs(env.mission.drawings.layers) do
for i, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then
for _, layer_object in pairs(layer.objects) do
if Util.startsWith(string.lower(layer_object.name), "buildable", true) == true then
for key, layer_object in pairs(layer.objects) do
if Util.startswith(string.lower(layer_object.name), "buildable", true) == true then
local airbaseData = self:getAirbaseDataForDrawLayer(layer_object)
if airbaseData then
self._logger:debug("found airbase data for " .. layer_object.name)
if layer_object.primitiveType == "TextBox" then
layer_object = layer_object --[[@as TextBox]]
local number = tonumber(layer_object.text)
airbaseData.buildingKilos = number
end
local number = tonumber(layer_object.text)
airbaseData.buildingKilos = number
end
end
end
@@ -232,15 +226,13 @@ function Database.New(Logger)
---@type table<integer, boolean>
do -- custom drawings
if env.mission.drawings and env.mission.drawings.layers then
for _, layer in pairs(env.mission.drawings.layers) do
for i, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then
for _, layer_object in pairs(layer.objects) do
if Util.startsWith(layer_object.name, "drawing_", true) then
for key, layer_object in pairs(layer.objects) do
if Util.startswith(layer_object.name, "drawing_", true) then
local object = layer_object --[[@as DrawingObject]]
local stageDrawing = StageDrawing.New(object)
if stageDrawing then
table.insert(self._tables.StageDrawings, stageDrawing)
end
local customDrawing = CustomDrawing.New(object)
table.insert(self._tables.CustomDrawings, customDrawing)
end
end
end
@@ -260,19 +252,16 @@ function Database.New(Logger)
-- Find and add Briefing
if env.mission.drawings and env.mission.drawings.layers then
for _, layer in pairs(env.mission.drawings.layers) do
for i, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then
for _, layer_object in pairs(layer.objects) do
if Util.startsWith(string.lower(layer_object.name), "stagebriefing_", true) == true then
for key, layer_object in pairs(layer.objects) do
if Util.startswith(string.lower(layer_object.name), "stagebriefing_", true) == true then
local zone = DcsUtil.getZoneByName(stageZoneName)
local vec2 = { x = layer_object.mapX, y = layer_object.mapY }
if zone and Util.is2dPointInZone(vec2, zone) == true then
if layer_object.primitiveType == "TextBox" then
layer_object = layer_object --[[@as TextBox]]
local description = layer_object.text
if description and description ~= "" then
stageData.StageBriefing = description
end
local description = layer_object.text
if description and description ~= "" then
stageData.StageBriefing = description
end
end
end
@@ -346,14 +335,14 @@ function Database.New(Logger)
end
-- fill missions
for _, missionZone in pairs(self._tables.MissionZones) do
for key, missionZone in pairs(self._tables.MissionZones) do
if DcsUtil.isZoneInZone(missionZone, stageZoneName) == true then
table.insert(stageData.MissionZones, missionZone)
end
end
-- fill random missions
for _, missionZone in pairs(self._tables.RandomMissionZones) do
for key, missionZone in pairs(self._tables.RandomMissionZones) do
if DcsUtil.isZoneInZone(missionZone, stageZoneName) == true then
table.insert(stageData.RandomMissionZones, missionZone)
end
@@ -475,7 +464,6 @@ function Database.New(Logger)
return self
end
---@type table<string, boolean>
local is_group_taken = {}
local getAvailableGroups = function()
@@ -492,7 +480,7 @@ end
local getAvailableCAPGroups = function()
local result = {}
for name, value in pairs(is_group_taken) do
if value == false and Util.startsWith(name, "CAP") then
if value == false and Util.startswith(name, "CAP") then
table.insert(result, name)
end
end
@@ -595,11 +583,11 @@ function Database:loadCapUnits()
for _, groupName in pairs(groups) do
is_group_taken[groupName] = true
if Util.startsWith(groupName, "CAP_A", true) or Util.startsWith(groupName, "CAP_B", true) then
if Util.startswith(groupName, "CAP_A", true) or Util.startswith(groupName, "CAP_B", true) then
table.insert(baseData.CapGroups, groupName)
elseif Util.startsWith(groupName, "CAP_I", true) then
elseif Util.startswith(groupName, "CAP_I", true) then
table.insert(baseData.InterceptGroups, groupName)
elseif Util.startsWith(groupName, "CAP_S", true) then
elseif Util.startswith(groupName, "CAP_S", true) then
table.insert(baseData.SweepGroups, groupName)
end
end
@@ -622,32 +610,10 @@ function Database:loadBlueSamUnits()
local triggerZone = DcsUtil.getZoneByName(blueSamZone)
if triggerZone then
for _, kvPair in pairs(triggerZone.properties) do
if kvPair.key and Util.startsWith(kvPair.key, "buildable") then
if kvPair.key and Util.startswith(kvPair.key, "buildable") then
local number = tonumber(kvPair.value)
if number and number > 0 then
samData.buildingKilos = number
if env.mission.drawings and env.mission.drawings.layers then
for _, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then
for _, layer_object in pairs(layer.objects) do
local vec2 = { x = layer_object.mapX, y = layer_object.mapY }
if layer_object.primitiveType == "TextBox" and triggerZone and Util.is2dPointInZone(vec2, triggerZone) then
if layer_object.name and Util.startsWith(layer_object.name, "supplybriefing_", true) then
layer_object = layer_object --[[@as TextBox]]
local description = layer_object.text
if description and description ~= "" then
samData.briefing = description
end
end
end
end
end
end
end
else
MissionEditorWarnings.Add("Buildable number for " .. blueSamZone .. " is invalid")
end
end
end
@@ -655,12 +621,12 @@ function Database:loadBlueSamUnits()
end
end
---@return Array<StageDrawing>
function Database:getStageDrawings()
if self._tables.StageDrawings == nil then
---@return Array<CustomDrawing>
function Database:getCustomDrawings()
if self._tables.CustomDrawings == nil then
return {}
end
return self._tables.StageDrawings
return self._tables.CustomDrawings
end
---Loads all units, data and briefings
@@ -679,13 +645,13 @@ function Database:LoadZoneData(missionZoneName)
for _, groupName in pairs(groups) do
if MizGroupsManager.IsGroupStatic(groupName) == true then
local object = StaticObject.getByName(groupName)
if object and object:getCoalition() == coalition.side.RED then
table.insert(self._tables.MissionZoneData[missionZoneName].RedGroups, groupName)
elseif object then
table.insert(self._tables.MissionZoneData[missionZoneName].BlueGroups, groupName)
end
else
local group = Group.getByName(groupName)
if group and group:getCoalition() == coalition.side.RED then
@@ -711,9 +677,9 @@ function Database:LoadZoneData(missionZoneName)
if triggerZone and triggerZone.properties then
for _, kvPair in pairs(triggerZone.properties) do
local key = kvPair.key
if Util.startsWith(key, "dependson", true) == true then
if Util.startswith(key, "dependson", true) == true then
table.insert(self._tables.MissionZoneData[missionZoneName].dependsOn, kvPair.value)
elseif Util.startsWith(key, "completeat") == true then
elseif Util.startswith(key, "completeat") == true then
local value = tonumber(kvPair.value)
if value then
if value > 1 and value <= 100 then
@@ -723,28 +689,19 @@ function Database:LoadZoneData(missionZoneName)
end
self._tables.MissionZoneData[missionZoneName].completeAt = value
end
elseif Util.startsWith(key, "primary", true) == true then
if type(kvPair.value) == "string" then
if kvPair.value == "true" then
self._tables.MissionZoneData[missionZoneName].primaryOverwrite = true
elseif kvPair.value == "false" then
self._tables.MissionZoneData[missionZoneName].primaryOverwrite = false
end
end
end
end
end
---Adds Briefings
if env.mission.drawings and env.mission.drawings.layers then
for _, layer in pairs(env.mission.drawings.layers) do
for i, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then
for _, layer_object in pairs(layer.objects) do
for key, layer_object in pairs(layer.objects) do
local vec2 = { x = layer_object.mapX, y = layer_object.mapY }
if layer_object.primitiveType == "TextBox" and triggerZone and Util.is2dPointInZone(vec2, triggerZone) then
if layer_object.name and Util.startsWith(layer_object.name, "briefing_", true) then
layer_object = layer_object --[[@as TextBox]]
if triggerZone and Util.is2dPointInZone(vec2, triggerZone) then
if layer_object.name and Util.startswith(layer_object.name, "briefing_", true) then
local description = layer_object.text
if description and description ~= "" then
self._tables.MissionZoneData[missionZoneName].description = description
@@ -787,33 +744,10 @@ function Database:loadFarpData()
local triggerZone = DcsUtil.getZoneByName(farpZone)
if triggerZone then
for _, kvPair in pairs(triggerZone.properties) do
if kvPair.key and Util.startsWith(kvPair.key, "buildable", true) == true then
if kvPair.key and Util.startswith(kvPair.key, "buildable", true) == true then
local number = tonumber(kvPair.value)
if number and number > 0 then
farpzoneData.buildingKilos = number
-- check briefings
if env.mission.drawings and env.mission.drawings.layers then
for _, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then
for _, layer_object in pairs(layer.objects) do
local vec2 = { x = layer_object.mapX, y = layer_object.mapY }
if layer_object.primitiveType == "TextBox" and triggerZone and Util.is2dPointInZone(vec2, triggerZone) then
layer_object = layer_object --[[@as TextBox]]
if layer_object.name and Util.startsWith(layer_object.name, "supplybriefing_", true) then
local description = layer_object.text
if description and description ~= "" then
farpzoneData.briefing = description
end
end
end
end
end
end
end
else
MissionEditorWarnings.Add("Buildable number for " .. farpZone .. " is invalid.")
end
end
end
@@ -969,7 +903,6 @@ function Database:getBriefingForStage(stagename)
return stageZone.StageBriefing or ""
end
---@param stagename string
---@return Array<string>
function Database:getMissionsForStage(stagename)
local stageZone = self._tables.StageZones[stagename]
@@ -977,7 +910,6 @@ function Database:getMissionsForStage(stagename)
return stageZone.MissionZones
end
---@param stagename string
---@return Array<string>
function Database:getRandomMissionsForStage(stagename)
local stageZone = self._tables.StageZones[stagename]
@@ -985,14 +917,13 @@ function Database:getRandomMissionsForStage(stagename)
return stageZone.RandomMissionZones
end
---@param missionZoneName string
---@return MissionZoneData?
function Database:getMissionDataForZone(missionZoneName)
return self._tables.MissionZoneData[missionZoneName]
end
---@param stageName string
---@return Array<string> result airbase Names
---@return table result airbase Names
function Database:getAirbaseNamesInStage(stageName)
local stageData = self._tables.StageZones[stageName]
if not stageData then return {} end
@@ -1006,7 +937,6 @@ function Database:getFarpNamesInStage(stageName)
return stageData.FarpZones or {}
end
---@param farpZoneName string
---@return FarpZoneData?
function Database:getFarpDataForZone(farpZoneName)
local farpData = self._tables.FarpZoneData[farpZoneName]
@@ -1022,7 +952,6 @@ function Database:getAirbaseDataForZone(baseName)
return baseData
end
---@param stageName string
---@return string?
function Database:getStageBriefingForStage(stageName)
local stageData = self._tables.StageZones[stageName]
+61 -81
View File
@@ -29,47 +29,49 @@ do
end
---@class OnStageChangedListener
---@field OnStageNumberChanged fun(self:OnStageChangedListener, number:integer, laneIdentifier:string?)
---@field OnStageNumberChanged fun(self:OnStageChangedListener, number:integer)
do -- STAGE NUMBER CHANGED
---@type Array<OnStageChangedListener>
local OnStageNumberChangedListeners = {}
---@type Array<fun(number:integer, laneIdentifier:string?)>
local OnStageNumberChangedHandlers = {}
---Add a stage zone number changed listener
---@param listener OnStageChangedListener object with function OnStageNumberChanged(self, number, stageLaneIdentifier)
---@param listener OnStageChangedListener object with function OnStageNumberChanged(self, number)
SpearheadEvents.AddStageNumberChangedListener = function(listener)
if type(listener) ~= "table" or type(listener.OnStageNumberChanged) ~= "function" then
warn("Event handler not of type table/object with function OnStageNumberChanged(self, number, stageLaneIdentifier)")
return
end
table.insert(OnStageNumberChangedListeners, listener)
end
---@class OnStageNumberChangeCompleteListener
---@field OnStageNumberChangeComplete fun(self:OnStageNumberChangeCompleteListener, number:integer, laneIdentifier:string?)
---Add a stage zone number changed listener
---@param handler function function(number)
SpearheadEvents.AddStageNumberChangedHandler = function(handler)
if type(handler) ~= "function" then
warn("Event handler not of type function, did you mean to use listener?")
return
end
table.insert(OnStageNumberChangedHandlers, handler)
end
---@type Array<OnStageNumberChangeCompleteListener>
---@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, laneIdentifier)")
warn("Event handler not of type table/object with function OnStageNumberChangeComplete(self, number)")
return
end
table.insert(OnStageNumberChangeCompleteListeners, listener)
end
---@param newStageNumber number
---@param laneIdentifier string?
SpearheadEvents.PublishStageNumberChanged = function(newStageNumber, laneIdentifier)
SpearheadEvents.PublishStageNumberChanged = function(newStageNumber)
pcall(function ()
Persistence.SetActiveStage(laneIdentifier, newStageNumber)
Persistence.SetActiveStage(newStageNumber)
end)
for _, callable in pairs(OnStageNumberChangedListeners) do
local _, err = pcall(function()
callable:OnStageNumberChanged(newStageNumber, laneIdentifier)
local succ, err = pcall(function()
callable:OnStageNumberChanged(newStageNumber)
end)
if err then
logError(err)
@@ -77,15 +79,15 @@ do
end
for _, callable in pairs(OnStageNumberChangedHandlers) do
local _, err = pcall(callable, newStageNumber, laneIdentifier)
local succ, err = pcall(callable, newStageNumber)
if err then
logError(err)
end
end
for _, callable in pairs(OnStageNumberChangeCompleteListeners) do
local _, err = pcall(function()
callable:OnStageNumberChangeComplete(newStageNumber, laneIdentifier)
local succ, err = pcall(function()
callable:OnStageNumberChangeComplete(newStageNumber)
end)
if err then
logError(err)
@@ -113,12 +115,9 @@ do
table.insert(onWeaponFiredListeners, weaponFiredListener)
end
---@param unit Unit
---@param weapon Weapon
---@param target Unit?
local triggerWeaponFired = function(unit, weapon, target)
for _, callable in pairs(onWeaponFiredListeners) do
local _, err = pcall(function()
local succ, err = pcall(function()
callable:OnWeaponFired(unit, weapon, target)
end)
@@ -127,15 +126,12 @@ do
end
end
end
---@class OnLandEventListener
---@field OnUnitLanded fun(self:OnLandEventListener, initiatorUnit:Unit, airbase:Airbase)
---@type table<string,Array<OnLandEventListener>>
local onLandEventListeners = {}
---Add an event listener to a specific unit
---@param unitName string to call when the unit lands
---@param landListener OnLandEventListener table with function OnUnitLanded(self, initiatorUnit, airbase)
---@param landListener table table with function OnUnitLanded(self, initiatorUnit, airbase)
SpearheadEvents.addOnUnitLandEventListener = function(unitName, landListener)
if type(landListener) ~= "table" then
warn("Event handler not of type table/object")
@@ -171,11 +167,6 @@ do
end
do -- ON RTB
---@class OnGroupRTBListener
---@field OnGroupRTB fun(self:OnGroupRTBListener, groupName:string)
---@type table<string,Array<OnGroupRTBListener>>
local OnGroupRTBListeners = {}
---Adds a function to the events listener that triggers when a group publishes themselves RTB.
---This is only available when a ROUTE is created via the Spearhead.RouteUtil
@@ -200,7 +191,7 @@ do
if groupName ~= nil then
if OnGroupRTBListeners[groupName] then
for _, callable in pairs(OnGroupRTBListeners[groupName]) do
local _, err = pcall(function()
local succ, err = pcall(function()
callable:OnGroupRTB(groupName)
end)
if err then
@@ -211,15 +202,11 @@ do
end
end
---@class OnGroupRTBInTenListener
---@field OnGroupRTBInTen fun(self:OnGroupRTBInTenListener, groupName:string)
---@type table<string,Array<OnGroupRTBInTenListener>>
local OnGroupRTBInTenListeners = {}
---Adds a function to the events listener that triggers when a group publishes themselves RTB.
---This is only available when a ROUTE is created via the Spearhead.RouteUtil
---@param groupName string the groupname to expect
---@param handlingObject OnGroupRTBInTenListener object with OnGroupRTBInTen(self, groupName)
---@param handlingObject table object with OnGroupRTBInTen(self, groupName)
SpearheadEvents.addOnGroupRTBInTenListener = function(groupName, handlingObject)
if type(handlingObject) ~= "table" then
warn("Event handler not of type table/object")
@@ -239,7 +226,7 @@ do
if groupName ~= nil then
if OnGroupRTBInTenListeners[groupName] then
for _, callable in pairs(OnGroupRTBInTenListeners[groupName]) do
local _, err = pcall(function()
local succ, err = pcall(function()
callable:OnGroupRTBInTen(groupName)
end)
if err then
@@ -252,17 +239,10 @@ do
end
do -- ON Station
---@class OnGroupOnStationListener
---@field OnGroupOnStation fun(self:OnGroupOnStationListener, groupName:string)
---@type table<string,Array<OnGroupOnStationListener>>
local OnGroupOnStationListeners = {}
---Adds a function to the events listener that triggers when a group publishes themselves RTB.
---This is only available when a ROUTE is created via the Spearhead.RouteUtil
---@param groupName string the groupname to expect
---@param handlingObject OnGroupOnStationListener object with OnGroupOnStation(self, groupName)
SpearheadEvents.addOnGroupOnStationListener = function(groupName, handlingObject)
if type(handlingObject) ~= "table" then
warn("Event handler not of type table/object")
@@ -282,7 +262,7 @@ do
if groupName ~= nil then
if OnGroupOnStationListeners[groupName] then
for _, callable in pairs(OnGroupOnStationListeners[groupName]) do
local _, err = pcall(function()
local succ, err = pcall(function()
callable:OnGroupOnStation(groupName)
end)
if err then
@@ -295,18 +275,15 @@ do
end
do -- PLAYER ENTER UNIT
---@class OnPlayerEnterUnitListener
---@field OnPlayerEntersUnit fun(self:OnPlayerEnterUnitListener, unit:Unit)
---@type Array<OnPlayerEnterUnitListener>
local playerEnterUnitListeners = {}
---comment
---@param listener OnPlayerEnterUnitListener object with OnPlayerEntersUnit(self, unit)
---@param listener table object with OnPlayerEntersUnit(self, unit)
SpearheadEvents.AddOnPlayerEnterUnitListener = function(listener)
if type(listener) ~= "table" then
warn("Unit lost Event listener not of type table/object")
return
end
table.insert(playerEnterUnitListeners, listener)
end
@@ -314,7 +291,7 @@ do
if unit ~= nil then
if playerEnterUnitListeners then
for _, callable in pairs(playerEnterUnitListeners) do
local _, err = pcall(function()
local succ, err = pcall(function()
callable:OnPlayerEntersUnit(unit)
end)
if err then
@@ -327,7 +304,7 @@ do
end
do -- Ejection events
local unitEjectListeners = {}
SpearheadEvents.AddOnUnitEjectedListener = function(listener)
if type(listener) ~= "table" then
@@ -343,13 +320,13 @@ do
local e = {}
function e:onEvent(event)
if event.id == world.event.S_EVENT_LAND or event.id == world.event.S_EVENT_RUNWAY_TOUCH then
local unit = event.initiator --[[@as Unit]]
local airbase = event.place --[[@as Airbase]]
local unit = event.initiator
local airbase = event.place
if unit ~= nil then
local name = unit:getName()
if onLandEventListeners[name] then
for _, callable in pairs(onLandEventListeners[name]) do
local _, err = pcall(function()
local succ, err = pcall(function()
callable:OnUnitLanded(unit, airbase)
end)
if err then
@@ -364,15 +341,15 @@ do
event.id == world.event.S_EVENT_CRASH or
event.id == world.event.S_EVENT_EJECTION or
event.id == world.event.S_EVENT_UNIT_LOST then
local object = event.initiator --[[@as Unit]]
local object = event.initiator
if object and object.getName then
logDebug("Receiving death event from: " .. object:getName())
end
if object and object.getName and OnUnitLostListeners[object:getName()] then
for _, callable in pairs(OnUnitLostListeners[object:getName()]) do
local _, err = pcall(function()
local succ, err = pcall(function()
callable:OnUnitLost(object)
end)
@@ -383,11 +360,15 @@ do
end
end
if event.id == world.event.S_EVENT_SHOT then
if event.id == world.event.S_EVENT_EJECTION then
end
local shooter = event.initiator --[[@as Unit]]
local weapon = event.weapon --[[@as Weapon]]
local target = event.target --[[@as Unit?]]
if event.id == world.event.S_EVENT_SHOT then
local shooter = event.initiator
local weapon = event.weapon
local target = event.target
triggerWeaponFired(shooter, weapon, target)
end
@@ -396,32 +377,28 @@ do
Persistence.UpdateNow()
end
---@type table<string, boolean>
local AI_GROUPS = {}
---@param unit Unit
---@param _ any
local function CheckAndTriggerSpawnAsync(unit, _)
---@param checkUnit Unit
---@return boolean, string?
local function isPlayer(checkUnit)
if checkUnit == nil then return false, "unit is nil" end
if checkUnit.getGroup == nil then return false, 'no get group function in unit object, most likely static' end
if Object.getCategory(checkUnit) ~= Object.Category.UNIT then
local function CheckAndTriggerSpawnAsync(unit, time)
local function isPlayer(unit)
if unit == nil then return false, "unit is nil" end
if unit.getGroup == nil then return false, 'no get group function in unit object, most likely static' end
if Object.getCategory(unit) ~= Object.Category.UNIT then
return false, "object is not a unit"
end
if checkUnit:isExist() ~= true then return false, "unit does not exist" end
local group = checkUnit:getGroup()
if unit:isExist() ~= true then return false, "unit does not exist" end
local group = unit:getGroup()
if group ~= nil then
if AI_GROUPS[group:getName()] == true then
return false
end
local players = DcsUtil.getAllPlayerUnits()
local unitName = checkUnit:getName()
for _, playerUnit in pairs(players) do
if playerUnit:getName() == unitName then
local unitName = unit:getName()
for i, unit in pairs(players) do
if unit:getName() == unitName then
return true
end
end
@@ -431,8 +408,11 @@ do
end
if isPlayer(unit) == true then
local groupId = unit:getGroup():getID()
SpearheadEvents.TriggerPlayerEntersUnit(unit)
end
return nil
end
if event.id == world.event.S_EVENT_BIRTH then
+5 -13
View File
@@ -8,7 +8,6 @@ do --setup route util
---@param attackHelos boolean
---@return table
local function GetCAPTargetTypes(attackHelos)
---@type table<number, string>
local targetTypes = {
[1] = "Planes",
}
@@ -54,9 +53,8 @@ do --setup route util
---comment
---@param groupName string
---@param position Vec3
---@param position table { x, y}
---@param altitude number
---@param deviationdistance number
---@param speed number
---@param duration number
---@param engageHelos boolean
@@ -183,7 +181,7 @@ do --setup route util
end
---comment
---@param position Vec3
---@param position table { x, y}
---@param altitude number
---@param speed number
---@param childTasks table
@@ -212,8 +210,7 @@ do --setup route util
---comment
---@param groupName string groupName you're creating this route for
---@param airdromeId number airdromeId
---@param capPoint Vec3
---@param racetrackSecondPoint Vec3?
---@param capPoint table { x, z }
---@param altitude number
---@param speed number
---@param durationOnStation number
@@ -261,8 +258,7 @@ do --setup route util
if racetrackSecondPoint then orbitType = "Race-Track" end
local basePoint = base:getPoint()
---@type Array<table>
local points
local points = {}
if racetrackSecondPoint == nil then
points = {
[1] = FlyToPointTask(capPoint, altitude, speed, additionalFlyOverTasks),
@@ -308,7 +304,6 @@ do --setup route util
end
local group = Group.getByName(groupName)
---@type Vec3?
local pos;
local i = 1
if group == nil then
@@ -398,9 +393,6 @@ do --setup route util
}, ""
end
---@param pointA Vec3
---@param pointB Vec3
---@return table, string?
ROUTE_UTIL.CreateCarrierRacetrack = function(pointA, pointB)
return {
id = "Mission",
@@ -469,7 +461,7 @@ do --setup route util
}
}
}
}, nil
}, ""
end
end
+202 -502
View File
@@ -2,14 +2,23 @@ local Events = require("classes.spearhead_events")
local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil")
local Logger = require("classes.util.Logger")
local PersistenceConfig = require("classes.configuration.PersistenceConfig")
local MissionEditorWarnings = require("classes.util.MissionEditorWarnings")
local ExtraStage = require("classes.stageClasses.Stages.ExtraStage")
local PrimaryStage = require("classes.stageClasses.Stages.PrimaryStage")
local WaitingStage = require("classes.stageClasses.Stages.WaitingStage")
local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper")
local StageRepository = require("classes.stageClasses.StageRepository")
local StageLane = require("classes.stageClasses.StageLane")
local Persistence = require("classes.persistence.Persistence")
local StagesByName = {}
---@type table<string, Array<Stage>>
local StagesByIndex = {}
---@type table<string, Array<Stage>>
local SideStageByIndex = {}
---@type table<string, Array<WaitingStage>>
local WaitingStagesByIndex = {}
local currentStage = -99
@@ -17,41 +26,29 @@ local currentStage = -99
---@field private database Database
---@field private logger Logger
---@field private stageConfig StageConfig
---@field private _stageRepository StageRepository
---@field private _missionCommandsHelper MissionCommandsHelper
local GlobalStageManager = {}
GlobalStageManager.__index = GlobalStageManager
GlobalStageManager.getCurrentStage = function() return currentStage end
---@type GlobalStageManager
local singletonInstance = nil
---comment
---@param database Database
---@param stageConfig StageConfig
---@param logLevel LogLevel
---@param spawnManager SpawnManager
---@return GlobalStageManager
function GlobalStageManager.new(database, stageConfig, logLevel, spawnManager)
if singletonInstance ~= nil then
return singletonInstance
end
---@return nil
function GlobalStageManager.NewAndStart(database, stageConfig, logLevel, spawnManager)
local logger = Logger.new("StageManager", logLevel)
logger:info("Using Stage Log Level: " .. logLevel)
local self = setmetatable({}, GlobalStageManager)
singletonInstance = self
self.database = database
self.stageConfig = stageConfig
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
self._stageRepository = StageRepository.getInstance()
self.logger = logger
if stageConfig.isAutoStages ~= true then
logger:warn(
"Spearhead will not automatically progress stages due to the given settings. If you manually have implemented this, please ignore this message")
logger:warn("Spearhead will not automatically progress stages due to the given settings. If you manually have implemented this, please ignore this message")
end
Events.AddStageNumberChangedListener(self)
@@ -59,281 +56,177 @@ function GlobalStageManager.new(database, stageConfig, logLevel, spawnManager)
for _, stageName in pairs(database:getStagezoneNames()) do
logger:debug("Found stage zone with name: " .. stageName)
local parseResult = self:ParseStageName(stageName)
if parseResult.isValid == false then
logger:warn("Stage zone with name " .. stageName .. " is not valid: " .. parseResult.invalidReason)
else
local initData = {
stageZoneName = parseResult.stageZoneName,
stageNumber = parseResult.orderNumber,
stageDisplayName = parseResult.stageDisplayName,
stageLaneIdentifier = parseResult.stageLaneIdentifier
}
if Util.startswith(stageName, "missionstage", true) then
local valid = true
local split = Util.split_string(stageName, "_")
if Util.tableLength(split) < 2 then
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a order number or valid format")
valid = false
end
if parseResult.stageType == "PrimaryStage" then
local stage = PrimaryStage.New(database, stageConfig, logger, initData, spawnManager)
if Util.tableLength(split) < 3 then
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a stage name")
end
stage:AddStageCompleteListener(self)
self._stageRepository:AddStage(stage)
elseif parseResult.stageType == "ExtraStage" then
local stage = ExtraStage.New(database, stageConfig, logger, initData, spawnManager)
stage:AddStageCompleteListener(self)
self._stageRepository:AddStage(stage)
elseif parseResult.stageType == "WaitingStage" then
local waitingStage = WaitingStage.New(database, stageConfig, logger, initData,
parseResult.waitingStageSeconds, spawnManager)
waitingStage:AddStageCompleteListener(self)
self._stageRepository:AddStage(waitingStage)
local orderNumber = nil
local isSideStage = false
if valid == true then
local orderNumberString = string.lower(split[2])
if Util.startswith(orderNumberString, "x") == true then
isSideStage = true
orderNumberString = string.gsub(orderNumberString, "x", "")
orderNumber = tonumber(orderNumberString)
else
orderNumber = tonumber(split[2])
end
if orderNumber == nil then
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a valid order number : " .. split[2])
valid = false
end
end
local stageDisplayName = split[3]
local stagelogger = Logger.new(stageName, logLevel)
if valid == true and orderNumber then
---@type StageInitData
local initData = {
stageDisplayName = stageDisplayName,
stageNumber = orderNumber,
stageZoneName = stageName,
}
if isSideStage == true then
local stage = ExtraStage.New(database, stageConfig, stagelogger, initData, spawnManager)
stage:AddStageCompleteListener(self)
if SideStageByIndex[tostring(orderNumber)] == nil then SideStageByIndex[tostring(orderNumber)] = {} end
table.insert(SideStageByIndex[tostring(orderNumber)], stage)
else
local stage = PrimaryStage.New(database, stageConfig, stagelogger, initData, spawnManager)
stage:AddStageCompleteListener(self)
if StagesByIndex[tostring(orderNumber)] == nil then StagesByIndex[tostring(orderNumber)] = {} end
table.insert(StagesByIndex[tostring(orderNumber)], stage)
end
end
end
if Util.startswith(stageName, "waitingstage", true) then
local valid = true
local split = Util.split_string(stageName, "_")
if Util.tableLength(split) < 3 then
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a order number or valid format")
valid = false
end
if valid == true then
local stageIndexString = split[2]
local stageIndex = tonumber(stageIndexString)
if not stageIndex then
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a valid order number")
valid = false
end
local waitingSecondsString = split[3]
local waitingSeconds = tonumber(waitingSecondsString)
if not waitingSeconds then
MissionEditorWarnings.Add("Waiting Stage zone with name " .. stageName .. " does not have a valid amount of seconds parameter")
valid = false
end
if valid == true then
local stagelogger = Logger.new(stageName, logLevel)
---@type WaitingStageInitData
local initData = {
stageDisplayName = "Waiting Stage " .. stageIndex,
stageNumber = stageIndex or -99,
stageZoneName = stageName,
waitingSeconds = waitingSeconds --[[@as integer]]
}
local waitingStage = WaitingStage.New(database, stageConfig, stagelogger, initData, spawnManager)
if WaitingStagesByIndex[tostring(stageIndex)] == nil then
WaitingStagesByIndex[tostring(stageIndex)] = {}
end
table.insert(WaitingStagesByIndex[tostring(stageIndex)], waitingStage)
waitingStage:AddStageCompleteListener(self)
end
end
end
end
singletonInstance = self
return self
end
function GlobalStageManager:Start()
self.logger:info("Starting GlobalStageManager")
local startingStage = self.stageConfig.startingStage or 1
local persistenceConfig = PersistenceConfig.new()
if persistenceConfig:isEnabled() == true then
self.logger:info("Persistence is enabled, loading stage state from persistence")
local stageLanes = self._stageRepository:getAllStageLanes()
for _, stageLane in pairs(stageLanes) do
local stageLaneIdentifier = stageLane:GetStageLaneIdentifier()
local persistedStage = Persistence.GetActiveStage(stageLaneIdentifier)
if persistedStage then
self.logger:info("Loaded persisted stage " ..
persistedStage .. " for lane " .. (stageLaneIdentifier or "default"))
stageLane:SetActiveStageIndex(persistedStage)
Events.PublishStageNumberChanged(persistedStage, stageLaneIdentifier)
else
stageLane:SetActiveStageIndex(startingStage)
Events.PublishStageNumberChanged(startingStage, stageLaneIdentifier)
end
end
else
self.logger:info("Persistence is disabled, starting at stage " .. startingStage)
local stageLanes = self._stageRepository:getAllStageLanes()
for _, stageLane in pairs(stageLanes) do
stageLane:SetActiveStageIndex(startingStage)
Events.PublishStageNumberChanged(startingStage, stageLane:GetStageLaneIdentifier())
end
end
end
---@class StageNameParseResult
---@field isValid boolean
---@field orderNumber integer
---@field stageLaneIdentifier string?
---@field stageDisplayName string
---@field stageZoneName string
---@field stageType StageType
---@field invalidReason string?
---@field waitingStageSeconds integer?
---@param self GlobalStageManager
---@param stageName string
---@return StageNameParseResult
function GlobalStageManager:ParseStageName(stageName)
local split = Util.split_string(stageName, "_")
if Util.tableLength(split) < 3 then
return { isValid = false, invalidReason = "Stage zone with name " ..
stageName .. " does not have a order number or valid format" }
end
local typePart = string.lower(split[1])
if typePart == "missionstage" then
local orderNumberString = string.lower(split[2])
---@type StageType
local stageType = "PrimaryStage"
if Util.endsWith(orderNumberString, "x", true) == true then
stageType = "ExtraStage"
orderNumberString = orderNumberString:sub(1, -1)
end
local stageLaneIdentifier = nil
local first = orderNumberString:sub(1, 1)
if tonumber(first) == nil then -- first character is the lane identifier only if it's not a number
stageLaneIdentifier = string.lower(first)
orderNumberString = orderNumberString:sub(2)
end
local orderNumber = tonumber(orderNumberString)
if orderNumber == nil then
return { isValid = false, invalidReason = "Stage zone with name " ..
stageName .. " does not have a valid order number : " .. orderNumberString }
end
local stageDisplayName = split[3]
---@type StageNameParseResult
local result = {
isValid = true,
orderNumber = orderNumber,
stageLaneIdentifier = stageLaneIdentifier,
stageDisplayName = stageDisplayName,
stageZoneName = stageName,
stageType = stageType
}
return result
elseif typePart == "waitingstage" then
local stageType = "WaitingStage"
local orderNumberString = split[2]
local orderNumber = tonumber(orderNumberString)
if orderNumber == nil then
return { isValid = false, invalidReason = "Waiting Stage zone with name " ..
stageName .. " does not have a valid order number : " .. orderNumberString }
end
local waitingSecondsString = split[3]
local waitingSeconds = tonumber(waitingSecondsString)
if waitingSeconds == nil then
return { isValid = false, invalidReason = "Waiting Stage zone with name " ..
stageName .. " does not have a valid amount of seconds parameter : " .. waitingSecondsString }
end
local stageDisplayName = "Waiting Stage " .. orderNumber
---@type StageNameParseResult
local result = {
isValid = true,
orderNumber = orderNumber,
stageLaneIdentifier = nil,
stageDisplayName = stageDisplayName,
stageZoneName = stageName,
stageType = stageType,
waitingStageSeconds = waitingSeconds
}
return result
end
return { isValid = false, invalidReason = "Stage zone with name " ..
stageName .. " has an unrecognized type: " .. typePart }
end
---@param stage Stage
function GlobalStageManager:OnStageComplete(stage)
self.logger:debug("Receiving stage complete event from: " .. stage.zoneName)
local stageIndex = stage:GetStageIndex()
local laneIdentifier = stage:GetStageLaneIdentifier()
local stageLane = self._stageRepository:getStageLane(laneIdentifier)
if not stageLane or not stageLane:IsCurrentStageIndexComplete() then
self.logger:debug("Stage lane " ..
(laneIdentifier or "default") .. " is not complete for stage index " .. stageIndex)
return
local anyIncomplete = false
self.logger:debug("Checking stages for index: " .. tostring(currentStage))
for index, stage in pairs(StagesByIndex[tostring(currentStage)]) do
if stage:IsComplete() == false then
anyIncomplete = true
self.logger:debug("Need to wait for Stage " .. stage.zoneName .. " to be completed")
else
self.logger:debug("Stage verified to be completed: " .. stage.zoneName)
end
end
if anyIncomplete == false and self.stageConfig.isAutoStages == true then
local nextStageIndex = stageIndex + 1
Events.PublishStageNumberChanged(nextStageIndex, laneIdentifier)
stageLane:SetActiveStageIndex(nextStageIndex)
if stageLane:IsDefaultStageLane() then
-- COMPLETION IN THE DEFAULT LANE
-- 1. Check if any lane is at status "BetweenChapters" and the next chapter start is "next stage index"
local allLanes = self._stageRepository:getAllStageLanes()
for _, lane in ipairs(allLanes) do
if lane:GetStageLaneIdentifier() ~= StageLane.DefaultLaneKey and lane:GetStageLaneState() == "BetweenChapters" then
local nextChapterStart = lane:GetNextChapterStart()
if nextChapterStart == nextStageIndex then
self.logger:debug("Lane " ..
(lane:GetStageLaneIdentifier() or "default") ..
" is at chapter start for next stage index " .. nextStageIndex)
Events.PublishStageNumberChanged(nextStageIndex, lane:GetStageLaneIdentifier())
lane:SetActiveStageIndex(nextStageIndex)
-- CHECK WAITING STAGES
local nextStage = currentStage + 1
if WaitingStagesByIndex[tostring(nextStage)] then
for _, waitingStage in pairs(WaitingStagesByIndex[tostring(nextStage)]) do
if waitingStage:IsActive() == false then
waitingStage:ActivateStage()
end
end
end
else
-- COMPLETION IN A SIDE LANE
-- 1. Check if the default lane is at status "BetweenChapters"
local defaultStageLane = self._stageRepository:getStageLane(StageLane.DefaultLaneKey)
if defaultStageLane and defaultStageLane:GetStageLaneState() == "BetweenChapters" then
local nextChapterStart = defaultStageLane:GetNextChapterStart()
if nextChapterStart == nextStageIndex then
-- 2. Check if all side lanes are at or beyond the next chapter starts
local allLanes = self._stageRepository:getAllStageLanes()
local allSideLanesReady = true
for _, lane in ipairs(allLanes) do
if lane:GetStageLaneIdentifier() ~= StageLane.DefaultLaneKey then
-- Check if the lane is at or beyond the next chapter starts when "InChapter" eq "Active"
if lane:GetStageLaneState() == "InChapter" and lane:GetActiveStageIndex() < nextStageIndex then
allSideLanesReady = false
self.logger:debug("Lane " ..
(lane:GetStageLaneIdentifier() or "default") ..
" is not ready for next stage index " .. nextStageIndex)
break
-- When in between chapters, check if the next chapter start is less than the next stage index, meaning there is still stages to be completed before the main lane can be activated again
elseif lane:GetStageLaneState() == "BetweenChapters" then
local nextChapter = lane:GetNextChapterStart()
if nextChapter and nextChapter < nextStageIndex then
allSideLanesReady = false
self.logger:debug("Lane " ..
(lane:GetStageLaneIdentifier() or "default") ..
" is not ready for next stage index " .. nextStageIndex)
break
end
end
end
end
if allSideLanesReady then
self.logger:debug("All side lanes are ready for next stage index " ..
nextStageIndex .. ", activating default lane")
Events.PublishStageNumberChanged(nextStageIndex, nil)
defaultStageLane:SetActiveStageIndex(nextStageIndex)
else
self.logger:debug("Not all side lanes are ready for next stage index " ..
nextStageIndex .. ", default lane will not be activated")
local anyWaiting = false
if WaitingStagesByIndex[tostring(nextStage)] then
for _, waitingStage in pairs(WaitingStagesByIndex[tostring(nextStage)]) do
if waitingStage:IsComplete() == false then
anyWaiting = true
end
end
end
end
end
---@param stageNumber number
---@param stageLaneIdentifier string?
---@return boolean?
function GlobalStageManager:IsStageComplete(stageNumber, stageLaneIdentifier)
local stageLane = self._stageRepository:getStageLane(stageLaneIdentifier)
if not stageLane then
self.logger:warn("Stage lane " .. (stageLaneIdentifier or "default") .. " does not exist")
return nil
if anyWaiting == false then
local newStageNumber = currentStage + 1
self.logger:debug("Setting next stage to: " .. tostring(newStageNumber))
Events.PublishStageNumberChanged(newStageNumber)
end
end
return stageLane:IsStageIndexComplete(stageNumber)
end
---@public
function GlobalStageManager:OnStageNumberChanged(stageNumber, stageLaneIdentifier)
-- only react on "main" lane changes, ignore other lanes for now
if stageLaneIdentifier ~= nil then return end
function GlobalStageManager:OnStageNumberChanged(stageNumber)
self.logger:debug("Stage number changed to: " .. tostring(stageNumber))
currentStage = stageNumber
self:UpdateDrawings(stageNumber)
end
function GlobalStageManager:OnStageNumberChangeComplete(stageNumber, stageLaneIdentifier)
-- only react on "main" lane changes, ignore other lanes for now
if stageLaneIdentifier ~= nil then return end
function GlobalStageManager:OnStageNumberChangeComplete(stageNumber)
self.logger:debug("Stage number change complete to: " .. tostring(stageNumber))
---@type Array<Group>
local groups = {}
for _, player in pairs(DcsUtil.getAllPlayerUnits()) do
local group = player:getGroup()
if group then
table.insert(groups, group)
groups[group:getID()] = group
end
end
@@ -343,276 +236,83 @@ function GlobalStageManager:OnStageNumberChangeComplete(stageNumber, stageLaneId
end
---@private
---@param stageNumber number
---@param stageLaneIdentifier string?
function GlobalStageManager:UpdateDrawings(stageNumber, stageLaneIdentifier)
function GlobalStageManager:UpdateDrawings(stageNumber)
self.logger:debug("Updating custom drawings for stage number: " .. tostring(stageNumber))
local drawings = self.database:getStageDrawings()
local drawings = self.database:getCustomDrawings()
for _, drawing in pairs(drawings) do
local startStage = drawing:GetStartingStage()
local stopStage = drawing:GetRemoveAtStage()
local laneIdentifier = drawing:GetStageLaneIdentifier()
if laneIdentifier == stageLaneIdentifier then
if stageNumber >= startStage and stageNumber < stopStage then
self.logger:debug("Drawing " ..
drawing:GetName() .. " is active for stage number: " .. tostring(stageNumber))
drawing:Draw()
else
self.logger:debug("Drawing " ..
drawing:GetName() .. " is not active for stage number: " .. tostring(stageNumber))
drawing:Remove()
end
local startStage, stopStage = drawing:GetStartAndStop()
if stageNumber >= startStage and stageNumber < stopStage then
self.logger:debug("Drawing " .. drawing:GetName() .. " is active for stage number: " .. tostring(stageNumber))
drawing:Draw()
else
self.logger:debug("Drawing " .. drawing:GetName() .. " is not active for stage number: " .. tostring(stageNumber))
drawing:Remove()
end
end
end
function GlobalStageManager:PrintMermaidStage()
local lanes = self._stageRepository:getAllStageLanes()
---@type table<string, string>
local nodes = {}
---@type table<string, string>
local edges = {}
---@type table<string, Array<number>>
local stageIndicesByLane = {} -- Track all stage indices per lane
local mainLaneId = "default"
GlobalStageManager.printFullOverview = function ()
local logger = Logger.new("StageOverview", "INFO")
logger:info("Stage overview:")
-- Color palette for lanes (26 vibrant colors optimized for dark mode)
local laneColors = {
"#FF6B6B", -- 1: Red
"#4ECDC4", -- 2: Teal
"#45B7D1", -- 3: Blue
"#FFA502", -- 4: Orange
"#95E1D3", -- 5: Mint
"#F38181", -- 6: Coral
"#AA96DA", -- 7: Purple
"#FCBAD3", -- 8: Pink
"#A8E6CF", -- 9: Light green
"#FFD3B6", -- 10: Peach
"#FFAAA5", -- 11: Light red
"#FF8B94", -- 12: Rose
"#FFEAA7", -- 13: Butter
"#DFE6E9", -- 14: Gray
"#00B894", -- 15: Emerald
"#0984E3", -- 16: Cobalt
"#6C5CE7", -- 17: Indigo
"#A29BFE", -- 18: Lavender
"#FD79A8", -- 19: Magenta
"#FDCB6E", -- 20: Gold
"#6C757D", -- 21: Slate
"#20C997", -- 22: Seafoam
"#E74C3C", -- 23: Scarlet
"#3498DB", -- 24: Dodger blue
"#9B59B6", -- 25: Amethyst
"#1ABC9C", -- 26: Turquoise
}
---@type table<string, string>
local laneColorMap = {} -- Map lane ID to color
---@type number
local colorIndex = 1
local max = 0
local lines = {}
for stageIndex, stages in pairs(StagesByIndex) do
local totalStrike = 0
local totalbai = 0
local totaldead = 0
local totalMissions = 0
local totalCas = 0
-- Build nodes and collect all stage indices per lane
for _, lane in ipairs(lanes) do
local laneId = lane:GetStageLaneIdentifier() or "default"
local stageIndices = lane:GetAllStageIndices()
stageIndicesByLane[laneId] = stageIndices
for _, stage in pairs(stages) do
local strike, dead, bai, cas = stage:GetStageStats()
-- Assign color to this lane
laneColorMap[laneId] = laneColors[colorIndex]
colorIndex = colorIndex + 1
if colorIndex > #laneColors then
colorIndex = 1
totalStrike = totalStrike + strike
totalbai = totalbai + bai
totaldead = totaldead + dead
totalCas = totalCas + cas
totalMissions = totalMissions + strike + dead + bai + cas
end
-- Create nodes for each stage index in this lane
for _, stageIndex in ipairs(stageIndices) do
local stages = lane:GetStagesAtIndex(stageIndex)
if stages then
for _, stage in ipairs(stages) do
local nodeId = laneId .. "_" .. stageIndex
local stageType = stage:GetStageType()
local stageName = stage.stageName or stage.zoneName
-- Generate bracket label: [1], [2] for default, [w1], [e2] for other lanes
---@type string
local bracketLabel
if laneId == "default" then
bracketLabel = "[" .. stageIndex .. "]"
else
bracketLabel = "[" .. laneId .. stageIndex .. "]"
end
-- Combined label: [bracket] Name
local label = bracketLabel .. " " .. stageName
-- Different node shapes for different stage types
local nodeShape = "["
local nodeEnd = "]"
if stageType == "ExtraStage" then
nodeShape = "(["
nodeEnd = "])"
elseif stageType == "WaitingStage" then
nodeShape = "[["
nodeEnd = "]]"
end
nodes[nodeId] = string.format(' %s%s"%s"%s',
nodeId, nodeShape, label, nodeEnd)
end
local index = tonumber(stageIndex)
if index then
if index > max then
max = index
end
lines[index] ="Stage# " .. tostring(stageIndex).. " | " .. totalStrike .. " strikes | " .. totaldead .. " dead | " .. totalbai .. " BAI | " .. totalCas .. " CAS | Total:" .. totalMissions
else
logger:warn("Stage index is not a number: " .. stageIndex)
end
end
-- Add edges for sequential stages within the same lane
for laneId, stageIndices in pairs(stageIndicesByLane) do
for i = 1, #stageIndices - 1 do
local fromIdx = stageIndices[i]
local toIdx = stageIndices[i + 1]
local fromNodeId = laneId .. "_" .. fromIdx
local toNodeId = laneId .. "_" .. toIdx
-- Check if this is a chapter boundary (gap)
local isChapter = false
for _, lane in ipairs(lanes) do
local normalizedLaneId = lane:GetStageLaneIdentifier() or "default"
if normalizedLaneId == laneId then
isChapter = lane:IsChapterStart(toIdx)
break
end
end
local label = isChapter and "|chapter|" or ""
table.insert(edges, string.format(' %s -->%s %s', fromNodeId, label, toNodeId))
for i = 1, max do
if lines[i] then
logger:info(lines[i])
end
end
-- Get main lane reference
local mainStageLane = nil
for _, lane in ipairs(lanes) do
if lane:IsDefaultStageLane() then
mainStageLane = lane
break
end
end
-- Add dependencies from DEFAULT lane to SIDE lanes at chapter starts
-- Side lanes activate when main completes the stage BEFORE the chapter start
if mainStageLane then
for _, lane in ipairs(lanes) do
if lane:IsDefaultStageLane() == false then
local sideId = lane:GetStageLaneIdentifier()
local sideIndices = stageIndicesByLane[sideId] or {} --[[@as Array<number>]]
-- For each stage in the side lane
for _, stageIdx in ipairs(sideIndices) do
-- Check if this is a chapter start in the side lane
if lane:IsChapterStart(stageIdx) then
-- Main lane must complete (stageIdx - 1) to activate side lane at stageIdx
local fromNodeId = mainLaneId .. "_" .. (stageIdx - 1)
local toNodeId = sideId .. "_" .. stageIdx
-- Side lane activates when main lane completes the prior stage
table.insert(edges, string.format(' %s -->|unlock| %s', fromNodeId, toNodeId))
end
end
end
end
end
-- Add dependencies from SIDE lanes gating the MAIN lane at chapter boundaries
-- Main cannot advance to the next chapter until ALL side lanes are at or above that chapter index
if mainStageLane then
local mainIndices = stageIndicesByLane[mainLaneId]
-- For each chapter start in main lane (skip first one)
for i = 2, #mainIndices do
local nextIdx = mainIndices[i]
-- Check if nextIdx is a chapter start (there's a gap before it)
if mainStageLane:IsChapterStart(nextIdx) then
-- Before main can progress to nextIdx, all side lanes must be at >= nextIdx
for _, lane in ipairs(lanes) do
if lane:IsDefaultStageLane() == false then
local sideId = lane:GetStageLaneIdentifier()
local sideIndices = stageIndicesByLane[sideId] or {} --[[@as Array<number>]]
-- Find the appropriate gate node for this side lane
-- Use first stage >= nextIdx if it exists, otherwise use the highest stage
local gateStageIdx = nil
for _, idx in ipairs(sideIndices) do
if idx >= nextIdx then
gateStageIdx = idx
break
end
end
-- If no stage >= nextIdx, use the highest stage in this lane
if gateStageIdx == nil and #sideIndices > 0 then
gateStageIdx = sideIndices[#sideIndices]
end
if gateStageIdx then
local fromNodeId = sideId .. "_" .. gateStageIdx
local toNodeId = mainLaneId .. "_" .. nextIdx
table.insert(edges, string.format(' %s -->|gate| %s', fromNodeId, toNodeId))
end
end
end
end
end
end
-- Build complete Mermaid diagram
local diagramLines = { "graph TD" }
-- Add class definitions for each lane with colors and contrasting text
for laneId, color in pairs(laneColorMap) do
table.insert(diagramLines,
string.format(' classDef lane_%s fill:%s,stroke:#333,stroke-width:2px,color:#000', laneId, color))
end
-- Add nodes
for _, nodeStr in pairs(nodes) do
table.insert(diagramLines, nodeStr)
end
-- Add edges
for _, edgeStr in pairs(edges) do
table.insert(diagramLines, edgeStr)
end
-- Apply classes to nodes
for nodeId in pairs(nodes) do
local laneId = nodeId:match("(.+)_[0-9]+$")
if laneId then
table.insert(diagramLines, string.format(' class %s lane_%s', nodeId, laneId))
end
end
-- Print as single multi-line message
local diagram = "========== STAGE FLOW DIAGRAM ==========\n" ..
table.concat(diagramLines, "\n") .. "\n========== END DIAGRAM =========="
self.logger:info(diagram)
end
---comment
---@param stageNumber number
---@param stageLaneIdentifier string? nil for default lan
---@return boolean | nil
GlobalStageManager.isStageComplete = function(stageNumber, stageLaneIdentifier)
if singletonInstance == nil then
Logger.new("StageManager", "INFO"):warn(
"GlobalStageManager.isStageComplete called before GlobalStageManager was initialized. Returning nil")
return nil
GlobalStageManager.isStageComplete = function (stageNumber)
local stageIndex = tostring(stageNumber)
if StagesByIndex[stageIndex] == nil then return nil end
for _, stage in ipairs(StagesByIndex[stageIndex]) do
if stage:IsComplete() == false then
return false
end
end
if stageLaneIdentifier then
stageLaneIdentifier = string.lower(stageLaneIdentifier)
end
return singletonInstance:IsStageComplete(stageNumber, stageLaneIdentifier)
return true
end
return GlobalStageManager
@@ -41,7 +41,7 @@ end
function SpearheadGroup:SpawnCorpsesOnly()
if self._isSpawned == true then return end
self._spawnManager:SpawnCorpsesOnly(self._groupName)
self._isSpawned = true
@@ -57,7 +57,7 @@ function SpearheadGroup:Spawn(lateStart)
uncontrolled = lateStart,
}
local _, isStatic = self._spawnManager:SpawnGroup(self._groupName, overrides, self._isPersistent)
local spawnedObject, isStatic = self._spawnManager:SpawnGroup(self._groupName, overrides, self._isPersistent)
self._isStatic = isStatic
self._isSpawned = true
end
@@ -91,13 +91,13 @@ function SpearheadGroup:GetCoalition()
end
---comment
---@return Array<Unit|StaticObject> result list of objects
---@return table result list of objects
function SpearheadGroup:GetObjects()
---@type Array<Unit|StaticObject>
local result = {}
if self._isStatic == true then
local staticObject = StaticObject.getByName(self._groupName)
if staticObject then
if staticObject then
table.insert(result, staticObject)
end
else
@@ -105,7 +105,7 @@ function SpearheadGroup:GetObjects()
if not group then return {} end
for _, unit in pairs(group:getUnits()) do
table.insert(result, unit)
end
end
end
return result
end
@@ -123,7 +123,7 @@ function SpearheadGroup:GetAsUnits()
if not group then return {} end
for _, unit in pairs(group:getUnits()) do
table.insert(result, unit)
end
end
return result
end
@@ -133,7 +133,7 @@ function SpearheadGroup:GetAllUnitPositions()
local result = {}
if self._isStatic == true then
local staticObject = StaticObject.getByName(self._groupName)
if staticObject then
if staticObject then
table.insert(result, staticObject:getPoint())
end
else
@@ -141,7 +141,7 @@ function SpearheadGroup:GetAllUnitPositions()
if not group then return {} end
for _, unit in pairs(group:getUnits()) do
table.insert(result, unit:getPoint())
end
end
end
return result
end
@@ -50,10 +50,16 @@ function BlueSam.New(database, logger, zoneName, spawnManager)
---@type table<string, Vec3>
local redUnitsPos = {}
local buildable = false
if self._buildableCrateKilos and self._buildableCrateKilos > 0 then
buildable = true
end
for _, groupName in pairs(blueSamData.groups) do
local spearheadGroup = SpearheadGroup.New(groupName, spawnManager, true)
if spearheadGroup then
if spearheadGroup:GetCoalition() == 2 or spearheadGroup:GetCoalition() == 0 then
table.insert(self._blueGroups, spearheadGroup)
end
@@ -72,7 +78,7 @@ function BlueSam.New(database, logger, zoneName, spawnManager)
--Cleanup units
local cleanup_distance = 5
for _, blueUnitPos in pairs(blueUnitsPos) do
for blueUnitName, blueUnitPos in pairs(blueUnitsPos) do
for redUnitName, redUnitPos in pairs(redUnitsPos) do
local distance = Util.VectorDistance3d(blueUnitPos, redUnitPos)
if distance <= cleanup_distance then
@@ -83,7 +89,7 @@ function BlueSam.New(database, logger, zoneName, spawnManager)
local zone = DcsUtil.getZoneByName(zoneName)
if zone then
BuildableZone.New(self, zone, self._buildableCrateKilos or 0, "SAM_CRATE", self._blueGroups, logger, database, blueSamData.briefing)
BuildableZone.New(self, zone, self._buildableCrateKilos or 0, "SAM_CRATE", self._blueGroups, logger, database)
end
return self
@@ -134,9 +140,7 @@ end
function BlueSam:SpawnGroups()
for unitName, needsCleanup in pairs(self._cleanupUnits) do
if needsCleanup then
DcsUtil.DestroyUnit(unitName)
end
DcsUtil.DestroyUnit(unitName)
end
for _, group in pairs(self._blueGroups) do
@@ -42,11 +42,11 @@ function FarpZone.New(database, logger, zoneName, spawnManager)
self._groups = {}
self._padNames = {}
self._supplyHubs = {}
if farpData then
self._padNames = farpData.padNames
for _, supplyHubName in pairs(farpData.supplyHubNames) do
local supplyHub = SupplyHub.new(database, logger, supplyHubName)
if supplyHub then
@@ -55,7 +55,7 @@ function FarpZone.New(database, logger, zoneName, spawnManager)
end
for _, groupName in pairs(farpData.groups) do
for _, groupName in pairs(farpData.groups) do
local group = SpearheadGroup.New(groupName, spawnManager, true)
table.insert(self._groups, group)
group:Destroy()
@@ -64,7 +64,7 @@ function FarpZone.New(database, logger, zoneName, spawnManager)
local zone = DcsUtil.getZoneByName(zoneName)
if zone then
self._logger:debug("Creating Buildable zone: " .. zoneName .. " with " .. (farpData.buildingKilos or "nil") .. " kilos")
BuildableZone.New(self, zone, farpData.buildingKilos or 0, "FARP_CRATE", self._groups, logger, database, farpData.briefing)
BuildableZone.New(self, zone, farpData.buildingKilos or 0, "FARP_CRATE", self._groups, logger, database)
end
end
self:Deactivate()
@@ -90,7 +90,7 @@ function StageBase.New(databaseManager, logger, airbaseName, spawnManager)
local cleanup_distance = 5
for _, blueUnitPos in pairs(blueUnitsPos) do
for blueUnitName, blueUnitPos in pairs(blueUnitsPos) do
for redUnitName, redUnitPos in pairs(redUnitsPos) do
local distance = Util.VectorDistance3d(blueUnitPos, redUnitPos)
if distance <= cleanup_distance then
@@ -113,7 +113,6 @@ end
function StageBase:SpawnRedUnits()
---comment
---@param groups Array<SpearheadGroup>
---@return number|nil
local spawnAsync = function(groups)
for _, group in pairs(groups) do
group:Spawn()
@@ -143,7 +142,6 @@ end
function StageBase:SpawnBlueUnits()
---comment
---@param groups Array<SpearheadGroup>
---@return number|nil
local spawnAsync = function(groups)
for _, group in pairs(groups) do
group:Spawn()
@@ -174,7 +172,7 @@ function StageBase:ActivateBlueStage()
else
self:FinaliseBlueStage()
end
end
function StageBase:FinaliseBlueStage()
@@ -42,7 +42,7 @@ function SupplyHub.new(database, logger, zoneName)
end
self._zone = DcsUtil.getZoneByName(zoneName)
self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate()
self._inZone = {}
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
@@ -20,8 +20,7 @@ BuildableZone.__index = BuildableZone
---@param database Database
---@param crateType SupplyType
---@param logger Logger
---@param briefing string?
function BuildableZone:New(targetZone, kilosRequired, crateType, buildableGroups, logger, database, briefing)
function BuildableZone:New(targetZone, kilosRequired, crateType, buildableGroups, logger, database)
self._targetZone = targetZone
self._requiredKilos = kilosRequired or 0
self._buildableGroups = buildableGroups or {}
@@ -37,7 +36,6 @@ function BuildableZone:New(targetZone, kilosRequired, crateType, buildableGroup
---@param params UnpackCrateParam
---@param time number
---@return number|nil
local startUnpackingCrate = function(params, time)
local unpacked = params.unpackedKilos + (params.kilosPerSecond * 2)
local alreadySpawned = params.unpackedItems / params.groupsPerKilo
@@ -51,7 +49,7 @@ function BuildableZone:New(targetZone, kilosRequired, crateType, buildableGroup
if params.unpackedKilos >= params.kilos or spawned == false then
return
end
return time + 0.5
end
@@ -71,12 +69,14 @@ function BuildableZone:New(targetZone, kilosRequired, crateType, buildableGroup
local noLandingZone = self:GetNoLandingZone()
if kilosRequired and kilosRequired > 0 then
self._buildableMission = BuildableMission.new(database, logger, targetZone, noLandingZone, kilosRequired, crateType, briefing)
self._buildableMission = BuildableMission.new(database, logger, targetZone, noLandingZone, kilosRequired, crateType)
self._buildableMission:AddOnCrateDroppedOfListener(self)
else
self._buildableMission = nil
end
if self._buildableMission == nil then
self._buildableLogger:debug("No buildable mission for zone: " .. targetZone.name)
end
@@ -98,14 +98,25 @@ function BuildableZone:OnBuildingComplete() end
---@field unpackedItems number
---@field unpackedKilos number
---@param _ BuildableMission?
---@param mission BuildableMission?
---@param kilos number
function BuildableZone:OnCrateDroppedOff(_, kilos)
function BuildableZone:OnCrateDroppedOff(mission, kilos)
self._buildableLogger:debug("Crate dropped off in zone: " .. self._targetZone.name)
local timeToUnpack = (kilos / 500) * 15
---@type UnpackCrateParam
local params = {
self = self,
groupsPerKilo = self._groupsPerKilo,
unpackedItems = 0,
kilosPerSecond = kilos/timeToUnpack,
unpackedKilos = 0,
kilos = kilos
}
---@param params UnpackCrateParam
---@param time number
---@return number|nil
local startUnpackingCrate = function(params, time)
local unpacked = params.unpackedKilos + (params.kilosPerSecond * 2)
local alreadySpawned = params.unpackedItems / params.groupsPerKilo
@@ -120,21 +131,10 @@ function BuildableZone:OnCrateDroppedOff(_, kilos)
params.self:FinaliseCrate(params.kilos)
return
end
return time + 2
end
local timeToUnpack = (kilos / 500) * 15
---@type UnpackCrateParam
local params = {
self = self,
groupsPerKilo = self._groupsPerKilo,
unpackedItems = 0,
kilosPerSecond = kilos/timeToUnpack,
unpackedKilos = 0,
kilos = kilos
}
timer.scheduleFunction(startUnpackingCrate, params, timer.getTime() + 2)
end
@@ -189,7 +189,7 @@ function BuildableZone:SpawnAmount(amount)
return nil
end
for _ = 1, amount do
for i = 1, amount do
local spawned = spawnOne()
if spawned ~= true then
self._buildableLogger:debug("No more groups to spawn in zone: " .. self._targetZone.name)
-181
View File
@@ -1,181 +0,0 @@
---@class StageLane
---@field private _isDefaultStageLane boolean
---@field private _stageLaneIdentifier string
---@field private _activeStageIndex number
---@field private _stagesInLaneByIndex table<string, Array<Stage>>
---@field private _chapterStarts table<string, boolean>?
---@field private _stageLaneState StageLaneState
---@field private _maxStageIndex number
local StageLane = {}
StageLane.__index = StageLane
---@alias StageLaneState
---| "InChapter"
---| "BetweenChapters"
---| "Completed"
---@type string?
StageLane.DefaultLaneKey = nil
function StageLane.New(laneIdentifier)
if not laneIdentifier then
laneIdentifier = StageLane.DefaultLaneKey
end
local self = setmetatable({}, { __index = StageLane }) --[[@as StageLane]]
self._isDefaultStageLane = laneIdentifier == StageLane.DefaultLaneKey
self._stageLaneIdentifier = laneIdentifier
self._activeStageIndex = nil
self._stagesInLaneByIndex = {}
self._chapterStarts = nil
self._stageLaneState = "BetweenChapters"
return self
end
---@param stage Stage
function StageLane:AddStage(stage)
local stageIndex = stage:GetStageIndex()
if not self._stagesInLaneByIndex[tostring(stageIndex)] then
self._stagesInLaneByIndex[tostring(stageIndex)] = {}
end
if not self._maxStageIndex or stageIndex > self._maxStageIndex then
self._maxStageIndex = stageIndex
end
table.insert(self._stagesInLaneByIndex[tostring(stageIndex)], stage)
end
---@param stageNumber number
---@return boolean?
function StageLane:IsStageIndexComplete(stageNumber)
local stages = self._stagesInLaneByIndex[tostring(stageNumber)]
if not stages then
return nil
end
for _, stage in ipairs(stages) do
if not stage:IsComplete() then
return false
end
end
return true
end
---@return boolean
function StageLane:IsCurrentStageIndexComplete()
return self:IsStageIndexComplete(self._activeStageIndex) == true
end
---@return string?
function StageLane:GetStageLaneIdentifier()
return self._stageLaneIdentifier
end
---@return boolean
function StageLane:IsDefaultStageLane()
return self._isDefaultStageLane
end
---@return number
function StageLane:GetActiveStageIndex()
return self._activeStageIndex
end
---@param stageNumber number
function StageLane:SetActiveStageIndex(stageNumber)
if self._stagesInLaneByIndex[tostring(stageNumber)] == nil then
self._stageLaneState = "BetweenChapters" -- stage number is not in this lane, so we are between chapters
elseif stageNumber > self._maxStageIndex then
self._stageLaneState = "Completed" -- stage number is beyond the max stage index
else
self._stageLaneState = "InChapter"
end
self._activeStageIndex = stageNumber
end
---@return StageLaneState
function StageLane:GetStageLaneState()
return self._stageLaneState
end
--- Checks if the given stage number is a chapter start. <br>
--- Chapter starts are the first StageIndex after a gap in stage numbers. <br>
--- For example, if the stage numbers are 1, 2, 3, 5, 6, 7, then stage number 5 is a chapter start because there is a gap between 3 and 5. <br>
---@param stageNumber number
---@return boolean
function StageLane:IsChapterStart(stageNumber)
if self._chapterStarts == nil then
self:FillChapterStarts()
end
return self._chapterStarts[tostring(stageNumber)] == true
end
--- fills chapter starts, chapter starts are where there's a gap in between stage numbers.
--- The first stage in a lane is always a chapter start.
---@private
function StageLane:FillChapterStarts()
local previousIndex = nil
---@type table<string, number>
local stageIndices = {}
for stageIndex, _ in pairs(self._stagesInLaneByIndex) do
table.insert(stageIndices, tonumber(stageIndex))
end
table.sort(stageIndices)
if self._chapterStarts == nil then self._chapterStarts = {} end
for _, stageIndex in ipairs(stageIndices) do
if previousIndex == nil then
-- First stage is always a chapter start
self._chapterStarts[tostring(stageIndex)] = true
elseif stageIndex > previousIndex + 1 then
-- Gap before this stage means it's a chapter start
self._chapterStarts[tostring(stageIndex)] = true
else
-- Consecutive stage, not a chapter start
self._chapterStarts[tostring(stageIndex)] = false
end
previousIndex = stageIndex
end
end
---@return Array<number>
function StageLane:GetAllStageIndices()
local indices = {}
for stageIndex, _ in pairs(self._stagesInLaneByIndex) do
table.insert(indices, tonumber(stageIndex))
end
table.sort(indices)
return indices
end
---@param stageIndex number
---@return Array<Stage>?
function StageLane:GetStagesAtIndex(stageIndex)
return self._stagesInLaneByIndex[tostring(stageIndex)]
end
---@return number?
function StageLane:GetNextChapterStart()
if self._chapterStarts == nil then
self:FillChapterStarts()
end
for stageIndex, _ in pairs(self._stagesInLaneByIndex) do
if tonumber(stageIndex) > self._activeStageIndex and self._chapterStarts[tostring(stageIndex)] == true then
return tonumber(stageIndex)
end
end
return nil
end
return StageLane
@@ -1,59 +0,0 @@
local StageLane = require("classes.stageClasses.StageLane")
---@class StageRepository
---@field private StageLanes table<string, StageLane>
local StageRepository = {}
StageRepository.__index = StageRepository
local instance = nil
---@private
---@return StageRepository
function StageRepository.new()
local self = setmetatable({}, StageRepository)
self.StageLanes = {}
self.StageLanes[tostring(StageLane.DefaultLaneKey)] = StageLane.New(StageLane.DefaultLaneKey)
instance = self
return self
end
---@return StageRepository
StageRepository.getInstance = function()
if instance == nil then
instance = StageRepository.new()
end
return instance
end
---@param laneName string? `nil` for default lane
---@return StageLane?
function StageRepository:getStageLane(laneName)
if not laneName then
laneName = StageLane.DefaultLaneKey
end
return self.StageLanes[tostring(laneName)]
end
---@return Array<StageLane>
function StageRepository:getAllStageLanes()
local lanes = {}
for _, lane in pairs(self.StageLanes) do
table.insert(lanes, lane)
end
return lanes
end
---@param stage Stage
function StageRepository:AddStage(stage)
local stageLaneIdentifier = stage:GetStageLaneIdentifier() or StageLane.DefaultLaneKey
if self.StageLanes[tostring(stageLaneIdentifier)] == nil then
self.StageLanes[tostring(stageLaneIdentifier)] = StageLane.New(stageLaneIdentifier)
end
self.StageLanes[tostring(stageLaneIdentifier)]:AddStage(stage)
end
return StageRepository
@@ -13,19 +13,12 @@ local Events = require("classes.spearhead_events")
local GlobalCapManager = require("classes.capClasses.GlobalCapManager")
local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
local StageRepository = require("classes.stageClasses.StageRepository")
local StageState = require("classes.stageClasses.Stages.BaseStage.StageState")
---@alias StageColor
---| "RED"
---| "BLUE"
---| "GRAY"
---@alias StageType
---| "PrimaryStage"
---| "ExtraStage"
---| "WaitingStage"
--- @class StageData
--- @field stageBriefing string?
--- @field missionsByCode table<string, Mission>
@@ -40,11 +33,9 @@ local StageState = require("classes.stageClasses.Stages.BaseStage.StageState")
--- @field supplyHubs Array<SupplyHub>
--- @class StageInitData
--- @field type StageType
--- @field stageZoneName string
--- @field stageNumber integer
--- @field stageDisplayName string
--- @field stageLaneIdentifier string?
--- @class StageCompleteListener
@@ -54,15 +45,13 @@ local StageState = require("classes.stageClasses.Stages.BaseStage.StageState")
--- @field zoneName string
--- @field stageName string?
--- @field stageNumber number
--- @field protected _stageLaneIdentifier string?
--- @field protected _stageType StageType
--- @field protected _currentStageState CurrentStageState
--- @field protected _missionCommandsHelper MissionCommandsHelper
--- @field protected _missionPriority MissionPriority
--- @field protected _database Database
--- @field protected _stageRepository StageRepository
--- @field protected _db StageData
--- @field protected _logger Logger
--- @field protected _activeStage integer
--- @field protected _stageConfig StageConfig
--- @field protected _customDrawing CustomDrawing
--- @field protected _spawnedGroups Array<string>
@@ -74,6 +63,14 @@ local Stage = {}
Stage.__index = Stage
---@enum CurrentStageState
Stage.CurrentStageState = {
INACTIVE = 0,
PREACTIVATED = 1,
ACTIVE = 2,
BLUE = 3
}
Stage.StageColors = {
INVISIBLE = { 0, 0, 0, 0 },
RED_ACTIVE = { 1, 0, 0, 0.20 },
@@ -89,23 +86,16 @@ Stage.StageColors = {
---@param initData StageInitData
---@param missionPriority MissionPriority
---@param spawnManager SpawnManager
---@param stageType StageType
---@return Stage
function Stage:superNew(database, stageConfig, logger, initData, stageType, missionPriority, spawnManager)
function Stage:superNew(database, stageConfig, logger, initData, missionPriority, spawnManager)
logger:debug("[BaseStage] Initiating stage with name: " .. initData.stageZoneName)
self._currentStageState = StageState.Inactive
self._currentStageState = Stage.CurrentStageState.INACTIVE
self.zoneName = initData.stageZoneName
self.stageNumber = initData.stageNumber
self._stageRepository = StageRepository.getInstance()
if initData.stageLaneIdentifier then
self._stageLaneIdentifier = string.lower(initData.stageLaneIdentifier)
else
self._stageLaneIdentifier = nil
end
self.stageName = initData.stageDisplayName
self._stageType = stageType
self._currentStageState = Stage.CurrentStageState.INACTIVE
self.OnPostStageComplete = nil
self.OnPostBlueActivated = nil
@@ -125,6 +115,7 @@ function Stage:superNew(database, stageConfig, logger, initData, stageType, miss
supplyHubs = {}
}
self._activeStage = -99
self._stageConfig = stageConfig or {}
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
@@ -160,26 +151,25 @@ function Stage:superNew(database, stageConfig, logger, initData, stageType, miss
self._logger:info("Initiating new Stage with name: " .. self.zoneName)
---comment
---@param selfA Stage
---@param self Stage
---@param time number?
---@return number|nil
self.CheckContinuousAsync = function (selfA, time)
selfA:CheckAndUpdateSelf()
if selfA:IsComplete() == true then
selfA:NotifyComplete()
self.CheckContinuousAsync = function (self, time)
self:CheckAndUpdateSelf()
if self:IsComplete() == true then
self:NotifyComplete()
return nil
end
return time + 20
end
do -- load tables
local missionZones = database:getMissionsForStage(self.zoneName)
self._logger:debug("Found " .. Util.tableLength(missionZones) .. " mission zones for stage: " .. self.zoneName)
for _, missionZone in pairs(missionZones) do
local mission = ZoneMission.new(missionZone, self._missionPriority, database, logger, self, spawnManager)
if mission then
self._db.missionsByCode[mission.code] = mission
@@ -234,7 +224,7 @@ function Stage:superNew(database, stageConfig, logger, initData, stageType, miss
table.insert(self._db.missions, mission)
end
end
else
else
self._logger:info("Using persisted random mission with name: " .. missionName .. " and zone: " .. missionZonePicked)
for _, mission in pairs(missions) do
if string.lower(mission.zoneName) == string.lower(missionZonePicked) then
@@ -283,61 +273,29 @@ end
---@return boolean
function Stage:IsComplete()
if self._currentStageState >= StageState.Blue then return true end
if self._currentStageState == Stage.CurrentStageState.BLUE then return true end
for _, mission in pairs(self._db.sams) do
for i, mission in pairs(self._db.sams) do
local state = mission:getState()
if state == "ACTIVE" or state == "NEW" or state =="WAITING" then
return false
end
end
for _, mission in pairs(self._db.missions) do
for i, mission in pairs(self._db.missions) do
local state = mission:getState()
if state == "ACTIVE" or state == "NEW" then
return false
end
end
self._currentStageState = Stage.CurrentStageState.BLUE
return true
end
---@return boolean
function Stage:IsActive()
return self._currentStageState == StageState.Activated
end
---@return StageType
function Stage:GetStageType()
return self._stageType
end
---@return string?
function Stage:GetStageLaneIdentifier()
return self._stageLaneIdentifier
end
---@return number
function Stage:GetStageIndex()
return self.stageNumber
end
---@return string
function Stage:GetStageName()
return self.stageName
end
---@return Array<Mission>
function Stage:GetMissions()
---@type Array<Mission>
local missions = {}
for _, mission in pairs(self._db.missions) do
table.insert(missions, mission)
end
for _, sam in pairs(self._db.sams) do
table.insert(missions, sam)
end
return missions
return self._currentStageState == Stage.CurrentStageState.ACTIVE
end
---comment
@@ -373,9 +331,9 @@ function Stage:CheckAndUpdateSelf()
local availableMissionsCount = Util.tableLength(getAvailableMissions())
local activeCount = getActiveMissionsCount()
if activeCount < max and availableMissionsCount > 0 then
for _ = activeCount+1, max do
for i = activeCount+1, max do
if availableMissionsCount == 0 then
break
i = max+1 --exits this loop
else
local mission = Util.randomFromList(getAvailableMissions()) --[[@as Mission]]
if mission then
@@ -400,7 +358,7 @@ function Stage:IsMissionComplete(missionName)
return mission:getState() == "COMPLETED"
end
-- private usage only
---private use only
function Stage:NotifyComplete()
self._logger:info("Stage complete: " .. (self.stageName or self.stageNumber or "unknown"))
@@ -423,28 +381,19 @@ end
---Activates all SAMS, Airbase units etc all at once.
function Stage:PreActivate()
if self._currentStageState >= StageState.PreActivated then
return
end
self._currentStageState = StageState.PreActivated
for _, mission in pairs(self._db.sams) do
if mission then
mission:SpawnInactive()
if self._currentStageState == Stage.CurrentStageState.INACTIVE then
self._currentStageState = Stage.CurrentStageState.PREACTIVATED
for key, mission in pairs(self._db.sams) do
if mission then
mission:SpawnInactive()
end
end
end
for _, mission in pairs(self._db.missions) do
if mission and mission.missionType == "DEEPSTRIKE" then
mission:SpawnInactive() --Not Really "inactive"
for _, airbase in pairs(self._db.airbases) do
airbase:ActivateRedStage()
end
end
for _, airbase in pairs(self._db.airbases) do
airbase:ActivateRedStage()
end
self:MarkStage()
end
@@ -458,22 +407,22 @@ function Stage:MarkStage()
end
if self._stageConfig.isDrawStagesEnabled == false then return end
if self._stageConfig.isDrawPreActivatedEnabled == false and self._currentStageState == StageState.PreActivated then
if self._stageConfig.isDrawPreActivatedEnabled == false and self._currentStageState == Stage.CurrentStageState.PREACTIVATED then
return
end
if self._customDrawing then
self._customDrawing:UpdateDrawingObject(function(drawingObject)
local drawing = drawingObject --[[@as Polygon]]
if self._currentStageState == StageState.Activated then
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 == StageState.PreActivated then
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 == StageState.Blue then
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"
@@ -482,7 +431,7 @@ function Stage:MarkStage()
drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.INVISIBLE)
drawing.style = "no line"
end
return drawing
end)
self._customDrawing:Draw()
@@ -491,13 +440,10 @@ function Stage:MarkStage()
end
function Stage:ActivateStage()
if self._currentStageState >= StageState.Activated then
return
end
self._currentStageState = Stage.CurrentStageState.ACTIVE
self:PreActivate()
self._currentStageState = StageState.Activated
pcall(function()
self:MarkStage()
end)
@@ -525,10 +471,6 @@ function Stage:ActivateStage()
end
end
if self._db and self._db.stageBriefing then
self._missionCommandsHelper:AddStageBriefing(self.zoneName, self._db.stageBriefing)
end
timer.scheduleFunction(self.CheckContinuousAsync, self, timer.getTime() + 3)
end
@@ -541,68 +483,52 @@ end
---comment
---@param self Stage
---@param number integer
---@param stageLaneIdentifier string?
function Stage:OnStageNumberChanged(number, stageLaneIdentifier)
function Stage:OnStageNumberChanged(number)
---@return boolean
local needsPreActivation = function()
if self._stageLaneIdentifier == stageLaneIdentifier then
if self.stageNumber <= number + self._stageConfig.AmountPreactivateStage then
return true
end
elseif self._stageLaneIdentifier == nil then
--main lane
--Check if this stage is a chapter stage
if self._stageRepository:getStageLane(self._stageLaneIdentifier):IsChapterStart(self.stageNumber)
and self.stageNumber < number + self._stageConfig.AmountPreactivateStage
then
return true
end
if GlobalCapManager.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then
return true
end
end
return false
if self._activeStage == number then --only activate once for a stage
return
end
---@return boolean
local needsActivation = function()
if self._stageLaneIdentifier == stageLaneIdentifier and self.stageNumber == number then
return true
end
return false
end
local previousActive = self._activeStage
self._activeStage = number
---@return boolean
local needsBlueActivation = function()
if self._stageLaneIdentifier == stageLaneIdentifier and self.stageNumber < number then
return true
end
return false
end
if needsPreActivation() == true then
if self.stageNumber - self._activeStage == self._stageConfig.AmountPreactivateStage then
self._logger:debug("Pre-activating stage: " .. self.zoneName .. " with number: " .. number)
self:PreActivate()
elseif GlobalCapManager.IsCapActiveWhenZoneIsActive(self.zoneName, number) == true then
self:PreActivate()
end
if needsActivation() == true then
if number == self.stageNumber then
self:ActivateStage()
if self._db and self._db.stageBriefing then
self._missionCommandsHelper:AddStageBriefing(self.zoneName, self._db.stageBriefing)
end
end
if needsBlueActivation() == true then
self:ActivateBlueStage()
if previousActive <= self.stageNumber then
if number > self.stageNumber then
self:ActivateBlueStage()
end
end
if number > self.stageNumber then
self._missionCommandsHelper:RemoveStageBriefing(self.zoneName)
end
end
function Stage:GetBriefing()
return "Briefing For "
end
---@param self Stage
---@param _ Mission
Stage.OnMissionComplete = function(self, _)
---@param mission Mission
Stage.OnMissionComplete = function(self, mission)
self:CheckAndUpdateSelf()
end
---private use only
function Stage:ActivateBlueGroups()
@@ -654,7 +580,7 @@ function Stage:GetStageStats()
end
end
for _, _ in pairs(self._db.sams) do
for _, mission in pairs(self._db.sams) do
dead = dead + 1
end
@@ -664,12 +590,8 @@ end
function Stage:ActivateBlueStage()
if self._currentStageState >= StageState.Blue then
return
end
self._logger:debug("Setting stage '" .. Util.toString(self.zoneName) .. "' to blue")
self._currentStageState = StageState.Blue
self._currentStageState = Stage.CurrentStageState.BLUE
for _, mission in pairs(self._db.missions) do
mission:SpawnPersistedState()
end
@@ -682,20 +604,17 @@ function Stage:ActivateBlueStage()
miscGroup:Spawn()
end
---@param selfA Stage
---@return number|nil
local ActivateBlueAsync = function(selfA)
---@param self Stage
local ActivateBlueAsync = function(self)
pcall(function()
selfA:MarkStage()
self:MarkStage()
end)
selfA:ActivateBlueGroups()
self:ActivateBlueGroups()
return nil
end
self._missionCommandsHelper:RemoveStageBriefing(self.zoneName)
timer.scheduleFunction(ActivateBlueAsync, self, timer.getTime() + 3)
end
@@ -1,12 +0,0 @@
--- StageState enumeration for the current stage state.
--- Chronological order, meaning that higher values should represent later stages in the sequence.
---@enum CurrentStageState
local StageState = {
Inactive = 0,
PreActivated = 1,
Activated = 2,
Blue = 3
}
return StageState
+6 -10
View File
@@ -1,7 +1,6 @@
local Stage = require("classes.stageClasses.Stages.BaseStage.Stage")
local GlobalCapManager = require("classes.capClasses.GlobalCapManager")
local StageState = require("classes.stageClasses.Stages.BaseStage.StageState")
---@class ExtraStage : Stage
local ExtraStage = {}
@@ -20,12 +19,13 @@ function ExtraStage.New(database, stageConfig, logger, initData, spawnManager)
setmetatable(ExtraStage, Stage)
local self = setmetatable({}, { __index = ExtraStage }) --[[@as ExtraStage]]
self:superNew(database, stageConfig, logger, initData, "ExtraStage", "secondary", spawnManager)
self:superNew(database, stageConfig, logger, initData, "secondary", spawnManager)
self.OnPostBlueActivated = function (selfStage)
selfStage:MarkStage()
end
self.OnPostStageComplete = function (selfStage)
selfStage:ActivateBlueStage()
end
@@ -36,17 +36,13 @@ end
---comment
---@param self ExtraStage
---@param number integer
---@param stageLaneIdentifier string?
function ExtraStage:OnStageNumberChanged(number, stageLaneIdentifier)
if stageLaneIdentifier ~= self._stageLaneIdentifier then
return
end
function ExtraStage:OnStageNumberChanged(number)
if self._activeStage == number then --only activate once for a stage
return
end
local previousActive = self._activeStage
self._activeStage = number
if self.stageNumber - self._activeStage == self._stageConfig.AmountPreactivateStage then
@@ -60,7 +56,7 @@ function ExtraStage:OnStageNumberChanged(number, stageLaneIdentifier)
self:ActivateStage()
end
if self._currentStageState == StageState.Blue then
if self._currentStageState == Stage.CurrentStageState.BLUE then
self:ActivateBlueStage()
end
@@ -18,7 +18,7 @@ function PrimaryStage.New(database, stageConfig, logger, initData, spawnManager)
setmetatable(PrimaryStage, Stage)
local self = setmetatable({}, { __index = PrimaryStage }) --[[@as PrimaryStage]]
self:superNew(database, stageConfig, logger, initData, "PrimaryStage", "primary", spawnManager)
self:superNew(database, stageConfig, logger, initData, "primary", spawnManager)
return self
end
@@ -4,28 +4,32 @@ local Stage = require("classes.stageClasses.Stages.BaseStage.Stage")
---@field private _waitTimeSeconds integer
---@field private _startTime number
local WaitingStage = {}
WaitingStage.__index = WaitingStage
---@class WaitingStageInitData : StageInitData
---@field waitingSeconds integer
local WaitingStageInitData = {}
---comment
---@param database Database
---@param stageConfig StageConfig
---@param logger any
---@param initData StageInitData
---@param initData WaitingStageInitData
---@param spawnManager SpawnManager
---@param waitingSeconds integer
---@return WaitingStage
function WaitingStage.New(database, stageConfig, logger, initData, waitingSeconds, spawnManager)
function WaitingStage.New(database, stageConfig, logger, initData, spawnManager)
setmetatable(WaitingStage, Stage)
local self = setmetatable({}, { __index = WaitingStage }) --[[@as WaitingStage]]
self:superNew(database, stageConfig, logger, initData, "WaitingStage", "none", spawnManager)
self:superNew(database, stageConfig, logger, initData, "none", spawnManager)
self._waitTimeSeconds = 5
if waitingSeconds and waitingSeconds > 5 then self._waitTimeSeconds = waitingSeconds end
if initData.waitingSeconds and initData.waitingSeconds > 5 then self._waitTimeSeconds = initData.waitingSeconds end
self._startTime = nil
self.CheckContinuousAsync = function (selfA, time)
if selfA:IsComplete() == true then
selfA:NotifyComplete()
return nil
@@ -47,7 +51,7 @@ function WaitingStage:ActivateStage()
timer.scheduleFunction(self.CheckContinuousAsync, self, self._startTime + self._waitTimeSeconds)
end
function WaitingStage:IsComplete()
function WaitingStage:IsComplete()
if timer.getTime() > (self._startTime + self._waitTimeSeconds) then return true end
return false
end
@@ -56,12 +60,12 @@ function WaitingStage:OnStageNumberChanged()
self._logger:debug("Waiting Stage OnStageNumberChanged override")
end
function WaitingStage:MarkStage(_)
function WaitingStage:MarkStage(stageColor)
self._logger:debug("Waiting Stage MarkStage override")
end
function WaitingStage:GetExpectedTime()
return self._startTime + self._waitTimeSeconds
return self._startTime + self._waitTimeSeconds
end
return WaitingStage
@@ -1,56 +1,38 @@
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
local Util = require("classes.util.Util")
local Logger = require("classes.util.Logger")
local drawingLogger = Logger.new("CustomDrawing")
---@class CustomDrawing
---@field protected _IDs Array<integer>?
---@field protected _name string?
---@field protected _drawingObject DrawingObject
---@field private _id integer?
---@field private _drawingObject DrawingObject
---@field private _startingStage number
---@field private _removeAtStage number
---@field private _logger Logger
local CustomDrawing = {}
CustomDrawing.__index = CustomDrawing
---@protected
---@param drawingObject DrawingObject
---@return CustomDrawing?
---@return CustomDrawing
function CustomDrawing.New(drawingObject)
local self = setmetatable({}, CustomDrawing)
self._drawingObject = drawingObject
self._IDs = {} -- initiate IDs as an empty array, when drawn it will be set to the IDs returned by DrawingHelper.Draw
self._name = drawingObject.name
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 split = Util.split_string(name or "", "_")
local secondPart = split[2] or "1"
local splitPart = Util.split_string(secondPart, ":")
self._startingStage = tonumber(splitPart[1]) or 1
self._removeAtStage = tonumber(splitPart[2]) or math.huge
return self
end
---@param points Array<Vec2>
---@param colorString string
---@param lineStyle LineType
---@param lineThickness number
---@return CustomDrawing?
function CustomDrawing.FromPoints(points, colorString, lineStyle, lineThickness)
if points == nil or #points < 3 then
drawingLogger:warn("CustomDrawing.FromPoints called with nil or less than 3 points")
return nil
end
---@type FreeLine
local drawingObject = {
mapX = 0,
mapY = 0,
points = points,
lineMode = "free",
name = "custom_drawing_" .. tostring(math.random(1000000)),
primitiveType = "Polygon",
polygonMode = "free",
visible = true,
style = DrawingHelper.ToLineStyleString(lineStyle),
colorString = colorString,
thickness = lineThickness,
closed = false
}
return CustomDrawing.New(drawingObject)
end
---@param zone SpearheadTriggerZone
---@param colorString string
---@param fillColorString string
@@ -83,6 +65,7 @@ function CustomDrawing.FromZone(zone, colorString, fillColorString, lineStyle, l
end
if zone.zone_type == "Polygon" then
---@type Free
local drawingObject = {
mapX = 0,
@@ -97,7 +80,7 @@ function CustomDrawing.FromZone(zone, colorString, fillColorString, lineStyle, l
fillColorString = fillColorString,
thickness = lineThickness,
}
return CustomDrawing.New(drawingObject)
end
end
@@ -107,25 +90,27 @@ function CustomDrawing:GetName()
return self._drawingObject.name
end
function CustomDrawing:Draw()
drawingLogger:debug("Drawing custom drawing with IDs " .. table.concat(self._IDs, ", "))
---@return number start
---@return number stop
function CustomDrawing:GetStartAndStop()
return self._startingStage, self._removeAtStage
end
if self._IDs ~= nil then
for _, id in ipairs(self._IDs) do
DrawingHelper.Remove(id)
end
function CustomDrawing:Draw()
self._logger:debug("Drawing custom drawing with ID " .. tostring(self._id))
if self._id ~= nil then
DrawingHelper.Remove(self._id)
end
self._IDs = DrawingHelper.Draw(self._drawingObject)
self._id = DrawingHelper.Draw(self._drawingObject, self._logger)
end
function CustomDrawing:Remove()
if self._IDs ~= nil then
for _, id in ipairs(self._IDs) do
drawingLogger:debug("Removing custom drawing with ID " .. tostring(id))
DrawingHelper.Remove(id)
end
self._IDs = {}
if self._id ~= nil then
self._logger:debug("Removing custom drawing with ID " .. tostring(self._id))
DrawingHelper.Remove(self._id)
self._id = nil
end
end
@@ -134,4 +119,4 @@ function CustomDrawing:UpdateDrawingObject(updateFunc)
self._drawingObject = updateFunc(self._drawingObject)
end
return CustomDrawing
return CustomDrawing
@@ -1,93 +0,0 @@
local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
local Util = require("classes.util.Util")
local MissionEditorWarnings = require("classes.util.MissionEditorWarnings")
---@class StageDrawing : CustomDrawing
---@field private _startingStage number
---@field private _removeAtStage number
---@field private _stageLaneIdentifier string?
local StageDrawing = {}
StageDrawing.__index = StageDrawing
setmetatable(StageDrawing, CustomDrawing)
---@param drawingObject DrawingObject
---@return StageDrawing?
function StageDrawing.New(drawingObject)
local super = CustomDrawing.New(drawingObject)
if not super then
return nil
end
local self = setmetatable(super, StageDrawing) --[[@as StageDrawing]]
local split = Util.split_string(self._name or "", "_")
local secondPart = split[2] or "1"
local splitPart = Util.split_string(secondPart, ":")
if tonumber(splitPart[1]) == nil then
local laneIdentifier = string.sub(splitPart[1], 1, 1)
if tonumber(laneIdentifier) == nil then
self._stageLaneIdentifier = laneIdentifier
end
local startNumber = string.sub(splitPart[1], 2)
local startNumberVal = tonumber(startNumber)
if startNumberVal == nil then
MissionEditorWarnings.Add("Start number for " ..
self._name .. " is not a valid number: " .. tostring(startNumber))
return nil
end
self._startingStage = startNumberVal
else
local startNumber = tonumber(splitPart[1])
if startNumber == nil then
MissionEditorWarnings.Add("Start number for " ..
self._name .. " is not a valid number: " .. tostring(splitPart[1]))
return nil
end
self._startingStage = startNumber
end
if tonumber(splitPart[2]) == nil then
local laneIdentifier = string.sub(splitPart[2], 1, 1)
if tonumber(laneIdentifier) == nil then
if self._stageLaneIdentifier and self._stageLaneIdentifier ~= laneIdentifier then
MissionEditorWarnings.Add("Lane identifiers do not match for " ..
self._name ..
": " ..
self._stageLaneIdentifier ..
" vs " .. laneIdentifier .. ". Will only use stage " .. self._stageLaneIdentifier)
end
end
local stopNumber = string.sub(splitPart[2], 2)
local stopNumberVal = tonumber(stopNumber)
if stopNumberVal == nil then
MissionEditorWarnings.Add("Stop number for " ..
self._name .. " is not a valid number: " .. tostring(stopNumber))
return nil
end
self._removeAtStage = stopNumberVal
else
local stopNumber = tonumber(splitPart[2])
if stopNumber == nil then
MissionEditorWarnings.Add("Stop number for " ..
self._name .. " is not a valid number: " .. tostring(splitPart[2]))
return nil
end
self._removeAtStage = stopNumber
end
return self
end
function StageDrawing:GetStageLaneIdentifier()
return self._stageLaneIdentifier
end
function StageDrawing:GetStartingStage()
return self._startingStage
end
function StageDrawing:GetRemoveAtStage()
return self._removeAtStage
end
return StageDrawing
@@ -1,44 +1,57 @@
local DcsUtil = require("classes.util.DcsUtil")
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.New():isDebugEnabled() then
if GlobalConfig:isDebugEnabled() then
level = "DEBUG"
end
local logger = Logger.new("DrawingHelper", level)
local drawingLogger = Logger.new("DrawingHelper", level)
---@class DrawingHelper
local DrawingHelper = {}
DrawingHelper.__index = DrawingHelper
local customDrawingIdIncrementer = 4210
---@param object DrawingObject
---@return Array<integer> id
function DrawingHelper.Draw(object)
---@param logger Logger?
---@return integer? id
function DrawingHelper.Draw(object, logger)
if object == nil then
if logger then
logger:warn("DrawingHelper.Draw called with nil object")
end
return {}
return nil
end
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
return DrawingHelper.DrawPolygon(object--[[@as Polygon]])
DrawingHelper.DrawPolygon(object--[[@as Polygon]], id, logger)
elseif(object.primitiveType == "Line") then
return DrawingHelper.DrawLine(object--[[@as Line]])
DrawingHelper.DrawLine(object--[[@as Line]], id, logger)
elseif(object.primitiveType == "TextBox") then
return DrawingHelper.DrawTextBox(object--[[@as TextBox]])
DrawingHelper.DrawTextBox(object--[[@as TextBox]], id)
else
logger:warn("Unknown primitive type: " .. tostring(object.primitiveType))
if logger then
logger:warn("Unknown primitive type: " .. tostring(object.primitiveType))
end
end
return {}
return id
end
function DrawingHelper.GetAndAddId()
return DcsUtil.GetNextDrawID()
customDrawingIdIncrementer = customDrawingIdIncrementer + 1
return customDrawingIdIncrementer
end
---@private
@@ -49,12 +62,13 @@ end
---@param lineColor table
---@param lineStyle LineType
---@param lineThickness number
local function MarkupToAll(shapeID, drawID, points, fillColor, lineColor, lineStyle, lineThickness)
---@param logger Logger?
local function MarkupToAll(shapeID, drawID, points, fillColor, lineColor, lineStyle, lineThickness, logger)
if lineThickness == nil or lineThickness <= 0 then
lineStyle = 0
end
---@type string
local functionString = "trigger.action.markupToAll(" .. shapeID .. ", -1, " .. drawID .. ","
for _, point in pairs(points) do
@@ -62,34 +76,37 @@ local function MarkupToAll(shapeID, drawID, points, fillColor, lineColor, lineS
end
functionString = functionString .. "{0,1,0,1}, {0,1,0,1}, " .. lineStyle .. ")"
logger:debug("Drawing complex drawing with ID " .. tostring(drawID) .. " and function string: " .. functionString)
if logger then
logger:debug("Drawing complex drawing with ID " .. tostring(drawID) .. " and function string: " .. functionString)
end
---@diagnostic disable-next-line: deprecated
local f, err = loadstring(functionString)
if f then
f()
else
logger:error("Something failed when drawing complex drawing" .. err)
env.error("Something failed when drawing complex drawing" .. err)
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
if fillColor then
trigger.action.setMarkupColorFill(drawID, fillColor)
end
trigger.action.setMarkupColorFill(drawID, fillColor)
trigger.action.setMarkupColor(drawID, lineColor)
trigger.action.setMarkupTypeLine(drawID, lineStyle)
end
---@private
---@param object Polygon
---@return Array<integer>
function DrawingHelper.DrawPolygon(object)
---@param id integer
---@param logger Logger?
function DrawingHelper.DrawPolygon(object, id, logger)
if object == nil then
return {}
return
end
local id = DrawingHelper.GetAndAddId()
---@param circle Circle
local function DrawCircle(circle)
local vec3 = { x = circle.mapX, y = 0, z = circle.mapY }
@@ -98,15 +115,15 @@ function DrawingHelper.DrawPolygon(object)
local style = DrawingHelper.ToLineStyleInteger(circle.style)
trigger.action.circleToAll(-1, id, vec3, circle.radius, colorString, fillColor, style, true)
end
---@param oval Oval
---@return integer?, Array<CustomDrawing>?
local function DrawOval(oval)
---@param logger Logger?
local function DrawOval(oval, logger)
---@type Array<Vec3>
local points = {}
local pointsNo = 30
local angleStep = (2 * math.pi) / pointsNo
local fillColor = DrawingHelper.ColorToColorTable(oval.fillColorString)
local color = DrawingHelper.ColorToColorTable(oval.colorString)
local lineStyle = DrawingHelper.ToLineStyleInteger(oval.style)
@@ -117,22 +134,23 @@ function DrawingHelper.DrawPolygon(object)
local y = oval.mapY + (oval.r2 * math.sin(angle))
table.insert(points, { x = x, y = 0, z = y } )
end
MarkupToAll(7, id, points, fillColor, color, lineStyle, oval.thickness)
MarkupToAll(7, id, points, fillColor, color, lineStyle, oval.thickness, logger)
end
---@param free Free
local function DrawFree(free)
---@param logger Logger?
local function DrawFree(free, logger)
local fillColor = DrawingHelper.ColorToColorTable(free.fillColorString)
local color = DrawingHelper.ColorToColorTable(free.colorString)
local lineStyle = DrawingHelper.ToLineStyleInteger(free.style)
---@type Array<number>
local keys = {}
for k, _ in pairs(free.points) do
table.insert(keys, k)
end
table.sort(keys, function(a, b) return a < b end)
---@type Array<Vec3>
local points = {}
for _, k in ipairs(keys) do
local point = free.points[k]
@@ -145,7 +163,7 @@ function DrawingHelper.DrawPolygon(object)
end
end
MarkupToAll(7, id, points, fillColor, color, lineStyle, free.thickness)
MarkupToAll(7, id, points, fillColor, color, lineStyle, free.thickness, logger)
end
---@param rect Rect
@@ -157,16 +175,19 @@ function DrawingHelper.DrawPolygon(object)
local pointA = { x = rect.mapX, y = 0, z = rect.mapY }
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)
end
---@param arrow Arrow
local function DrawArrow(arrow)
---@param logger Logger?
local function DrawArrow(arrow, logger)
local fillColor = DrawingHelper.ColorToColorTable(arrow.fillColorString)
local color = DrawingHelper.ColorToColorTable(arrow.colorString)
local lineStyle = DrawingHelper.ToLineStyleInteger(arrow.style)
logger:debug("Drawing arrow with start point: " .. tostring(arrow.mapX) .. ", " .. tostring(arrow.mapY) .. " and angle: " .. tostring(arrow.angle) .. " and length: " .. tostring(arrow.length))
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)
@@ -182,56 +203,44 @@ function DrawingHelper.DrawPolygon(object)
if object.polygonMode == "circle" then
DrawCircle(object--[[@as Circle]])
elseif object.polygonMode == "oval" then
DrawOval(object--[[@as Oval]])
DrawOval(object--[[@as Oval]], logger)
elseif object.polygonMode == "free" then
DrawFree(object--[[@as Free]])
DrawFree(object--[[@as Free]], logger)
elseif object.polygonMode == "rect" then
DrawRect(object--[[@as Rect]])
elseif object.polygonMode == "arrow" then
DrawArrow(object--[[@as Arrow]])
DrawArrow(object--[[@as Arrow]], logger)
end
return {id}
end
---@private
---@param object Line
---@return Array<integer>
function DrawingHelper.DrawLine(object)
---@param id integer
---@param logger Logger?
function DrawingHelper.DrawLine(object, id, logger)
---@type Array<Vec3>
local points = {}
local ids = {}
for _, point in ipairs(object.points) do
table.insert(points, { x = object.mapX + point.x, y = 0, z = object.mapY + point.y } )
table.insert(points, { x = point.x, y = 0, z = point.y } )
end
local color = DrawingHelper.ColorToColorTable(object.colorString)
local lineStyle = DrawingHelper.ToLineStyleInteger(object.style)
for i = 1, #points - 1 do
local id = DrawingHelper.GetAndAddId()
trigger.action.lineToAll(-1, id, points[i], points[i + 1], color, lineStyle, true)
table.insert(ids, id)
end
return ids
MarkupToAll(1, id, points, color, color, lineStyle, object.thickness, logger)
end
---@private
---@param object TextBox
---@return Array<integer>
function DrawingHelper.DrawTextBox(object)
local id = DrawingHelper.GetAndAddId()
---@param id integer
function DrawingHelper.DrawTextBox(object, id)
trigger.action.textToAll(-1, id, { x= object.mapX, y = 0, z = object.mapY },
DrawingHelper.ColorToColorTable(object.colorString),
DrawingHelper.ColorToColorTable(object.fillColorString),
object.fontSize or 12,
true,
object.text or "")
return {id}
end
function DrawingHelper.Remove(id)
@@ -243,7 +252,7 @@ end
function DrawingHelper.ColorToColorTable(hexStr)
if hexStr == nil then
logger:warn("ColorToColorTable called with nil hexStr, returning default color {0, 0, 0, 0}")
drawingLogger:warn("ColorToColorTable called with nil hexStr, returning default color {0, 0, 0, 0}")
return { 0, 0, 0, 0 }
end
@@ -257,14 +266,14 @@ function DrawingHelper.ColorToColorTable(hexStr)
return { r, g , b , a }
end
---@param rgba table { r, g, b, a }
---@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
logger:warn("ColorTableToColorString called with invalid rgba table, returning default color string '0x00000000'")
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))
@@ -274,7 +283,6 @@ function DrawingHelper.ColorTableToColorString(rgba)
end
---@param lineStyle string
---@return integer
function DrawingHelper.ToLineStyleInteger(lineStyle)
lineStyle = lineStyle:lower()
if lineStyle == "no line" then
@@ -285,11 +293,11 @@ function DrawingHelper.ToLineStyleInteger(lineStyle)
return 2
elseif lineStyle == "dotted" then
return 3
elseif lineStyle == "dotdash" then
elseif lineStyle == "dot dash" then
return 4
elseif lineStyle == "longdash" then
elseif lineStyle == "long dash" then
return 5
elseif lineStyle == "twodash" then
elseif lineStyle == "two dash" then
return 6
else
return 0
@@ -306,11 +314,11 @@ function DrawingHelper.ToLineStyleString(lineStyle)
elseif lineStyle == 3 then
return "dotted"
elseif lineStyle == 4 then
return "dotdash"
return "dot dash"
elseif lineStyle == 5 then
return "longdash"
return "long dash"
elseif lineStyle == 6 then
return "twodash"
return "two dash"
else
return "no line"
end
@@ -1,5 +1,6 @@
local Logger = require("classes.util.Logger")
local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil")
---@class BattleManager
---@field private _name string
@@ -12,6 +13,8 @@ local Util = require("classes.util.Util")
local BattleManager = {}
BattleManager.__index = BattleManager
local debugDrawing = false
---@param redGroups Array<SpearheadGroup>
---@param blueGroups Array<SpearheadGroup>
---@param name string
@@ -27,8 +30,8 @@ function BattleManager.New(redGroups, blueGroups, name, logLevel)
self._redGroups = redGroups
self._blueGroups = blueGroups
self._logger:debug("BattleManager created with name: " .. self._name
.. ", red groups: " .. #self._redGroups
self._logger:debug("BattleManager created with name: " .. self._name
.. ", red groups: " .. #self._redGroups
.. ", blue groups: " .. #self._blueGroups)
return self
@@ -36,7 +39,6 @@ end
---@param self BattleManager
---@param time number
---@return number?
local function CheckTask(self, time)
local interval = self:Update()
if not interval then return end
@@ -88,6 +90,8 @@ function BattleManager:Update()
self._logger:debug("BattleManager Update called for " .. self._name)
local shootChance = 1 -- Adjust this value to control the shooting probability (0.0 to 1.0)
self:LetUnitsShoot(self._redGroups, self._blueGroups)
self:LetUnitsShoot(self._blueGroups, self._redGroups)
@@ -132,7 +136,7 @@ function BattleManager:LetUnitsShoot(groups, targetGroups)
expendQtyEnabled = true
}
}
local controller = unit:getController()
if controller then
controller:setTask(shootTask)
@@ -142,14 +146,14 @@ function BattleManager:LetUnitsShoot(groups, targetGroups)
end
end
end
end
end
---@param unit Unit
---@return number
---@return number
function BattleManager:getBestAmmo(unit)
local ammo = unit:getAmmo() --[[@as table<number, table>]]
local ammo = unit:getAmmo()
if not ammo then return 3221225470, 1 end -- Default ammo if no ammo is found
@@ -163,9 +167,9 @@ function BattleManager:getBestAmmo(unit)
end
end
local entry = Util.randomFromList(shells) --[[@as table]]
local entry = Util.randomFromList(shells)
if entry and entry.desc and entry.desc.warhead then
local caliber = entry.desc.warhead.caliber --[[@as number]]
local caliber = entry.desc.warhead.caliber
if caliber > 50 then
return 258503344128, 1
else
@@ -192,7 +196,7 @@ function BattleManager:IsUnitApplicable(unit)
end
return true
end
---@private
@@ -210,7 +214,6 @@ function BattleManager:ToShootingHulls(groups)
end
local hulls = Util.getSeparatedConvexHulls(points, 50)
---@type Array<Array<Vec2>>
local enlargedHulls = {}
for _, hull in pairs(hulls) do
local enlarged = Util.enlargeConvexHull(hull, 25)
@@ -233,7 +236,7 @@ end
---@param groupHulls Array<Array<Vec2>>
---@return Vec2?
function BattleManager:GetRandomPoint(origin, groupHulls)
local hull = Util.randomFromList(groupHulls) --[[@as Array<Vec2>]]
if not hull then return nil end
local shootPoints = Util.GetTangentHullPointsFromOrigin(hull, origin)
@@ -7,7 +7,7 @@ local SupplyConfigHelper = require("classes.stageClasses.helpers.SupplyConfigHel
local StageConfig = require("classes.configuration.StageConfig")
---@class MissionCommandsHelper : OnPlayerEnterUnitListener
---@class MissionCommandsHelper
---@field missionsByCode table<string, Mission> @table of missions by their code
---@field enabledByCode table<string, boolean> @table of enabled missions by their code
---@field updateNeeded boolean @flag to indicate if an update is needed
@@ -31,6 +31,8 @@ local function sortMissions(list, groupPos)
end)
end
local id = 0
local instance = nil
---@return MissionCommandsHelper
@@ -55,16 +57,16 @@ function MissionCommandsHelper.getOrCreate()
instance._supplyUnitsTracker:AddOnSupplyUnitEventListener(
{
enteredSupplyHub = function(_, unit)
enteredSupplyHub = function(self, unit)
if unit == nil then return end
instance.updateNeeded = true
instance:updateCommandsForGroup(unit:getGroup():getID())
end,
exitedSupplyHub = function(_, unit)
exitedSupplyHub = function(self, unit)
instance.updateNeeded = true
instance:updateCommandsForGroup(unit:getGroup():getID())
end,
supplyUnitSpawned = function(_, unit)
supplyUnitSpawned = function(self, unit)
instance.updateNeeded = true
instance:updateCommandsForGroup(unit:getGroup():getID())
end
@@ -210,7 +212,7 @@ function MissionCommandsHelper:OverviewToGroup(groupID)
for code, enabled in pairs(self.enabledByCode) do
if enabled == true then
local mission = self.missionsByCode[code]
if mission and mission:getState() == "ACTIVE" and mission:GetPriority() == "primary" then
if mission and mission:getState() == "ACTIVE" and mission.priority == "primary" then
table.insert(primaryMissions, mission)
end
end
@@ -230,7 +232,7 @@ function MissionCommandsHelper:OverviewToGroup(groupID)
for code, enabled in pairs(self.enabledByCode) do
if enabled == true then
local mission = self.missionsByCode[code]
if mission and mission:getState() == "ACTIVE" and mission:GetPriority() == "secondary" then
if mission and mission:getState() == "ACTIVE" and mission.priority == "secondary" then
table.insert(secondaryMissions, mission)
end
end
@@ -345,7 +347,7 @@ function MissionCommandsHelper:AddAllMissionCommandsToGroup(groupID)
for code, enabled in pairs(self.enabledByCode) do
if enabled == true then
local mission = self.missionsByCode[code]
if mission and mission:GetPriority() == "primary" then
if mission and mission.priority == "primary" then
table.insert(primaryMissions, mission)
end
end
@@ -369,12 +371,12 @@ function MissionCommandsHelper:AddAllMissionCommandsToGroup(groupID)
do --- secondary missions
local count = 0
local path = { [1] = folderNames.secondary }
---@type Array<Mission>
local secondaryMissions = {}
for code, enabled in pairs(self.enabledByCode) do
if enabled == true then
local mission = self.missionsByCode[code]
if mission and mission:GetPriority() == "secondary" then
if mission and mission.priority == "secondary" then
table.insert(secondaryMissions, mission)
end
end
@@ -416,7 +418,7 @@ function MissionCommandsHelper:addMissionCommands(groupId, path, mission)
end
local missionFolderName = "[" ..
mission.code .. "]" .. distance .. mission.name .. "(" .. mission.missionTypeDisplay .. ")"
mission.code .. "]" .. distance .. mission.name .. "( " .. mission.missionTypeDisplay .. " )"
missionCommands.addSubMenuForGroup(groupId, missionFolderName, path)
table.insert(path, missionFolderName)
@@ -507,6 +509,7 @@ function MissionCommandsHelper:AddCargoCommands(groupID)
local unloadCargoCommand = function(params)
local unitID = params.unitID
local crateType = params.crateType
local supplyUnitsTracker = params.supplyUnitsTracker
params.supplyUnitsTracker:UnloadRequested(unitID, crateType, params.commandHelper)
end
@@ -515,7 +518,7 @@ function MissionCommandsHelper:AddCargoCommands(groupID)
for cargoType, amount in pairs(cargo) do
local cargoConfig = SupplyConfigHelper.getSupplyConfig(cargoType)
if cargoConfig then
for _ = 1, amount do
for i = 1, amount do
local path = { [1] = folderNames.cargo }
---@type UnloadCargoCommandParams
local params = { unitID = unit:getID(), crateType = cargoType, supplyUnitsTracker = self
@@ -76,15 +76,14 @@ local SupplyConfigHelper = {}
---@return SupplyConfig?
function SupplyConfigHelper.fromObjectName(name)
for configName, config in pairs(SupplyConfig) do
if Util.startsWith(name, configName, true) == true then
if Util.startswith(name, configName, true) == true then
return config
end
end
return nil
end
---@param type CrateType
---@return SupplyConfig?
---@param type CrateType
function SupplyConfigHelper.getSupplyConfig(type)
return SupplyConfig[type]
end
@@ -29,7 +29,7 @@ local SupplyLoadConfig = {
{ 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 = {
@@ -10,7 +10,7 @@ local SupplyLoadConfig = require("classes.stageClasses.helpers.SupplyLoadConfig"
---@field enteredSupplyHub fun(self:SupplyUnitEventListener, unit:Unit, hub:SupplyHub) | nil
---@field exitedSupplyHub fun(self:SupplyUnitEventListener, unit:Unit, hub:SupplyHub) | nil
---@class SupplyUnitsTracker : OnPlayerEnterUnitListener
---@class SupplyUnitsTracker
---@field private _supplyUnitsByName table<string, Unit>
---@field private _cargoInUnits table<string, table<CrateType, number>>
---@field private _logger Logger
@@ -43,8 +43,6 @@ function SupplyUnitsTracker.getOrCreate()
SpearheadEvents.AddOnPlayerEnterUnitListener(singleton)
---@param selfA SupplyUnitsTracker
---@param time number
---@return number?
local function updateTask(selfA, time)
selfA:Update()
@@ -136,7 +134,6 @@ end
---@private
---@param unit Unit
---@return boolean
function SupplyUnitsTracker:IsSupplyUnit(unit)
if unit == nil then return false end
@@ -161,17 +158,15 @@ function SupplyUnitsTracker:AddCargoToUnit(unitID, crateType)
local unit = DcsUtil.GetPlayerUnitByID(unitID)
if unit == nil then return end
local unitIdStr = tostring(unitID)
if self._cargoInUnits[unitIdStr] == nil then
self._cargoInUnits[unitIdStr] = {}
if self._cargoInUnits[unitID] == nil then
self._cargoInUnits[unitID] = {}
end
if self._cargoInUnits[unitIdStr][crateType] == nil then
self._cargoInUnits[unitIdStr][crateType] = 0
if self._cargoInUnits[unitID][crateType] == nil then
self._cargoInUnits[unitID][crateType] = 0
end
self._cargoInUnits[unitIdStr][crateType] = self._cargoInUnits[unitIdStr][crateType] + 1
self._cargoInUnits[unitID][crateType] = self._cargoInUnits[unitID][crateType] + 1
end
@@ -190,7 +185,7 @@ function SupplyUnitsTracker:RemoveCargoFromUnit(unitID, crateType)
self._cargoInUnits[unitIDStr][crateType] = self._cargoInUnits[unitIDStr][crateType] - 1
local hasCargo = false
for _, count in pairs(self._cargoInUnits[unitIDStr]) do
for type, count in pairs(self._cargoInUnits[unitIDStr]) do
if count > 0 then
hasCargo = true
break
@@ -221,11 +216,13 @@ end
function SupplyUnitsTracker:CheckUnitsInZones()
for _, unit in pairs(self._supplyUnitsByName) do
for name, unit in pairs(self._supplyUnitsByName) do
if unit ~= nil and unit:isExist() == true then
self._logger:debug("Checking unit: " .. unit:getName())
local pos = unit:getPoint()
local group = unit:getGroup()
for hub, enabled in pairs(self._registeredHubs) do
if enabled == true then
local zone = hub:GetZone()
@@ -241,7 +238,7 @@ function SupplyUnitsTracker:CheckUnitsInZones()
end)
end
end
else
if self._unitInSupplyHub[tostring(unit:getID())] == true then
self._unitInSupplyHub[tostring(unit:getID())] = false
@@ -258,7 +255,7 @@ function SupplyUnitsTracker:CheckUnitsInZones()
end
end
self._unitPositions[tostring(unit:getID())] = pos
self._unitPositions[unit:getID()] = pos
end
end
end
@@ -295,11 +292,11 @@ local cargoCount = 0
---@param crateType CrateType
---@param missionCommandsHelper MissionCommandsHelper
function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHelper)
self._logger:debug("Unload requested for unit: " .. unitID .. " crateType: " .. crateType)
local unit = DcsUtil.GetPlayerUnitByID(unitID)
if unit == nil or unit:isExist() == false then
if unit == nil or unit:isExist() == false then
self._logger:warn("Unload requested for non-existent unit: " .. unitID)
return
end
@@ -308,7 +305,7 @@ function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHe
self._logger:warn("Unload requested for unit with no group: " .. unit:getName())
return
end
local cargoConfig = SupplyConfigHelper.getSupplyConfig(crateType)
if cargoConfig == nil then
@@ -328,7 +325,6 @@ function SupplyUnitsTracker:UnloadRequested(unitID, crateType, missionCommandsHe
self:UpdateWeightForUnit(unit)
cargoCount = cargoCount + 1
---@type {name: string, type: string, x: number, y: number}
local cargoSpawnObject = {
name = crateType .. "_" .. cargoCount,
type = cargoConfig.staticType,
@@ -366,11 +362,11 @@ function SupplyUnitsTracker:UnitRequestCrateLoading(groupID, crateType, missionC
end
local unit = group:getUnit(1)
if unit == nil then return end
if unit:isExist() == false then return end
if unit:inAir() == true then
trigger.action.outTextForUnit(unit:getID(), "Land first before crates can be loaded", 10)
return
@@ -387,7 +383,7 @@ function SupplyUnitsTracker:UnitRequestCrateLoading(groupID, crateType, missionC
---@param params LoadCargoParams
local LoadCrateTask = function(params)
local loaded = params.self:TryLoadCrateInUnit(params.unit, params.crateType, params.commandHelper)
if loaded ~= false then
trigger.action.outTextForUnit(unit:getID(), "Loaded crate :" .. params.crateType, 10)
@@ -414,7 +410,7 @@ end
---@param commandHelper MissionCommandsHelper
---@return boolean
function SupplyUnitsTracker:TryLoadCrateInUnit(unit, crateType, commandHelper)
local crateConfigA = SupplyConfigHelper.getSupplyConfig(crateType)
if crateConfigA == nil then
trigger.action.outTextForUnit(unit:getID(), "Invalid crate type: " .. crateType, 5)
@@ -448,7 +444,7 @@ function SupplyUnitsTracker:TryLoadCrateInUnit(unit, crateType, commandHelper)
if group == nil then return false end
local groupID = group:getID()
commandHelper:updateCommandsForGroup(groupID)
return true
end
@@ -466,7 +462,7 @@ function SupplyUnitsTracker:UnitRequestCrateSpawn(groupID, crateType)
return
end
end
end
@@ -488,11 +484,11 @@ function SupplyUnitsTracker:GetBoundingBoxes(foundObject)
if desc == nil or desc.box == nil then
return nil
end
local objPos = foundObject:getPoint()
local box = desc.box --[[@type table]]
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()
@@ -501,13 +497,13 @@ function SupplyUnitsTracker:GetBoundingBoxes(foundObject)
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 --[[@as number]]
local maxX = box.max.x --[[@as number]]
local minZ = box.min.z --[[@as number]]
local maxZ = box.max.z --[[@as number]]
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
@@ -517,21 +513,21 @@ function SupplyUnitsTracker:GetBoundingBoxes(foundObject)
{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) --[[@as number]]
local rotZ = corner[1] * math.sin(heading) + corner[2] * math.cos(heading) --[[@as number]]
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 {
@@ -555,7 +551,7 @@ end
---@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,
@@ -567,7 +563,7 @@ function SupplyUnitsTracker:CheckBBoxCollision(crateBBox, objBBox, 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
@@ -581,7 +577,7 @@ end
function SupplyUnitsTracker:GetCargoPlacePosition(unit, crateTypeName)
local unitPos = unit:getPosition()
-- Get unit's heading from the forward vector (x component)
-- Heading is calculated as: atan2(forward.z, forward.x)
local unitHeading = math.atan2(unitPos.x.z, unitPos.x.x)
@@ -592,14 +588,14 @@ function SupplyUnitsTracker:GetCargoPlacePosition(unit, crateTypeName)
self._logger:error("Could not get bbox for crate type: " .. crateTypeName)
return nil
end
local crateRelativeBBox = crateDesc.box --[[@as table]]
local crateRelativeBBox = crateDesc.box
-- Get drop zone config for this unit
local dropZones = {
{ centerAngle = 180, angleWidth = 60, minRadius = 15, maxRadius = 50, spacing = 5 }
}
if SupplyLoadConfig[unit:getTypeName()] ~= nil then
dropZones = SupplyLoadConfig[unit:getTypeName()].dropZones
end
@@ -616,12 +612,9 @@ function SupplyUnitsTracker:GetCargoPlacePosition(unit, crateTypeName)
radius = 100 -- Search a large area
}
}
---@type Array<{ pos: { x: number, y: number, z: number }, bbox: { min: { x: number, y: number, z: number }, max: { x: number, y: number, z: number } } }>
local occupiedObjects = {}
---@param foundItem Object
---@param _ any
local found = function(foundItem, _)
local occupiedObjects = {}
local found = function(foundItem, val)
local bbox = self:GetBoundingBoxes(foundItem)
if bbox then
self._logger:debug("Found object: " .. foundItem:getTypeName() .. " at (" .. foundItem:getPoint().x .. ", " .. foundItem:getPoint().z .. ")")
@@ -655,21 +648,20 @@ function SupplyUnitsTracker:GetCargoPlacePosition(unit, crateTypeName)
for distance = zone.minRadius, zone.maxRadius, zone.spacing do
-- Check multiple positions within the angular slice
local angleStep = math.min(15, zone.angleWidth / 3) -- Divide slice into sections
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
---@type { min: { x: number, y: number, z: number }, max: { x: number, y: number, z: number } }
local crateBBoxWorldSpace = {
min = {
x = candidateX + crateRelativeBBox.min.x,
@@ -21,20 +21,9 @@ local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelpe
---@field private _dropOffZone SpearheadTriggerZone?
---@field private _noLandingZoneDrawing CustomDrawing?
---@field private _dropOffZoneDrawing CustomDrawing?
---@field private _briefing string?
local BuildableMission = {}
BuildableMission.__index = BuildableMission
---@param siteType string
---@param coords string
---@return string
local function getDefaultBriefing(siteType, coords)
return "We've dispatched forward units to find a proper spot for a new " .. siteType .. "." ..
"\nYou will need to drop off supplies so they can start building." ..
"\nThe coords are: " .. coords ..
"\n\n"
end
---@class OnCrateDroppedListener
---@field OnCrateDroppedOff fun(self:OnCrateDroppedListener, mission:BuildableMission, kilos:number)
@@ -44,19 +33,16 @@ end
---@param requiredCrateType SupplyType
---@param noLandingZone SpearheadTriggerZone?
---@param logger Logger
---@param briefing string?
---@return BuildableMission
function BuildableMission.new(database, logger, targetZone, noLandingZone, requiredKilos, requiredCrateType, briefing)
function BuildableMission.new(database, logger, targetZone, noLandingZone, requiredKilos, requiredCrateType)
setmetatable(BuildableMission, Mission)
local self = setmetatable({}, { __index = BuildableMission })
self._targetZone = targetZone
self._database = database
self._requiredKilos = requiredKilos
self._droppedKilos = 0
self._briefing = briefing
self._noLandingZone = noLandingZone
@@ -78,12 +64,13 @@ function BuildableMission.new(database, logger, targetZone, noLandingZone, requi
end
self.code = tostring(database:GetNewMissionCode())
self.name = "Resupply"
local splitTargetZoneName = Util.split_string(targetZone.name, "_")
if splitTargetZoneName and splitTargetZoneName[3] and splitTargetZoneName[3] ~= "" then
self.name = splitTargetZoneName[3]
else
self.name = "Resupply"
local type = "site"
if requiredCrateType == "SAM_CRATE" then
type = "SAM site"
elseif requiredCrateType == "FARP_CRATE" then
type = "FARP"
end
self.zoneName = targetZone.name .. "_supply"
@@ -94,6 +81,7 @@ function BuildableMission.new(database, logger, targetZone, noLandingZone, requi
self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate()
self._state = "NEW"
self.location = targetZone.location
self.missionType = "LOGISTICS"
@@ -120,7 +108,6 @@ function BuildableMission:ShowBriefing(groupID)
local unitType = DcsUtil.getUnitTypeFromGroup(group)
local coords = DcsUtil.convertVec2ToUnitUsableType(self.location, unitType)
if coords == nil then coords = "Could not make conversion" end
local siteType = "FARP"
if self._crateType == "SAM_CRATE" then
@@ -129,16 +116,11 @@ function BuildableMission:ShowBriefing(groupID)
siteType = "airbase"
end
local briefingPart = self._briefing
if briefingPart then
briefingPart = Util.replaceString(briefingPart, "{{coords}}", coords)
else
briefingPart = getDefaultBriefing(siteType, coords)
end
local briefing = "Mission [" .. self.code .. "] " .. self.name ..
local briefing = "Mission [" .. self.code .. "] " .. self.name ..
"\n \n" ..
briefingPart ..
"We've dispatched forward units to find a proper spot for a new " .. siteType .. "." ..
"\nYou will need to drop off supplies so they can start building." ..
"\nThe coords are: " .. coords ..
"\n\n" ..
"\nKilos still required: " .. self._requiredKilos - self._droppedKilos ..
"\n\n" ..
@@ -147,20 +129,17 @@ function BuildableMission:ShowBriefing(groupID)
trigger.action.outTextForGroup(groupID, briefing, GlobalConfig:getBriefingTime())
end
---@param groupID number
function BuildableMission:MarkMissionAreaToGroup(groupID)
local groupIdStr = tostring(groupID)
if self._markIDsPerGroup[groupIdStr] then
DcsUtil.RemoveMark(self._markIDsPerGroup[groupIdStr])
if self._markIDsPerGroup[groupID] then
DcsUtil.RemoveMark(self._markIDsPerGroup[groupID])
end
---@type string
local text = "[" .. self.code .. "] " .. self.name .. " | " .. self._crateType
local location = { x= self.location.x, y=land.getHeight(self.location), z=self.location.y }
local markID = DcsUtil.AddMarkToGroup(groupID, text, location)
self._markIDsPerGroup[groupIdStr] = markID
self._markIDsPerGroup[groupID] = markID
end
---@private
@@ -201,10 +180,9 @@ function BuildableMission:SpawnActive()
local fillColor2 = DrawingHelper.ColorTableToColorString({ 0, 0, 1, 0})
self._dropOffZoneDrawing = CustomDrawing.FromZone(self._dropOffZone, lineColor2, fillColor2, 2, 6)
self._dropOffZoneDrawing:Draw()
---@param selfA BuildableMission
---@param time number
---@return number?
local checkForCrateTasks = function (selfA, time)
selfA:CheckCratesInZone()
@@ -259,7 +237,7 @@ function BuildableMission:CheckCratesInZone()
local crates = self._supplyUnitsTracker:GetCargoCratesDropped()
for _, staticObject in pairs(crates) do
if staticObject and staticObject:isExist() and Util.startsWith(staticObject:getName(), self._crateType, true) then
if staticObject and staticObject:isExist() and Util.startswith(staticObject:getName(), self._crateType, true) then
local pos = staticObject:getPoint()
if Util.is3dPointInZone(pos, self._dropOffZone) then
@@ -267,8 +245,8 @@ function BuildableMission:CheckCratesInZone()
end
end
end
for _, foundCrate in pairs(foundCrates) do
for _, foundCrate in pairs(foundCrates) do
local crateConfig = SupplyConfigHelper.fromObjectName(foundCrate:getName())
if crateConfig then
self._droppedKilos = self._droppedKilos + crateConfig.weight
@@ -283,7 +261,7 @@ function BuildableMission:CheckCratesInZone()
self:NotifyMissionComplete()
self._state = "COMPLETED"
end
if self._state == "COMPLETED" then
for groupID, markID in pairs(self._markIDsPerGroup) do
if markID then
@@ -18,13 +18,12 @@ local RunwayStrikeMission = {}
---@field corners Array<Vec2>
---@field kilosHit number
---@field repairGroups Array<string>
---@field drawID Array<number>
---@field drawID number?
---@param runway Runway
---@param database Database
---@param logger Logger
---@param airbaseName string
---@param runwayBombingTracker RunwayBombingTracker
---@return RunwayStrikeMission?
function RunwayStrikeMission.new(runway, airbaseName, database, logger, runwayBombingTracker)
@@ -43,7 +42,7 @@ function RunwayStrikeMission.new(runway, airbaseName, database, logger, runwayBo
self._runwayZone = self:RunwayToSpearheadZone(runway)
self._repairInProgress = false
self._minKilosForDamage = 100
local sections = self:ToSections(runway, 5)
--[[
+-----------+-----------+-----------+-----------+-----------+
@@ -85,6 +84,7 @@ function RunwayStrikeMission:RunwayHit(impactPoint, explosiveMass)
self._logger:debug("Runway hit: " .. self._airportName .. ":" .. self._runway.Name)
for _, section in pairs(self._runwaySections) do
local zone = self:SectionToSpearheadZone(section)
if Util.is3dPointInZone({ x = impactPoint.x, z = impactPoint.y, y = 0 }, zone) then
if section.kilosHit == nil then
@@ -96,7 +96,7 @@ function RunwayStrikeMission:RunwayHit(impactPoint, explosiveMass)
end
---@param selfA RunwayStrikeMission
local updateState = function(selfA, _)
local updateState = function(selfA, time)
selfA:UpdateState()
end
@@ -154,7 +154,7 @@ function RunwayStrikeMission:Draw()
if runwaySection.drawID == nil then
local zone = self:SectionToSpearheadZone(runwaySection)
---@type Free
local drawObject = {
primitiveType = "Polygon",
@@ -172,10 +172,8 @@ function RunwayStrikeMission:Draw()
runwaySection.drawID = DrawingHelper.Draw(drawObject)
else
for _, drawID in pairs(runwaySection.drawID) do
DcsUtil.SetFillColor(drawID, fillColor)
DcsUtil.SetLineColor(drawID, lineColor)
end
DcsUtil.SetFillColor(runwaySection.drawID, fillColor)
DcsUtil.SetLineColor(runwaySection.drawID, lineColor)
end
end
@@ -206,18 +204,17 @@ function RunwayStrikeMission:StartRepair()
self._repairInProgress = true
self._logger:debug("Starting repair of runway strike mission " .. self._airportName .. ":" .. self._runway.Name)
---comment
---@param selfA RunwayStrikeMission
---@param time number
---@return number?
---@param selfA any
---@return unknown
local repairTask = function (selfA, time)
local interval = selfA:DoRepairCycle()
if interval == nil then return nil end
return time + interval
end
timer.scheduleFunction(repairTask, self, timer.getTime() + 5)
end
---@return number?
function RunwayStrikeMission:DoRepairCycle()
local interval = 5
@@ -250,14 +247,12 @@ function RunwayStrikeMission:DoRepairCycle()
end
---@class StaticSpawn : table
---@class StaticSpawn
---@field category string
---@field type string
---@field y number
---@field x number
---@field heading number
---@field name string?
---@field hidden boolean?
local counter = 1
@@ -446,7 +441,6 @@ end
---@private
---@param runway Runway
---@param numSections number
---@return Array<RunwaySection>
function RunwayStrikeMission:ToSections(runway, numSections)
@@ -503,7 +497,6 @@ function RunwayStrikeMission:ToSections(runway, numSections)
corners = corners,
kilosHit = 0,
repairGroups = {},
drawID = {},
}
table.insert(sections, section)
@@ -14,7 +14,7 @@ local DcsUtil = require("classes.util.DcsUtil")
---@field private _completeAtIndex number
---@field private _parentStage Stage
---@field private _battleManager? BattleManager
---@field private _lastContactMarkerID number
---@field private _lastContactMarkerID number;
local ZoneMission = {}
--- @class MissionGroups
@@ -24,7 +24,7 @@ local ZoneMission = {}
--- @field unitsAlive table<string, table<string, boolean>>
--- @field targetsAlive table<string, table<string, boolean>>
--- @field sceneryTargets Array<SpearheadSceneryObject>
--- @field groupNamesPerUnit table<string,string>
--- @field groupNamesPerunit table<string,string>
---@class ParsedMissionName
---@field missionName string
@@ -36,7 +36,7 @@ local ZoneMission = {}
local function ParseZoneName(input)
local split_name = Util.split_string(input, "_")
local split_length = Util.tableLength(split_name)
if Util.startsWith(input, "RANDOMMISSION") == true and split_length < 4 then
if Util.startswith(input, "RANDOMMISSION") == true and split_length < 4 then
MissionEditorWarnings.Add("Random Mission with zonename " .. input .. " not in right format")
return nil
elseif split_length < 3 then
@@ -53,10 +53,9 @@ local function ParseZoneName(input)
if inputType == "cas" then parsedType = "CAS" end
if inputType == "bai" then parsedType = "BAI" end
if inputType == "sam" then parsedType = "SAM" end
if inputType == "deepstrike" then parsedType = "DEEPSTRIKE" end
if parsedType == "nil" then
MissionEditorWarnings.Add("Mission with zoneName '" ..
MissionEditorWarnings.Add("Mission with zonename '" ..
input .. "' has an unsupported type '" .. (type or "nil"))
return nil
end
@@ -67,6 +66,8 @@ local function ParseZoneName(input)
}
end
MINIMAL_UNITS_ALIVE_RATIO = 0.21
---comment
---@param zoneName string
---@param priority MissionPriority
@@ -111,7 +112,7 @@ function ZoneMission.new(zoneName, priority, database, logger, parentStage, spaw
unitsAlive = {},
targetsAlive = {},
hasTargets = false,
groupNamesPerUnit = {},
groupNamesPerunit = {},
sceneryTargets = {}
}
@@ -149,17 +150,17 @@ function ZoneMission.new(zoneName, priority, database, logger, parentStage, spaw
local spearheadGroup = SpearheadGroup.New(groupName, spawnManager, true)
table.insert(self._missionGroups.redGroups, spearheadGroup)
local isGroupTarget = Util.startsWith(string.lower(groupName), "tgt_")
local isGroupTarget = Util.startswith(string.lower(groupName), "tgt_")
for _, unit in pairs(spearheadGroup:GetObjects()) do
local unitName = unit:getName()
local isUnitTarget = Util.startsWith(string.lower(unitName), "tgt_")
local isUnitTarget = Util.startswith(string.lower(unitName), "tgt_")
if self._missionGroups.unitsAlive[groupName] == nil then
self._missionGroups.unitsAlive[groupName] = {}
end
self._missionGroups.unitsAlive[groupName][unitName] = true
self._missionGroups.groupNamesPerUnit[unitName] = groupName
self._missionGroups.groupNamesPerunit[unitName] = groupName
if isGroupTarget == true or isUnitTarget == true then
self._missionGroups.hasTargets = true
@@ -179,6 +180,7 @@ function ZoneMission.new(zoneName, priority, database, logger, parentStage, spaw
if self.missionType == "CAS" then
self._battleManager = BattleManager.New(self._missionGroups.redGroups, self._missionGroups.blueGroups, self.zoneName, self._logger.LogLevel)
end
self._logger:debug("Mission " .. self.name .. " group count: " .. Util.tableLength(missionData.RedGroups))
@@ -209,7 +211,7 @@ end
---@return boolean
function ZoneMission:AllDependenciesMet()
local allDependenciesMet = true
for missionName, _ in pairs(self._dependencies) do
for missionName, value in pairs(self._dependencies) do
if self._parentStage:IsMissionComplete(missionName) == false then
allDependenciesMet = false
self._dependencies[missionName] = false
@@ -227,9 +229,10 @@ end
---@internal
---@param checkHealth boolean
---@param _ boolean
function ZoneMission:UpdateState(checkHealth, _)
---@param messageIfDone boolean
function ZoneMission:UpdateState(checkHealth, messageIfDone)
if checkHealth == nil then checkHealth = false end
if messageIfDone == false then messageIfDone = true end
if checkHealth == true then
@@ -237,7 +240,7 @@ function ZoneMission:UpdateState(checkHealth, _)
local staticObject = StaticObject.getByName(unitName)
if staticObject then
if staticObject:isExist() == true then
local life0 = staticObject:getDesc().life --[[@as number]]
local life0 = staticObject:getDesc().life
if staticObject:getLife() / life0 < 0.3 then
self._logger:debug("exploding unit")
trigger.action.explosion(staticObject:getPoint(), 100)
@@ -283,9 +286,7 @@ function ZoneMission:UpdateState(checkHealth, _)
end
if self._missionGroups.hasTargets == true then
---@type number
local total = 0
---@type number
local alive = 0
for _, units in pairs(self._missionGroups.targetsAlive) do
@@ -298,13 +299,13 @@ function ZoneMission:UpdateState(checkHealth, _)
end
for _, sceneryObject in pairs(self._missionGroups.sceneryTargets) do
total = total + 1 --[[@as number]]
total = total + 1
if sceneryObject:IsAlive() == true then
alive = alive + 1
end
end
local deadRatio = (total - alive) / total --[[@as number]]
local deadRatio = (total - alive) / total
if deadRatio >= self._completeAtIndex then
self._logger:debug("Dead ratio " .. self.zoneName .. deadRatio .. " >= " .. self._completeAtIndex)
self._state = "COMPLETED"
@@ -353,19 +354,6 @@ end
function ZoneMission:SpawnInactive()
self._logger:info("PreActivating " .. self.name)
if self.missionType == "DEEPSTRIKE" then
local missionData = self._database:getMissionDataForZone(self.zoneName)
---@type MissionPriority
local priority = "secondary"
if missionData and missionData.primaryOverwrite ~= nil then
if missionData.primaryOverwrite == true then
priority = "primary"
end
end
self._priority = priority
self._missionCommandsHelper:AddMissionToCommands(self)
end
for _, group in pairs(self._missionGroups.redGroups) do
group:Spawn()
end
@@ -402,17 +390,6 @@ function ZoneMission:SpawnActive()
self._battleManager:Start()
end
self._missionCommandsHelper:RemoveMissionToCommands(self) -- Remove first
if self.missionType == "DEEPSTRIKE" then
local missionData = self._database:getMissionDataForZone(self.zoneName)
local priority = "primary"
if missionData and missionData.primaryOverwrite ~= nil then
if missionData.primaryOverwrite == false then
priority = "secondary"
end
end
self._priority = priority
end
self._missionCommandsHelper:AddMissionToCommands(self)
self:StartCheckingContinuous()
@@ -500,18 +477,16 @@ function ZoneMission:OnUnitLost(object)
local category = Object.getCategory(object)
if category == Object.Category.UNIT then
object = object --[[@as Unit]]
local unitName = object:getName()
self._logger:debug("UnitName:" .. unitName)
local groupName = self._missionGroups.groupNamesPerUnit[unitName]
local groupName = self._missionGroups.groupNamesPerunit[unitName]
self._missionGroups.unitsAlive[groupName][unitName] = false
if self._missionGroups.targetsAlive[groupName] and self._missionGroups.targetsAlive[groupName][unitName] then
self._missionGroups.targetsAlive[groupName][unitName] = false
end
elseif category == Object.Category.STATIC then
object = object --[[@as StaticObject]]
local name = object:getName()
self._missionGroups.unitsAlive[name][name] = false
@@ -528,12 +503,12 @@ end
---@param unit Object
function ZoneMission:MarkLastContact(unit)
if not unit then
if not unit then
self._logger:error("MarkLastContact called with nil unit")
return
end
local point = unit:getPoint()
local point = unit:getPoint()
if not point then
self._logger:error("MarkLastContact called with unit without point")
return
@@ -9,7 +9,7 @@ local GlobalConfig = require("classes.configuration.GlobalConfig")
---@field zoneName string
---@field missionType MissionType
---@field missionTypeDisplay string
---@field protected _priority MissionPriority
---@field priority MissionPriority
---@field location Vec2?
---@field code string
---@field protected _state MissionState
@@ -40,7 +40,7 @@ function Mission.newSuper(self, zoneName, missionName, missionType, missionBrief
self.zoneName = zoneName
self.name = missionName
self.missionType = missionType
self._priority = priority
self.priority = priority
self._state = "NEW"
self._logger = logger
self._database = database
@@ -51,7 +51,7 @@ function Mission.newSuper(self, zoneName, missionName, missionType, missionBrief
self.location = database:GetLocationForMissionZone(zoneName)
self.missionTypeDisplay = self.missionType
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
return true, "success"
@@ -64,19 +64,13 @@ end
--region PUBLIC
---@return MissionPriority
function Mission:GetPriority()
return self._priority
end
function Mission:SpawnPersistedState() end
function Mission:SpawnActive() end
---comment
---@param _checkHealth boolean
---@param _messageIfDone boolean
---@diagnostic disable-next-line: unused-local
function Mission:UpdateState(_checkHealth, _messageIfDone) end
---@param checkHealth boolean
---@param messageIfDone boolean
function Mission:UpdateState(checkHealth, messageIfDone) end
function Mission:StartCheckingContinuous() end
function Mission:PercentageComplete()
@@ -91,7 +85,6 @@ function Mission:ShowBriefing(groupId)
local unitType = DcsUtil.getUnitTypeFromGroup(group)
local coords = DcsUtil.convertVec2ToUnitUsableType(self.location, unitType)
if coords == nil then coords = "Could not make conversion" end
self._logger:debug("Coords converted: " .. coords)
local stateString = self:ToStateString()
@@ -119,7 +112,7 @@ end
function Mission:NotifyMissionComplete()
self._missionCommandsHelper:RemoveMissionToCommands(self)
self._logger:info("Mission Completed: " .. self.zoneName)
trigger.action.outText("Mission " .. self.name .. " [" .. self.code .. "] was completed successfully", 20)
trigger.action.outText("Mission " .. self.name .. " [" .. self.code .. "] was completed succesfully", 20)
for _, listener in pairs(self._completeListeners) do
pcall(function()
@@ -127,20 +120,13 @@ function Mission:NotifyMissionComplete()
end)
end
local _, _ = pcall(function()
local succ, err = pcall(function()
SpearheadAPI.Internal.notifyMissionComplete(self.zoneName)
end)
end
function Mission:ForceMissionComplete()
self._state = "COMPLETED"
self:NotifyMissionComplete()
end
---@param _groupId number
---@diagnostic disable-next-line: unused-local
function Mission:MarkMissionAreaToGroup(_groupId) end
function Mission:MarkMissionAreaToGroup(groupId) end
---endregion
@@ -152,27 +138,29 @@ function Mission:ToStateString() return "status: in progress" end
--endregion
--- @alias MissionPriority
--- | "none"
--- | "primary"
--- | "secondary"
do --aliases
--- @alias MissionType
--- | "nil"
--- | "STRIKE"
--- | "DEEPSTRIKE"
--- | "CAS"
--- | "BAI"
--- | "DEAD"
--- | "SAM"
--- | "OCA"
--- | "LOGISTICS"
---
--- @alias MissionState
--- | "NEW"
--- | "WAITING"
--- | "ACTIVE"
--- | "COMPLETED"
--- @alias MissionPriority
--- | "none"
--- | "primary"
--- | "secondary"
--- @alias MissionType
--- | "nil"
--- | "STRIKE"
--- | "CAS"
--- | "BAI"
--- | "DEAD"
--- | "SAM"
--- | "OCA"
--- | "LOGISTICS"
--- @alias MissionState
--- | "NEW"
--- | "WAITING"
--- | "ACTIVE"
--- | "COMPLETED"
end
return Mission
@@ -1,112 +0,0 @@
local Logger = require("classes.util.Logger")
local logger = Logger.new("ImageDisplay")
---@class ImageDisplay
local ImageDisplay = {}
---@param unit Unit
---@param settings ImageDisplaySettings
function ImageDisplay.outImageToUnit(unit, settings)
if unit == nil then return end
settings:ValidateOrSetDefaults() --Overwrites wrong settings
if settings.picPath == nil then
logger:warn("No picture path provided for unit display.")
return -- Nothing to display
end
ImageDisplay.outImage("a_out_picture_u",
unit,
settings.picPath,
settings.duration,
settings.clearview,
settings.delay,
settings.horizontalAlignment,
settings.verticalAlignment,
settings.size,
settings.sizeUnits
)
end
---@param group Group
---@param settings ImageDisplaySettings
function ImageDisplay.outImageToGroup(group, settings)
if group == nil then return end
settings:ValidateOrSetDefaults() --Overwrites wrong settings
if settings.picPath == nil then
logger:warn("No picture path provided for group display.")
return -- Nothing to display
end
ImageDisplay.outImage("a_out_picture_g",
group,
settings.picPath,
settings.duration,
settings.clearview,
settings.delay,
settings.horizontalAlignment,
settings.verticalAlignment,
settings.size,
settings.sizeUnits
)
end
---@alias OutPictureTriggerName
---| "a_out_picture_u"
---| "a_out_picture_g"
---@private
---@param object Unit|Group
---@param triggerName OutPictureTriggerName
---@param picPath string
---@param duration number
---@param clearview boolean
---@param delay number
---@param horizontalAlignment ImageDisplaySettings.HorizontalAlignment
---@param verticalAlignment ImageDisplaySettings.VerticalAlignment
---@param size number
---@param sizeUnits ImageDisplaySettings.SizeUnits
function ImageDisplay.outImage(triggerName, object, picPath, duration, clearview, delay, horizontalAlignment, verticalAlignment, size, sizeUnits)
---@type number
local id
if triggerName == "a_out_picture_u" then
object = object --[[@as Unit]]
id = object:getID()
elseif triggerName == "a_out_picture_g" then
object = object --[[@as Group]]
id = object:getID()
end
--[[
a_out_picture_u(
unitID:number,
filePath:string,
showTime:number,
clearview:boolean,
startDelayTime:number,
horzAlignment:string,
vertAlignment:string,
size:number,
size_units:string
) -- args may not correct.
]]
local cmdString = string.format(triggerName .. '(%d, "%s", %d, %s, %d, "%d", "%d", %d, "%d")',
id,
picPath,
duration,
clearview,
delay,
horizontalAlignment,
verticalAlignment,
size,
sizeUnits)
local f, err = loadstring(cmdString)
if f then
f()
else
logger:error("Something failed when drawing complex drawing" .. err)
end
end
return ImageDisplay
@@ -1,59 +0,0 @@
---@class ImageDisplaySettings
---@field picPath string?
---@field duration number
---@field clearview boolean
---@field delay number
---@field horizontalAlignment ImageDisplaySettings.HorizontalAlignment
---@field verticalAlignment ImageDisplaySettings.VerticalAlignment
---@field size number
---@field sizeUnits ImageDisplaySettings.SizeUnits
local ImageDisplaySettings = {}
ImageDisplaySettings.__index = ImageDisplaySettings
---@enum ImageDisplaySettings.VerticalAlignment
ImageDisplaySettings.VerticalAlignment = {
Top = "0",
Center = "1",
Bottom = "2"
}
---@enum ImageDisplaySettings.HorizontalAlignment
ImageDisplaySettings.HorizontalAlignment = {
Left = "0",
Center = "1",
Right = "2"
}
---@enum ImageDisplaySettings.SizeUnits
ImageDisplaySettings.SizeUnits = {
OriginalSize = "0",
WindowSize = "1"
}
function ImageDisplaySettings.new()
local self = setmetatable({}, ImageDisplaySettings)
self.picPath = nil
self.duration = 30
self.clearview = false
self.delay = 0
self.horizontalAlignment = ImageDisplaySettings.HorizontalAlignment.Center
self.verticalAlignment = ImageDisplaySettings.VerticalAlignment.Center
self.size = 100
self.sizeUnits = ImageDisplaySettings.SizeUnits.OriginalSize
return self
end
function ImageDisplaySettings:ValidateOrSetDefaults()
if self.picPath == "" then self.picPath = nil end
if self.duration == nil or self.duration <= 0 then self.duration = 30 end
if self.clearview == nil then self.clearview = false end
if self.delay == nil then self.delay = 0 end
if self.horizontalAlignment == nil then self.horizontalAlignment = ImageDisplaySettings.HorizontalAlignment.Center end
if self.verticalAlignment == nil then self.verticalAlignment = ImageDisplaySettings.VerticalAlignment.Center end
if self.size == nil or self.size <= 0 then self.size = 100 end
if self.sizeUnits == nil then self.sizeUnits = ImageDisplaySettings.SizeUnits.OriginalSize end
end
return ImageDisplaySettings
+41 -86
View File
@@ -32,10 +32,6 @@ do -- INIT DCS_UTIL
---| "Cilinder"
---| "Polygon"
---@class KeyValuePair
---@field key string
---@field value any
---@class SpearheadTriggerZone
---@field name string
---@field location Vec2
@@ -44,7 +40,7 @@ do -- INIT DCS_UTIL
---@field zone_type SpearheadTriggerZoneType
---@field properties Array<KeyValuePair>?
---@type table<string, SpearheadTriggerZone>
---@type Array<SpearheadTriggerZone>
DCS_UTIL.__trigger_zones = {}
end
@@ -70,21 +66,18 @@ do -- INIT DCS_UTIL
STATIC = 5 --CUSTOM CATEGORY
}
---@type table<string, string>
DCS_UTIL.__airbaseNamesById = {}
---@type table<string, SpearheadTriggerZone>
DCS_UTIL.__airbaseZonesByName = {}
---@type table<string, CoalitionSide>
DCS_UTIL.__airportsStartingCoalition = {}
---@type table<string, CoalitionSide>
DCS_UTIL.__warehouseStartingCoalition = {}
function DCS_UTIL.__INIT()
do -- INITS ALL TABLES WITH DATA THAT's from the MIZ environment
do --init trigger zones
for _, trigger_zone in pairs(env.mission.triggers.zones) do
for i, trigger_zone in pairs(env.mission.triggers.zones) do
-- reorder verts as they are not ordered correctly in the ME
local verts = {}
if Util.tableLength(trigger_zone.verticies) >= 4 then
@@ -111,8 +104,8 @@ do -- INIT DCS_UTIL
if trigger_zone.properties then
for _, kvPair in pairs(trigger_zone.properties) do
local key = kvPair.key
local value = kvPair.value
local key = kvPair["key"]
local value = kvPair["value"]
zone.properties[#zone.properties + 1] = { key = key, value = value }
end
end
@@ -123,7 +116,6 @@ do -- INIT DCS_UTIL
do -- init airports and warehouses
if env.warehouses.airports then
env.warehouses.airports = env.warehouses.airports --[[@as table<string, any>]]
for warehouse_id, value in pairs(env.warehouses.airports) do
if warehouse_id ~= nil then
warehouse_id = tostring(warehouse_id) or "nil"
@@ -134,12 +126,12 @@ do -- INIT DCS_UTIL
end
if env.warehouses.warehouses then
env.warehouses.warehouses = env.warehouses.warehouses --[[@as table<number, any>]]
DCS_UTIL.__warehouseStartingCoalition[-1] = "placeholder"
for warehouse_id, value in pairs(env.warehouses.warehouses) do
if warehouse_id ~= nil then
local warehouse_id_str = tostring(warehouse_id) or "nil"
warehouse_id = tostring(warehouse_id) or "nil"
local coalitionNumber = DCS_UTIL.stringToCoalition(value.coalition)
DCS_UTIL.__warehouseStartingCoalition[warehouse_id_str] = coalitionNumber
DCS_UTIL.__warehouseStartingCoalition[warehouse_id] = coalitionNumber
end
end
end
@@ -200,7 +192,7 @@ do -- INIT DCS_UTIL
BLUE = 2
}
]] --
input = string.lower(input)
local input = string.lower(input)
if input == 'neutrals' or input == "neutral" or input == "0" then
return DCS_UTIL.Coalition.NEUTRAL
end
@@ -226,13 +218,11 @@ do -- INIT DCS_UTIL
end
end
--- takes a list of units and returns all the units that are in any of the zones
---@param unit_names Array<string> unit names
---@param zone_names Array<string> zone names
---@return table<number, { unit : Unit|StaticObject, zone_name: string }>
---@param unit_names table unit names
---@param zone_names table zone names
---@return table unit list of objects { unit = UNIT, zone_name = zoneName}
function DCS_UTIL.getUnitsInZones(unit_names, zone_names)
---@type Array<Unit|StaticObject>
local units = {}
---@type Array<SpearheadTriggerZone>
@@ -245,25 +235,20 @@ do -- INIT DCS_UTIL
end
end
for _, zone_name in pairs(zone_names) do
for index, zone_name in pairs(zone_names) do
local zone = DCS_UTIL.__trigger_zones[zone_name]
if zone then
zones[#zones + 1] = zone
end
end
---@type table<number, { unit : Unit|StaticObject, zone_name: string }>
local in_zone_units = {}
for units_ind = 1, #units do
local lUnit = units[units_ind]
local isActive = true
local lCat = Object.getCategory(lUnit)
if lCat == Object.Category.UNIT then
local unit = lUnit --[[@as Unit]]
isActive = unit:isActive() == true
end
local unit_pos = lUnit:getPosition().p
for _, zone in pairs(zones) do
if unit_pos and isActive == true then -- it is a unit and is active or it is not a unit
local lCat = Object.getCategory(lUnit)
for zone_name, zone in pairs(zones) do
if unit_pos and ((lCat == 1 and lUnit:isActive() == true) or lCat ~= 1) then -- it is a unit and is active or it is not a unit
local isInZone = Util.is3dPointInZone(unit_pos, zone)
if isInZone == true then
in_zone_units[#in_zone_units + 1] = { unit = lUnit, zone_name = zone.name }
@@ -277,9 +262,9 @@ do -- INIT DCS_UTIL
--- takes a list of groups and returns all the group leaders that are in any of the zones
---@param group_names table unit names
---@param zone_name string zone names
---@return Array<string> groupnames list of group names
---@return table groupnames list of group names
function DCS_UTIL.getGroupsInZone(group_names, zone_name)
local zone = DCS_UTIL.__trigger_zones[zone_name] --[[@as SpearheadTriggerZone?]]
local zone = DCS_UTIL.__trigger_zones[zone_name]
if zone == nil then
return {}
end
@@ -292,13 +277,11 @@ do -- INIT DCS_UTIL
---@param zone SpearheadTriggerZone
---@return Array<string> groupnames list of groups that are in the zone
function DCS_UTIL.areGroupsInCustomZone(group_names, zone)
---@type Array<{ unit : Unit|StaticObject, groupname: string }>
local units = {}
if Util.tableLength(group_names) < 1 then return {} end
for k = 1, #group_names do
---@type { unit : Unit|StaticObject|nil, groupname: string }|nil
local entry
local entry = nil
local group = Group.getByName(group_names[k])
if group ~= nil then
entry = { unit = group:getUnit(1), groupname = group_names[k] }
@@ -307,7 +290,7 @@ do -- INIT DCS_UTIL
end
if entry and entry.unit and entry.unit:isExist() == true then
units[#units + 1] = { groupname = entry.groupname, unit = entry.unit }
units[#units + 1] = entry
end
end
@@ -325,20 +308,20 @@ do -- INIT DCS_UTIL
--- takes a x, y poistion and checks if it is inside any of the zones
---@param x number North South position
---@param z number West East position
---@param zone_names Array<string> zone names
---@return Array<string> zones list of objects { zone_name = zoneName}
---@param zone_names table zone names
---@return table zones list of objects { zone_name = zoneName}
function DCS_UTIL.isPositionInZones(x, z, zone_names)
---@type Array<SpearheadTriggerZone>
local zones = {}
for _, zone_name in pairs(zone_names) do
for index, zone_name in pairs(zone_names) do
local zone = DCS_UTIL.__trigger_zones[zone_name]
if zone then
zones[#zones + 1] = zone
end
end
---@type Array<string>
local result_zones = {}
for _, zone in pairs(zones) do
for zone_name, zone in pairs(zones) do
if Util.is3dPointInZone({ x = x, z = z, y = 0 }, zone) == true then
result_zones[#result_zones + 1] = zone.name
end
@@ -413,18 +396,6 @@ do -- INIT DCS_UTIL
return nil;
end
---@return Array<string>
function DCS_UTIL.getAllGroupCategoryNames()
return {
"airplane",
"helicopter",
"ground",
"ship",
"train",
"static"
}
end
---@type table<string, CoordType>
local config =
{
@@ -437,14 +408,13 @@ do -- INIT DCS_UTIL
---@param location Vec2
---@param unitType string?
---@return string?
---@param unitType string
function DCS_UTIL.convertVec2ToUnitUsableType(location, unitType)
local height = land.getHeight(location)
local vec3 = { x = location.x, y = height, z = location.y }
unitType = string.lower(unitType or "")
local unitType = string.lower(unitType or "")
local conversionType = config[unitType]
if not conversionType then conversionType = "DDM" end
@@ -459,7 +429,6 @@ do -- INIT DCS_UTIL
---@private
---@param location Vec3
---@param coordType CoordType
---@return string?
function DCS_UTIL.convertToDisplayCoord(location, coordType)
local lattitude, longitude, altitude = coord.LOtoLL(location)
@@ -471,7 +440,7 @@ do -- INIT DCS_UTIL
-- Convert DD to DDM (Degrees Decimal Minutes)
local function dd_to_ddm(dd)
local degrees = math.floor(math.abs(dd))
local minutes = (math.abs(dd) - degrees) * 60 --[[@as number]]
local minutes = (math.abs(dd) - degrees) * 60
local sign = dd >= 0 and 1 or -1
return degrees * sign, minutes
end
@@ -502,7 +471,7 @@ do -- INIT DCS_UTIL
lon_hemisphere, math.abs(lon_deg), lon_min_display, lon_sec_display,
altitude * 3,28084)
end
end
---@param zone SpearheadTriggerZone
@@ -590,7 +559,6 @@ do -- INIT DCS_UTIL
end
---@param group Group
---@return string? unit type name
function DCS_UTIL.getUnitTypeFromGroup(group)
for _, unit in pairs(group:getUnits()) do
if unit and unit:isExist() then
@@ -603,11 +571,10 @@ do -- INIT DCS_UTIL
---comment Get all units that are players
---@return Array<Unit> units
function DCS_UTIL.getAllPlayerUnits()
---@type Array<Unit>
local units = {}
for i = 0, 2 do
local players = coalition.getPlayers(i)
for _, unit in pairs(players) do
for key, unit in pairs(players) do
units[#units + 1] = unit
end
end
@@ -649,9 +616,9 @@ do -- INIT DCS_UTIL
return result
end
---@param unitName string
function DCS_UTIL.CleanCorpse(unitName)
unitName = "dead_" .. unitName
local unitName = "dead_" .. unitName
local object = StaticObject.getByName(unitName)
if object then
@@ -665,21 +632,15 @@ do -- INIT DCS_UTIL
---@field b number
---@field a number
local __drawID = 4210
function DCS_UTIL.GetNextDrawID()
__drawID = __drawID + 1
return __drawID
end
---@param groupID number
---@param text string
---@param location Vec3
---@return number markID
function DCS_UTIL.AddMarkToGroup(groupID, text, location)
local nextId = DCS_UTIL.GetNextDrawID()
trigger.action.markToGroup(nextId, text, location, groupID, true, nil)
return nextId
drawID = drawID + 1
trigger.action.markToGroup(drawID, text, location, groupID, true, nil)
return drawID
end
---comment
@@ -687,9 +648,9 @@ do -- INIT DCS_UTIL
---@param location Vec3
---@return integer
function DCS_UTIL.AddMarkToAll(text, location)
local nextId = DCS_UTIL.GetNextDrawID()
trigger.action.markToAll(nextId, text, location, true, nil)
return nextId
drawID = drawID + 1
trigger.action.markToAll(drawID, text, location, true, nil)
return drawID
end
---@param markId number
@@ -716,10 +677,6 @@ do -- INIT DCS_UTIL
---@param drawID number
---@param fillColor DrawColor
function DCS_UTIL.SetFillColor(drawID, fillColor)
if fillColor == nil then
return
end
local lineColorMapped = {
fillColor.r or 0,
fillColor.g or 0,
@@ -738,7 +695,7 @@ do -- INIT DCS_UTIL
---@return number? id
function DCS_UTIL.GetNeutralCountry()
for _, id in pairs(country.id) do
for name, id in pairs(country.id) do
if coalition.getCountryCoalition(id) == DCS_UTIL.Coalition.NEUTRAL then
return id
end
@@ -768,8 +725,6 @@ do -- INIT DCS_UTIL
return false
end
---@param groupName string
---@param offset number?
---@return boolean
function DCS_UTIL.IsBingoFuel(groupName, offset)
if offset == nil then offset = 0 end
@@ -794,7 +749,7 @@ do -- INIT DCS_UTIL
function DCS_UTIL.GetPlayerGroupByGroupID(groupId)
for i = 0, 2 do
local players = coalition.getPlayers(i)
for _, unit in pairs(players) do
for key, unit in pairs(players) do
if unit and unit:isExist() == true then
local group = unit:getGroup()
if group and group:getID() == groupId then
@@ -810,7 +765,7 @@ do -- INIT DCS_UTIL
function DCS_UTIL.GetPlayerUnitByID(unitID)
for i = 0, 2 do
local players = coalition.getPlayers(i)
for _, unit in pairs(players) do
for key, unit in pairs(players) do
if unit and unit:getID() == unitID then
return unit
end
-7
View File
@@ -3,13 +3,6 @@ local Util = require("classes.util.Util")
local SpearheadConfig = require("classes.configuration.GlobalConfig")
--- @alias LogLevel
--- | "DEBUG"
--- | "INFO"
--- | "WARN"
--- | "ERROR"
--- | "NONE"
---@type LogLevel
local defaultLogLevel = "INFO"
+3 -7
View File
@@ -1,12 +1,8 @@
---@class MissionEditingWarnings
local MissionEditingWarnings = {}
---@type table<number, string>
MissionEditingWarnings.warnings = {}
function MissionEditingWarnings.Add(warningMessage)
table.insert(MissionEditingWarnings.warnings, warningMessage or "skip")
table.insert(MissionEditingWarnings, warningMessage or "skip")
end
---@param logger Logger
@@ -16,12 +12,12 @@ function MissionEditingWarnings.WriteAll(logger)
return
end
if not MissionEditingWarnings.warnings or #MissionEditingWarnings.warnings == 0 then
if not MissionEditingWarnings or #MissionEditingWarnings == 0 then
return
end
logger:warn("Mission Editor Warnings:")
for _, warning in ipairs(MissionEditingWarnings.warnings) do
for _, warning in ipairs(MissionEditingWarnings) do
logger:warn("- " .. warning)
end
+27 -65
View File
@@ -1,13 +1,13 @@
---@class Util
local UTIL = {}
do -- INIT UTIL
---splits a string in sub parts by separator
---splits a string in sub parts by seperator
---@param input string
---@param separator string
---@return Array<string> result list of strings
function UTIL.split_string(input, separator)
if separator == nil then
separator = " "
---@param seperator string
---@return table result list of strings
function UTIL.split_string(input, seperator)
if seperator == nil then
seperator = " "
end
local result = {}
@@ -15,14 +15,14 @@ do -- INIT UTIL
return result
end
for str in string.gmatch(input, "[^" .. separator .. "]+") do
for str in string.gmatch(input, "[^" .. seperator .. "]+") do
table.insert(result, str)
end
return result
end
---comment
---@param table table<any, any>
---@param table any
---@return number
function UTIL.tableLength(table)
if table == nil then return 0 end
@@ -35,34 +35,23 @@ do -- INIT UTIL
---@param orig table
---@return table copy
function UTIL.deepCopyTable(orig)
---@param original any
---@return any
local function deepCopy(original)
local orig_type = type(original)
---@type any
local copy
if orig_type == 'table' then
original = original --[[@as table<any, any>]]
copy = {} --[[@as table<any, any>]]
for orig_key, orig_value in pairs(original) do
local copiedKey = deepCopy(orig_key) --[[@as any]]
copy[copiedKey] = deepCopy(orig_value)
end
setmetatable(copy, deepCopy(getmetatable(original)))
else -- number, string, boolean, etc
copy = original --[[@as any]]
local orig_type = type(orig)
local copy
if orig_type == 'table' then
copy = {}
for orig_key, orig_value in next, orig, nil do
copy[UTIL.deepCopyTable(orig_key)] = UTIL.deepCopyTable(orig_value)
end
return copy
setmetatable(copy, UTIL.deepCopyTable(getmetatable(orig)))
else -- number, string, boolean, etc
copy = orig
end
return deepCopy(orig)
return copy
end
---Gets a random from the list
---@generic T
---@param list Array<T>
---@return T @random element from the list
---@param list Array
---@return any @random element from the list
function UTIL.randomFromList(list)
local max = #list
@@ -76,13 +65,11 @@ do -- INIT UTIL
return list[random]
end
---@generic T
---@param list Array<T>
---@param list Array
---@param start number start
---@param n number length
---@return Array<T>
---@return Array
function UTIL.sublist(list, start, n)
---@type table<number, any>
local result = {}
for i = start, n do
result[#result + 1] = list[i]
@@ -103,10 +90,9 @@ do -- INIT UTIL
end
local function table_print(tt, indent, done)
done = done or {} --[[@as table<any, boolean>]]
done = done or {}
indent = indent or 0
if type(tt) == "table" then
tt = tt --[[@as table<any, any>]]
local sb = {}
for key, value in pairs(tt) do
table.insert(sb, string.rep(" ", indent)) -- indent it
@@ -134,7 +120,7 @@ do -- INIT UTIL
---@param findable string
---@param ignoreCase boolean?
---@return boolean
UTIL.startsWith = function(str, findable, ignoreCase)
UTIL.startswith = function(str, findable, ignoreCase)
if ignoreCase == true then
return string.lower(str):find('^' .. string.lower(findable)) ~= nil
end
@@ -142,14 +128,6 @@ do -- INIT UTIL
return str:find('^' .. findable) ~= nil
end
UTIL.endsWith = function(str, findable, ignoreCase)
if ignoreCase == true then
return string.lower(str):find(string.lower(findable) .. '$') ~= nil
end
return str:find(findable .. '$') ~= nil
end
---comment
---@param str string
---@param findable string
@@ -160,11 +138,11 @@ do -- INIT UTIL
---comment
---@param str string
---@param findableTable table<any,any>
---@param findableTable table
---@return boolean
UTIL.startswithAny = function(str, findableTable)
for _, value in pairs(findableTable) do
if type(value) == "string" and UTIL.startsWith(str, value) then return true end
for key, value in pairs(findableTable) do
if type(value) == "string" and UTIL.startswith(str, value) then return true end
end
return false
end
@@ -252,13 +230,7 @@ do -- INIT UTIL
return ((vec1Norm.x * vec2Norm.x) + (vec1Norm.y * vec2Norm.y) + (vec1Norm.z * vec2Norm.z))
end
---@param polygon Array<Vec2> of pairs { x, y }
---@param x number X location
---@param y number Y location
---@return boolean
local function isInComplexPolygon(polygon, x, y)
---@param poly Array<Vec2> of pairs { x, y }
---@return Array<{ x1: number, z1: number, x2: number, z2: number }>
local function getEdges(poly)
local result = {}
for i = 1, #poly do
@@ -295,7 +267,6 @@ do -- INIT UTIL
---@param point Vec3
---@param zone SpearheadTriggerZone
---@return boolean
function UTIL.is3dPointInZone(point, zone)
if zone.zone_type == "Polygon" and zone.verts then
if UTIL.IsPointInPolygon(zone.verts, point.x, point.z) == true then
@@ -312,7 +283,6 @@ do -- INIT UTIL
---@param point Vec2
---@param zone SpearheadTriggerZone
---@return boolean
function UTIL.is2dPointInZone(point, zone)
if zone.zone_type == "Polygon" and zone.verts then
if UTIL.IsPointInPolygon(zone.verts, point.x, point.y) == true then
@@ -421,9 +391,7 @@ do -- INIT UTIL
if #points == 0 then return {} end
-- Simple clustering: group points that are within minSeparation of each other
---@type Array<Array<Vec2>>
local clusters = {}
---@type table<integer, boolean>
local assigned = {}
for i, p in ipairs(points) do
@@ -464,8 +432,6 @@ do -- INIT UTIL
end
---@param points Array<Vec2>
---@param meters number
---@return Array<Vec2>
function UTIL.enlargeConvexHull(points, meters)
if points == nil or #points == 0 then
return {}
@@ -543,10 +509,6 @@ function UTIL.GetTangentHullPointsFromOrigin(hull, origin)
return {}
end
---@param a Vec2
---@param b Vec2
---@param c Vec2
---@return number
local function orientation(a, b, c)
-- Returns >0 if c is to the left of ab, <0 if to the right, 0 if colinear
return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
+22 -22
View File
@@ -1,14 +1,13 @@
--Single player purpose
local Logger = require("classes.util.Logger")
local DcsUtil = require("classes.util.DcsUtil")
local Database = require("classes.spearhead_db")
local SpearheadEvents = require("classes.spearhead_events")
local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper")
local GlobalConfig = require("classes.configuration.GlobalConfig")
local CapConfig = require("classes.configuration.CapConfig")
local StageConfig = require("classes.configuration.StageConfig")
local Persistence = require("classes.persistence.Persistence")
local PersistenceConfig = require("classes.configuration.PersistenceConfig")
local SpawnManager = require("classes.helpers.SpawnManager")
local DetectionManager = require("classes.capClasses.detection.DetectionManager")
local GlobalCapManager = require("classes.capClasses.GlobalCapManager")
@@ -34,11 +33,19 @@ MissionCommandsHelper.getOrCreate() -- initiate
local capConfig = CapConfig:new();
local stageConfig = StageConfig:getInstance();
local persistenceConfig = PersistenceConfig.new()
if persistenceConfig and persistenceConfig:isEnabled() == true then
local startingStage = stageConfig.startingStage or 1
if SpearheadConfig and SpearheadConfig.Persistence and SpearheadConfig.Persistence.enabled == true then
standardLogger:info("Persistence enabled")
local persistenceLogger = Logger.new("Persistence", defaultLogLevel)
Persistence.Init(persistenceLogger)
local persistanceStage = Persistence.GetActiveStage()
if persistanceStage then
standardLogger:info("Persistance activated and using persistant active stage: " .. persistanceStage)
startingStage = persistanceStage
end
else
standardLogger:info("Persistence disabled")
end
local spawnLogger = Logger.new("SpawnManager", defaultLogLevel)
@@ -46,29 +53,22 @@ local spawnManager = SpawnManager.new(spawnLogger)
local detectionLogger = Logger.new("DetectionManager", defaultLogLevel)
local detectionManager = DetectionManager.New(detectionLogger)
GlobalCapManager.start(databaseManager, capConfig, detectionManager, defaultLogLevel, spawnManager)
local globalStageManager = GlobalStageManager.new(databaseManager, stageConfig, defaultLogLevel, spawnManager)
GlobalCapManager.start(databaseManager, capConfig, detectionManager, stageConfig, defaultLogLevel, spawnManager)
GlobalStageManager.NewAndStart(databaseManager, stageConfig, defaultLogLevel, spawnManager)
GlobalFleetManager.start(databaseManager)
local SetStageDelayed = function(number, time)
SpearheadEvents.PublishStageNumberChanged(number)
return nil
end
timer.scheduleFunction(SetStageDelayed, startingStage, timer.getTime() + 3)
env.info(startTime .. "ms / " .. timer.getTime() * 1000 .. "ms")
local duration = (timer.getTime() * 1000) - startTime
standardLogger:info("Spearhead Initialisation duration: " .. tostring(duration) .. "ms")
local missionEditorWarningsLogger = Logger.new("MissionEditorWarnings", defaultLogLevel)
MissionEditorWarnings.WriteAll(missionEditorWarningsLogger)
globalStageManager:PrintMermaidStage()
local startDelayed = function()
globalStageManager:Start()
return nil
end
timer.scheduleFunction(startDelayed, nil, timer.getTime() + 5) -- delay start by 5 seconds so every deleted item it first removed entirely
local globalConfig = GlobalConfig.New()
if globalConfig and globalConfig:isDebugMenuEnabled() == true then
local DebugMenu = require("classes.debug.DebugMenu")
local debugMenu = DebugMenu.new()
debugMenu:RegisterMenus()
end
GlobalStageManager:printFullOverview()