Author SHA1 Message Date
dutchie031 95fe7e1058 updated releasenotes 2026-09-07 19:22:52 +02:00
dutchie031 b8fe242c43 merged develop 2026-09-07 19:13:11 +02:00
dutchie031 772397250f Feature/lanes (#40)
Update Docs / prepare-docs (push) Successful in 37s
Publish Release / build (push) Successful in 39s
Update Docs / build-image (push) Successful in 30s
Update Docs / deploy-container (push) Successful in 20s
this fixes #35

does not close but starts work partly on #41

Reviewed-on: #40
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-09-07 17:08:26 +00:00
dutchie031 61ba4de338 buildable mission updates 2026-08-30 20:24:20 +02:00
dutchie031 674160abbf Update doc slightly 2026-08-30 19:17:35 +02:00
dutchie031 8ff25ffeb0 Merge remote-tracking branch 'origin/develop' into feature/customBriefingSupplyMissions 2026-08-30 19:13:26 +02:00
dutchie031 0a7e8ea977 also added the beta block on home screen
Update Docs / prepare-docs (push) Successful in 21s
Update Docs / build-image (push) Successful in 12s
Update Docs / deploy-container (push) Successful in 12s
2026-08-30 18:25:03 +02:00
dutchie031 96507f1d34 updated the sidebar when it's a beta version
Update Docs / prepare-docs (push) Successful in 22s
Update Docs / build-image (push) Successful in 20s
Update Docs / deploy-container (push) Successful in 16s
2026-08-30 18:17:55 +02:00
dutchie031 96ada242a4 updated to change the release name so two releases can co-exist (#46)
Reviewed-on: #46
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-30 12:16:19 +00:00
dutchie031 2bce1985e2 beta.spearhead.rocks now for the beta branch (#45)
Reviewed-on: #45
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-30 11:49:52 +00:00
dutchie031 428fe75a12 wip 2026-08-29 16:12:43 +02:00
dutchie031 2e5a31faef Custom briefings for BlueSam and FARPS 2026-08-29 15:57:29 +02:00
dutchie031 5601091b8f Merge branch 'main' into develop 2026-08-22 15:02:35 +02:00
dutchie031 36ff621759 updated the config 2026-08-21 09:48:25 +02:00
dutchie031 18eff4b85a merged main 2026-08-20 18:50:24 +02:00
dutchie031 952daa2897 Cutting a release
Publish Release / build (push) Canceled after 22s
2026-08-20 16:58:43 +02:00
dutchie031 7c0c5e4a85 updated briefings to be shown during spawn (#31)
Publish Release / build (push) Successful in 22s
fixes #26

Reviewed-on: #31
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-20 14:06:42 +00:00
dutchie031 970f049440 Updated max distance to be configurable (#29)
Publish Release / build (push) Successful in 12s
Partly addresses #24

Does not fully fix thoughReviewed-on: #29

Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-18 18:23:07 +00:00
dutchie031 63a1ea44db Fix/ pre activation (#28)
Publish Release / build (push) Successful in 39s
fixes: #8Reviewed-on: #28

Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-18 12:50:12 +00:00
dutchie031 c8329108dd added link to the release
Publish Release / build (push) Successful in 14s
2026-08-17 12:31:25 +02:00
dutchie031 a88520a413 trigger beta branch
Publish Release / build (push) Successful in 14s
2026-08-17 12:14:32 +02:00
dutchie031 d8764c6824 Fix/briefing warnings (#20)
Reviewed-on: #20
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-17 10:13:25 +00:00
dutchie031 d0e7f2aae4 fixed briefings faulty giving warnings (#19)
Publish Release / build (push) Successful in 15s
Changed order of checking for mission briefings

fixes #9Reviewed-on: #19

Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-17 10:12:24 +00:00
dutchie031 8c75ba5027 Fix/custom drawings (#18)
Publish Release / build (push) Successful in 13s
Reviewed-on: #18
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-17 09:32:01 +00:00
dutchie031 52f97f7c92 webhook embed in release action
Publish Release / build (push) Successful in 13s
2026-08-16 14:45:14 +02:00
dutchie031 e5abfe19d6 webhook embed in release action 2026-08-16 14:44:55 +02:00
dutchie031 b34918ff28 webhook embed in release action
Publish Release / build (push) Successful in 56s
2026-08-16 14:40:49 +02:00
dutchie031 ab910b8faf webhook embed in release action 2026-08-16 14:38:46 +02:00
dutchie031 f677a83c8e Crate spawning update (#17)
Publish Release / build (push) Successful in 42s
Reviewed-on: #16
Co-authored-by: dutchie031 <timrorije@gmail.com>

Issue: #11

-[ ] Tested
-[ ] Reviewed
-[ ] Release notes updatedReviewed-on: #17
2026-08-16 11:55:34 +00:00
dutchie031 a1e36aa5d7 supplyHub Commands wiring and event triggers wired for more real time updates (#15)
Publish Release / build (push) Successful in 16s
Issue: #10

- [x] Tested
- [x] Reviewed
- [x] ReleaseNotes UpdatedReviewed-on: #15

Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-15 15:06:32 +00:00
dutchie031 bb9144e3a0 updating develop branch now automatically creates beta release (#16)
Reviewed-on: #16
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-14 18:57:33 +00:00
48 changed files with 4666 additions and 502 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

+10
View File
@@ -21,6 +21,9 @@
<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>
@@ -59,6 +62,13 @@
<footer>
<p>&copy; 2025 Spearhead Project</p>
</footer>
<script>
if (true || 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>
+2 -1
View File
@@ -19,9 +19,10 @@ 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/map-markings.html">Map Markings</a>
<a href="/pages/advanced/lanes.html">Advanced: Lanes</a>
</div>
</div>
+6
View File
@@ -168,8 +168,14 @@ 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">
${betaHtml}
<h4 class="side-nav-title"></h4>
<ul>
<!-- Navigation items will be populated automatically -->
+183
View File
@@ -0,0 +1,183 @@
<!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>
+7
View File
@@ -253,6 +253,13 @@
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/>
+1 -2
View File
@@ -262,7 +262,7 @@ header nav a:hover::after {
overflow: hidden;
top: 100%; /* Position below the dropdown */
left: 0; /* Align left edge */
min-width: 160px;
min-width: 240px;
background-color: var(--color-bg-secondary);
box-shadow: var(--shadow-medium);
@@ -336,7 +336,6 @@ main {
}
.side-nav {
width: 200px;
min-width: 200px;
padding-left: 0;
overflow-y: auto;
+19 -3
View File
@@ -4,13 +4,14 @@ on:
push:
branches:
- main
- develop
paths:
- '.docs/**'
env:
DOCKER_REGISTRY: registry.dutchie031.net
DOCKER_IMAGE: spearhead-docs
DOCKER_TAG: latest
DOCKER_TAG: ${{ github.ref_name == 'main' && 'latest' || 'beta' }}
jobs:
## Run replacements and cleanups on the docs
@@ -36,7 +37,7 @@ jobs:
sed -i '/@@API_CODE@@/d' .docs/web/pages/spearheadapi.html
rm .docs/web/pages/temp_api_code.html
- name: Highlight API code and update HTML
- name: Highlight Config code and update HTML
run: |
pygmentize -f html -l lua -O noclasses,style=monokai ./config.lua > .docs/web/pages/temp_config_code.html
# Insert the highlighted code into the placeholder in spearheadapi.html
@@ -105,15 +106,30 @@ jobs:
chmod 600 $HOME/.kube/config
cat $HOME/.kube/config
- name: Run Helm upgrade/install
- name: Run Helm upgrade/install spearhead-docs (main)
if: github.ref_name == 'main'
run: |
helm upgrade --install spearhead-docs .helm \
--namespace spearhead-docs \
--create-namespace \
-f .helm/values.yaml \
--set image.repository=${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }} \
--set image.tag=${{ env.DOCKER_TAG }} \
--wait
- name: Run Helm upgrade/install spearhead-docs (develop)
if: github.ref_name == 'develop'
run: |
helm upgrade --install spearhead-docs-beta .helm \
--namespace spearhead-docs \
--create-namespace \
-f .helm/values.yaml \
-f .helm/values.beta.yaml \
--set image.repository=${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }} \
--set image.tag=${{ env.DOCKER_TAG }} \
--wait
# - name: 'Deploy'
# uses: deliverybot/helm@v1
# with:
+1
View File
@@ -14,3 +14,4 @@ sources:
maintainers:
- name: Spearhead Team
+1 -1
View File
@@ -25,7 +25,7 @@ spec:
type: RuntimeDefault
containers:
- name: {{ .Values.app }}
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: Always
ports:
- containerPort: {{ .Values.ports.containerPort }}
+10 -17
View File
@@ -14,7 +14,7 @@ spec:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Values.app }}-ingress
name: "{{ .Values.app }}-ingress"
namespace: spearhead-docs
annotations:
kubernetes.io/tls-acme: "true"
@@ -22,28 +22,21 @@ metadata:
spec:
ingressClassName: nginx
rules:
- host: spearhead.dutchie031.com
{{- range .Values.ingress.hosts }}
- host: {{ . }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ .Values.app }}
name: {{ $.Values.app }}
port:
number: {{ .Values.ports.servicePort }}
- host: spearhead.rocks
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ .Values.app }}
port:
number: {{ .Values.ports.servicePort }}
number: {{ $.Values.ports.servicePort }}
{{- end }}
tls:
- hosts:
- spearhead.dutchie031.com
- spearhead.rocks
secretName: spearhead-docs-tls
{{- range .Values.ingress.hosts }}
- {{ . }}
{{- end }}
secretName: "{{ .Values.app }}-docs-tls"
+9
View File
@@ -0,0 +1,9 @@
app: spearhead-docs-beta
image:
tag: "beta"
ingress:
hosts:
- beta.spearhead.rocks
+5
View File
@@ -8,3 +8,8 @@ image:
ports:
servicePort: 80
containerPort: 80
ingress:
hosts:
- spearhead.dutchie031.com
- spearhead.rocks
+15 -1
View File
@@ -5,11 +5,25 @@
### New Features
- Issue: #41
Adds Stage Complete debug methods for local testing of the Stage flow.
#40
- Issue: #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".
#40
- Issue: #37
Now enabled the mission editor to add a custom briefing to the "Buildable" missions.
#44
### 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>
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
+6
View File
@@ -4,6 +4,9 @@ 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
@@ -73,6 +76,9 @@ SpearheadConfig = {
--The location will continously update for the last killed unit.
markLastContact = false, -- default false
--If enabled, the stage briefing including the current missions will be shown to players on spawn.
briefingOnSpawn = true, -- default true
--AutoStages will continue to the next stage automatically on completion of the missions within the stage.
-- If you want to make it so the next stage triggers only when you want to disable it here and manually implement the actions needed.
--[[
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -10,7 +10,6 @@ local MissionCompleteListeners = {}
---@class SpearheadAPIInternal
---@field notifyMissionComplete fun(zone_name: string)
---@type FullSpearheadAPI
SpearheadAPI = {
Stages = {
@@ -25,12 +24,12 @@ SpearheadAPI = {
getCurrentStage = function()
return GlobalStageManager.getCurrentStage() or nil
end,
isStageComplete = function(stageNumber)
isStageComplete = function(stageNumber, stageLaneIdentifier)
if type(stageNumber) ~= "number" then
return false, "stageNumber " .. stageNumber .. " is not a valid number"
end
local isComplete = GlobalStageManager.isStageComplete(stageNumber)
local isComplete = GlobalStageManager.isStageComplete(stageNumber, stageLaneIdentifier)
if isComplete == nil then
return nil, "no stage found with number " .. stageNumber
end
+1 -2
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): boolean | nil, string @returns whether a stage (by index) is complete. <br/> @param stageNumber number <br/> @return boolean | nil <br/> @return string
---@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
---@class OnMissionCompleteListener
---@field onMissionComplete fun(self: OnMissionCompleteListener, zone_name: string) @Called when a mission is completed. @return void
@@ -14,4 +14,3 @@ SpearheadAPI = SpearheadAPI
---@class MissionAPI
---@field addOnMissionCompleteListener fun(listener: OnMissionCompleteListener) @Adds a listener to the mission. <br/> @param listener OnMissionCompleteListener <br/> @return void
+5 -2
View File
@@ -392,9 +392,12 @@ function CapBase:CheckAndScheduleIntercept()
end
end
end
end
function CapBase:OnStageNumberChanged(number)
function CapBase:OnStageNumberChanged(number, laneIdentifier)
-- only react on "main" lane changes, ignore other lanes for now
if laneIdentifier ~= nil then return end
self.activeStage = number
if self:IsBaseActiveWhenStageIsActive(number) == true then
@@ -2,6 +2,9 @@ local SpearheadEvents = require("classes.spearhead_events")
local RTBMission = require("classes.capClasses.taskings.RTB")
local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil")
local GlobalConfig = require("classes.configuration.GlobalConfig")
local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
---@class AirGroup : OnUnitLostListener
---@field protected _logger Logger
@@ -12,9 +15,12 @@ local DcsUtil = require("classes.util.DcsUtil")
---@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
@@ -28,6 +34,7 @@ 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
@@ -92,6 +99,27 @@ 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
for _, wp in pairs(mission.params.route.points) do
if 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
@@ -6,6 +6,7 @@ local briefingMessageTime = nil
---@class GlobalConfig
---@field private _briefingTime number
---@field private _debugEnabled boolean
---@field private _debugMenuEnabled boolean
local GlobalConfig = {}
GlobalConfig.__index = GlobalConfig;
@@ -27,6 +28,8 @@ function GlobalConfig.New()
self._debugEnabled = false
end
self._debugMenuEnabled = SpearheadConfig.debugMenuEnabled == true
end
return self
@@ -40,6 +43,10 @@ function GlobalConfig:isDebugEnabled()
return self._debugEnabled or false
end
---@return boolean
function GlobalConfig:isDebugMenuEnabled()
return self._debugMenuEnabled or false
end
return GlobalConfig
@@ -0,0 +1,31 @@
---@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
+80
View File
@@ -0,0 +1,80 @@
local Logger = require("classes.util.Logger")
local Util = require("classes.util.Util")
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
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
+3 -1
View File
@@ -127,7 +127,9 @@ function FleetGroup:new(fleetGroupName, database, logger)
end
end
o.OnStageNumberChanged = function(self, number)
o.OnStageNumberChanged = function(self, 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]
+23 -12
View File
@@ -13,7 +13,8 @@ do
---@field unitsStates table<string, UnitState>
---@field random_missions table<string, MissionState>
---@field deliveredKilos table<string, number>
---@field activeStage integer|nil
---@field activeStage number? required for backwards compatibility
---@field activeStageInStageLane table<string, number>
---@class UnitState
---@field isDead boolean
@@ -33,7 +34,8 @@ do
unitsStates = {},
random_missions = {},
deliveredKilos = {},
activeStage = nil
activeStage = nil, -- Backwards compatibility for previous version without lanes
activeStageInStageLane = {}
}
local logger = {}
@@ -230,12 +232,29 @@ do
end
---Sets the stage in the persistence table
---@param stageLane string?
---@param stageNumber number
Persistence.SetActiveStage = function(stageNumber)
tables.activeStage = stageNumber
Persistence.SetActiveStage = function(stageLane, stageNumber)
stageLane = stageLane or "nil"
tables.activeStageInStageLane[stageLane] = 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
@@ -255,14 +274,6 @@ 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
+58 -9
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 CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
local StageDrawing = require("classes.stageClasses.drawings.StageDrawing")
---@class DatabaseTables
@@ -27,7 +27,7 @@ local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
---@field MissionZoneData table<string, MissionZoneData>
---@field FarpZoneData table<string,FarpZoneData>
---@field missionCodes table<string, boolean>
---@field CustomDrawings Array<CustomDrawing>
---@field StageDrawings Array<StageDrawing>
---@class CapRoute
---@field zones Array<SpearheadTriggerZone>
@@ -58,6 +58,7 @@ local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
---@class BlueSamData
---@field groups Array<string>
---@field buildingKilos number?
---@field briefing string?
---@class MissionZoneData
---@field ZoneName string
@@ -74,6 +75,7 @@ local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
---@field padNames Array<string>
---@field buildingKilos number?
---@field supplyHubNames Array<string>
---@field briefing string?
---@class Database
---@field private _tables DatabaseTables
@@ -107,7 +109,7 @@ function Database.New(Logger)
FarpZoneData = {},
missionCodes = {},
SupplyHubZones = {},
CustomDrawings = {}
StageDrawings = {}
}
Database.__index = Database
@@ -231,8 +233,10 @@ function Database.New(Logger)
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 customDrawing = CustomDrawing.New(object)
table.insert(self._tables.CustomDrawings, customDrawing)
local stageDrawing = StageDrawing.New(object)
if stageDrawing then
table.insert(self._tables.StageDrawings, stageDrawing)
end
end
end
end
@@ -614,6 +618,27 @@ function Database:loadBlueSamUnits()
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 i, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then
for key, layer_object in pairs(layer.objects) do
local vec2 = { x = layer_object.mapX, y = layer_object.mapY }
if triggerZone and Util.is2dPointInZone(vec2, triggerZone) then
if layer_object.name and Util.startswith(layer_object.name, "supplybriefing_", true) then
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
@@ -621,12 +646,12 @@ function Database:loadBlueSamUnits()
end
end
---@return Array<CustomDrawing>
function Database:getCustomDrawings()
if self._tables.CustomDrawings == nil then
---@return Array<StageDrawing>
function Database:getStageDrawings()
if self._tables.StageDrawings == nil then
return {}
end
return self._tables.CustomDrawings
return self._tables.StageDrawings
end
---Loads all units, data and briefings
@@ -748,7 +773,31 @@ function Database:loadFarpData()
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 i, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then
for key, layer_object in pairs(layer.objects) do
local vec2 = { x = layer_object.mapX, y = layer_object.mapY }
if triggerZone and Util.is2dPointInZone(vec2, triggerZone) then
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
end
+14 -19
View File
@@ -29,49 +29,44 @@ do
end
---@class OnStageChangedListener
---@field OnStageNumberChanged fun(self:OnStageChangedListener, number:integer)
---@field OnStageNumberChanged fun(self:OnStageChangedListener, number:integer, laneIdentifier:string?)
do -- STAGE NUMBER CHANGED
local OnStageNumberChangedListeners = {}
local OnStageNumberChangedHandlers = {}
---Add a stage zone number changed listener
---@param listener OnStageChangedListener object with function OnStageNumberChanged(self, number)
---@param listener OnStageChangedListener object with function OnStageNumberChanged(self, number, stageLaneIdentifier)
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
---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
---@class OnStageNumberChangeCompleteListener
---@field OnStageNumberChangeComplete fun(self:OnStageNumberChangeCompleteListener, number:integer)
---@field OnStageNumberChangeComplete fun(self:OnStageNumberChangeCompleteListener, number:integer, laneIdentifier:string?)
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)")
warn("Event handler not of type table/object with function OnStageNumberChangeComplete(self, number, laneIdentifier)")
return
end
table.insert(OnStageNumberChangeCompleteListeners, listener)
end
---@param newStageNumber number
SpearheadEvents.PublishStageNumberChanged = function(newStageNumber)
---@param laneIdentifier string?
SpearheadEvents.PublishStageNumberChanged = function(newStageNumber, laneIdentifier)
pcall(function ()
Persistence.SetActiveStage(newStageNumber)
Persistence.SetActiveStage(laneIdentifier, newStageNumber)
end)
for _, callable in pairs(OnStageNumberChangedListeners) do
local succ, err = pcall(function()
callable:OnStageNumberChanged(newStageNumber)
callable:OnStageNumberChanged(newStageNumber, laneIdentifier)
end)
if err then
logError(err)
@@ -79,7 +74,7 @@ do
end
for _, callable in pairs(OnStageNumberChangedHandlers) do
local succ, err = pcall(callable, newStageNumber)
local succ, err = pcall(callable, newStageNumber, laneIdentifier)
if err then
logError(err)
end
@@ -87,7 +82,7 @@ do
for _, callable in pairs(OnStageNumberChangeCompleteListeners) do
local succ, err = pcall(function()
callable:OnStageNumberChangeComplete(newStageNumber)
callable:OnStageNumberChangeComplete(newStageNumber, laneIdentifier)
end)
if err then
logError(err)
+479 -194
View File
@@ -3,22 +3,14 @@ local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil")
local Logger = require("classes.util.Logger")
local MissionEditorWarnings = require("classes.util.MissionEditorWarnings")
local PersistenceConfig = require("classes.configuration.PersistenceConfig")
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 StagesByName = {}
---@type table<string, Array<Stage>>
local StagesByIndex = {}
---@type table<string, Array<Stage>>
local SideStageByIndex = {}
---@type table<string, Array<WaitingStage>>
local WaitingStagesByIndex = {}
local StageRepository = require("classes.stageClasses.StageRepository")
local StageLane = require("classes.stageClasses.StageLane")
local Persistence = require("classes.persistence.Persistence")
local currentStage = -99
@@ -26,25 +18,37 @@ 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 nil
function GlobalStageManager.NewAndStart(database, stageConfig, logLevel, spawnManager)
---@return GlobalStageManager
function GlobalStageManager.new(database, stageConfig, logLevel, spawnManager)
if singletonInstance ~= nil then
return singletonInstance
end
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
@@ -56,169 +60,266 @@ function GlobalStageManager.NewAndStart(database, stageConfig, logLevel, spawnMa
for _, stageName in pairs(database:getStagezoneNames()) do
logger:debug("Found stage zone with name: " .. stageName)
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
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.tableLength(split) < 3 then
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a stage name")
end
if parseResult.stageType == "PrimaryStage" then
local stage = PrimaryStage.New(database, stageConfig, logger, initData, spawnManager)
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
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)
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 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
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
end
if anyIncomplete == false and self.stageConfig.isAutoStages == true then
-- CHECK WAITING STAGES
local nextStage = currentStage + 1
local nextStageIndex = stageIndex + 1
if WaitingStagesByIndex[tostring(nextStage)] then
for _, waitingStage in pairs(WaitingStagesByIndex[tostring(nextStage)]) do
if waitingStage:IsActive() == false then
waitingStage:ActivateStage()
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)
end
end
end
else
-- COMPLETION IN A SIDE LANE
local anyWaiting = false
if WaitingStagesByIndex[tostring(nextStage)] then
for _, waitingStage in pairs(WaitingStagesByIndex[tostring(nextStage)]) do
if waitingStage:IsComplete() == false then
anyWaiting = true
-- 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")
end
end
end
if anyWaiting == false then
local newStageNumber = currentStage + 1
self.logger:debug("Setting next stage to: " .. tostring(newStageNumber))
Events.PublishStageNumberChanged(newStageNumber)
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
end
return stageLane:IsStageIndexComplete(stageNumber)
end
---@public
function GlobalStageManager:OnStageNumberChanged(stageNumber)
self.logger:debug("Stage number changed to: " .. tostring(stageNumber))
currentStage = stageNumber
function GlobalStageManager:OnStageNumberChanged(stageNumber, stageLaneIdentifier)
-- only react on "main" lane changes, ignore other lanes for now
if stageLaneIdentifier ~= nil then return end
self.logger:debug("Stage number changed to: " .. tostring(stageNumber))
self:UpdateDrawings(stageNumber)
end
function GlobalStageManager:OnStageNumberChangeComplete(stageNumber)
function GlobalStageManager:OnStageNumberChangeComplete(stageNumber, stageLaneIdentifier)
-- only react on "main" lane changes, ignore other lanes for now
if stageLaneIdentifier ~= nil then return end
self.logger:debug("Stage number change complete to: " .. tostring(stageNumber))
local groups = {}
@@ -236,83 +337,267 @@ function GlobalStageManager:OnStageNumberChangeComplete(stageNumber)
end
---@private
function GlobalStageManager:UpdateDrawings(stageNumber)
---@param stageNumber number
---@param stageLaneIdentifier string?
function GlobalStageManager:UpdateDrawings(stageNumber, stageLaneIdentifier)
self.logger:debug("Updating custom drawings for stage number: " .. tostring(stageNumber))
local drawings = self.database:getCustomDrawings()
local drawings = self.database:getStageDrawings()
for _, drawing in pairs(drawings) do
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()
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
end
end
end
GlobalStageManager.printFullOverview = function ()
function GlobalStageManager:PrintMermaidStage()
local lanes = self._stageRepository:getAllStageLanes()
local logger = Logger.new("StageOverview", "INFO")
logger:info("Stage overview:")
local nodes = {}
local edges = {}
local stageIndicesByLane = {} -- Track all stage indices per lane
local mainLaneId = "default"
local max = 0
local lines = {}
for stageIndex, stages in pairs(StagesByIndex) do
-- 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
}
local laneColorMap = {} -- Map lane ID to color
local colorIndex = 1
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()
totalStrike = totalStrike + strike
totalbai = totalbai + bai
totaldead = totaldead + dead
totalCas = totalCas + cas
totalMissions = totalMissions + strike + dead + bai + cas
-- Assign color to this lane
laneColorMap[laneId] = laneColors[colorIndex]
colorIndex = colorIndex + 1
if colorIndex > #laneColors then
colorIndex = 1
end
local index = tonumber(stageIndex)
if index then
if index > max then
max = index
-- 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
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
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
for i = 1, max do
if lines[i] then
logger:info(lines[i])
-- 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))
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]
-- 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]
-- 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)
GlobalStageManager.isStageComplete = function (stageNumber, stageLaneIdentifier)
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
if singletonInstance == nil then
Logger.new("StageManager", "INFO"):warn("GlobalStageManager.isStageComplete called before GlobalStageManager was initialized. Returning nil")
return nil
end
return true
if stageLaneIdentifier then
stageLaneIdentifier = string.lower(stageLaneIdentifier)
end
return singletonInstance:IsStageComplete(stageNumber, stageLaneIdentifier)
end
return GlobalStageManager
@@ -89,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)
BuildableZone.New(self, zone, self._buildableCrateKilos or 0, "SAM_CRATE", self._blueGroups, logger, database, blueSamData.briefing)
end
return self
@@ -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)
BuildableZone.New(self, zone, farpData.buildingKilos or 0, "FARP_CRATE", self._groups, logger, database, farpData.briefing)
end
end
self:Deactivate()
@@ -20,7 +20,8 @@ BuildableZone.__index = BuildableZone
---@param database Database
---@param crateType SupplyType
---@param logger Logger
function BuildableZone:New(targetZone, kilosRequired, crateType, buildableGroups, logger, database)
---@param briefing string?
function BuildableZone:New(targetZone, kilosRequired, crateType, buildableGroups, logger, database, briefing)
self._targetZone = targetZone
self._requiredKilos = kilosRequired or 0
self._buildableGroups = buildableGroups or {}
@@ -69,7 +70,7 @@ 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)
self._buildableMission = BuildableMission.new(database, logger, targetZone, noLandingZone, kilosRequired, crateType, briefing)
self._buildableMission:AddOnCrateDroppedOfListener(self)
else
self._buildableMission = nil
+178
View File
@@ -0,0 +1,178 @@
---@class StageLane
---@field private _isDefaultStageLane boolean
---@field private _stageLaneIdentifier string
---@field private _activeStageIndex number
---@field private _stagesInLaneByIndex table<number, Array<Stage>>
---@field private _chapterStarts table<number, boolean>?
---@field private _stageLaneState StageLaneState
---@field private _maxStageIndex number
local StageLane = {}
StageLane.__index = StageLane
---@alias StageLaneState
---| "InChapter"
---| "BetweenChapters"
---| "Completed"
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[stageIndex] then
self._stagesInLaneByIndex[stageIndex] = {}
end
if not self._maxStageIndex or stageIndex > self._maxStageIndex then
self._maxStageIndex = stageIndex
end
table.insert(self._stagesInLaneByIndex[stageIndex], stage)
end
---@param stageNumber number
---@return boolean?
function StageLane:IsStageIndexComplete(stageNumber)
local stages = self._stagesInLaneByIndex[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[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
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
local stageIndices = {}
for stageIndex, _ in pairs(self._stagesInLaneByIndex) do
table.insert(stageIndices, tonumber(stageIndex))
end
table.sort(stageIndices)
self._chapterStarts = self._chapterStarts or {}
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[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
@@ -0,0 +1,60 @@
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,12 +13,19 @@ 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>
@@ -33,9 +40,11 @@ local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelpe
--- @field supplyHubs Array<SupplyHub>
--- @class StageInitData
--- @field type StageType
--- @field stageZoneName string
--- @field stageNumber integer
--- @field stageDisplayName string
--- @field stageLaneIdentifier string?
--- @class StageCompleteListener
@@ -45,13 +54,15 @@ local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelpe
--- @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>
@@ -63,14 +74,6 @@ 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 },
@@ -86,15 +89,22 @@ Stage.StageColors = {
---@param initData StageInitData
---@param missionPriority MissionPriority
---@param spawnManager SpawnManager
---@param stageType StageType
---@return Stage
function Stage:superNew(database, stageConfig, logger, initData, missionPriority, spawnManager)
function Stage:superNew(database, stageConfig, logger, initData, stageType, missionPriority, spawnManager)
logger:debug("[BaseStage] Initiating stage with name: " .. initData.stageZoneName)
self._currentStageState = Stage.CurrentStageState.INACTIVE
self._currentStageState = StageState.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._currentStageState = Stage.CurrentStageState.INACTIVE
self._stageType = stageType
self.OnPostStageComplete = nil
self.OnPostBlueActivated = nil
@@ -115,7 +125,6 @@ function Stage:superNew(database, stageConfig, logger, initData, missionPriority
supplyHubs = {}
}
self._activeStage = -99
self._stageConfig = stageConfig or {}
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
@@ -273,7 +282,7 @@ end
---@return boolean
function Stage:IsComplete()
if self._currentStageState == Stage.CurrentStageState.BLUE then return true end
if self._currentStageState >= StageState.Blue then return true end
for i, mission in pairs(self._db.sams) do
local state = mission:getState()
@@ -288,14 +297,44 @@ function Stage:IsComplete()
return false
end
end
self._currentStageState = Stage.CurrentStageState.BLUE
return true
end
---@return boolean
function Stage:IsActive()
return self._currentStageState == Stage.CurrentStageState.ACTIVE
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
function Stage:GetMissions()
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
end
---comment
@@ -358,7 +397,7 @@ function Stage:IsMissionComplete(missionName)
return mission:getState() == "COMPLETED"
end
---private use only
-- private usage only
function Stage:NotifyComplete()
self._logger:info("Stage complete: " .. (self.stageName or self.stageNumber or "unknown"))
@@ -381,19 +420,22 @@ end
---Activates all SAMS, Airbase units etc all at once.
function Stage:PreActivate()
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
for _, airbase in pairs(self._db.airbases) do
airbase:ActivateRedStage()
if self._currentStageState >= StageState.PreActivated then
return
end
self._currentStageState = StageState.PreActivated
for key, mission in pairs(self._db.sams) do
if mission then
mission:SpawnInactive()
end
end
for _, airbase in pairs(self._db.airbases) do
airbase:ActivateRedStage()
end
self:MarkStage()
end
@@ -407,22 +449,22 @@ function Stage:MarkStage()
end
if self._stageConfig.isDrawStagesEnabled == false then return end
if self._stageConfig.isDrawPreActivatedEnabled == false and self._currentStageState == Stage.CurrentStageState.PREACTIVATED then
if self._stageConfig.isDrawPreActivatedEnabled == false and self._currentStageState == StageState.PreActivated then
return
end
if self._customDrawing then
self._customDrawing:UpdateDrawingObject(function(drawingObject)
local drawing = drawingObject --[[@as Polygon]]
if self._currentStageState == Stage.CurrentStageState.ACTIVE then
if self._currentStageState == StageState.Activated then
drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_ACTIVE)
drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_ACTIVE)
drawing.style = "dot dash"
elseif self._currentStageState == Stage.CurrentStageState.PREACTIVATED then
elseif self._currentStageState == StageState.PreActivated then
drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_PREACTIVE)
drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.RED_PREACTIVE)
drawing.style = "no line"
elseif self._currentStageState == Stage.CurrentStageState.BLUE then
elseif self._currentStageState == StageState.Blue then
drawing.fillColorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.BLUE)
drawing.colorString = DrawingHelper.ColorTableToColorString(Stage.StageColors.BLUE)
drawing.style = "two dash"
@@ -440,10 +482,13 @@ function Stage:MarkStage()
end
function Stage:ActivateStage()
self._currentStageState = Stage.CurrentStageState.ACTIVE
if self._currentStageState >= StageState.Activated then
return
end
self:PreActivate()
self._currentStageState = StageState.Activated
pcall(function()
self:MarkStage()
end)
@@ -471,6 +516,10 @@ 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
@@ -483,43 +532,60 @@ end
---comment
---@param self Stage
---@param number integer
function Stage:OnStageNumberChanged(number)
---@param stageLaneIdentifier string?
function Stage:OnStageNumberChanged(number, stageLaneIdentifier)
if self._activeStage == number then --only activate once for a stage
return
---@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
end
local previousActive = self._activeStage
self._activeStage = number
---@return boolean
local needsActivation = function()
if self._stageLaneIdentifier == stageLaneIdentifier and self.stageNumber == number then
return true
end
return false
end
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
---@return boolean
local needsBlueActivation = function()
if self._stageLaneIdentifier == stageLaneIdentifier and self.stageNumber < number then
return true
end
return false
end
if needsPreActivation() == true then
self:PreActivate()
end
if number == self.stageNumber then
if needsActivation() == true then
self:ActivateStage()
if self._db and self._db.stageBriefing then
self._missionCommandsHelper:AddStageBriefing(self.zoneName, self._db.stageBriefing)
end
end
if previousActive <= self.stageNumber then
if number > self.stageNumber then
self:ActivateBlueStage()
end
if needsBlueActivation() == true then
self:ActivateBlueStage()
end
if number > self.stageNumber then
self._missionCommandsHelper:RemoveStageBriefing(self.zoneName)
end
end
function Stage:GetBriefing()
return "Briefing For "
end
---@param self Stage
@@ -528,7 +594,6 @@ Stage.OnMissionComplete = function(self, mission)
self:CheckAndUpdateSelf()
end
---private use only
function Stage:ActivateBlueGroups()
@@ -590,8 +655,12 @@ 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 = Stage.CurrentStageState.BLUE
self._currentStageState = StageState.Blue
for _, mission in pairs(self._db.missions) do
mission:SpawnPersistedState()
end
@@ -615,6 +684,8 @@ function Stage:ActivateBlueStage()
return nil
end
self._missionCommandsHelper:RemoveStageBriefing(self.zoneName)
timer.scheduleFunction(ActivateBlueAsync, self, timer.getTime() + 3)
end
@@ -0,0 +1,12 @@
--- 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
@@ -19,7 +19,7 @@ function ExtraStage.New(database, stageConfig, logger, initData, spawnManager)
setmetatable(ExtraStage, Stage)
local self = setmetatable({}, { __index = ExtraStage }) --[[@as ExtraStage]]
self:superNew(database, stageConfig, logger, initData, "secondary", spawnManager)
self:superNew(database, stageConfig, logger, initData, "ExtraStage", "secondary", spawnManager)
self.OnPostBlueActivated = function (selfStage)
@@ -36,7 +36,12 @@ end
---comment
---@param self ExtraStage
---@param number integer
function ExtraStage:OnStageNumberChanged(number)
---@param stageLaneIdentifier string?
function ExtraStage:OnStageNumberChanged(number, stageLaneIdentifier)
if stageLaneIdentifier ~= self._stageLaneIdentifier then
return
end
if self._activeStage == number then --only activate once for a stage
return
@@ -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, "primary", spawnManager)
self:superNew(database, stageConfig, logger, initData, "PrimaryStage", "primary", spawnManager)
return self
end
@@ -4,28 +4,24 @@ 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 WaitingStageInitData
---@param initData StageInitData
---@param spawnManager SpawnManager
---@param waitingSeconds integer
---@return WaitingStage
function WaitingStage.New(database, stageConfig, logger, initData, spawnManager)
function WaitingStage.New(database, stageConfig, logger, initData, waitingSeconds, spawnManager)
setmetatable(WaitingStage, Stage)
local self = setmetatable({}, { __index = WaitingStage }) --[[@as WaitingStage]]
self:superNew(database, stageConfig, logger, initData, "none", spawnManager)
self:superNew(database, stageConfig, logger, initData, "WaitingStage", "none", spawnManager)
self._waitTimeSeconds = 5
if initData.waitingSeconds and initData.waitingSeconds > 5 then self._waitTimeSeconds = initData.waitingSeconds end
if waitingSeconds and waitingSeconds > 5 then self._waitTimeSeconds = waitingSeconds end
self._startTime = nil
self.CheckContinuousAsync = function (selfA, time)
@@ -1,38 +1,58 @@
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
local Util = require("classes.util.Util")
local Logger = require("classes.util.Logger")
local MissionEditorWarnings = require("classes.util.MissionEditorWarnings")
local drawingLogger = Logger.new("CustomDrawing")
---@class CustomDrawing
---@field private _id integer?
---@field private _drawingObject DrawingObject
---@field private _startingStage number
---@field private _removeAtStage number
---@field private _logger Logger
---@field protected _IDs Array<integer>?
---@field protected _name string?
---@field protected _drawingObject DrawingObject
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._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
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
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
@@ -65,7 +85,6 @@ function CustomDrawing.FromZone(zone, colorString, fillColorString, lineStyle, l
end
if zone.zone_type == "Polygon" then
---@type Free
local drawingObject = {
mapX = 0,
@@ -90,27 +109,25 @@ function CustomDrawing:GetName()
return self._drawingObject.name
end
---@return number start
---@return number stop
function CustomDrawing:GetStartAndStop()
return self._startingStage, self._removeAtStage
end
function CustomDrawing:Draw()
self._logger:debug("Drawing custom drawing with ID " .. tostring(self._id))
drawingLogger:debug("Drawing custom drawing with IDs " .. table.concat(self._IDs, ", "))
if self._id ~= nil then
DrawingHelper.Remove(self._id)
if self._IDs ~= nil then
for _, id in ipairs(self._IDs) do
DrawingHelper.Remove(id)
end
end
self._id = DrawingHelper.Draw(self._drawingObject, self._logger)
self._IDs = DrawingHelper.Draw(self._drawingObject)
end
function CustomDrawing:Remove()
if self._id ~= nil then
self._logger:debug("Removing custom drawing with ID " .. tostring(self._id))
DrawingHelper.Remove(self._id)
self._id = nil
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 = {}
end
end
@@ -0,0 +1,93 @@
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,57 +1,45 @@
local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil")
local Logger = require("classes.util.Logger")
local GlobalConfig = require("classes.configuration.GlobalConfig")
---@type LogLevel
local level = "INFO"
if GlobalConfig:isDebugEnabled() then
if GlobalConfig.New():isDebugEnabled() then
level = "DEBUG"
end
local drawingLogger = Logger.new("DrawingHelper", level)
local logger = Logger.new("DrawingHelper", level)
---@class DrawingHelper
local DrawingHelper = {}
DrawingHelper.__index = DrawingHelper
local customDrawingIdIncrementer = 4210
---@param object DrawingObject
---@param logger Logger?
---@return integer? id
function DrawingHelper.Draw(object, logger)
---@return Array<integer> id
function DrawingHelper.Draw(object)
if object == nil then
if logger then
logger:warn("DrawingHelper.Draw called with nil object")
end
return nil
end
local id = DrawingHelper.GetAndAddId()
if logger then
logger:debug("Drawing object with ID " .. tostring(id) .. " and primitive type: " .. tostring(object.primitiveType))
return {}
end
if(object.primitiveType == "Polygon") then
DrawingHelper.DrawPolygon(object--[[@as Polygon]], id, logger)
return DrawingHelper.DrawPolygon(object--[[@as Polygon]])
elseif(object.primitiveType == "Line") then
DrawingHelper.DrawLine(object--[[@as Line]], id, logger)
return DrawingHelper.DrawLine(object--[[@as Line]])
elseif(object.primitiveType == "TextBox") then
DrawingHelper.DrawTextBox(object--[[@as TextBox]], id)
return DrawingHelper.DrawTextBox(object--[[@as TextBox]])
else
if logger then
logger:warn("Unknown primitive type: " .. tostring(object.primitiveType))
end
logger:warn("Unknown primitive type: " .. tostring(object.primitiveType))
end
return id
return {}
end
function DrawingHelper.GetAndAddId()
customDrawingIdIncrementer = customDrawingIdIncrementer + 1
return customDrawingIdIncrementer
return DcsUtil.GetNextDrawID()
end
---@private
@@ -62,8 +50,7 @@ end
---@param lineColor table
---@param lineStyle LineType
---@param lineThickness number
---@param logger Logger?
local function MarkupToAll(shapeID, drawID, points, fillColor, lineColor, lineStyle, lineThickness, logger)
local function MarkupToAll(shapeID, drawID, points, fillColor, lineColor, lineStyle, lineThickness)
if lineThickness == nil or lineThickness <= 0 then
lineStyle = 0
@@ -76,23 +63,19 @@ local function MarkupToAll(shapeID, drawID, points, fillColor, lineColor, lineS
end
functionString = functionString .. "{0,1,0,1}, {0,1,0,1}, " .. lineStyle .. ")"
if logger then
logger:debug("Drawing complex drawing with ID " .. tostring(drawID) .. " and function string: " .. functionString)
end
logger:debug("Drawing complex drawing with ID " .. tostring(drawID) .. " and function string: " .. functionString)
---@diagnostic disable-next-line: deprecated
local f, err = loadstring(functionString)
if f then
f()
else
env.error("Something failed when drawing complex drawing" .. err)
logger: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))
if fillColor then
trigger.action.setMarkupColorFill(drawID, fillColor)
end
trigger.action.setMarkupColorFill(drawID, fillColor)
trigger.action.setMarkupColor(drawID, lineColor)
trigger.action.setMarkupTypeLine(drawID, lineStyle)
@@ -100,13 +83,14 @@ end
---@private
---@param object Polygon
---@param id integer
---@param logger Logger?
function DrawingHelper.DrawPolygon(object, id, logger)
---@return Array<integer>
function DrawingHelper.DrawPolygon(object)
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 }
@@ -117,8 +101,8 @@ function DrawingHelper.DrawPolygon(object, id, logger)
end
---@param oval Oval
---@param logger Logger?
local function DrawOval(oval, logger)
---@return integer?, Array<CustomDrawing>?
local function DrawOval(oval)
---@type Array<Vec3>
local points = {}
local pointsNo = 30
@@ -134,12 +118,11 @@ function DrawingHelper.DrawPolygon(object, id, logger)
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, logger)
MarkupToAll(7, id, points, fillColor, color, lineStyle, oval.thickness)
end
---@param free Free
---@param logger Logger?
local function DrawFree(free, logger)
local function DrawFree(free)
local fillColor = DrawingHelper.ColorToColorTable(free.fillColorString)
local color = DrawingHelper.ColorToColorTable(free.colorString)
local lineStyle = DrawingHelper.ToLineStyleInteger(free.style)
@@ -163,7 +146,7 @@ function DrawingHelper.DrawPolygon(object, id, logger)
end
end
MarkupToAll(7, id, points, fillColor, color, lineStyle, free.thickness, logger)
MarkupToAll(7, id, points, fillColor, color, lineStyle, free.thickness)
end
---@param rect Rect
@@ -175,19 +158,16 @@ function DrawingHelper.DrawPolygon(object, id, logger)
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
---@param logger Logger?
local function DrawArrow(arrow, logger)
local function DrawArrow(arrow)
local fillColor = DrawingHelper.ColorToColorTable(arrow.fillColorString)
local color = DrawingHelper.ColorToColorTable(arrow.colorString)
local lineStyle = DrawingHelper.ToLineStyleInteger(arrow.style)
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
logger:debug("Drawing arrow with start point: " .. tostring(arrow.mapX) .. ", " .. tostring(arrow.mapY) .. " and angle: " .. tostring(arrow.angle) .. " and length: " .. tostring(arrow.length))
local endPoint = { x = arrow.mapX, y = 0, z = arrow.mapY }
local rad = math.rad(arrow.angle or 0)
@@ -203,44 +183,56 @@ function DrawingHelper.DrawPolygon(object, id, logger)
if object.polygonMode == "circle" then
DrawCircle(object--[[@as Circle]])
elseif object.polygonMode == "oval" then
DrawOval(object--[[@as Oval]], logger)
DrawOval(object--[[@as Oval]])
elseif object.polygonMode == "free" then
DrawFree(object--[[@as Free]], logger)
DrawFree(object--[[@as Free]])
elseif object.polygonMode == "rect" then
DrawRect(object--[[@as Rect]])
elseif object.polygonMode == "arrow" then
DrawArrow(object--[[@as Arrow]], logger)
DrawArrow(object--[[@as Arrow]])
end
return {id}
end
---@private
---@param object Line
---@param id integer
---@param logger Logger?
function DrawingHelper.DrawLine(object, id, logger)
---@return Array<integer>
function DrawingHelper.DrawLine(object)
---@type Array<Vec3>
local points = {}
local ids = {}
for _, point in ipairs(object.points) do
table.insert(points, { x = point.x, y = 0, z = point.y } )
table.insert(points, { x = object.mapX + point.x, y = 0, z = object.mapY + point.y } )
end
local color = DrawingHelper.ColorToColorTable(object.colorString)
local lineStyle = DrawingHelper.ToLineStyleInteger(object.style)
MarkupToAll(1, id, points, color, color, lineStyle, object.thickness, logger)
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
end
---@private
---@param object TextBox
---@param id integer
function DrawingHelper.DrawTextBox(object, id)
---@return Array<integer>
function DrawingHelper.DrawTextBox(object)
local id = DrawingHelper.GetAndAddId()
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)
@@ -252,7 +244,7 @@ end
function DrawingHelper.ColorToColorTable(hexStr)
if hexStr == nil then
drawingLogger:warn("ColorToColorTable called with nil hexStr, returning default color {0, 0, 0, 0}")
logger:warn("ColorToColorTable called with nil hexStr, returning default color {0, 0, 0, 0}")
return { 0, 0, 0, 0 }
end
@@ -266,11 +258,11 @@ function DrawingHelper.ColorToColorTable(hexStr)
return { r, g , b , a }
end
---@param rgba table
---@param rgba table { r, g, b, a }
---@return string
function DrawingHelper.ColorTableToColorString(rgba)
if rgba == nil or #rgba < 4 or rgba[1] == nil or rgba[2] == nil or rgba[3] == nil or rgba[4] == nil then
drawingLogger:warn("ColorTableToColorString called with invalid rgba table, returning default color string '0x00000000'")
logger:warn("ColorTableToColorString called with invalid rgba table, returning default color string '0x00000000'")
return "0x00000000"
end
@@ -293,11 +285,11 @@ function DrawingHelper.ToLineStyleInteger(lineStyle)
return 2
elseif lineStyle == "dotted" then
return 3
elseif lineStyle == "dot dash" then
elseif lineStyle == "dotdash" then
return 4
elseif lineStyle == "long dash" then
elseif lineStyle == "longdash" then
return 5
elseif lineStyle == "two dash" then
elseif lineStyle == "twodash" then
return 6
else
return 0
@@ -314,11 +306,11 @@ function DrawingHelper.ToLineStyleString(lineStyle)
elseif lineStyle == 3 then
return "dotted"
elseif lineStyle == 4 then
return "dot dash"
return "dotdash"
elseif lineStyle == 5 then
return "long dash"
return "longdash"
elseif lineStyle == 6 then
return "two dash"
return "twodash"
else
return "no line"
end
@@ -21,9 +21,18 @@ 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
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)
@@ -33,7 +42,8 @@ BuildableMission.__index = BuildableMission
---@param requiredCrateType SupplyType
---@param noLandingZone SpearheadTriggerZone?
---@param logger Logger
function BuildableMission.new(database, logger, targetZone, noLandingZone, requiredKilos, requiredCrateType)
---@param briefing string?
function BuildableMission.new(database, logger, targetZone, noLandingZone, requiredKilos, requiredCrateType, briefing)
setmetatable(BuildableMission, Mission)
@@ -43,6 +53,7 @@ function BuildableMission.new(database, logger, targetZone, noLandingZone, requi
self._database = database
self._requiredKilos = requiredKilos
self._droppedKilos = 0
self._briefing = briefing
self._noLandingZone = noLandingZone
@@ -64,7 +75,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"
end
local type = "site"
if requiredCrateType == "SAM_CRATE" then
@@ -81,7 +98,6 @@ function BuildableMission.new(database, logger, targetZone, noLandingZone, requi
self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate()
self._state = "NEW"
self.location = targetZone.location
self.missionType = "LOGISTICS"
@@ -116,11 +132,16 @@ 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 ..
"\n \n" ..
"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 ..
briefingPart ..
"\n\n" ..
"\nKilos still required: " .. self._requiredKilos - self._droppedKilos ..
"\n\n" ..
@@ -18,7 +18,7 @@ local RunwayStrikeMission = {}
---@field corners Array<Vec2>
---@field kilosHit number
---@field repairGroups Array<string>
---@field drawID number?
---@field drawID Array<number>
---@param runway Runway
@@ -172,8 +172,10 @@ function RunwayStrikeMission:Draw()
runwaySection.drawID = DrawingHelper.Draw(drawObject)
else
DcsUtil.SetFillColor(runwaySection.drawID, fillColor)
DcsUtil.SetLineColor(runwaySection.drawID, lineColor)
for _, drawID in pairs(runwaySection.drawID) do
DcsUtil.SetFillColor(drawID, fillColor)
DcsUtil.SetLineColor(drawID, lineColor)
end
end
end
@@ -497,6 +499,7 @@ function RunwayStrikeMission:ToSections(runway, numSections)
corners = corners,
kilosHit = 0,
repairGroups = {},
drawID = {},
}
table.insert(sections, section)
@@ -126,6 +126,11 @@ function Mission:NotifyMissionComplete()
end
function Mission:ForceMissionComplete()
self._state = "COMPLETED"
self:NotifyMissionComplete()
end
function Mission:MarkMissionAreaToGroup(groupId) end
---endregion
+12 -2
View File
@@ -632,13 +632,19 @@ 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)
drawID = drawID + 1
local drawID = DCS_UTIL.GetNextDrawID()
trigger.action.markToGroup(drawID, text, location, groupID, true, nil)
return drawID
end
@@ -648,7 +654,7 @@ do -- INIT DCS_UTIL
---@param location Vec3
---@return integer
function DCS_UTIL.AddMarkToAll(text, location)
drawID = drawID + 1
local drawID = DCS_UTIL.GetNextDrawID()
trigger.action.markToAll(drawID, text, location, true, nil)
return drawID
end
@@ -677,6 +683,10 @@ 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,
+8
View File
@@ -128,6 +128,14 @@ 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
+21 -20
View File
@@ -5,9 +5,11 @@ 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")
@@ -33,19 +35,11 @@ MissionCommandsHelper.getOrCreate() -- initiate
local capConfig = CapConfig:new();
local stageConfig = StageConfig:getInstance();
local startingStage = stageConfig.startingStage or 1
if SpearheadConfig and SpearheadConfig.Persistence and SpearheadConfig.Persistence.enabled == true then
standardLogger:info("Persistence enabled")
local persistenceConfig = PersistenceConfig.new()
if persistenceConfig and persistenceConfig:isEnabled() == true then
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)
@@ -54,21 +48,28 @@ local detectionLogger = Logger.new("DetectionManager", defaultLogLevel)
local detectionManager = DetectionManager.New(detectionLogger)
GlobalCapManager.start(databaseManager, capConfig, detectionManager, stageConfig, defaultLogLevel, spawnManager)
GlobalStageManager.NewAndStart(databaseManager, stageConfig, defaultLogLevel, spawnManager)
local globalStageManager = GlobalStageManager.new(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:printFullOverview()
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