64 Commits
Author SHA1 Message Date
dutchie031 77f4c41a6b changed releasentes lineending
Publish Beta Release / build (push) Successful in 26s
2026-09-21 22:59:48 +02:00
dutchie031 07a234489f changed releasentes lineending 2026-09-21 22:59:40 +02:00
dutchie031 e363b3fe94 changed releasentes lineending
Publish Beta Release / build (push) Successful in 24s
2026-09-21 22:53:39 +02:00
dutchie031 b68f95b748 update release notes
Publish Beta Release / build (push) Successful in 21s
2026-09-21 22:35:55 +02:00
dutchie031 8341a5fb6d update release notes 2026-09-21 22:34:14 +02:00
dutchie031 e32518f48d updated rleease notes
Publish Beta Release / build (push) Successful in 24s
2026-09-20 22:32:18 +02:00
dutchie031 2e7a930473 Feature/deep strike (#48)
Update Docs / prepare-docs (push) Successful in 42s
Publish Beta Release / build (push) Successful in 41s
Update Docs / build-image (push) Successful in 19s
Update Docs / deploy-container (push) Successful in 21s
Closes #42

Reviewed-on: #48
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-09-20 16:56:30 +00:00
dutchie031 cbccedf710 updated pipeline
Publish Beta Release / build (push) Successful in 28s
2026-09-20 18:28:56 +02:00
dutchie031 e986887dd8 updated pipeline
Publish Release / build (push) Successful in 41s
2026-09-20 18:28:29 +02:00
dutchie031 9549e8c48e added lua check CI step (#49)
Publish Release / build (push) Failing after 3s
Reviewed-on: #49
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-09-20 16:08:04 +00:00
dutchie031 9b2540d461 Fixed an issue
Publish Release / build (push) Successful in 30s
2026-09-09 22:06:29 +02:00
dutchie031 a1355ff05d fixed minor bug
Publish Release / build (push) Failing after 12s
2026-09-08 07:44:24 +02:00
dutchie031 2e636b2ded minor udpdate to docs
Update Docs / prepare-docs (push) Successful in 30s
Update Docs / build-image (push) Successful in 9s
Update Docs / deploy-container (push) Successful in 13s
2026-09-08 07:25:29 +02:00
dutchie031 488a492e39 Custom briefings for BlueSam and FARPS (#44)
Update Docs / prepare-docs (push) Successful in 32s
Publish Release / build (push) Successful in 34s
Update Docs / build-image (push) Successful in 15s
Update Docs / deploy-container (push) Successful in 12s
Fixes #37

<img width="761" alt="image.png" src="attachments/bdc9d61a-8bbc-41f3-8de4-b97edfae28da">
<img width="1095" alt="image.png" src="attachments/9c77798d-f371-4035-84dd-7c60cb79d91f">

Reviewed-on: #44
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-09-07 17:23:30 +00: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 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 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 ee5a591c8e fixed release pipeline
Publish Release / build (push) Successful in 1m8s
2026-08-20 17:03:58 +02:00
dutchie031 dd0d16cd1a fixed release pipeline
Publish Release / build (push) Successful in 17s
2026-08-20 17:02:05 +02:00
dutchie031 e67200bddd fixed release pipeline 2026-08-20 17:01:35 +02:00
dutchie031 f7ff367f5b Develop to Main (#32)
Reviewed-on: #32
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-08-20 15:00:07 +00: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
dutchie031 7475c03db2 updated docs 2026-07-30 14:59:28 +02:00
dutchie031 6390aeb527 updated docs 2026-07-30 14:55:53 +02:00
dutchie031 109db7cc6e updated docs
Update Docs / prepare-docs (push) Successful in 36s
Update Docs / build-image (push) Successful in 17s
Update Docs / deploy-container (push) Failing after 16s
2026-07-30 14:53:44 +02:00
dutchie031 5277c5e566 updated docs
Update Docs / prepare-docs (push) Successful in 19s
Update Docs / build-image (push) Canceled after 0s
Update Docs / deploy-container (push) Canceled after 0s
2026-07-30 14:53:33 +02:00
dutchie031 e3c53c253d pull policy updated 2026-07-30 14:40:02 +02:00
dutchie031 f6012031e2 pull policy updated
Update Docs / prepare-docs (push) Successful in 34s
Update Docs / build-image (push) Successful in 22s
Update Docs / deploy-container (push) Successful in 18s
2026-07-30 14:37:10 +02:00
dutchie031 09bc2629bb Merge branch 'main' of https://git.dutchie031.com/Spearhead/spearhead 2026-07-30 14:29:51 +02:00
dutchie031 87578b0cb2 Feature/automation (#6)
Update Docs / prepare-docs (push) Successful in 1m19s
Update Docs / build-image (push) Successful in 18s
Update Docs / deploy-container (push) Successful in 18s
Reviewed-on: #6
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-07-30 12:26:54 +00:00
dutchie031 d11860d696 updated releasenotes 2026-07-29 19:22:07 +02:00
dutchie031 a354022035 custom drawing updates now updated on Stage Change not only on completion (#5)
Publish Release / build (push) Successful in 1m39s
Reviewed-on: #5
Co-authored-by: dutchie031 <timrorije@gmail.com>
2026-07-29 17:16:05 +00:00
dutchie031 3ce1b60d09 Merge pull request 'Feature/automation' (#4) from feature/automation into main
Update Docs / prepare-docs (push) Successful in 11s
Deploy static content to Pages / deploy (push) Failing after 15s
Update Docs / build-image (push) Canceled after 50s
Reviewed-on: #4
2026-07-27 15:15:14 +00:00
dutchie031 4fb7d799d5 Merge branch 'main' into feature/automation 2026-07-27 15:15:02 +00:00
dutchie031 9a04e2f07a work in progress on publishing docs 2026-07-26 23:51:36 +02:00
dutchie031 55138d7e2a work in progress on publishing docs 2026-07-26 23:31:44 +02:00
dutchie031 5a906cdea0 work in progress on publishing docs 2026-07-26 23:24:07 +02:00
dutchie031 44942e5b7a Merge pull request 'fixed multiline' (#3) from feature/automation into main
Reviewed-on: #3
2026-07-26 15:57:05 +00:00
dutchie031 5859c889e7 Merge branch 'main' of https://git.dutchie031.com/Spearhead/spearhead 2026-07-26 17:55:20 +02:00
dutchie031 2cec4de32e fixed multiline 2026-07-26 17:53:32 +02:00
dutchie031 c052b1a14a Merge pull request 'added release notes automation' (#2) from feature/automation into main
Reviewed-on: #2
2026-07-26 15:51:07 +00:00
dutchie031 4b52d322de added release notes automation 2026-07-26 17:50:31 +02:00
dutchie031 8a47b0a35f Merge pull request 'added release notes automation' (#1) from feature/automation into main
Reviewed-on: #1
2026-07-26 15:04:55 +00:00
dutchie031 088c14e4d6 added release notes automation 2026-07-26 17:03:28 +02:00
127 changed files with 7648 additions and 2251 deletions
+3
View File
@@ -0,0 +1,3 @@
FROM nginx:latest
COPY ./web /usr/share/nginx/html

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 193 KiB

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 296 KiB

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

Before

Width:  |  Height:  |  Size: 330 KiB

After

Width:  |  Height:  |  Size: 330 KiB

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 2.8 MiB

Before

Width:  |  Height:  |  Size: 2.9 MiB

After

Width:  |  Height:  |  Size: 2.9 MiB

Before

Width:  |  Height:  |  Size: 332 KiB

After

Width:  |  Height:  |  Size: 332 KiB

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 260 KiB

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

+10
View File
@@ -21,6 +21,9 @@
<main> <main>
<div class="reference-container"> <div class="reference-container">
<div class="side-nav"> <div class="side-nav">
<div id="beta-box-div"></div>
<h4>Quick Links</h4> <h4>Quick Links</h4>
<ul> <ul>
<li><a href="/pages/tutorials.html" class="side-nav-h2">Tutorials</a></li> <li><a href="/pages/tutorials.html" class="side-nav-h2">Tutorials</a></li>
@@ -59,6 +62,13 @@
<footer> <footer>
<p>&copy; 2025 Spearhead Project</p> <p>&copy; 2025 Spearhead Project</p>
</footer> </footer>
<script>
if (window.location.hostname.toLowerCase() == 'beta.spearhead.rocks') {
betaHtml = '<note-box type="warning" title="Beta Version">You are looking at docs that are released with the beta version.</note-box>';
}
document.getElementById('beta-box-div').innerHTML = betaHtml || '';
</script>
</body> </body>
</html> </html>
@@ -1,29 +1,29 @@
class CodeBlock extends HTMLElement { class CodeBlock extends HTMLElement {
connectedCallback() { connectedCallback() {
// Get the code content as text, preserving whitespace // Get the code content as text, preserving whitespace
let code = this.textContent.replace(/\r\n?/g, "\n"); let code = this.textContent.replace(/\r\n?/g, "\n");
// Remove leading/trailing blank lines // Remove leading/trailing blank lines
code = code.replace(/^\s*\n/, '').replace(/\n\s*$/, ''); code = code.replace(/^\s*\n/, '').replace(/\n\s*$/, '');
// Find leading spaces from the first non-empty line // Find leading spaces from the first non-empty line
const lines = code.split("\n"); const lines = code.split("\n");
const firstLine = lines.find(line => line.trim().length > 0) || ''; const firstLine = lines.find(line => line.trim().length > 0) || '';
const leadingSpaces = firstLine.match(/^\s*/)[0].length; const leadingSpaces = firstLine.match(/^\s*/)[0].length;
// Remove that many leading spaces from all lines // Remove that many leading spaces from all lines
const stripped = lines.map(line => line.slice(leadingSpaces)).join("\n"); const stripped = lines.map(line => line.slice(leadingSpaces)).join("\n");
// Escape HTML special characters // Escape HTML special characters
const escaped = stripped const escaped = stripped
.replace(/&/g, "&amp;") .replace(/&/g, "&amp;")
.replace(/</g, "&lt;") .replace(/</g, "&lt;")
.replace(/>/g, "&gt;"); .replace(/>/g, "&gt;");
const lang = this.getAttribute('lang') || ''; const lang = this.getAttribute('lang') || '';
const langClass = lang ? `language-${lang}` : ''; const langClass = lang ? `language-${lang}` : '';
this.innerHTML = `<pre class=\"custom-code-block\"><code class=\"${langClass}\">${escaped}</code></pre>`; this.innerHTML = `<pre class=\"custom-code-block\"><code class=\"${langClass}\">${escaped}</code></pre>`;
// If Prism or highlight.js is present, trigger highlighting // If Prism or highlight.js is present, trigger highlighting
if (window.Prism && Prism.highlightElement) { if (window.Prism && Prism.highlightElement) {
Prism.highlightElement(this.querySelector('code')); Prism.highlightElement(this.querySelector('code'));
} else if (window.hljs && hljs.highlightElement) { } else if (window.hljs && hljs.highlightElement) {
hljs.highlightElement(this.querySelector('code')); hljs.highlightElement(this.querySelector('code'));
} }
} }
} }
customElements.define('code-block', CodeBlock); customElements.define('code-block', CodeBlock);
@@ -1,32 +1,32 @@
class CodeInline extends HTMLElement { class CodeInline extends HTMLElement {
connectedCallback() { connectedCallback() {
// Get the code content as text // Get the code content as text
let code = this.textContent; let code = this.textContent;
// Escape HTML special characters // Escape HTML special characters
code = code code = code
.replace(/&/g, "&amp;") .replace(/&/g, "&amp;")
.replace(/</g, "&lt;") .replace(/</g, "&lt;")
.replace(/>/g, "&gt;"); .replace(/>/g, "&gt;");
// Highlight variables: $variable, {variable}, or %variable% // Highlight variables: $variable, {variable}, or %variable%
code = code.replace(/(\$[a-zA-Z_][\w]*)|(\{[^\}]+\})|(%[^%]+%)/g, match => code = code.replace(/(\$[a-zA-Z_][\w]*)|(\{[^\}]+\})|(%[^%]+%)/g, match =>
`<span class="inline-var">${match}</span>` `<span class="inline-var">${match}</span>`
); );
// Get language attribute for potential syntax highlighting // Get language attribute for potential syntax highlighting
const lang = this.getAttribute('lang') || ''; const lang = this.getAttribute('lang') || '';
const langClass = lang ? `language-${lang}` : ''; const langClass = lang ? `language-${lang}` : '';
this.innerHTML = `<code class="custom-inline-code ${langClass}">${code}</code>`; this.innerHTML = `<code class="custom-inline-code ${langClass}">${code}</code>`;
// If Prism or highlight.js is present, trigger highlighting // If Prism or highlight.js is present, trigger highlighting
if (window.Prism && Prism.highlightElement) { if (window.Prism && Prism.highlightElement) {
Prism.highlightElement(this.querySelector('code')); Prism.highlightElement(this.querySelector('code'));
} else if (window.hljs && hljs.highlightElement) { } else if (window.hljs && hljs.highlightElement) {
hljs.highlightElement(this.querySelector('code')); hljs.highlightElement(this.querySelector('code'));
} }
} }
} }
customElements.define('code-inline', CodeInline); customElements.define('code-inline', CodeInline);
@@ -19,9 +19,10 @@ class Header extends HTMLElement {
<div class="dropdown-content"> <div class="dropdown-content">
<a href="/pages/include-script.html">Include the Script</a> <a href="/pages/include-script.html">Include the Script</a>
<a href="/pages/first-start.html">First Start</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/CAP.html">Advanced: CAP</a>
<a href="/pages/advanced/missions.html">Advanced: Missions</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>
</div> </div>
@@ -38,7 +38,7 @@ class DownloadScript extends HTMLElement {
} }
try { try {
const response = await fetch('https://api.github.com/repos/dutchie031/Spearhead/releases/latest'); const response = await fetch('https://git.dutchie031.com/api/v1/repos/Spearhead/spearhead/releases/latest');
const data = await response.json(); const data = await response.json();
// Cache the data // Cache the data
@@ -49,33 +49,21 @@ class DownloadScript extends HTMLElement {
return null; return null;
} }
} }
async fetchReleaseAssets(url){
try {
const response = await fetch(url);
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching release assets:', error);
return null;
}
}
async connectedCallback() { async connectedCallback() {
var version = "?"; var version = "?";
var timestamp = "?"; var timestamp = "?";
var size = "?"; var size = "?";
var href = "https://github.com/dutchie031/Spearhead/releases" var href = "https://git.dutchie031.com/api/v1/repos/Spearhead/spearhead/releases"
const latestRelease = await this.fetchLatestRelease(); const latestRelease = await this.fetchLatestRelease();
if (latestRelease) { if (latestRelease) {
version = latestRelease.tag_name; version = latestRelease.tag_name;
timestamp = new Date(latestRelease.published_at).toLocaleDateString('en-CA'); timestamp = new Date(latestRelease.published_at).toLocaleDateString('en-CA');
const assets = latestRelease.assets;
const assets = await this.fetchReleaseAssets(latestRelease.assets_url);
if (assets && assets.length > 0) { if (assets && assets.length > 0) {
const spearheadAsset = assets.find(asset => asset.name.includes('spearhead')); const spearheadAsset = assets.find(asset => asset.name.includes('spearhead.v'));
if (spearheadAsset) { if (spearheadAsset) {
size = (spearheadAsset.size / 1024).toFixed(2) + 'kb'; size = (spearheadAsset.size / 1024).toFixed(2) + 'kb';
href = spearheadAsset.browser_download_url; href = spearheadAsset.browser_download_url;
@@ -105,7 +93,7 @@ class DownloadScript extends HTMLElement {
<p> <p>
<span class="version-text">${version}</span> (${timestamp}) <br> <span class="version-text">${version}</span> (${timestamp}) <br>
<a style="text-decoration: underline;" target="_blank" href="${href}">Download</a> (${size}) <br> <a style="text-decoration: underline;" target="_blank" href="${href}">Download</a> (${size}) <br>
<a style="text-decoration: underline;" target="_blank" href="https://github.com/dutchie031/Spearhead/releases">See All Versions here</a> <a style="text-decoration: underline;" target="_blank" href="https://git.dutchie031.com/Spearhead/spearhead/releases">See All Versions here</a>
</p> </p>
</div> </div>
@@ -1,35 +1,35 @@
class Note extends HTMLElement { class Note extends HTMLElement {
connectedCallback() { connectedCallback() {
// Get the note content // Get the note content
const content = this.innerHTML; const content = this.innerHTML;
// Get optional type attribute for different note styles // Get optional type attribute for different note styles
const type = this.getAttribute('type') || 'default'; const type = this.getAttribute('type') || 'default';
// Get optional title attribute // Get optional title attribute
const title = this.getAttribute('title'); const title = this.getAttribute('title');
// Build the note HTML // Build the note HTML
let noteHTML = '<div class="note'; let noteHTML = '<div class="note';
// Add type-specific class if provided // Add type-specific class if provided
if (type !== 'default') { if (type !== 'default') {
noteHTML += ` note-${type}`; noteHTML += ` note-${type}`;
} }
noteHTML += '">'; noteHTML += '">';
// Add title if provided // Add title if provided
if (title) { if (title) {
noteHTML += `<h4 class="note-title">${title}</h4>`; noteHTML += `<h4 class="note-title">${title}</h4>`;
} }
// Add the content // Add the content
noteHTML += content; noteHTML += content;
noteHTML += '</div>'; noteHTML += '</div>';
this.innerHTML = noteHTML; this.innerHTML = noteHTML;
} }
} }
customElements.define('note-box', Note); customElements.define('note-box', Note);
@@ -1,198 +1,207 @@
class Sidebar extends HTMLElement { class Sidebar extends HTMLElement {
constructor() { constructor() {
super(); super();
this.navItems = []; this.navItems = [];
} }
connectedCallback() { connectedCallback() {
this.render(); this.render();
this.scanHeaders(); this.scanHeaders();
this.setupScrollListener(); this.setupScrollListener();
this.highlightActiveSection(); this.highlightActiveSection();
} }
scanHeaders() { scanHeaders() {
// Clear existing nav items // Clear existing nav items
this.navItems = []; this.navItems = [];
// Find all h2, h3, and h4 elements with IDs in the content area // Find all h2, h3, and h4 elements with IDs in the content area
const contentWrapper = document.querySelector('.content-wrapper'); const contentWrapper = document.querySelector('.content-wrapper');
if (!contentWrapper) return; if (!contentWrapper) return;
const headers = contentWrapper.querySelectorAll('h2[id], h3[id], h4[id]'); const headers = contentWrapper.querySelectorAll('h2[id], h3[id], h4[id]');
headers.forEach(header => { headers.forEach(header => {
const id = header.getAttribute('id'); const id = header.getAttribute('id');
const text = header.textContent.trim(); const text = header.textContent.trim();
const level = header.tagName.toLowerCase(); const level = header.tagName.toLowerCase();
this.navItems.push({ this.navItems.push({
id, id,
text, text,
level, level,
element: header element: header
}); });
}); });
this.renderNavigation(); this.renderNavigation();
} }
renderNavigation() { renderNavigation() {
const ul = this.querySelector('ul'); const ul = this.querySelector('ul');
if (!ul) return; if (!ul) return;
// Clear existing content // Clear existing content
ul.innerHTML = ''; ul.innerHTML = '';
let currentH2Li = null; let currentH2Li = null;
let currentH3Li = null; let currentH3Li = null;
this.navItems.forEach(item => { this.navItems.forEach(item => {
if (item.level === 'h2') { if (item.level === 'h2') {
// Create h2 item // Create h2 item
const li = document.createElement('li'); const li = document.createElement('li');
const a = document.createElement('a'); const a = document.createElement('a');
a.href = `#${item.id}`; a.href = `#${item.id}`;
a.className = 'side-nav-h2'; a.className = 'side-nav-h2';
a.textContent = item.text; a.textContent = item.text;
a.addEventListener('click', (e) => this.handleNavClick(e, item.id)); a.addEventListener('click', (e) => this.handleNavClick(e, item.id));
li.appendChild(a); li.appendChild(a);
ul.appendChild(li); ul.appendChild(li);
currentH2Li = li; currentH2Li = li;
currentH3Li = null; currentH3Li = null;
} else if (item.level === 'h3' && currentH2Li) { } else if (item.level === 'h3' && currentH2Li) {
// Create h3 item under current h2 // Create h3 item under current h2
let subUl = currentH2Li.querySelector('ul'); let subUl = currentH2Li.querySelector('ul');
if (!subUl) { if (!subUl) {
subUl = document.createElement('ul'); subUl = document.createElement('ul');
currentH2Li.appendChild(subUl); currentH2Li.appendChild(subUl);
} }
const li = document.createElement('li'); const li = document.createElement('li');
const a = document.createElement('a'); const a = document.createElement('a');
a.href = `#${item.id}`; a.href = `#${item.id}`;
a.className = 'side-nav-h3'; a.className = 'side-nav-h3';
a.textContent = item.text; a.textContent = item.text;
a.addEventListener('click', (e) => this.handleNavClick(e, item.id)); a.addEventListener('click', (e) => this.handleNavClick(e, item.id));
li.appendChild(a); li.appendChild(a);
subUl.appendChild(li); subUl.appendChild(li);
currentH3Li = li; currentH3Li = li;
} else if (item.level === 'h4' && currentH3Li) { } else if (item.level === 'h4' && currentH3Li) {
// Create h4 item under current h3 // Create h4 item under current h3
let subUl = currentH3Li.querySelector('ul'); let subUl = currentH3Li.querySelector('ul');
if (!subUl) { if (!subUl) {
subUl = document.createElement('ul'); subUl = document.createElement('ul');
currentH3Li.appendChild(subUl); currentH3Li.appendChild(subUl);
} }
const li = document.createElement('li'); const li = document.createElement('li');
const a = document.createElement('a'); const a = document.createElement('a');
a.href = `#${item.id}`; a.href = `#${item.id}`;
a.className = 'side-nav-h4'; a.className = 'side-nav-h4';
a.textContent = item.text; a.textContent = item.text;
a.addEventListener('click', (e) => this.handleNavClick(e, item.id)); a.addEventListener('click', (e) => this.handleNavClick(e, item.id));
li.appendChild(a); li.appendChild(a);
subUl.appendChild(li); subUl.appendChild(li);
} }
}); });
} }
handleNavClick(e, targetId) { handleNavClick(e, targetId) {
e.preventDefault(); e.preventDefault();
// Remove active class from all links // Remove active class from all links
this.querySelectorAll('a').forEach(link => { this.querySelectorAll('a').forEach(link => {
link.classList.remove('active'); link.classList.remove('active');
}); });
// Add active class to clicked link // Add active class to clicked link
e.target.classList.add('active'); e.target.classList.add('active');
// Smooth scroll to target // Smooth scroll to target
const targetElement = document.getElementById(targetId); const targetElement = document.getElementById(targetId);
if (targetElement) { if (targetElement) {
targetElement.scrollIntoView({ targetElement.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'start' block: 'start'
}); });
} }
} }
setupScrollListener() { setupScrollListener() {
let ticking = false; let ticking = false;
const handleScroll = () => { const handleScroll = () => {
if (!ticking) { if (!ticking) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.highlightActiveSection(); this.highlightActiveSection();
ticking = false; ticking = false;
}); });
ticking = true; ticking = true;
} }
}; };
window.addEventListener('scroll', handleScroll); window.addEventListener('scroll', handleScroll);
} }
highlightActiveSection() { highlightActiveSection() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const offset = 100; // Offset for highlighting const offset = 100; // Offset for highlighting
let activeId = ''; let activeId = '';
// Find the currently visible section // Find the currently visible section
this.navItems.forEach(item => { this.navItems.forEach(item => {
const element = item.element; const element = item.element;
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
const elementTop = rect.top + scrollTop; const elementTop = rect.top + scrollTop;
if (elementTop <= scrollTop + offset) { if (elementTop <= scrollTop + offset) {
activeId = item.id; activeId = item.id;
} }
}); });
// Update active state // Update active state
this.querySelectorAll('a').forEach(link => { this.querySelectorAll('a').forEach(link => {
link.classList.remove('active'); link.classList.remove('active');
}); });
if (activeId !== nil && activeId !== '') { if (activeId !== nil && activeId !== '') {
const activeLink = this.querySelector(`a[href="#${activeId}"]`); const activeLink = this.querySelector(`a[href="#${activeId}"]`);
if (activeLink) { if (activeLink) {
activeLink.classList.add('active'); activeLink.classList.add('active');
} }
} }
} }
render() { render() {
this.innerHTML = ` var betaHtml = '';
<div class="side-nav"> if (window.location.hostname.toLowerCase() == 'beta.spearhead.rocks') {
<h4 class="side-nav-title"></h4> betaHtml = '<note-box type="warning" title="Beta Version">You are looking at docs that are released with the beta version.</note-box>';
<ul> }
<!-- Navigation items will be populated automatically -->
</ul> this.innerHTML = `
</div> <div class="side-nav">
`; <div style="width:100%">
} ${betaHtml}
</div>
// Method to refresh the sidebar when content changes
refresh() { <h4 class="side-nav-title"></h4>
this.scanHeaders(); <ul>
} <!-- Navigation items will be populated automatically -->
</ul>
// Method to set the sidebar title </div>
setTitle(title) { `;
const titleElement = this.querySelector('.side-nav-title'); }
if (titleElement) {
titleElement.textContent = title; // Method to refresh the sidebar when content changes
} refresh() {
} this.scanHeaders();
} }
// Register the custom element // Method to set the sidebar title
customElements.define('app-sidebar', Sidebar); setTitle(title) {
const titleElement = this.querySelector('.side-nav-title');
if (titleElement) {
titleElement.textContent = title;
}
}
}
// Register the custom element
customElements.define('app-sidebar', Sidebar);
export default Sidebar; export default Sidebar;
File diff suppressed because one or more lines are too long
+90 -90
View File
@@ -1,91 +1,91 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.getElementById('theme-toggle'); const themeToggle = document.getElementById('theme-toggle');
const sunIcon = document.getElementById('sun-icon'); const sunIcon = document.getElementById('sun-icon');
const moonIcon = document.getElementById('moon-icon'); const moonIcon = document.getElementById('moon-icon');
if (!themeToggle || !sunIcon || !moonIcon) return; if (!themeToggle || !sunIcon || !moonIcon) return;
function setTheme(theme) { function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme); document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme); localStorage.setItem('theme', theme);
sunIcon.style.display = theme === 'light' ? 'block' : 'none'; sunIcon.style.display = theme === 'light' ? 'block' : 'none';
moonIcon.style.display = theme === 'light' ? 'none' : 'block'; moonIcon.style.display = theme === 'light' ? 'none' : 'block';
} }
themeToggle.addEventListener('click', function() { themeToggle.addEventListener('click', function() {
const current = document.documentElement.getAttribute('data-theme') || 'dark'; const current = document.documentElement.getAttribute('data-theme') || 'dark';
setTheme(current === 'dark' ? 'light' : 'dark'); setTheme(current === 'dark' ? 'light' : 'dark');
}); });
// Initialize // Initialize
const saved = localStorage.getItem('theme') || 'dark'; const saved = localStorage.getItem('theme') || 'dark';
setTheme(saved); setTheme(saved);
// Set side-nav-title to the first h1's text // Set side-nav-title to the first h1's text
var h1 = document.querySelector('.content-wrapper h1'); var h1 = document.querySelector('.content-wrapper h1');
var sideNavTitle = document.querySelector('.side-nav-title'); var sideNavTitle = document.querySelector('.side-nav-title');
if (h1 && sideNavTitle) { if (h1 && sideNavTitle) {
sideNavTitle.textContent = h1.textContent; sideNavTitle.textContent = h1.textContent;
} }
}); });
// Highlight sidenav link on scroll (shared for all pages) // Highlight sidenav link on scroll (shared for all pages)
function setupSideNavHighlight() { function setupSideNavHighlight() {
const navLinks = document.querySelectorAll('.side-nav a'); const navLinks = document.querySelectorAll('.side-nav a');
if (!navLinks.length) return; if (!navLinks.length) return;
const sections = Array.from(navLinks).map(link => { const sections = Array.from(navLinks).map(link => {
const id = link.getAttribute('href').replace('#', ''); const id = link.getAttribute('href').replace('#', '');
return document.getElementById(id); return document.getElementById(id);
}); });
function getHeaderOffset() { function getHeaderOffset() {
const header = document.querySelector('header'); const header = document.querySelector('header');
return header ? header.offsetHeight : 0; return header ? header.offsetHeight : 0;
} }
// Custom scroll on nav click // Custom scroll on nav click
navLinks.forEach((link, i) => { navLinks.forEach((link, i) => {
link.addEventListener('click', function(e) { link.addEventListener('click', function(e) {
const section = sections[i]; const section = sections[i];
if (section) { if (section) {
e.preventDefault(); e.preventDefault();
const headerOffset = getHeaderOffset(); const headerOffset = getHeaderOffset();
const targetY = window.innerHeight * 0.10; const targetY = window.innerHeight * 0.10;
const sectionTop = section.getBoundingClientRect().top + window.scrollY; const sectionTop = section.getBoundingClientRect().top + window.scrollY;
const scrollTo = sectionTop - headerOffset - targetY; const scrollTo = sectionTop - headerOffset - targetY;
window.scrollTo({ top: scrollTo, behavior: 'smooth' }); window.scrollTo({ top: scrollTo, behavior: 'smooth' });
} }
}); });
}); });
function onScroll() { function onScroll() {
const headerOffset = getHeaderOffset(); const headerOffset = getHeaderOffset();
const targetY = window.innerHeight * 0.1; // 30% from the top const targetY = window.innerHeight * 0.1; // 30% from the top
let closestIdx = 0; let closestIdx = 0;
let minDist = Infinity; let minDist = Infinity;
for (let i = 0; i < sections.length; i++) { for (let i = 0; i < sections.length; i++) {
const section = sections[i]; const section = sections[i];
if (section) { if (section) {
const dist = Math.abs(section.getBoundingClientRect().top - headerOffset - targetY); const dist = Math.abs(section.getBoundingClientRect().top - headerOffset - targetY);
if (dist < minDist) { if (dist < minDist) {
minDist = dist; minDist = dist;
closestIdx = i; closestIdx = i;
} }
} }
} }
navLinks.forEach((link, i) => { navLinks.forEach((link, i) => {
if (i === closestIdx) { if (i === closestIdx) {
link.classList.add('active'); link.classList.add('active');
} else { } else {
link.classList.remove('active'); link.classList.remove('active');
} }
}); });
} }
window.addEventListener('scroll', onScroll, { passive: true }); window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll); window.addEventListener('resize', onScroll);
onScroll(); // Initial call onScroll(); // Initial call
} }
// Run on DOMContentLoaded // Run on DOMContentLoaded
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupSideNavHighlight); document.addEventListener('DOMContentLoaded', setupSideNavHighlight);
} else { } else {
setupSideNavHighlight(); setupSideNavHighlight();
} }
@@ -80,17 +80,12 @@
</tbody> </tbody>
</table> </table>
<note-box type="info" title="Combining Tasks"> <note-box type="info" title="Combining Tasks">
Currently a group can do 1 thing. Either be Primary, Seconday, Sweep or Intercept. <br> Currently a group can do 1 thing. Either be Primary, Seconday, Sweep or Intercept. <br>
If you want to have different types during different stages, it's best to copy paste groups and If you want to have different types during different stages, it's best to copy paste groups and
rename them accordingly. <br> rename them accordingly. <br>
</note-box> </note-box>
<note-box type="info" title="Airfield Defenses">
</note-box>
<h3 id="cap-zones-ref">Zones</h3> <h3 id="cap-zones-ref">Zones</h3>
<table class="table-no-style"> <table class="table-no-style">
<tbody> <tbody>
@@ -99,6 +94,7 @@
<td>=></td> <td>=></td>
<td>A Cap ROUTE. The two most outer points are used to define the fly-over points.</td> <td>A Cap ROUTE. The two most outer points are used to define the fly-over points.</td>
</tr> </tr>
<tr><td>______________________</td><td></td><td></td> </tr>
<tr> <tr>
<td><code-inline>INTERCEPTZONE_[ZoneID]_[NAME]</code-inline></td> <td><code-inline>INTERCEPTZONE_[ZoneID]_[NAME]</code-inline></td>
<td>=></td> <td>=></td>
@@ -112,6 +108,19 @@
</p> </p>
<h2 id="setup">Setting up groups</h2> <h2 id="setup">Setting up groups</h2>
<h3 id="airbases-grouping">Airbases</h3>
<p>
Before talking about the groups, it's good to mention that groups are currently logically grouped and managed by an Airbase. <br>
Meaning, Airbase A does not know how many flights Airbase B is sending out. <br>
This might change in the future where multiple Airbases can work together.
</p>
<note-box type="info" title="Airbase Defenses">
When an Airbase is "active" and thus has aircraft being scheduled for taskings, all RED groups at that airfield will also be spawned. <br>
Additionally, all missions of type <code-inline>SAM</code-inline> will be spawned (without being available in Primary Missions). <br>
This should hopefully give you all the tools to make distance CAP airbases still feel very well defended.
</note-box>
<h3 id="types-explained">Types explained</h3> <h3 id="types-explained">Types explained</h3>
<p> <p>
Before we go into depth on each type it's best to first look at the types briefly and what we Before we go into depth on each type it's best to first look at the types briefly and what we
@@ -140,7 +149,7 @@
The ground attack units mentioned are not in yet and thus escort groups are not either. <br> The ground attack units mentioned are not in yet and thus escort groups are not either. <br>
<note-box type="warning" title="Overriding Taskings"> <note-box type="warning" title="Overriding Taskings">
Each tasking has very specific waypoints, both for departure, arrical and ofcourse all point in between. <br> Each tasking has very specific waypoints, both for departure, arrival and ofcourse all point in between. <br>
Overriding the "tasking" will break part of the cap flow as "RTBInTen" and RTB events are triggered by waypoints. <br> Overriding the "tasking" will break part of the cap flow as "RTBInTen" and RTB events are triggered by waypoints. <br>
Overriding these waypoints and taskings will break the management by Spearhead. Overriding these waypoints and taskings will break the management by Spearhead.
</note-box> </note-box>
@@ -154,7 +163,7 @@
The way the trigger zone is mapped to a route is as follows: <br> The way the trigger zone is mapped to a route is as follows: <br>
<br> <br>
The 2 points that are furthest apart from each other are chosen as the "leg" of the route. <br> The 2 points that are furthest apart from each other are chosen as the "leg" of the route. <br>
The other 2 points are discarded. <br> The other 2 points are discarded at this time. <br>
<br> <br>
The Orbit route is then created with an "Anchored" task. With the closest point being the starting The Orbit route is then created with an "Anchored" task. With the closest point being the starting
point with an outbound leg to the furthest point. <br> point with an outbound leg to the furthest point. <br>
+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>
+147
View File
@@ -0,0 +1,147 @@
<!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 Missions</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>Advanced Tutorial: Missions</h1>
<div id="strike-region">
<h2 id="strike">Strike</h2>
<p>
Naming: <code-inline>MISSION_STRIKE_[Name]</code-inline> <br>
</p>
<h3 id="specific_targets">Specific Targets</h3>
<p>TBD</p>
<h3 id="scenery_targets">Scenery Targets</h3>
<p>
Bridges, buildings or other type of targets that are baked into the map are awesome targets.<br>
We wanted to make sure they were easily integrated in Spearhead.
</p>
<note-box type="info" title="Implementation details">
Spearhead detects scenery targets by the trigger zone name. A zone named
<strong>scenerytarget_[freeform]</strong> or
<strong>scenerytargets</strong> (case-insensitive) will be scanned and any scenery objects that
has the attribute "Buildings" inside the zone
will be added as mission scenery targets.
</note-box>
<p>
Scenery targets are treated like mission targets: they are included in the mission completion
calculation
(their `IsAlive()` state is checked) and their state is persisted/updated when missions spawn or
resume.
</p>
<note-box type="warning" title="Changing IDs">
Due to Object IDs changing when maps are updated and edited the "Assign as..." functionality
does not work for scenery targets.
Hence the need to use trigger zones to define them.
</note-box>
<p>Here's two images showing how a bridge is added to a mission.</p>
<img src="/img/bridge.png" style="max-height: 35remrem;" />
</div>
<div id="cas-region">
<h2 id="cas">CAS</h2>
<p>
Naming: <code-inline>MISSION_CAS_[Name]</code-inline> <br>
</p>
<h3 id="BattleManager">Battle Manager</h3>
<p>TBD</p>
</div>
<div id="dead-region">
<h2 id="dead">DEAD</h2>
<p>
Naming: <code-inline>MISSION_DEAD_[Name]</code-inline> <br>
Naming: <code-inline>MISSION_SAM_[Name]</code-inline> <br>
</p>
<h3 id="sam-vs-dead">SAM vs DEAD</h3>
<p>TBD</p>
</div>
<div id="bai-region">
<h2 id="bai">BAI</h2>
<p>
Naming: <code-inline>MISSION_BAI_[Name]</code-inline> <br>
</p>
</div>
<div id="deepstrike-region">
<h2 id="deepstrike">DEEPSTRIKE</h2>
<p>
Naming: <code-inline>MISSION_DEEPSTRIKE_[Name]</code-inline> <br>
<br>
When you, as a mission editor, want to create a mission that's further away from the current front-line you can use the Deep Strike mission type. <br>
This allows you to plan missions that require more strategic positioning and timing. <br>
</p>
<h3 id="activation">Activation</h3>
<p>
Deep Strike missions are activated when the Stage in which they are located becomes "PreActive" or "Active". <br>
"PreActive" is when either the current active StageNumber is a certain number of stages away OR the stage has "CAP" airbases active . <br>
<code-inline>SpearheadConfig.StageConfig.preactivateStage</code-inline> changes the configuration. <br>
<br>
For example: <br>
If the current active stage is <code-inline>MissionStage_2_[name]</code-inline> and preActivate config is set to 2. <br>
Then both <code-inline>MissionStage_3_[name]</code-inline> and <code-inline>MissionStage_4_[name]</code-inline> would become "PreActive".
</p>
<h3 id="primary-vs-secondary">Make Primary</h3>
<p>
By default DEEPSTRIKE missions are "Secondary" missions when spawned as a pre-activated stage.<br>
Then when the stage they are in is "Active", the mission becomes a "Primary" mission. <br>
<br>
If you want to make it a "Primary" mission from the start, you can overwrite it.<br>
Click the mission's trigger zone. Add a property named <code-inline>primary</code-inline> and set it to <code-inline>true</code-inline>.
This will make the mission "Primary" from the start. <br>
<br>
If you want it to "Secondary" even when its stage is the active stage, you can set "primary" to false. <br>
<br>
If you want to have it be "Secondary" when its stage is "Pre-Active" and "primary" when its stage is "Active" you don't have to do anything.
</p>
<h3 id="future-additions">Future</h3>
<p>
In the future we're thinking it would be cool to make the Deep Strike missions
be more integrated with the CAP airbases and/or logistics. <br>
Eg. Completing a DEEPSTRIKE mission would disable CAP for X amount of minutes after completion. <br>
This still needs to be refined, but we've already have ideas.
</p>
</div>
</div>
</div>
</main>
<footer>
<p>&copy; 2025 Spearhead Project</p>
</footer>
</body>
</html>
@@ -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/> 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/> 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> <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/> 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/> 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,60 +1,60 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-theme="dark"> <html lang="en" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spearhead API</title> <title>Spearhead API</title>
<link rel="stylesheet" href="/style/prism.css"> <link rel="stylesheet" href="/style/prism.css">
<link rel="stylesheet" href="/style/style.css"> <link rel="stylesheet" href="/style/style.css">
<script src="/js/prism.js"></script> <script src="/js/prism.js"></script>
<script src="/js/site.js"></script> <script src="/js/site.js"></script>
<script type="module" src="../js/components.js"></script> <script type="module" src="../js/components.js"></script>
</head> </head>
<body> <body>
<header> <header>
<app-header></app-header> <app-header></app-header>
</header> <main> </header> <main>
<div class="reference-container"> <div class="reference-container">
<app-sidebar></app-sidebar> <app-sidebar></app-sidebar>
<div class="content-wrapper"> <div class="content-wrapper">
<h1>Spearhead API</h1> <h1>Spearhead API</h1>
<div class="note"> <div class="note">
<p><strong>NOTE:</strong> The Spearhead.API space is only released in the Beta branch at the moment.</p> <p><strong>NOTE:</strong> The Spearhead.API space is only released in the Beta branch at the moment.</p>
</div> </div>
<h2 id="introduction">Introduction</h2> <h2 id="introduction">Introduction</h2>
<p> <p>
The <span class="inline-lua"><span class="lua-variable">Spearhead.API</span></span> space is specifically created to make sure mission makers can interact with the framework. The <span class="inline-lua"><span class="lua-variable">Spearhead.API</span></span> space is specifically created to make sure mission makers can interact with the framework.
</p> </p>
<p> <p>
Simply alter logic, get the current state in Spearhead, and give the whole Mission Editor more control. Simply alter logic, get the current state in Spearhead, and give the whole Mission Editor more control.
</p> </p>
<p> <p>
For example, late activate the entire framework by calling <span class="inline-lua"><span class="lua-function">Spearhead.API.Stages.changeStage</span>(<span class="lua-variable">1</span>)</span> later or on demand and setting the starting config stage to -1 in the Spearhead configuration file. For example, late activate the entire framework by calling <span class="inline-lua"><span class="lua-function">Spearhead.API.Stages.changeStage</span>(<span class="lua-variable">1</span>)</span> later or on demand and setting the starting config stage to -1 in the Spearhead configuration file.
</p> </p>
<h2 id="stages">Stages</h2> <h2 id="stages">Stages</h2>
<pre> <pre>
@@API_CODE@@ @@API_CODE@@
</pre> </pre>
</div> </div>
</div> </div>
</main> </main>
<footer> <footer>
<p>&copy; 2025 Spearhead Project</p> <p>&copy; 2025 Spearhead Project</p>
</footer> </footer>
<style> <style>
.side-nav a.active { .side-nav a.active {
font-weight: bold; font-weight: bold;
color: #4fc3f7; color: #4fc3f7;
} }
</style> </style>
</body> </body>
</html> </html>
@@ -1,3 +1,3 @@
/* PrismJS 1.30.0 /* PrismJS 1.30.0
https://prismjs.com/download#themes=prism-okaidia&languages=lua */ https://prismjs.com/download#themes=prism-okaidia&languages=lua */
code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help} code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
@@ -8,7 +8,7 @@
--color-bg-hover: rgba(120, 140, 255, 0.12); /* More visible hover */ --color-bg-hover: rgba(120, 140, 255, 0.12); /* More visible hover */
--color-bg-note-dark: rgba(40, 35, 30, 0.85); /* Slightly darker note box */ --color-bg-note-dark: rgba(40, 35, 30, 0.85); /* Slightly darker note box */
--color-text-primary: #a5a5a5; /* Brighter text */ --color-text-primary: #b4b3b3; /* Brighter text */
--color-text-secondary: #bfc8d8; /* Lighter secondary text */ --color-text-secondary: #bfc8d8; /* Lighter secondary text */
--color-text-accent: #7ecbff; /* Brighter accent */ --color-text-accent: #7ecbff; /* Brighter accent */
--color-text-light: #ffffff; --color-text-light: #ffffff;
@@ -262,7 +262,7 @@ header nav a:hover::after {
overflow: hidden; overflow: hidden;
top: 100%; /* Position below the dropdown */ top: 100%; /* Position below the dropdown */
left: 0; /* Align left edge */ left: 0; /* Align left edge */
min-width: 160px; min-width: 240px;
background-color: var(--color-bg-secondary); background-color: var(--color-bg-secondary);
box-shadow: var(--shadow-medium); box-shadow: var(--shadow-medium);
@@ -336,8 +336,8 @@ main {
} }
.side-nav { .side-nav {
width: 200px;
min-width: 200px; min-width: 200px;
max-width: 200px;
padding-left: 0; padding-left: 0;
overflow-y: auto; overflow-y: auto;
border-right: none; border-right: none;
+140
View File
@@ -0,0 +1,140 @@
name: PR Prerequisites
on:
pull_request:
branches:
- main
- develop
env:
LUA_VERSION: "5.1"
LUA_LANGUAGE_SERVER_VERSION: "3.19.1"
jobs:
bundles_without_error:
runs-on: ubuntu-latest
name: Bundles Without Errors
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Lua
uses: leafo/gh-actions-lua@v13
with:
luaVersion: ${{ env.LUA_VERSION }}
- name: Compile Spearhead
uses: https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools/github-actions/bundle-script@bundle-script/v1
with:
source-root: ./src
output-file: ./output/spearhead.lua
- name: Verify Compiled Lua Syntax
run: lua -e "assert(loadfile('./output/spearhead.lua'))"
- name: Set up Luarocks
uses: leafo/gh-actions-luarocks@v4
- name: Install luacheck
run: luarocks install luacheck
# "lfs",
# "setmetatable", "getmetatable", "table", "string", "math", "pairs", "ipairs",
# "require", "type", "tostring", "io", "error", "pcall", "tonumber",
# "loadstring",
- name: Create LuaCheck Config
run: |
cat << EOF > .luacheckrc
stds.spearhead_lib = {
globals = {
"SpearheadConfig", "SpearheadAPI", "GlobalCapCallBacks",
}
}
stds.dcs_lib = {
globals = {
"env", "net", "trigger", "timer", "Object", "land", "coord", "coalition",
"Unit", "Group", "Airbase", "StaticObject", "SceneryObject", "world",
"country", "missionCommands", "Controller", "Weapon", "atmosphere", "country",
"lfs", "AI"
}
}
std = "lua51c+spearhead_lib+dcs_lib"
self = false
max_line_length = false
EOF
- name: Show LuaCheck Config
run: cat .luacheckrc
- name: Lint Compiled Output
run: luacheck ./output/spearhead.lua
lua_check:
runs-on: ubuntu-latest
name: Lua Check
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Lua
uses: leafo/gh-actions-lua@v13
with:
luaVersion: ${{ env.LUA_VERSION }}
- name: Install Dcs Plugins
uses: https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools/github-actions/install-lua-addon@install-lua-addon/v1
with:
destination-path: "./.vscode/lua-addons" ##same as local so .luarc.json can be the same
- name: Verify Dcs Plugins Installation
run: ls -la ./.vscode/lua-addons
- name: Install lua-language-server
run: |
mkdir -p ./luals
# For Ubuntu/Debian
wget https://github.com/LuaLS/lua-language-server/releases/download/${{ env.LUA_LANGUAGE_SERVER_VERSION }}/lua-language-server-${{ env.LUA_LANGUAGE_SERVER_VERSION }}-linux-x64.tar.gz
tar -xzf lua-language-server-*.tar.gz -C ./luals
rm lua-language-server-*.tar.gz
- name: Verify Lua Language Server Installation
run: ./luals/bin/lua-language-server --version
- name: Update Json
run: |
FULL_PATH=$(realpath ./.vscode/lua-addons)
echo "full-path: $FULL_PATH"
ls -R $FULL_PATH
echo "Updating .luarc.json with full path"
jq --arg full_path "$FULL_PATH" '.workspace.library[0] = $full_path' ./.luarc.json > ./.luarc.tmp.json
mv ./.luarc.tmp.json ./.luarc.json
- name: Show Config
run: cat ./.luarc.json
- name: Run Lua Language Server
run: |
CONFIG_FULL_PATH=$(realpath ./.luarc.json)
echo "Config full path: $CONFIG_FULL_PATH"
./luals/bin/lua-language-server \
--check ./src \
--checklevel=Warning \
--loglevel=trace \
--logpath="./luals/logs" \
--configpath="$CONFIG_FULL_PATH" \
# --develop
- name: Show Log
if: failure()
run: |
ls -R ./luals/logs
cat ./luals/logs/* || true
+105
View File
@@ -0,0 +1,105 @@
name: Publish Beta Release
on:
push:
branches:
- develop
paths:
- src/**
- .gitea/workflows/release-beta.yml
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read Version from version.txt
id: read_version
run: |
VERSION=beta
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION"
- name: Extract Release Notes
id: release_notes
run: |
VERSION=beta
RELEASE_NOTES=$(awk -v ver="Unreleased" '
/^## \[/ {
if (found) exit
if (index($0, "[" ver "]") > 0) found = 1
next
}
found { print }
' RELEASENOTES.md)
# Write multi-line output properly
echo "body<<EOF" >> "$GITHUB_OUTPUT"
echo "$RELEASE_NOTES" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Compile Spearhead
uses: https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools/github-actions/bundle-script@bundle-script/v1
with:
source-root: ./src
output-file: ./output/spearhead.lua
- name: Remove existing tag if it exists
id: check_tag
run: |
TAG="${{ steps.read_version.outputs.tag }}"
if git tag | grep -q "^$TAG$"; then
echo "Tag exists. Removing it..."
git push origin :refs/tags/"$TAG" || true
git tag -d "$TAG" || true
fi
echo "Ready to create fresh tag: $TAG"
- name: Apply Tag to Current Commit
run: |
git config user.name "github-actions"
git config user.email "github-actions@github.com"
git tag ${{ steps.read_version.outputs.tag }}
git push origin ${{ steps.read_version.outputs.tag }}
- name: Copy Release to Versioned File
run: |
cp ./output/spearhead.lua ./output/spearhead.${{ steps.read_version.outputs.tag }}.lua
echo "Versioned file created: spearhead.${{ steps.read_version.outputs.tag }}.lua"
- name: Create Release and Upload Assets
uses: akkuman/gitea-release-action@v1
with:
tag_name: ${{ steps.read_version.outputs.tag }}
name: Release ${{ steps.read_version.outputs.tag }}
body: ${{ steps.release_notes.outputs.body }}
prerelease: true
files: |-
./output/spearhead.lua
./output/spearhead.${{ steps.read_version.outputs.tag }}.lua
#token: ${{ secrets.GITHUB_TOKEN }}
env:
NODE_OPTIONS: '--experimental-fetch'
- name: Announce Release
uses: tsickert/discord-webhook@v7.0.0
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
content: "New Beta Release"
username: "Spearhead Release Bot"
#TODO: avatar-url spearhead avatar
thread-id: 1538520488442331227
embed-title: "Spearhead Release Beta"
embed-color: 3093247
embed-description:
${{ steps.release_notes.outputs.body }}
embed-url:
https://git.dutchie031.com/Spearhead/spearhead/releases/tag/${{ steps.read_version.outputs.tag }}
+35 -1
View File
@@ -26,8 +26,26 @@ jobs:
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION" echo "Version: $VERSION"
- name: Extract Release Notes
id: release_notes
run: |
VERSION=$(cat version.txt)
RELEASE_NOTES=$(awk -v ver="$VERSION" '
/^## \[/ {
if (found) exit
if (index($0, "[" ver "]") > 0) found = 1
next
}
found { print }
' RELEASENOTES.md)
# Write multi-line output properly
echo "body<<EOF" >> "$GITHUB_OUTPUT"
echo "$RELEASE_NOTES" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Compile Spearhead - name: Compile Spearhead
uses: https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools/github-action@action/v0 uses: https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools/github-actions/bundle-script@bundle-script/v1
with: with:
source-root: ./src source-root: ./src
output-file: ./output/spearhead.lua output-file: ./output/spearhead.lua
@@ -57,6 +75,7 @@ jobs:
with: with:
tag_name: ${{ steps.read_version.outputs.tag }} tag_name: ${{ steps.read_version.outputs.tag }}
name: Release ${{ steps.read_version.outputs.tag }} name: Release ${{ steps.read_version.outputs.tag }}
body: ${{ steps.release_notes.outputs.body }}
draft: true draft: true
files: |- files: |-
./output/spearhead.lua ./output/spearhead.lua
@@ -65,3 +84,18 @@ jobs:
env: env:
NODE_OPTIONS: '--experimental-fetch' NODE_OPTIONS: '--experimental-fetch'
- name: Announce Release
uses: tsickert/discord-webhook@v7.0.0
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
content: "New Beta Release"
username: "Spearhead Release Bot"
#TODO: avatar-url spearhead avatar
thread-id: 1538520596802175037
embed-title: "Spearhead Release"
embed-color: 3093247
embed-description:
${{ steps.release_notes.outputs.body }}
embed-url:
https://git.dutchie031.com/Spearhead/spearhead/releases/tag/${{ steps.read_version.outputs.tag }}
+5 -9
View File
@@ -2,16 +2,12 @@
name: Deploy static content to Pages name: Deploy static content to Pages
on: on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:
release: push:
types: [published] branches:
- main
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages paths:
permissions: - '_docs/**'
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
+144
View File
@@ -0,0 +1,144 @@
name: Update Docs
on:
workflow_dispatch:
push:
branches:
- main
- develop
paths:
- '.docs/**'
env:
DOCKER_REGISTRY: registry.dutchie031.net
DOCKER_IMAGE: spearhead-docs
DOCKER_TAG: ${{ github.ref_name == 'main' && 'latest' || 'beta' }}
jobs:
## Run replacements and cleanups on the docs
prepare-docs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
## --break-system-packages is used to avoid conflicts with system-installed packages on Ubuntu runners.
- name: Install Python Pygments
run: |
python3 -m pip install --upgrade pip --break-system-packages --no-cache-dir --ignore-installed
pip install pygments --break-system-packages
- name: Highlight API code and update HTML
run: |
pygmentize -f html -l lua -O noclasses,style=monokai src/classes/api/SpearheadApiDoc.lua > .docs/web/pages/temp_api_code.html
# Insert the highlighted code into the placeholder in spearheadapi.html
sed -i '/@@API_CODE@@/r .docs/web/pages/temp_api_code.html' .docs/web/pages/spearheadapi.html
sed -i '/@@API_CODE@@/d' .docs/web/pages/spearheadapi.html
rm .docs/web/pages/temp_api_code.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
sed -i '/@@CONFIG_CODE@@/r .docs/web/pages/temp_config_code.html' .docs/web/pages/reference.html
sed -i '/@@CONFIG_CODE@@/d' .docs/web/pages/reference.html
rm .docs/web/pages/temp_config_code.html
- name: List files in .docs directory
run: |
echo "Listing files in .docs directory:"
ls -R .docs
- name: Upload spearhead.lua artifact
uses: actions/upload-artifact@v3
with:
name: spearhead-docs
path: |
./.docs/Dockerfile
./.docs/web/**/*
retention-days: 1
build-image:
runs-on: ubuntu-latest
needs: prepare-docs
steps:
# Download prepared pages from the previous job
- name: Download spearhead-docs artifact
uses: actions/download-artifact@v3
with:
name: spearhead-docs
path: ./spearhead-docs
- name: DEBUG ls
run: |
ls -R ./spearhead-docs
- name: Build Docker image for docs
run: |
docker build -t ${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ env.DOCKER_TAG }} ./spearhead-docs
- name: Push Docker image
run: |
docker push ${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ env.DOCKER_TAG }}
deploy-container:
runs-on: ubuntu-latest
needs: build-image
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Helm
uses: azure/setup-helm@v5
with:
version: 'latest'
id: install
- name: Set up Kubeconfig
run: |
mkdir -p $HOME/.kube
echo "${{ secrets.KUBECONFIG_CONTENT }}" > $HOME/.kube/config
chmod 600 $HOME/.kube/config
cat $HOME/.kube/config
- 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:
# release: spearhead-docs
# namespace: spearhead-docs
# chart: '.helm'
# value-files: >-
# [
# ".helm/values.yaml"
# ]
# env:
# KUBECONFIG_FILE: ${{ secrets.KUBECONFIG_CONTENT }}
+3 -1
View File
@@ -3,4 +3,6 @@
/dist /dist
.vscode/settings.json .vscode/lua-addons
.vscode/settings.json
**\settings.json
+17
View File
@@ -0,0 +1,17 @@
apiVersion: v1
name: spearhead-docs
description: Helm chart for deploying Spearhead documentation
type: application
version: 1.0.0
appVersion: "1.0"
keywords:
- spearhead
- documentation
- dcs
home: https://git.dutchie031.com/Spearhead/spearhead
sources:
- https://git.dutchie031.com/Spearhead/Spearhead
maintainers:
- name: Spearhead Team
+47
View File
@@ -0,0 +1,47 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Values.app }}
labels:
app: {{ .Values.app }}
namespace: spearhead-docs
spec:
replicas: 1
selector:
matchLabels:
app: {{ .Values.app }}
template:
metadata:
labels:
app: {{ .Values.app }}
annotations:
deployment-timestamp: "{{ now.Unix }}"
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65534
seccompProfile:
type: RuntimeDefault
containers:
- name: {{ .Values.app }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: Always
ports:
- containerPort: {{ .Values.ports.containerPort }}
volumeMounts:
- name: cache
mountPath: /var/cache/nginx
- name: run
mountPath: /var/run
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
volumes:
- name: cache
emptyDir: {}
- name: run
emptyDir: {}
+42
View File
@@ -0,0 +1,42 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.app }}
namespace: spearhead-docs
spec:
selector:
app: {{ .Values.app }}
ports:
- protocol: TCP
port: {{ .Values.ports.servicePort }}
targetPort: {{ .Values.ports.containerPort }}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: "{{ .Values.app }}-ingress"
namespace: spearhead-docs
annotations:
kubernetes.io/tls-acme: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
rules:
{{- range .Values.ingress.hosts }}
- host: {{ . }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ $.Values.app }}
port:
number: {{ $.Values.ports.servicePort }}
{{- end }}
tls:
- hosts:
{{- 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
+15
View File
@@ -0,0 +1,15 @@
app: spearhead-docs
image:
repository: registry.dutchie031.net/spearhead-docs
tag: latest
ports:
servicePort: 80
containerPort: 80
ingress:
hosts:
- spearhead.dutchie031.com
- spearhead.rocks
+51
View File
@@ -0,0 +1,51 @@
{
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
"runtime": {
"version": "Lua 5.1"
},
"workspace": {
"checkThirdParty": false,
"maxPreload": 1600,
"preloadFileSize": 1000,
"ignoreDir": [ "config.lua" ]
},
"diagnostics": {
"enable": true,
"globals": [
"lfs"
],
"groupSeverity": {
"duplicate": "Warning",
"global":"Warning",
"luadoc": "Warning",
"redefined": "Warning",
"strong": "Warning",
"type-check": "Warning",
"unbalanced" : "Warning",
"ambiguity": "Warning",
"strict": "Warning",
"unused": "Warning"
},
"groupFileStatus": {
"ambiguity": "Any",
"await": "Any",
"duplicate": "Any",
"global": "Any",
"luadoc": "Any",
"redefined": "Any",
"strict": "Any",
"type-check": "Any",
"unbalanced": "Any",
"unused": "Any",
"strong": "Any"
},
"ignoredFiles": "Opened",
"libraryFiles": "Opened"
},
"hint": {
"enable": false
},
"completion": {
"enable": true
}
}
+5 -4
View File
@@ -1,5 +1,6 @@
{ {
"recommendations": [ "recommendations": [
"sumneko.lua" "dutchie031.dutchies-dcs-scripting-tools",
] "sumneko.lua",
]
} }
-25
View File
@@ -1,25 +0,0 @@
{
"cSpell.words": [
"farp",
"Sams",
"SEAD"
],
"Lua.diagnostics.globals": [
"lfs"
],
"maptz.regionfolder": {
"[lua]" : {
"foldEnd": "--endregion",
"foldEndRegex": "[\\s]*--endregion",
"foldStart": "--region [NAME]",
"foldStartRegex": "[\\s]*--region[\\s]*(.*)",
"disableFolding": false
}
},
"livePreview.serverRoot": "/_docs",
"Lua.workspace.library": [
"/Users/ex61wi/Developer/personal/DcsMissionScriptingTools/dutchies-dcs-scripting-tools/lua-addons",
"c:\\Users\\Tim\\.vscode\\extensions\\dutchie031.dutchies-dcs-scripting-tools-0.0.3\\lua-addons"
],
"dutchies-dcs-scripting-tools.compileAt": "onSave",
}
+29 -39
View File
@@ -1,53 +1,43 @@
# Spearhead # Spearhead
Hello and welcome to Spearhead!
## Contributing. If you're a developer that wants to contribute, look at the code or understand what we're doing, feel free to read on.
I'm always happy to see contributions! <br/> If you're a mission maker that wants to see the documentation and build you mission with this framework. <br>
Especially if you've found an issue, annoyance or feature that you know how to solve, fix or create! <br/> It's best to go to our fully fledged documentation website:
- Stable/Main: https://spearhead.rocks
- Beta/Develop: https://beta.spearhead.rocks
### Keep it small. ## Project Structure
Keeping your contributions small and to the point helps both me and yourself. <br/> The Spearhead project is structured in a lot of separate files. <br>
It'll help the PR review and the speed of improvements. <br/> While Lua isn't specifically created for it, most of it has been written with the standards of OOP in mind. <br>
If you have a big feature in mind? Go for it! <br/> Classes, Objects and helper methods.
Does it require an entire refactor of parts or all of the program? Maybe make sure to do step 0 first.
### Steps (for first timers) You're find in the `/src` folder all the files. <br>
0. Reach out (Optional) They are separated in a somewhat logical order, but some things might feel out of place. <br>
If you want to let me know you want to do something to contribute, please do! <br/> ### Bundling, Intellisense, Linting
Maybe I'm already working on the thing you wanted to build, or someone else is. <br/>
Creating an issue is even better! <br/>
In DCS the easiest way to import a script is with 1 single file, however, that's not as "maintainable".
In order to keep it maintainable we're using a self-built tool to bundle, validate and rewrite some of the lua in order to get it into a single file.
The tool is open-source here: https://git.dutchie031.com/dutchie031/DcsMissionScriptingTools. <br>
It has a `VSCode` extensions and two `Github Action` steps. <br>
If your favorite IDE isn't `VSCode` (I respect that), it might be very easy to create an extension for it, if it does allow that.
1. Fork the repository The `VSCode` extensions comes with Intellisense as well, in the form of a full file of DCS lua type annotations. <br>
This gives the Lua Language Server by Sumneko (which can be used in `VSCode`) all the type definitions it needs.
When writing Spearhead code it is also important to keep our own LuaLS Annotations up to date and complete. <br>
Read all about those annotations here: https://luals.github.io/wiki/annotations/
By forking you can create your own working set of code. <br/> When a pull-request to the `develop` or `main` branches is created, before it can be merged it requires to pass the linter checks.
Whether you keep that fork public or private is up to you! <br/> The first linting check is done by the Lua Language Server. It checks for whitespace and formatting, but it also checks if all types, type-checking and casts are in order. It marks unused variables etc. `.luarc.json` has all the config for those checks. <br>
The second linter job is with `luacheck` which first "bundles" the script, and then runs a full linter check over the bundled script. <br>
This is to make sure that also the bundled script does not contain any errors.
Keep in mind, forking is alright, but it's best with the intent to contribute back. <br/> ## Contributing
After all, instead of having 5 slightly different versions and products, it might be nicer to have 1 much better version. <br/>
2. Create a Draft PR as soon as possible! ### Setting up the project
As soon as you've found some times to create an initial version, please create a draft PR. <br/> If you've kept on reading I assume you either want to brwose
That way you can let me and everyone know it's being worked on and people can see what conflicts might arise with their own changes. <br/>
3. Test, test, test
With DCS and Lua there's a bunch of edge cases that are really hard to get to. <br/>
However, you kinda need to make sure most if not all cases are caught and tested. <br/>
Since automated testing for now doesn't seem feasible, please make sure to verify and test functionality after touching stuff. <br/>
4. Finalise and publish the PR.
Finalise the PR and let the maintainers know! <br/>
We can all have a look and discuss the changes. <br/>
# TODO:
- Would it be possible to generate optional markers on mission briefing locations for Tomcat / Phantom navigation? Then Jester can type it in for the pilots automatically.
+87
View File
@@ -0,0 +1,87 @@
## [Unreleased]
### Breaking Changes
### New Features
- Addresses #41
Adds Stage Complete debug methods for local testing of the Stage flow.
PR #40
- Closes #35
Creating Lane option for stages.
Additionally to the primary stage lane, stages can now be created in different lanes.
These lanes can be used to create different stage progression flows and create different "side-stories".
PR #40
- Closes #37
Now enabled the mission editor to add a custom briefing to the "Buildable" missions.
PR #44
- Closes #42
DEEPSTRIKE now added as a possible feature.
This enables to have missions active in "Pre-Activated" Stages.
PR #48
### Bug Fixes
## [0.13.0] 2026-08
A good first release that finally has all major bugs fixed that were caused by the migration from both the underlying script transpiler and the migration to Gitea.
### Breaking Changes
### New Features
- Issue #26
Possibility to have Stage Overview briefings (which include current missions sorted by distance) to be shown on spawning of a player.
PR #31
### Bug Fixes
- Issue #11
Supply crate spawning now checks for free space and will not spawn if the area is too crowded.
Additionally different units will spawn in different areas depending on loading side.
PR #17
- Fixed command wiring for supply hubs for better and more accurate detection of units spawning and entering/exiting zone.
PR #15
- Fixed custom drawings not being drawn correctly.
PR #18
- Fixed CAP Callbacks not working since the change to a transpiled script. Now a global callback circumvents this issue.
PR #18
- Fixed #9
Changed order of checking mission briefings
PR #19
- Fixed Configuration defaulting to true for all booleans in StageConfig
PR #28
- Fixed Pre-Activated stages not always drawing or pre-activating correctly.
PR #28
- Addressed #24
CAP max commit range is now configurable in the config.lua file.
Issue remains open in order to apply further fine grained tuning.
PR #29
## [0.12.0] 2026-06
Migration to Gitea. <br>
First version of the Gitea repository. <br>
Everything is new, but also old.
### Breaking Changes
Nothing worth mentioning
### New Features
All and none
### Bug Fixes
No bugs fixed, they're all still there.
## [x.x.x] Year - Month Release Template
### Breaking Changes
### New Features
### Bug Fixes
-71
View File
@@ -1,71 +0,0 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spearhead Missions</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>Advanced Tutorial: Missions</h1>
<h2 id="strike">Strike</h2>
<p>
Naming: <span class="inline-lua"><span class="lua-variable">MISSION_STRIKE_[Name]</span></span> <br>
</p>
<h3 id="specific_targets">Specific Targets</h3>
<p>TBD</p>
<h3 id="scenery_targets">Scenery Targets</h3>
<p>
Bridges, buildings or other type of targets that are baked into the map are awesome targets.<br>
We wanted to make sure they were easily integrated in Spearhead.
</p>
<note-box type="info" title="Implementation details">
Spearhead detects scenery targets by the trigger zone name. A zone named <strong>scenerytarget_[freeform]</strong> or
<strong>scenerytargets</strong> (case-insensitive) will be scanned and any scenery objects that has the attribute "Buildings" inside the zone
will be added as mission scenery targets.
</note-box>
<p>
Scenery targets are treated like mission targets: they are included in the mission completion calculation
(their `IsAlive()` state is checked) and their state is persisted/updated when missions spawn or resume.
</p>
<note-box type="warning" title="Changing IDs">
Due to Object IDs changing when maps are updated and edited the "Assign as..." functionality does not work for scenery targets.
Hence the need to use trigger zones to define them.
</note-box>
<p>Here's two images showing how a bridge is added to a mission.</p>
<img src="/img/bridge.png" style="max-height: 35remrem;"/>
</div>
</div>
</main>
<footer>
<p>&copy; 2025 Spearhead Project</p>
</footer>
</body>
</html>
+16 -4
View File
@@ -4,15 +4,18 @@ SpearheadConfig = {
---DEBUG LOGGING ---DEBUG LOGGING
debugEnabled = false, -- default false debugEnabled = false, -- default false
--- DEBUG MENU
debugMenuEnabled = false, -- default false
--- The time briefings should be displayed by default. --- The time briefings should be displayed by default.
--- Players can always "Clear Messages" through the F10 menu, so setting it to a high value can be --- Players can always "Clear Messages" through the F10 menu, so setting it to a high value can be
briefingMessageDuration = 30, --default 30 briefingMessageDuration = 60, --default 60
CapConfig = { CapConfig = {
--quickly enable of disable the entire CAP Logic --quickly enable of disable the entire CAP Logic
--(you can also just rename all units to not be named "CAP_") --(you can also just rename all units to not be named "CAP_")
enabled = true, -- default true enabled = true, -- default true
--min ground speed for CAP aircraft during patrol --min ground speed for CAP aircraft during patrol
-- unit: knots -- unit: knots
minSpeed = 400, -- default 400 minSpeed = 400, -- default 400
@@ -29,6 +32,12 @@ SpearheadConfig = {
-- unit: feet -- unit: feet
maxAlt = 28000, -- default 28000 maxAlt = 28000, -- default 28000
--The "maxDistance" that's set on the CAP tasking for the aircraft to commit to a target.
--This is the distance from the aircraft to the target that the aircraft will commit to engaging
--This is shared for CAP, Intercept and Sweep missions for now.
-- unit: nautical miles
maxCommitRange = 35, -- default 35
-- DELAYS. -- DELAYS.
-- Delays work as follow. -- Delays work as follow.
-- When an aircraft lands alive and well it will be rearmed and ready to go. -- When an aircraft lands alive and well it will be rearmed and ready to go.
@@ -53,7 +62,7 @@ SpearheadConfig = {
deathDelay = 1800, -- default 1800 deathDelay = 1800, -- default 1800
}, },
StageConfig = { StageConfig = {
-- management of stages and its missions. -- management of stages and its missions.
-- This is not related to CAP managers which will continue to work even if stage management is disabled -- This is not related to CAP managers which will continue to work even if stage management is disabled
enabled = true, -- default true enabled = true, -- default true
@@ -67,6 +76,9 @@ SpearheadConfig = {
--The location will continously update for the last killed unit. --The location will continously update for the last killed unit.
markLastContact = false, -- default false 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. --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. -- 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.
--[[ --[[
@@ -86,7 +98,7 @@ SpearheadConfig = {
}, },
Persistence = { Persistence = {
--- io and lfs cannot be sanitized in the MissionScripting.lua --- io and lfs cannot be sanitized in the MissionScripting.lua
--- enables or disables the persistence logic in spearhead --- enables or disables the persistence logic in spearhead
enabled = false, enabled = false,
-73
View File
@@ -1,73 +0,0 @@
--assert(loadfile("C:\\Repos\\DCS\\Spearhead\\dev\\dev_classes.lua"))()
local basePath = "C:\\Repos\\DCS\\Spearhead\\"
local classPath = basePath .. "classes\\"
assert(loadfile(classPath .. "_baseClasses\\Queue.lua"))()
assert(loadfile(classPath .. "spearhead_base.lua"))()
assert(loadfile(classPath .. "spearhead_routeutil.lua"))()
assert(loadfile(classPath .. "spearhead_events.lua"))()
assert(loadfile(classPath .. "spearhead_db.lua"))()
assert(loadfile(classPath .. "fleetClasses\\FleetGroup.lua"))()
assert(loadfile(classPath .. "fleetClasses\\GlobalFleetManager.lua"))()
assert(loadfile(classPath .. "helpers\\MizGroupsManager.lua"))()
assert(loadfile(classPath .. "helpers\\SpawnManager.lua"))()
assert(loadfile(classPath .. "configuration\\CapConfig.lua"))()
assert(loadfile(classPath .. "configuration\\StageConfig.lua"))()
assert(loadfile(classPath .. "stageClasses\\GlobalStageManager.lua"))()
assert(loadfile(classPath .. "stageClasses\\missions\\baseMissions\\Mission.lua"))()
assert(loadfile(classPath .. "stageClasses\\missions\\ZoneMission.lua"))()
assert(loadfile(classPath .. "stageClasses\\missions\\RunwayStrikeMission.lua"))()
assert(loadfile(classPath .. "stageClasses\\missions\\BuildableMission.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\BaseStage\\Stage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\PrimaryStage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\ExtraStage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Stages\\WaitingStage.lua"))()
assert(loadfile(classPath .. "stageClasses\\Groups\\SpearheadGroup.lua"))()
assert(loadfile(classPath .. "stageClasses\\Groups\\SpearheadSceneryObject.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\MissionCommandsHelper.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\SupplyConfig.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\SupplyUnitsTracker.lua"))()
assert(loadfile(classPath .. "stageClasses\\helpers\\BattleManager.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\abstract\\BuildableZone.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\StageBase.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\BlueSam.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\FarpZone.lua"))()
assert(loadfile(classPath .. "stageClasses\\SpecialZones\\SupplyHub.lua"))()
assert(loadfile(classPath .. "capClasses\\taskings\\RTB.lua"))()
assert(loadfile(classPath .. "capClasses\\taskings\\CAP.lua"))()
assert(loadfile(classPath .. "capClasses\\taskings\\SWEEP.lua"))()
assert(loadfile(classPath .. "capClasses\\taskings\\INTERCEPT.lua"))()
assert(loadfile(classPath .. "capClasses\\airGroups\\AirGroup.lua"))()
assert(loadfile(classPath .. "capClasses\\airGroups\\CapGroup.lua"))()
assert(loadfile(classPath .. "capClasses\\airGroups\\SweepGroup.lua"))()
assert(loadfile(classPath .. "capClasses\\airGroups\\InterceptGroup.lua"))()
assert(loadfile(classPath .. "capClasses\\detection\\DetectionManager.lua"))()
assert(loadfile(classPath .. "capClasses\\GlobalCapManager.lua"))()
assert(loadfile(classPath .. "capClasses\\CapAirbase.lua"))()
assert(loadfile(classPath .. "capClasses\\runwayBombing\\RunwayBombingTracker.lua"))()
assert(loadfile(classPath .. "persistence\\Persistence.lua"))()
-- Startup:
assert(loadfile(basePath .. "main.lua"))()
-79
View File
@@ -1,79 +0,0 @@
SpearheadConfig = {
debugEnabled = true,
CapConfig = {
--quickly enable of disable the entire CAP Logic
--(you can also just rename all units to not be named "CAP_")
enabled = true, -- default true
--min ground speed for CAP aircraft during patrol
-- unit: knots
minSpeed = 400, -- default 400
--max speed for CAP aircraft during patrol
-- unit: knots
maxSpeed = 500, -- default 500
--minAlt for aircraft on patrol
-- unit: feet
minAlt = 18000, -- default 18000
--maxAlt for aircraft on patrol
-- unit: feet
maxAlt = 28000, -- default 28000
--Delay for aircraft from touchdown to off the chocks.
-- unit: seconds
rearmDelay = 180, -- default 600
repairDelay = 600, -- default 600
--Delay for aircraft from death to takeoff.
--When the seconds remaining is the same at the rearmDelay it will be spawned on the ramp and follow the rearm logic.
-- !! Can not be lower than rearmDelay
-- unit: seconds
deathDelay = 1800, -- default 1800
},
StageConfig = {
-- management of stages and its missions.
-- This is not related to CAP managers which will continue to work even if stage management is disabled
enabled = true, -- default true
--Will draw the active and the next stage
drawStages = true, -- default true
drawPreActivated = true,
markLastContact = true,
--AutoStages will continue to the next stage automatically on completion of the missions within the stage.
-- If you want to make it so the next stage triggers only when you want to disable it here and manually implement the actions needed.
--[[
TODO: Add manual stage transition documentation
]]
autoStages = true, --default true
--Maximum missions per stage (includes all types of missions)
maxMissionStage = 100,
--Stage starting number
startingStage = 1,
---DEBUG logging. Consider keeping this disabled
debugEnabled = true
},
Persistence = {
--- io and lfs cannot be sanitized in the MissionScripting.lua
--- enables or disables the persistence logic in spearhead
enabled = true,
--- sets the directory where the persistence file is stored
--- if nil then lfs.writedir() will be used.
--- which will
directory = nil ,
--- the filename of the persistence file. Should end with .json for convention, but any text extension should do.
fileName = "Spearhead_Persistence_Dev"
}
}
File diff suppressed because it is too large Load Diff
-47
View File
@@ -1,47 +0,0 @@
---@class Queue
---@field private _items Array
---@field private _first number
---@field private _last number
local Queue = {}
Queue.__index = Queue
---@return Queue
function Queue.new()
local self = setmetatable({}, Queue)
self._items = {}
self._first = 1
self._last = 0
return self
end
---@return nil
function Queue:push(item)
self._last = self._last + 1
self._items[self._last] = item
end
---@return any?
function Queue:pop()
if self._first > self._last then
return nil
end
local item = self._items[self._first]
self._items[self._first] = nil
self._first = self._first + 1
return item
end
---@return Array<any>
function Queue:toList()
local items = {}
for i = self._first, self._last do
items[#items + 1] = self._items[i]
end
return items
end
return Queue
+2 -3
View File
@@ -10,7 +10,6 @@ local MissionCompleteListeners = {}
---@class SpearheadAPIInternal ---@class SpearheadAPIInternal
---@field notifyMissionComplete fun(zone_name: string) ---@field notifyMissionComplete fun(zone_name: string)
---@type FullSpearheadAPI ---@type FullSpearheadAPI
SpearheadAPI = { SpearheadAPI = {
Stages = { Stages = {
@@ -25,12 +24,12 @@ SpearheadAPI = {
getCurrentStage = function() getCurrentStage = function()
return GlobalStageManager.getCurrentStage() or nil return GlobalStageManager.getCurrentStage() or nil
end, end,
isStageComplete = function(stageNumber) isStageComplete = function(stageNumber, stageLaneIdentifier)
if type(stageNumber) ~= "number" then if type(stageNumber) ~= "number" then
return false, "stageNumber " .. stageNumber .. " is not a valid number" return false, "stageNumber " .. stageNumber .. " is not a valid number"
end end
local isComplete = GlobalStageManager.isStageComplete(stageNumber) local isComplete = GlobalStageManager.isStageComplete(stageNumber, stageLaneIdentifier)
if isComplete == nil then if isComplete == nil then
return nil, "no stage found with number " .. stageNumber return nil, "no stage found with number " .. stageNumber
end end
+1 -2
View File
@@ -6,7 +6,7 @@ SpearheadAPI = SpearheadAPI
---@class SpearheadStagesAPI ---@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 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 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 ---@class OnMissionCompleteListener
---@field onMissionComplete fun(self: OnMissionCompleteListener, zone_name: string) @Called when a mission is completed. @return void ---@field onMissionComplete fun(self: OnMissionCompleteListener, zone_name: string) @Called when a mission is completed. @return void
@@ -14,4 +14,3 @@ SpearheadAPI = SpearheadAPI
---@class MissionAPI ---@class MissionAPI
---@field addOnMissionCompleteListener fun(listener: OnMissionCompleteListener) @Adds a listener to the mission. <br/> @param listener OnMissionCompleteListener <br/> @return void ---@field addOnMissionCompleteListener fun(listener: OnMissionCompleteListener) @Adds a listener to the mission. <br/> @param listener OnMissionCompleteListener <br/> @return void
+21 -22
View File
@@ -2,7 +2,6 @@ local Util = require("classes.util.Util")
local CapGroup = require("classes.capClasses.airGroups.CapGroup") local CapGroup = require("classes.capClasses.airGroups.CapGroup")
local SweepGroup = require("classes.capClasses.airGroups.SweepGroup") local SweepGroup = require("classes.capClasses.airGroups.SweepGroup")
local InterceptGroup = require("classes.capClasses.airGroups.InterceptGroup") local InterceptGroup = require("classes.capClasses.airGroups.InterceptGroup")
local RunwayBombingTracker = require("classes.capClasses.runwayBombing.RunwayBombingTracker")
local SpearheadEvents = require("classes.spearhead_events") local SpearheadEvents = require("classes.spearhead_events")
local RunwayStrikeMission = require("classes.stageClasses.missions.RunwayStrikeMission") local RunwayStrikeMission = require("classes.stageClasses.missions.RunwayStrikeMission")
@@ -37,12 +36,11 @@ end
---@param database Database ---@param database Database
---@param logger table ---@param logger table
---@param capConfig table ---@param capConfig table
---@param stageConfig table
---@param runwayBombingTracker RunwayBombingTracker ---@param runwayBombingTracker RunwayBombingTracker
---@param detectionManager DetectionManager ---@param detectionManager DetectionManager
---@param spawnManager SpawnManager ---@param spawnManager SpawnManager
---@return CapBase ---@return CapBase
function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runwayBombingTracker, detectionManager, spawnManager) function CapBase.new(airbaseName, database, logger, capConfig, runwayBombingTracker, detectionManager, spawnManager)
CapBase.__index = CapBase CapBase.__index = CapBase
local self = setmetatable({}, { __index = CapBase }) --[[@as CapBase]] local self = setmetatable({}, { __index = CapBase }) --[[@as CapBase]]
@@ -61,7 +59,7 @@ function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runw
local baseData = database:getAirbaseDataForZone(airbaseName) local baseData = database:getAirbaseDataForZone(airbaseName)
if baseData and baseData.CapGroups then if baseData and baseData.CapGroups then
for key, name in pairs(baseData.CapGroups) do for _, name in pairs(baseData.CapGroups) do
local capGroup = CapGroup.New(name, capConfig, logger, spawnManager) local capGroup = CapGroup.New(name, capConfig, logger, spawnManager)
if capGroup then if capGroup then
self.capGroupsByName[name] = capGroup self.capGroupsByName[name] = capGroup
@@ -70,7 +68,7 @@ function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runw
end end
if baseData and baseData.SweepGroups then if baseData and baseData.SweepGroups then
for key, name in pairs(baseData.SweepGroups) do for _, name in pairs(baseData.SweepGroups) do
local sweepGroup = SweepGroup.New(name, capConfig, logger, spawnManager) local sweepGroup = SweepGroup.New(name, capConfig, logger, spawnManager)
if sweepGroup then if sweepGroup then
self.sweepGroupsByName[name] = sweepGroup self.sweepGroupsByName[name] = sweepGroup
@@ -79,7 +77,7 @@ function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runw
end end
if baseData and baseData.InterceptGroups then if baseData and baseData.InterceptGroups then
for key, name in pairs(baseData.InterceptGroups) do for _, name in pairs(baseData.InterceptGroups) do
local interceptGroup = InterceptGroup.New(name, capConfig, logger, detectionManager, spawnManager) local interceptGroup = InterceptGroup.New(name, capConfig, logger, detectionManager, spawnManager)
if interceptGroup then if interceptGroup then
self.interceptGroupsByName[name] = interceptGroup self.interceptGroupsByName[name] = interceptGroup
@@ -97,7 +95,6 @@ function CapBase.new(airbaseName, database, logger, capConfig, stageConfig, runw
SpearheadEvents.AddStageNumberChangedListener(self) SpearheadEvents.AddStageNumberChangedListener(self)
timer.scheduleFunction(CheckStateContinuous, self, timer.getTime() + 15) timer.scheduleFunction(CheckStateContinuous, self, timer.getTime() + 15)
return self return self
end end
@@ -127,7 +124,7 @@ end
function CapBase:SpawnIfApplicable() function CapBase:SpawnIfApplicable()
self.logger:debug("Check spawns for airbase " .. self.airbaseName) self.logger:debug("Check spawns for airbase " .. self.airbaseName)
for groupName, capGroup in pairs(self.capGroupsByName) do for _, capGroup in pairs(self.capGroupsByName) do
local targetStage = capGroup:GetZoneIDWhenStageID(tostring(self.activeStage)) local targetStage = capGroup:GetZoneIDWhenStageID(tostring(self.activeStage))
if targetStage ~= nil and capGroup:GetState() == "UnSpawned" then if targetStage ~= nil and capGroup:GetState() == "UnSpawned" then
@@ -135,7 +132,7 @@ function CapBase:SpawnIfApplicable()
end end
end end
for groupName, sweepGroup in pairs(self.sweepGroupsByName) do for _, sweepGroup in pairs(self.sweepGroupsByName) do
local targetStage = sweepGroup:GetZoneIDWhenStageID(tostring(self.activeStage)) local targetStage = sweepGroup:GetZoneIDWhenStageID(tostring(self.activeStage))
if targetStage ~= nil and sweepGroup:GetState() == "UnSpawned" then if targetStage ~= nil and sweepGroup:GetState() == "UnSpawned" then
@@ -143,7 +140,7 @@ function CapBase:SpawnIfApplicable()
end end
end end
for groupName, interceptGroup in pairs(self.interceptGroupsByName) do for _, interceptGroup in pairs(self.interceptGroupsByName) do
local targetStage = interceptGroup:GetZoneIDWhenStageID(tostring(self.activeStage)) local targetStage = interceptGroup:GetZoneIDWhenStageID(tostring(self.activeStage))
if targetStage ~= nil and interceptGroup:GetState() == "UnSpawned" then if targetStage ~= nil and interceptGroup:GetState() == "UnSpawned" then
@@ -154,8 +151,9 @@ end
function CapBase:CheckAndScheduleCAP() function CapBase:CheckAndScheduleCAP()
self.logger:debug("Check taskings for airbase " .. self.airbaseName) self.logger:debug("Check taskings for airbase " .. self.airbaseName)
---@type table<string, number>
local countPerStage = {} local countPerStage = {}
---@type table<string, number>
local requiredPerStage = {} local requiredPerStage = {}
local airbase = Airbase.getByName(self.airbaseName) local airbase = Airbase.getByName(self.airbaseName)
@@ -170,7 +168,7 @@ function CapBase:CheckAndScheduleCAP()
if group:IsBackup() == true then if group:IsBackup() == true then
local state = group:GetState() local state = group:GetState()
if state == "InTransit" or state == "OnStation" or state == "RtbInTen" then if state == "InTransit" or state == "OnStation" or state == "RtbInTen" then
local supposedTargetZoneID = group:GetZoneIDWhenStageID(activeStageID) local supposedTargetZoneID = group:GetZoneIDWhenStageID(activeStageID)
local currentTargetZone = group:GetCurrentTargetZoneID() local currentTargetZone = group:GetCurrentTargetZoneID()
@@ -192,7 +190,7 @@ function CapBase:CheckAndScheduleCAP()
end end
end end
end end
if countPerStage[supposedTargetZoneID] == nil then if countPerStage[supposedTargetZoneID] == nil then
countPerStage[supposedTargetZoneID] = 0 countPerStage[supposedTargetZoneID] = 0
end end
@@ -231,7 +229,7 @@ function CapBase:CheckAndScheduleCAP()
countPerStage[supposedZone] = countPerStage[supposedZone] + 1 countPerStage[supposedZone] = countPerStage[supposedZone] + 1
end end
elseif state == "InTransit" or state == "OnStation" then elseif state == "InTransit" or state == "OnStation" then
if supposedZone ~= group:GetCurrentTargetZoneID() then if supposedZone ~= group:GetCurrentTargetZoneID() then
if countPerStage[supposedZone] < requiredPerStage[supposedZone] then if countPerStage[supposedZone] < requiredPerStage[supposedZone] then
local triggerZone = self.database:GetCapZoneForZoneID(supposedZone) local triggerZone = self.database:GetCapZoneForZoneID(supposedZone)
@@ -273,7 +271,7 @@ function CapBase:CheckAndScheduleCAP()
if triggerZone then if triggerZone then
group:SendToZone(triggerZone, supposedZone, airbase) group:SendToZone(triggerZone, supposedZone, airbase)
end end
countPerStage[supposedZone] = countPerStage[supposedZone] + 1 countPerStage[supposedZone] = countPerStage[supposedZone] + 1
end end
end end
@@ -312,8 +310,7 @@ function CapBase:CheckAndScheduleIntercept()
self.logger:debug("Check intercept taskings for airbase " .. self.airbaseName) self.logger:debug("Check intercept taskings for airbase " .. self.airbaseName)
local interceptZoneIDs = {} ---@type table<string, boolean>
local interceptZoneIDs = {} local interceptZoneIDs = {}
local airbase = Airbase.getByName(self.airbaseName) local airbase = Airbase.getByName(self.airbaseName)
@@ -321,7 +318,7 @@ function CapBase:CheckAndScheduleIntercept()
return nil return nil
end end
for name, group in pairs(self.interceptGroupsByName) do for _, group in pairs(self.interceptGroupsByName) do
local targetZoneID = group:GetZoneIDWhenStageID(tostring(self.activeStage)) local targetZoneID = group:GetZoneIDWhenStageID(tostring(self.activeStage))
if targetZoneID then if targetZoneID then
interceptZoneIDs[targetZoneID] = true interceptZoneIDs[targetZoneID] = true
@@ -357,7 +354,7 @@ function CapBase:CheckAndScheduleIntercept()
end end
end end
end end
---RATIO. Amount of enemy fighters required per zone before another group gets added. ---RATIO. Amount of enemy fighters required per zone before another group gets added.
local ratio = 4 -- Ratio of units to intercept per zone, can be adjusted local ratio = 4 -- Ratio of units to intercept per zone, can be adjusted
@@ -379,7 +376,6 @@ function CapBase:CheckAndScheduleIntercept()
if total < required then if total < required then
for _, group in pairs(self.interceptGroupsByName) do for _, group in pairs(self.interceptGroupsByName) do
if total < required then if total < required then
local zoneID = group:GetZoneIDWhenStageID(tostring(self.activeStage))
if group:GetState() == "ReadyOnTheRamp" then if group:GetState() == "ReadyOnTheRamp" then
group:SendToInterceptUnits(targets, name, airbase) group:SendToInterceptUnits(targets, name, airbase)
total = total + 1 total = total + 1
@@ -392,9 +388,12 @@ function CapBase:CheckAndScheduleIntercept()
end end
end end
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 self.activeStage = number
if self:IsBaseActiveWhenStageIsActive(number) == true then if self:IsBaseActiveWhenStageIsActive(number) == true then
+6 -9
View File
@@ -6,21 +6,19 @@ local CapAirbase = require("classes.capClasses.CapAirbase")
---@class GlobalCapManager ---@class GlobalCapManager
local GlobalCapManager = {} local GlobalCapManager = {}
do do
---@type table<string, CapBase[]>
local airbasesPerStage = {} local airbasesPerStage = {}
---@type table<string, CapBase>
local allAirbasesByName = {} local allAirbasesByName = {}
local activeAirbasesPerActiveStage = {}
local unitsPerzonePerStage = {}
local initiated = false local initiated = false
---comment ---comment
---@param database Database ---@param database Database
---@param capConfig table ---@param capConfig table
---@param stageConfig StageConfig
---@param detectionManager DetectionManager ---@param detectionManager DetectionManager
---@param logLevel LogLevel ---@param logLevel LogLevel
---@param spawnManager SpawnManager ---@param spawnManager SpawnManager
function GlobalCapManager.start(database, capConfig, detectionManager, stageConfig, logLevel, spawnManager) function GlobalCapManager.start(database, capConfig, detectionManager, logLevel, spawnManager)
if initiated == true then return end if initiated == true then return end
local logger = Logger.new("AirbaseManager", logLevel) local logger = Logger.new("AirbaseManager", logLevel)
@@ -29,7 +27,7 @@ do
local zones = database:getStagezoneNames() local zones = database:getStagezoneNames()
if zones then if zones then
for key, stageName in pairs(zones) do for _, stageName in pairs(zones) do
if airbasesPerStage[stageName] == nil then if airbasesPerStage[stageName] == nil then
airbasesPerStage[stageName] = {} airbasesPerStage[stageName] = {}
end end
@@ -39,9 +37,8 @@ do
for _, airbaseName in pairs(airbaseNames) do for _, airbaseName in pairs(airbaseNames) do
if airbaseName then if airbaseName then
local airbaseSpecificLogger = Logger.new("CAP_" .. airbaseName, logLevel) local airbaseSpecificLogger = Logger.new("CAP_" .. airbaseName, logLevel)
local airbase = CapAirbase.new(airbaseName, database, airbaseSpecificLogger, capConfig, stageConfig, runwayBombingTracker, detectionManager, spawnManager) local airbase = CapAirbase.new(airbaseName, database, airbaseSpecificLogger, capConfig, runwayBombingTracker, detectionManager, spawnManager)
if airbase then if airbase then
table.insert(airbasesPerStage[stageName], airbase) table.insert(airbasesPerStage[stageName], airbase)
allAirbasesByName[airbaseName] = airbase allAirbasesByName[airbaseName] = airbase
+39 -8
View File
@@ -1,9 +1,11 @@
local SpearheadEvents = require("classes.spearhead_events") local SpearheadEvents = require("classes.spearhead_events")
local RTBMission = require("classes.capClasses.taskings.RTB") local RTBMission = require("classes.capClasses.taskings.RTB")
local Util = require("classes.util.Util") 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 ---@class AirGroup : OnUnitLostListener, OnGroupRTBListener, OnGroupRTBInTenListener, OnLandEventListener, OnGroupOnStationListener
---@field protected _logger Logger ---@field protected _logger Logger
---@field protected _groupName string ---@field protected _groupName string
---@field protected _groupType AirGroupType ---@field protected _groupType AirGroupType
@@ -12,9 +14,12 @@ local DcsUtil = require("classes.util.DcsUtil")
---@field protected _config CapConfig ---@field protected _config CapConfig
---@field protected _checkLivenessNumber number ---@field protected _checkLivenessNumber number
---@field protected _spawnManager SpawnManager ---@field protected _spawnManager SpawnManager
---@field protected _routeDrawing CustomDrawing?
local AirGroup = {} local AirGroup = {}
AirGroup.__index = AirGroup AirGroup.__index = AirGroup
local globalConfig = GlobalConfig.New()
---@param logger Logger ---@param logger Logger
---@param groupName string ---@param groupName string
---@param groupType AirGroupType ---@param groupType AirGroupType
@@ -28,6 +33,7 @@ function AirGroup:New(groupName, groupType, config, logger, spawnManager)
self._config = config self._config = config
self._logger = logger self._logger = logger
self._spawnManager = spawnManager self._spawnManager = spawnManager
self._routeDrawings = {}
local group = Group.getByName(self._groupName) local group = Group.getByName(self._groupName)
if group then if group then
@@ -74,7 +80,7 @@ function AirGroup:SetMission(mission)
end end
end end
local setMissionDelayed = function(data, time) local setMissionDelayed = function(data, _)
data.self:SetMissionPrivate(data.mission) data.self:SetMissionPrivate(data.mission)
end end
@@ -92,6 +98,28 @@ function AirGroup:SetMissionPrivate(mission)
if group and mission then if group and mission then
group:getController():setTask(mission) group:getController():setTask(mission)
self._logger:debug("mission - Task set for group: " .. self._groupName) self._logger:debug("mission - Task set for group: " .. self._groupName)
if globalConfig:isDebugMenuEnabled() == true then
-- draw the mission route for debugging purposes
if self._routeDrawing then
self._routeDrawing:Remove()
end
local points = {}
if mission and mission.params and mission.params.route and mission.params.route.points then
local routePoints = mission.params.route.points --[[@as Array<Vec2> ]]
for _, wp in pairs(routePoints) do
if wp and wp.x and wp.y then
table.insert(points, { x = wp.x, y = wp.y })
end
end
end
local colorString = DrawingHelper.ColorTableToColorString({ 1, 0, 0, 1 })
self._routeDrawing = CustomDrawing.FromPoints(points, colorString, 5, 2)
self._routeDrawing:Draw()
end
end end
end end
@@ -171,6 +199,8 @@ function AirGroup:SpawnInternal(force, withoutLoadout)
end end
---@param selfA AirGroup ---@param selfA AirGroup
---@param time number
---@return number?
local function CheckLivenessTask(selfA, time) local function CheckLivenessTask(selfA, time)
local interval = selfA:CheckLiveness() local interval = selfA:CheckLiveness()
if not interval then return end if not interval then return end
@@ -236,6 +266,7 @@ function AirGroup:OnLastUnitLanded()
---@param data CheckGroupForRestartData ---@param data CheckGroupForRestartData
---@param time number ---@param time number
---@return number?
local checkGroupForRestart = function(data, time) local checkGroupForRestart = function(data, time)
local group = Group.getByName(data.self:GetName()) local group = Group.getByName(data.self:GetName())
if not group then if not group then
@@ -329,7 +360,7 @@ do --- RESPAWN FUNCTIONS
self:SetState("Dead") self:SetState("Dead")
---@param selfA AirGroup ---@param selfA AirGroup
local respawnTask = function(selfA, time) local respawnTask = function(selfA, _)
selfA:StartRepair() selfA:StartRepair()
end end
@@ -348,8 +379,7 @@ do --- RESPAWN FUNCTIONS
end end
---comment ---comment
---@param selfA AirGroup local rearmTask = function(_, _)
local rearmTask = function(selfA, time)
self:StartRearm() self:StartRearm()
end end
@@ -374,7 +404,7 @@ do --- RESPAWN FUNCTIONS
end end
---@param selfA AirGroup ---@param selfA AirGroup
local rearmTask = function(selfA, time) local rearmTask = function(selfA, _)
selfA:MarkRearmComplete() selfA:MarkRearmComplete()
end end
@@ -387,6 +417,7 @@ end
do --EVENT LISTENERS do --EVENT LISTENERS
---@param unit Unit ---@param unit Unit
function AirGroup:OnUnitLost(unit) function AirGroup:OnUnitLost(unit)
if unit == nil then return end
self:CheckLiveness() self:CheckLiveness()
end end
@@ -406,7 +437,7 @@ do --EVENT LISTENERS
end end
end end
function AirGroup:OnUnitLanded(unit, airbase) function AirGroup:OnUnitLanded(_, _)
local anyInAir = false local anyInAir = false
local group = Group.getByName(self._groupName) local group = Group.getByName(self._groupName)
if group then if group then
@@ -34,9 +34,10 @@ function CapGroup:IsBackup()
return self._isBackup return self._isBackup
end end
---@param stageID string
---@return string? ---@return string?
function CapGroup:GetZoneIDWhenStageID(stageID) function CapGroup:GetZoneIDWhenStageID(stageID)
return self._targetZoneIdPerStage[stageID] return self._targetZoneIdPerStage[tostring(stageID)]
end end
---@return string? ---@return string?
@@ -103,17 +104,17 @@ function CapGroup:InitWithName(groupName)
return return
end end
local subsplit = Util.split_string(configPart, "|") local subsplit = Util.split_string(configPart, "|")
if subsplit then if subsplit then
for key, value in pairs(subsplit) do for _, value in pairs(subsplit) do
local keySplit = Util.split_string(value, "]") local keySplit = Util.split_string(value, "]")
local targetZone = keySplit[2] local targetZone = keySplit[2]
local allActives = string.sub(keySplit[1], 2, #keySplit[1]) local allActives = string.sub(keySplit[1], 2, #keySplit[1])
local commaSeperated = Util.split_string(allActives, ",") local commaSeperated = Util.split_string(allActives, ",")
for _, value in pairs(commaSeperated) do for _, childValue in pairs(commaSeperated) do
local dashSeperated = Util.split_string(value, "-") local dashSeperated = Util.split_string(childValue, "-")
if Util.tableLength(dashSeperated) > 1 then if Util.tableLength(dashSeperated) > 1 then
local from = tonumber(dashSeperated[1]) local from = tonumber(dashSeperated[1])
local till = tonumber(dashSeperated[2]) local till = tonumber(dashSeperated[2])
@@ -60,6 +60,7 @@ end
---comment ---comment
---@param units Array<string> ---@param units Array<string>
---@param zoneName string
---@param homeAirbase Airbase ---@param homeAirbase Airbase
function InterceptGroup:SendToInterceptUnits(units, zoneName, homeAirbase) function InterceptGroup:SendToInterceptUnits(units, zoneName, homeAirbase)
@@ -68,9 +69,10 @@ function InterceptGroup:SendToInterceptUnits(units, zoneName, homeAirbase)
self:SetTargetUnits(units) self:SetTargetUnits(units)
end end
---@param stageID string
---@return string? ---@return string?
function InterceptGroup:GetZoneIDWhenStageID(stageID) function InterceptGroup:GetZoneIDWhenStageID(stageID)
return self._targetZoneIdPerStage[stageID] return self._targetZoneIdPerStage[tostring(stageID)]
end end
---@return string? ---@return string?
@@ -91,6 +93,9 @@ function InterceptGroup:SetTargetUnits(unitNames)
end) end)
end end
---@param selfA InterceptGroup
---@param time number
---@return number?
local updateContinous = function(selfA, time) local updateContinous = function(selfA, time)
local next = selfA:UpdateTask() local next = selfA:UpdateTask()
if next then if next then
@@ -102,6 +107,7 @@ function InterceptGroup:SetTargetUnits(unitNames)
self._updateTaskID = timer.scheduleFunction(updateContinous, self, timer.getTime() + 30) self._updateTaskID = timer.scheduleFunction(updateContinous, self, timer.getTime() + 30)
end end
---@param unit Unit
function InterceptGroup:RemoveTargetUnit(unit) function InterceptGroup:RemoveTargetUnit(unit)
if not unit then return end if not unit then return end
@@ -190,7 +196,7 @@ function InterceptGroup:UpdateTask()
self:SendRTB(self._airbase) self:SendRTB(self._airbase)
return nil -- Return to base if bingo fuel return nil -- Return to base if bingo fuel
end end
if selfDetected and self:IsInAir() == true then if selfDetected and self:IsInAir() == true then
if self._currentTargetName and self._currentTargetName == closestUnit:getName() then if self._currentTargetName and self._currentTargetName == closestUnit:getName() then
local distance = Util.VectorDistance3d(closestUnit:getPoint(), groupPoint) local distance = Util.VectorDistance3d(closestUnit:getPoint(), groupPoint)
@@ -213,8 +219,6 @@ function InterceptGroup:UpdateTask()
self._currentTargetName = nil self._currentTargetName = nil
end end
local speed = self._config:getMaxSpeed() local speed = self._config:getMaxSpeed()
local interceptPoint = self:GetInterceptPoint(groupPoint, speed, closestUnit) local interceptPoint = self:GetInterceptPoint(groupPoint, speed, closestUnit)
@@ -222,7 +226,8 @@ function InterceptGroup:UpdateTask()
return 30 -- Return to base if no intercept point could be calculated return 30 -- Return to base if no intercept point could be calculated
end end
local mission = nil ---@type table
local mission
if self:IsInAir() == true then if self:IsInAir() == true then
-- If the group is in the air, create an intercept mission -- If the group is in the air, create an intercept mission
mission = INTERCEPT.getMissionFromInAir( mission = INTERCEPT.getMissionFromInAir(
@@ -323,13 +328,13 @@ function InterceptGroup:InitWithName(groupName)
configPart = string.sub(configPart, 2, #configPart) configPart = string.sub(configPart, 2, #configPart)
local subsplit = Util.split_string(configPart, "|") local subsplit = Util.split_string(configPart, "|")
if subsplit then if subsplit then
for key, value in pairs(subsplit) do for _, value in pairs(subsplit) do
local keySplit = Util.split_string(value, "]") local keySplit = Util.split_string(value, "]")
local targetZone = keySplit[2] local targetZone = keySplit[2]
local allActives = string.sub(keySplit[1], 2, #keySplit[1]) local allActives = string.sub(keySplit[1], 2, #keySplit[1])
local commaSeperated = Util.split_string(allActives, ",") local commaSeperated = Util.split_string(allActives, ",")
for _, value in pairs(commaSeperated) do for _, childValue in pairs(commaSeperated) do
local dashSeperated = Util.split_string(value, "-") local dashSeperated = Util.split_string(childValue, "-")
if Util.tableLength(dashSeperated) > 1 then if Util.tableLength(dashSeperated) > 1 then
local from = tonumber(dashSeperated[1]) local from = tonumber(dashSeperated[1])
local till = tonumber(dashSeperated[2]) local till = tonumber(dashSeperated[2])
@@ -4,7 +4,7 @@ local Util = require("classes.util.Util")
local MissionEditorWarner = require("classes.util.MissionEditorWarnings") local MissionEditorWarner = require("classes.util.MissionEditorWarnings")
---@class SweepGroup : AirGroup ---@class SweepGroup : AirGroup
---@field _targetZoneIdPerStage table<number, string> ---@field _targetZoneIdPerStage table<string, string>
---@field _currentTargetZoneID string? ---@field _currentTargetZoneID string?
local SweepGroup = {} local SweepGroup = {}
SweepGroup.__index = SweepGroup SweepGroup.__index = SweepGroup
@@ -26,9 +26,10 @@ function SweepGroup.New(groupName, config, logger, spawnManager)
return self return self
end end
---@param stageID string
---@return string? ---@return string?
function SweepGroup:GetZoneIDWhenStageID(stageID) function SweepGroup:GetZoneIDWhenStageID(stageID)
return self._targetZoneIdPerStage[stageID] return self._targetZoneIdPerStage[tostring(stageID)]
end end
---@return string? ---@return string?
@@ -41,7 +42,7 @@ end
---@field self SweepGroup ---@field self SweepGroup
---@param params SetTaskParams ---@param params SetTaskParams
local setMissionDelayedTask = function(params, time) local setMissionDelayedTask = function(params, _)
params.self:SetMissionPrivate(params.task) params.self:SetMissionPrivate(params.task)
end end
@@ -50,8 +51,6 @@ function SweepGroup:SendToZone(zone, targetZoneID, airbase)
self._currentTargetZoneID = targetZoneID self._currentTargetZoneID = targetZoneID
local group = Group.getByName(self._groupName)
local mission = SWEEP.getAsMissionFromAirbase(self._groupName, airbase, zone, self._config) local mission = SWEEP.getAsMissionFromAirbase(self._groupName, airbase, zone, self._config)
if mission then if mission then
---@type SetTaskParams ---@type SetTaskParams
@@ -79,16 +78,16 @@ function SweepGroup:InitWithName(groupName)
local configPart = split_string[2] local configPart = split_string[2]
configPart = string.sub(configPart, 2, #configPart) configPart = string.sub(configPart, 2, #configPart)
local subsplit = Util.split_string(configPart, "|") local subsplit = Util.split_string(configPart, "|")
if subsplit then if subsplit then
for key, value in pairs(subsplit) do for _, value in pairs(subsplit) do
local keySplit = Util.split_string(value, "]") local keySplit = Util.split_string(value, "]")
local targetZone = keySplit[2] local targetZone = keySplit[2]
local allActives = string.sub(keySplit[1], 2, #keySplit[1]) local allActives = string.sub(keySplit[1], 2, #keySplit[1])
local commaSeperated = Util.split_string(allActives, ",") local commaSeperated = Util.split_string(allActives, ",")
for _, value in pairs(commaSeperated) do for _, childValue in pairs(commaSeperated) do
local dashSeperated = Util.split_string(value, "-") local dashSeperated = Util.split_string(childValue, "-")
if Util.tableLength(dashSeperated) > 1 then if Util.tableLength(dashSeperated) > 1 then
local from = tonumber(dashSeperated[1]) local from = tonumber(dashSeperated[1])
local till = tonumber(dashSeperated[2]) local till = tonumber(dashSeperated[2])
@@ -8,6 +8,7 @@ DetectionManager.__index = DetectionManager
---@param logger Logger ---@param logger Logger
---@return DetectionManager
function DetectionManager.New(logger) function DetectionManager.New(logger)
local self = setmetatable({}, DetectionManager) local self = setmetatable({}, DetectionManager)
@@ -24,6 +25,7 @@ function DetectionManager.New(logger)
---@param selfA DetectionManager ---@param selfA DetectionManager
---@param time number ---@param time number
---@return number?
local updateDetectingUnitsTask = function(selfA, time) local updateDetectingUnitsTask = function(selfA, time)
selfA:UpdateDetectingUnits() selfA:UpdateDetectingUnits()
return time + 120 return time + 120
@@ -32,18 +34,19 @@ function DetectionManager.New(logger)
---@param selfA DetectionManager ---@param selfA DetectionManager
---@param time number ---@param time number
---@return number?
local updateDetected = function(selfA, time) local updateDetected = function(selfA, time)
selfA:UpdateDetectedUnits() selfA:UpdateDetectedUnits()
return time + 10 return time + 10
end end
timer.scheduleFunction(updateDetected, self, timer.getTime() + 130) timer.scheduleFunction(updateDetected, self, timer.getTime() + 130)
return self return self
end end
---@param unitName string ---@param unitName string
---@param coalitionSide CoalitionSide ---@param coalitionSide CoalitionSide
---@return boolean
function DetectionManager:IsUnitDetectedBy(unitName, coalitionSide) function DetectionManager:IsUnitDetectedBy(unitName, coalitionSide)
local coalitionString = tostring(coalitionSide) local coalitionString = tostring(coalitionSide)
if not self._detectedUnits[coalitionString] then if not self._detectedUnits[coalitionString] then
@@ -57,7 +60,8 @@ function DetectionManager:IsUnitDetectedBy(unitName, coalitionSide)
return timer.getTime() - self._detectedUnits[coalitionString][unitName] < 20 return timer.getTime() - self._detectedUnits[coalitionString][unitName] < 20
end end
---@return Array<string> ---@param coalitionSide CoalitionSide
---@return Array<string> detectedUnits
function DetectionManager:GetDetectedUnitsBy(coalitionSide) function DetectionManager:GetDetectedUnitsBy(coalitionSide)
local coalitionString = tostring(coalitionSide) local coalitionString = tostring(coalitionSide)
if not self._detectedUnits[coalitionString] then if not self._detectedUnits[coalitionString] then
@@ -21,7 +21,7 @@ end
---comment ---comment
---@param weapon Weapon ---@param weapon Weapon
function RunwayBombingTracker:OnWeaponFired(unit, weapon, target) function RunwayBombingTracker:OnWeaponFired(_, weapon, _)
if weapon == nil then if weapon == nil then
return return
@@ -30,7 +30,7 @@ function RunwayBombingTracker:OnWeaponFired(unit, weapon, target)
local desc = weapon:getDesc() local desc = weapon:getDesc()
local isTrackable = desc.category == Weapon.Category.BOMB or (desc.category == Weapon.Category.MISSILE and desc.missileCategory == Weapon.MissileCategory.CRUISE) local isTrackable = desc.category == Weapon.Category.BOMB or (desc.category == Weapon.Category.MISSILE and desc.missileCategory == Weapon.MissileCategory.CRUISE)
if isTrackable == true then if isTrackable == true then
---@type WeaponTrackingArgs ---@type WeaponTrackingArgs
local weaponTrackingArgs = { local weaponTrackingArgs = {
weapon = weapon, weapon = weapon,
@@ -56,12 +56,14 @@ end
---@field self RunwayBombingTracker ---@field self RunwayBombingTracker
---@private ---@private
---@param time number
---@param weaponTrackingArgs WeaponTrackingArgs ---@param weaponTrackingArgs WeaponTrackingArgs
---@return number?
function RunwayBombingTracker.trackWeaponTask(weaponTrackingArgs, time) function RunwayBombingTracker.trackWeaponTask(weaponTrackingArgs, time)
local weapon = weaponTrackingArgs.weapon local weapon = weaponTrackingArgs.weapon
local self = weaponTrackingArgs.self local self = weaponTrackingArgs.self
if not weapon or weapon:isExist() == false then return nil end if not weapon or weapon:isExist() == false then return nil end
local pos = weapon:getPoint() local pos = weapon:getPoint()
@@ -83,11 +85,7 @@ function RunwayBombingTracker.trackWeaponTask(weaponTrackingArgs, time)
x = pos.x + velocity.x * nextInterval, x = pos.x + velocity.x * nextInterval,
y = pos.z + velocity.z * nextInterval y = pos.z + velocity.z * nextInterval
} }
self:OnWeaponImpact(weapon:getDesc(), impactPoint) self:OnWeaponImpact(weapon:getDesc(), impactPoint)
return nil return nil
end end
@@ -106,11 +104,10 @@ function RunwayBombingTracker:OnWeaponImpact(weaponDesc, impactPoint)
self._logger:debug("RunwayBombingTracker:OnWeaponImpact") self._logger:debug("RunwayBombingTracker:OnWeaponImpact")
local warhead = weaponDesc.warhead local warhead = weaponDesc.warhead --[[@as table]]
local explosiveMass = warhead.explosiveMass or warhead.shapedExplosiveMass local explosiveMass = (warhead.explosiveMass or warhead.shapedExplosiveMass) --[[@as number]]
for runway, strikeMission in pairs(self.trackedRunways) do
for _, strikeMission in pairs(self.trackedRunways) do
local zone= strikeMission:GetRunwayZone() local zone= strikeMission:GetRunwayZone()
if Util.is3dPointInZone({ x = impactPoint.x, z = impactPoint.y, y = 0 }, zone) then if Util.is3dPointInZone({ x = impactPoint.x, z = impactPoint.y, y = 0 }, zone) then
+13 -8
View File
@@ -7,6 +7,7 @@ local CAP = {}
---@param attackHelos boolean ---@param attackHelos boolean
---@return table ---@return table
local function GetCAPTargetTypes(attackHelos) local function GetCAPTargetTypes(attackHelos)
---@type Array<string>
local targetTypes = { local targetTypes = {
[1] = "Planes", [1] = "Planes",
} }
@@ -33,9 +34,6 @@ local function GetCAPPointFromTriggerZone(airBase, capZone)
local furthestA = nil local furthestA = nil
local furthestB = nil local furthestB = nil
local furthestFromBase = nil
local furthestFromBaseDistance = 0
local furthestDistance = 0 local furthestDistance = 0
for indexA, pointA in ipairs(capZone.verts) do for indexA, pointA in ipairs(capZone.verts) do
@@ -56,7 +54,10 @@ local function GetCAPPointFromTriggerZone(airBase, capZone)
---@type Vec2 ---@type Vec2
local baseVec2 = { x = baseVec3.x, y = baseVec3.z } local baseVec2 = { x = baseVec3.x, y = baseVec3.z }
---@type Vec2
local pointA = capZone.verts[furthestA] local pointA = capZone.verts[furthestA]
---@type Vec2
local pointB = capZone.verts[furthestB] local pointB = capZone.verts[furthestB]
local furthest = pointA local furthest = pointA
local closest = pointB local closest = pointB
@@ -87,6 +88,7 @@ end
---@param airbase Airbase ---@param airbase Airbase
---@param capZone SpearheadTriggerZone ---@param capZone SpearheadTriggerZone
---@param capConfig CapConfig ---@param capConfig CapConfig
---@return table
local GetOutboundTask = function(airbase, capZone, capConfig) local GetOutboundTask = function(airbase, capZone, capConfig)
local airbaseVec3 = airbase:getPoint() local airbaseVec3 = airbase:getPoint()
local airbaseVec2 = { x = airbaseVec3.x, y = airbaseVec3.z } local airbaseVec2 = { x = airbaseVec3.x, y = airbaseVec3.z }
@@ -128,6 +130,7 @@ end
---@param airbase Airbase ---@param airbase Airbase
---@param capZone SpearheadTriggerZone ---@param capZone SpearheadTriggerZone
---@param capConfig CapConfig ---@param capConfig CapConfig
---@return table
function CAP.getAsMissionFromAirbase(groupName, airbase, capZone, capConfig) function CAP.getAsMissionFromAirbase(groupName, airbase, capZone, capConfig)
local points = { local points = {
[1] = GetOutboundTask(airbase, capZone, capConfig), [1] = GetOutboundTask(airbase, capZone, capConfig),
@@ -155,6 +158,7 @@ end
---@param airbase Airbase ---@param airbase Airbase
---@param capZone SpearheadTriggerZone ---@param capZone SpearheadTriggerZone
---@param capConfig CapConfig ---@param capConfig CapConfig
---@return table
function CAP.getAsMission(groupName, airbase, capZone, capConfig) function CAP.getAsMission(groupName, airbase, capZone, capConfig)
local points = { local points = {
[1] = CAP.getAsTasking(groupName, airbase, capZone, capConfig), [1] = CAP.getAsTasking(groupName, airbase, capZone, capConfig),
@@ -181,6 +185,7 @@ end
---@param airbase Airbase ---@param airbase Airbase
---@param capZone SpearheadTriggerZone ---@param capZone SpearheadTriggerZone
---@param capConfig CapConfig ---@param capConfig CapConfig
---@return table
function CAP.getAsTasking(groupName, airbase, capZone, capConfig) function CAP.getAsTasking(groupName, airbase, capZone, capConfig)
local duration = math.random(capConfig:getMinDurationOnStation(), capConfig:getMaxDurationOnStation()) or 1500 local duration = math.random(capConfig:getMinDurationOnStation(), capConfig:getMaxDurationOnStation()) or 1500
@@ -220,7 +225,7 @@ function CAP.getAsTasking(groupName, airbase, capZone, capConfig)
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishOnStation, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishOnStation, \"" .. groupName .. "\")"
} }
} }
} }
@@ -260,7 +265,7 @@ function CAP.getAsTasking(groupName, airbase, capZone, capConfig)
}, },
stopCondition = { stopCondition = {
duration = durationBefore10, duration = durationBefore10,
condition = "return Spearhead.DcsUtil.NeedsRTBInTen(\"" .. groupName .. "\", 0.10)", condition = "return GlobalCapCallBacks.NeedsRTBInTen(\"" .. groupName .. "\", 0.10)",
} }
} }
}, },
@@ -273,7 +278,7 @@ function CAP.getAsTasking(groupName, airbase, capZone, capConfig)
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishRTBInTen, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishRTBInTen, \"" .. groupName .. "\")"
} }
} }
} }
@@ -299,7 +304,7 @@ function CAP.getAsTasking(groupName, airbase, capZone, capConfig)
}, },
stopCondition = { stopCondition = {
duration = durationAfter10, duration = durationAfter10,
condition = "return Spearhead.DcsUtil.IsBingoFuel(\"" .. groupName .. "\")", condition = "return GlobalCapCallBacks.IsBingoFuel(\"" .. groupName .. "\", 0.10)",
} }
} }
}, },
@@ -312,7 +317,7 @@ function CAP.getAsTasking(groupName, airbase, capZone, capConfig)
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")"
} }
} }
} }
@@ -0,0 +1,26 @@
local DcsUtil = require("classes.util.DcsUtil")
local Events = require("classes.spearhead_events")
GlobalCapCallBacks = {}
function GlobalCapCallBacks.IsBingoFuel(groupName, fuelPercent)
return DcsUtil.IsBingoFuel(groupName, fuelPercent)
end
function GlobalCapCallBacks.NeedsRTBInTen(groupName, fuelOffset)
return DcsUtil.NeedsRTBInTen(groupName, fuelOffset)
end
function GlobalCapCallBacks.PublishRTBInTen(groupName)
return Events.PublishRTBInTen(groupName)
end
function GlobalCapCallBacks.PublishRTB(groupName)
return Events.PublishRTB(groupName)
end
function GlobalCapCallBacks.PublishOnStation(groupName)
return Events.PublishOnStation(groupName)
end
@@ -11,6 +11,7 @@ local INTERCEPT = {}
---@param speed number? ---@param speed number?
---@param alt number? ---@param alt number?
---@param config CapConfig ---@param config CapConfig
---@return table
function INTERCEPT.getMissionFromAirbase(groupName, interceptPoint, airbase, config, speed, alt) function INTERCEPT.getMissionFromAirbase(groupName, interceptPoint, airbase, config, speed, alt)
local airbaseVec3 = airbase:getPoint() local airbaseVec3 = airbase:getPoint()
@@ -38,7 +39,7 @@ function INTERCEPT.getMissionFromAirbase(groupName, interceptPoint, airbase, con
id = 'Mission', id = 'Mission',
params = { params = {
airborne = true, airborne = true,
route = { route = {
points = points points = points
} }
} }
@@ -92,7 +93,7 @@ end
---@param currentPoint Vec2 ---@param currentPoint Vec2
---@param targetPosition Vec2 ---@param targetPosition Vec2
---@param targetUnit Unit ---@param targetUnit Unit
---@param airbase any ---@param airbase Airbase
---@param config any ---@param config any
---@param speed number? ---@param speed number?
---@param alt number? ---@param alt number?
@@ -159,10 +160,10 @@ function INTERCEPT.getInterceptTaskPoint(groupName, currentPoint, targetPoint, a
params = { params = {
tasks = { tasks = {
id = 'EngageTargetsInZone', id = 'EngageTargetsInZone',
params = { params = {
point = targetPoint, point = targetPoint,
zoneRadius = 10 * 1852, -- 10 NM, point will be updated, so target should be in this zone. zoneRadius = 10 * 1852, -- 10 NM, point will be updated, so target should be in this zone.
targetTypes = { targetTypes = {
[1] = "Planes", [1] = "Planes",
}, },
priority = 0 priority = 0
@@ -188,10 +189,10 @@ function INTERCEPT.getInterceptTaskPoint(groupName, currentPoint, targetPoint, a
params = { params = {
tasks = { tasks = {
id = 'EngageTargetsInZone', id = 'EngageTargetsInZone',
params = { params = {
point = targetPoint, point = targetPoint,
zoneRadius = 10 * 1852, -- 10 NM, point will be updated, so target should be in this zone. zoneRadius = 10 * 1852, -- 10 NM, point will be updated, so target should be in this zone.
targetTypes = { targetTypes = {
[1] = "Planes", [1] = "Planes",
}, },
priority = 0 priority = 0
@@ -247,7 +248,7 @@ function INTERCEPT.getInterceptTaskPoint(groupName, currentPoint, targetPoint, a
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")"
} }
} }
} }
@@ -379,7 +380,7 @@ function INTERCEPT.getUnitInterceptTaskPoint(groupName, currentPoint, targetPosi
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")"
} }
} }
} }
+8 -3
View File
@@ -7,6 +7,7 @@ local RTB = {}
---@param airbase Airbase ---@param airbase Airbase
---@param missionPoint Vec2 ---@param missionPoint Vec2
---@param capConfig CapConfig ---@param capConfig CapConfig
---@return table
function RTB.getAsMission(airbase, missionPoint, capConfig) function RTB.getAsMission(airbase, missionPoint, capConfig)
return { return {
id = "Mission", id = "Mission",
@@ -47,7 +48,7 @@ local getRunwayIntoWindCourseRad = function(airbase)
else else
rad = 0 - rad rad = 0 - rad
end end
local runwayVec = {x = math.cos(rad), z = math.sin(rad), y = 0} local runwayVec = {x = math.cos(rad), z = math.sin(rad), y = 0}
local alignment = Util.vectorAlignment(windVec, runwayVec) local alignment = Util.vectorAlignment(windVec, runwayVec)
@@ -60,14 +61,14 @@ local getRunwayIntoWindCourseRad = function(airbase)
do --inverse do --inverse
local degree = math.deg(runway.course) local degree = math.deg(runway.course)
degree = (degree + 180) % 360 degree = (degree + 180) % 360
local rad = math.rad(degree) local rad = math.rad(degree)
if rad < 0 then if rad < 0 then
rad = math.abs(rad) rad = math.abs(rad)
else else
rad = 0 - rad rad = 0 - rad
end end
local runwayVec = {x = math.cos(rad), z = math.sin(rad), y = 0} local runwayVec = {x = math.cos(rad), z = math.sin(rad), y = 0}
local alignment = Util.vectorAlignment(windVec, runwayVec) local alignment = Util.vectorAlignment(windVec, runwayVec)
@@ -113,6 +114,7 @@ end
---comment ---comment
---@param airbase Airbase ---@param airbase Airbase
---@param missionPoint Vec2 ---@param missionPoint Vec2
---@return table
---@param capConfig CapConfig ---@param capConfig CapConfig
function RTB.getApproachPoint(airbase, missionPoint, capConfig) function RTB.getApproachPoint(airbase, missionPoint, capConfig)
@@ -147,6 +149,7 @@ function RTB.getApproachPoint(airbase, missionPoint, capConfig)
end end
---@param airbase Airbase ---@param airbase Airbase
---@return table
function RTB.getInitialPoint(airbase) function RTB.getInitialPoint(airbase)
local point = calcInitialPoint(airbase) local point = calcInitialPoint(airbase)
return { return {
@@ -170,6 +173,8 @@ function RTB.getInitialPoint(airbase)
} }
end end
---@param airbase Airbase
---@return table
function RTB.getLandingPoint(airbase) function RTB.getLandingPoint(airbase)
local basePoint = airbase:getPoint() local basePoint = airbase:getPoint()
return { return {
+7 -2
View File
@@ -7,6 +7,7 @@ local SWEEP = {}
---@param attackHelos boolean ---@param attackHelos boolean
---@return table ---@return table
local function GetCAPTargetTypes(attackHelos) local function GetCAPTargetTypes(attackHelos)
---@type Array<string>
local targetTypes = { local targetTypes = {
[1] = "Planes", [1] = "Planes",
} }
@@ -49,7 +50,9 @@ local function GetCAPPointFromTriggerZone(airBase, capZone)
---@type Vec2 ---@type Vec2
local baseVec2 = { x = baseVec3.x, y = baseVec3.z } local baseVec2 = { x = baseVec3.x, y = baseVec3.z }
---@type Vec2
local pointA = capZone.verts[furthestA] local pointA = capZone.verts[furthestA]
---@type Vec2
local pointB = capZone.verts[furthestB] local pointB = capZone.verts[furthestB]
local furthest = pointA local furthest = pointA
local closest = pointB local closest = pointB
@@ -80,6 +83,7 @@ end
---@param airbase Airbase ---@param airbase Airbase
---@param capZone SpearheadTriggerZone ---@param capZone SpearheadTriggerZone
---@param capConfig CapConfig ---@param capConfig CapConfig
---@return table
local GetOutboundTask = function(airbase, capZone, capConfig) local GetOutboundTask = function(airbase, capZone, capConfig)
local airbaseVec3 = airbase:getPoint() local airbaseVec3 = airbase:getPoint()
local airbaseVec2 = { x = airbaseVec3.x, y = airbaseVec3.z } local airbaseVec2 = { x = airbaseVec3.x, y = airbaseVec3.z }
@@ -121,6 +125,7 @@ end
---@param airbase Airbase ---@param airbase Airbase
---@param capZone SpearheadTriggerZone ---@param capZone SpearheadTriggerZone
---@param capConfig CapConfig ---@param capConfig CapConfig
---@return table
function SWEEP.getAsMissionFromAirbase(groupName, airbase, capZone, capConfig) function SWEEP.getAsMissionFromAirbase(groupName, airbase, capZone, capConfig)
local pointA, pointB, pointC = SWEEP.getAsTasking(groupName, airbase, capZone, capConfig) local pointA, pointB, pointC = SWEEP.getAsTasking(groupName, airbase, capZone, capConfig)
@@ -189,7 +194,7 @@ function SWEEP.getAsTasking(groupName, airbase, capZone, capConfig)
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishOnStation, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishOnStation, \"" .. groupName .. "\")"
} }
} }
} }
@@ -270,7 +275,7 @@ function SWEEP.getAsTasking(groupName, airbase, capZone, capConfig)
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")"
} }
} }
} }
+5 -1
View File
@@ -21,6 +21,10 @@ function CapConfig.new()
if SpearheadConfig == nil then SpearheadConfig = {} end if SpearheadConfig == nil then SpearheadConfig = {} end
if SpearheadConfig.CapConfig == nil then SpearheadConfig.CapConfig = {} end if SpearheadConfig.CapConfig == nil then SpearheadConfig.CapConfig = {} end
if type(SpearheadConfig.CapConfig.enabled) ~= "boolean" then
SpearheadConfig.CapConfig.enabled = true
end
local enabled = SpearheadConfig.CapConfig.enabled local enabled = SpearheadConfig.CapConfig.enabled
if enabled == nil then enabled = true end if enabled == nil then enabled = true end
self._isEnabled = enabled self._isEnabled = enabled
@@ -34,7 +38,7 @@ function CapConfig.new()
self._minDurationOnStation = 1200 self._minDurationOnStation = 1200
self._maxDurationOnStation = 2700 self._maxDurationOnStation = 2700
self._maxDeviationRange = 35 * 1852 -- in meters self._maxDeviationRange = (tonumber(SpearheadConfig.CapConfig.maxCommitRange) or 35) * 1852 -- in meters
self._rearmDelay = tonumber(SpearheadConfig.CapConfig.rearmDelay) or 600 self._rearmDelay = tonumber(SpearheadConfig.CapConfig.rearmDelay) or 600
self._repairDelay = tonumber(SpearheadConfig.CapConfig.repairDelay) or 600 self._repairDelay = tonumber(SpearheadConfig.CapConfig.repairDelay) or 600
self._deathDelay = tonumber(SpearheadConfig.CapConfig.deathDelay) or 1800 self._deathDelay = tonumber(SpearheadConfig.CapConfig.deathDelay) or 1800
+21 -6
View File
@@ -1,10 +1,8 @@
local briefingMessageTime = nil
---@class GlobalConfig ---@class GlobalConfig
---@field private _briefingTime number ; ---@field private _briefingTime number
---@field private _debugEnabled boolean
---@field private _debugMenuEnabled boolean
local GlobalConfig = {} local GlobalConfig = {}
GlobalConfig.__index = GlobalConfig; GlobalConfig.__index = GlobalConfig;
@@ -19,15 +17,32 @@ function GlobalConfig.New()
if SpearheadConfig.briefingMessageDuration then if SpearheadConfig.briefingMessageDuration then
self._briefingTime = SpearheadConfig.briefingMessageDuration self._briefingTime = SpearheadConfig.briefingMessageDuration
end end
if SpearheadConfig.debugEnabled ~= nil then
self._debugEnabled = SpearheadConfig.debugEnabled
else
self._debugEnabled = false
end
self._debugMenuEnabled = SpearheadConfig.debugMenuEnabled == true
end end
return self return self
end end
function GlobalConfig:getBriefingTime() function GlobalConfig:getBriefingTime()
return self._briefingTime or 30 return self._briefingTime or 60
end end
function GlobalConfig:isDebugEnabled()
return self._debugEnabled or false
end
---@return boolean
function GlobalConfig:isDebugMenuEnabled()
return self._debugMenuEnabled or false
end
return GlobalConfig 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
+34 -16
View File
@@ -7,29 +7,47 @@
--- @field startingStage integer --- @field startingStage integer
--- @field maxMissionsPerStage integer --- @field maxMissionsPerStage integer
--- @field AmountPreactivateStage integer --- @field AmountPreactivateStage integer
--- @field briefingOnSpawnEnabled boolean
local StageConfig = {}; local StageConfig = {};
StageConfig.__index = StageConfig
local Logger = require("classes.util.Logger")
local _logger = Logger.new("StageConfig", Logger.LogLevel)
---comment ---comment
---@return StageConfig ---@return StageConfig
function StageConfig:new() local function new()
if SpearheadConfig == nil then SpearheadConfig = {} end if SpearheadConfig == nil then
if SpearheadConfig.StageConfig == nil then SpearheadConfig.StageConfig = {} end _logger:warn("SpearheadConfig is nil, creating default SpearheadConfig")
SpearheadConfig = {}
end
---@type StageConfig if SpearheadConfig.StageConfig == nil then
local o = { _logger:warn("SpearheadConfig.StageConfig is nil, creating default StageConfig")
isEnabled = SpearheadConfig.StageConfig.enabled or true, SpearheadConfig.StageConfig = {}
isDrawStagesEnabled = SpearheadConfig.StageConfig.drawStages or true, end
isAutoStages = SpearheadConfig.StageConfig.autoStages or true,
startingStage = SpearheadConfig.StageConfig.startingStage or 1,
maxMissionsPerStage = SpearheadConfig.StageConfig.maxMissionStage or 10,
isDrawPreActivatedEnabled = SpearheadConfig.StageConfig.drawPreActivated or true,
AmountPreactivateStage = SpearheadConfig.StageConfig.preactivateStage or 1,
}
setmetatable(o, { __index = self })
return o; local self = setmetatable({}, StageConfig)
self.isEnabled = SpearheadConfig.StageConfig.enabled ~= false
self.isDrawStagesEnabled = SpearheadConfig.StageConfig.drawStages ~= false
self.isAutoStages = SpearheadConfig.StageConfig.autoStages ~= false
self.startingStage = SpearheadConfig.StageConfig.startingStage or 1
self.maxMissionsPerStage = SpearheadConfig.StageConfig.maxMissionStage or 10
self.isDrawPreActivatedEnabled = SpearheadConfig.StageConfig.drawPreActivated ~= false
self.AmountPreactivateStage = SpearheadConfig.StageConfig.preactivateStage or 1
self.briefingOnSpawnEnabled = SpearheadConfig.StageConfig.briefingOnSpawn ~= false
_logger:info("Successfully created StageConfig Object")
return self;
end
local config = new();
---@return StageConfig
function StageConfig:getInstance()
return config
end end
return StageConfig return StageConfig
+79
View File
@@ -0,0 +1,79 @@
local Logger = require("classes.util.Logger")
local StageRepository = require("classes.stageClasses.StageRepository")
---@class DebugMenu
---@field private _logger Logger
---@field private _stageRepository StageRepository
local DebugMenu = {}
DebugMenu.__index = DebugMenu
function DebugMenu.new()
local self = setmetatable({}, DebugMenu)
self._logger = Logger.new("DebugMenu")
self._stageRepository = StageRepository.getInstance()
return self
end
local menuName = "Spearhead Debug"
local debugMenuPath = { [1] = menuName }
function DebugMenu:RegisterMenus()
missionCommands.addSubMenu(menuName, {})
local refresh = function(params)
local selfA = params.self --[[@as DebugMenu]]
selfA:RefreshMenu()
end
missionCommands.addCommand("Refresh Menu", debugMenuPath, refresh, { self = self })
self:AddStageOptions()
end
function DebugMenu:RefreshMenu()
missionCommands.removeItem(debugMenuPath)
self:RegisterMenus()
end
function DebugMenu:AddStageOptions()
local stageMenuTable = missionCommands.addSubMenu("Stages", debugMenuPath)
local stageLanes = self._stageRepository:getAllStageLanes()
for _, stageLane in pairs(stageLanes) do
local stages = stageLane:GetStagesAtIndex(stageLane:GetActiveStageIndex())
if stages then
for _, stage in pairs(stages) do
local stageLaneId = stageLane:GetStageLaneIdentifier() or ""
local stageIndex = stage:GetStageIndex()
local stageName = stage:GetStageName()
local stageMenuName = stageLaneId .. stageIndex .. "_" .. stageName
local currentStageMenuTable = missionCommands.addSubMenu(stageMenuName, stageMenuTable)
---@class CompleteStageParams
---@field self DebugMenu
---@field stage Stage
---@param params CompleteStageParams
local completeStage = function(params)
local stageA = params.stage
local missions = stageA:GetMissions()
for _, mission in pairs(missions) do
mission:ForceMissionComplete()
end
end
---@type CompleteStageParams
local params = { self = self, stage = stage }
missionCommands.addCommand("Complete Stage", currentStageMenuTable, completeStage, params)
end
else
self._logger:info("No stages found for lane: " .. (stageLane:GetStageLaneIdentifier() or "default") .. " at index: " .. (stageLane:GetActiveStageIndex() or "nil"))
end
end
end
return DebugMenu
-19
View File
@@ -1,19 +0,0 @@
do -- mission aliases
--- @alias LogLevel
--- | "DEBUG"
--- | "INFO"
--- | "WARN"
--- | "ERROR"
--- | "NONE"
---@class Array<T>: { [integer]: T }
end
---@class KeyValuePair
---@field key string
---@field value any
+55 -41
View File
@@ -4,37 +4,43 @@ local MissionEditorWarning = require("classes.util.MissionEditorWarnings")
local RouteUtil = require("classes.spearhead_routeutil") local RouteUtil = require("classes.spearhead_routeutil")
local SpearheadEvents = require("classes.spearhead_events") local SpearheadEvents = require("classes.spearhead_events")
---@class FleetGroup ---@class FleetGroup : OnStageChangedListener
---@field private fleetGroupName string
---@field private logger Logger
---@field private fleetNameIdentifier string
---@field private targetZonePerStage table<string, string>
---@field private currentTargetZone string
---@field private pointsPerZone table<string, {pointA: Vec3, pointB: Vec3}>
local FleetGroup = {} local FleetGroup = {}
FleetGroup.__index = FleetGroup
---comment ---comment
---@param fleetGroupName string ---@param fleetGroupName string
---@param database Database ---@param database Database
---@param logger Logger ---@param logger Logger
---@return nil ---@return FleetGroup?
function FleetGroup:new(fleetGroupName, database, logger) function FleetGroup.new(fleetGroupName, database, logger)
local o = {}
setmetatable(o, { __index = self }) local self = setmetatable({}, FleetGroup)
o.fleetGroupName = fleetGroupName self.fleetGroupName = fleetGroupName
o.logger = logger self.logger = logger
local split_name = Util.split_string(fleetGroupName, "_") local split_name = Util.split_string(fleetGroupName, "_")
if Util.tableLength(split_name) < 2 then if Util.tableLength(split_name) < 2 then
MissionEditorWarning.Add("CARRIERGROUP should have at least 2 parts. CARRIERGROUP_<fleetname>") MissionEditorWarning.Add("CARRIERGROUP should have at least 2 parts. CARRIERGROUP_<fleetname>")
return nil return nil
end end
o.fleetNameIdentifier = split_name[2] self.fleetNameIdentifier = split_name[2]
o.targetZonePerStage = {} self.targetZonePerStage = {}
o.currentTargetZone = nil self.currentTargetZone = nil
o.pointsPerZone = {} self.pointsPerZone = {}
do --INIT do --INIT
local carrierRouteZones = database:getCarrierRouteZones() local carrierRouteZones = database:getCarrierRouteZones()
for _, zoneName in pairs(carrierRouteZones) do for _, zoneName in pairs(carrierRouteZones) do
if Util.strContains(string.lower(zoneName), "_".. string.lower(o.fleetNameIdentifier) .. "_" ) == true then if Util.strContains(string.lower(zoneName), "_".. string.lower(self.fleetNameIdentifier) .. "_" ) == true then
local zone = DcsUtil.getZoneByName(zoneName) local zone = DcsUtil.getZoneByName(zoneName)
if zone and zone.zone_type == DcsUtil.ZoneType.Polygon then if zone and zone.zone_type == DcsUtil.ZoneType.Polygon then
local split_string = Util.split_string(zoneName, "_") local split_string = Util.split_string(zoneName, "_")
@@ -43,18 +49,18 @@ function FleetGroup:new(fleetGroupName, database, logger)
"CARRIERROUTE should at least have 3 parts. Check the documentation for: " .. zoneName) "CARRIERROUTE should at least have 3 parts. Check the documentation for: " .. zoneName)
else else
---@param zone SpearheadTriggerZone ---@param zoneA SpearheadTriggerZone
---@return Vec2, Vec2 ---@return Vec2, Vec2
local function GetTwoFurthestPoints(zone) local function GetTwoFurthestPoints(zoneA)
local biggest = nil local biggest = nil
local biggestA = zone.verts[1] local biggestA = zoneA.verts[1]
local biggestB = zone.verts[2] local biggestB = zoneA.verts[2]
for i = 1, 3 do for i = 1, 3 do
for ii = i + 1, 4 do for ii = i + 1, 4 do
local a = zone.verts[i] local a = zoneA.verts[i]
local b = zone.verts[ii] local b = zoneA.verts[ii]
local dist = Util.VectorDistance2d(a, b) local dist = Util.VectorDistance2d(a, b)
if biggest == nil or dist > biggest then if biggest == nil or dist > biggest then
@@ -72,7 +78,7 @@ function FleetGroup:new(fleetGroupName, database, logger)
return nil, nil return nil, nil
end end
if Util.startswith(namePart, "%[") == true then if Util.startsWith(namePart, "%[") == true then
namePart = Util.split_string(namePart, "[")[1] namePart = Util.split_string(namePart, "[")[1]
end end
@@ -100,9 +106,15 @@ function FleetGroup:new(fleetGroupName, database, logger)
local first, second = getMinMaxStage(split_string[3]) local first, second = getMinMaxStage(split_string[3])
if first ~= nil and second ~= nil then if first ~= nil and second ~= nil then
for i = first, second do for i = first, second do
o.targetZonePerStage[tostring(i)] = zoneName self.targetZonePerStage[tostring(i)] = zoneName
end end
o.pointsPerZone[zoneName] = { pointA = { x = pointA.x, z = pointA.y, y = 0 }, pointB = { x = pointB.x, z = pointB.y, y = 0} } ---@type {pointA: Vec3, pointB: Vec3}
local entry = {
pointA = {x = pointA.x, z = pointA.y, y = 0},
pointB = {x = pointB.x, z = pointB.y, y = 0}
}
self.pointsPerZone[zoneName] = entry
else else
MissionEditorWarning.Add("CARRIERROUTE zone stage numbers not in the format _[<number>-<number>]: " .. zoneName) MissionEditorWarning.Add("CARRIERROUTE zone stage numbers not in the format _[<number>-<number>]: " .. zoneName)
end end
@@ -114,30 +126,32 @@ function FleetGroup:new(fleetGroupName, database, logger)
end end
end end
local SetTaskAsync = function(input, time) SpearheadEvents.AddStageNumberChangedListener(self)
local targetZone = input.targetZone return self
local task = input.task end
local groupName = input.groupName
local logger = input.logger
local group = Group.getByName(groupName) local SetTaskAsync = function(input, _)
if group then local targetZone = input.targetZone --[[@as string]]
logger:info("Sending " .. groupName .. " to " .. targetZone) local task = input.task --[[@as table]]
group:getController():setTask(task) local groupName = input.groupName --[[@as string]]
end local l_logger = input.logger --[[@as Logger]]
local group = Group.getByName(groupName)
if group then
l_logger:info("Sending " .. groupName .. " to " .. targetZone)
group:getController():setTask(task)
end end
end
o.OnStageNumberChanged = function(self, number) function FleetGroup:OnStageNumberChanged(number, laneIdentifier)
local targetZone = self.targetZonePerStage[tostring(number)] -- only react on "main" lane changes, ignore other lanes for now
if targetZone and targetZone ~= self.currentTargetZone then if laneIdentifier ~= nil then return end
local points = self.pointsPerZone[targetZone] local targetZone = self.targetZonePerStage[tostring(number)]
local task = RouteUtil.CreateCarrierRacetrack(points.pointA, points.pointB) if targetZone and targetZone ~= self.currentTargetZone then
timer.scheduleFunction(SetTaskAsync, { task = task, targetZone = targetZone, groupName = self.fleetGroupName, logger = self.logger }, timer.getTime() + 5) local points = self.pointsPerZone[targetZone]
end local task = RouteUtil.CreateCarrierRacetrack(points.pointA, points.pointB)
timer.scheduleFunction(SetTaskAsync, { task = task, targetZone = targetZone, groupName = self.fleetGroupName, logger = self.logger }, timer.getTime() + 5)
end end
SpearheadEvents.AddStageNumberChangedListener(o)
return o
end end
return FleetGroup return FleetGroup
@@ -15,9 +15,9 @@ GlobalFleetManager.start = function(database)
local all_groups = MizGroupsManager.getAllGroupNames() local all_groups = MizGroupsManager.getAllGroupNames()
for _, groupName in pairs(all_groups) do for _, groupName in pairs(all_groups) do
if Util.startswith(string.lower(groupName), "carriergroup" ) == true then if Util.startsWith(string.lower(groupName), "carriergroup" ) == true then
logger:info("Registering " .. groupName .. " as a managed fleet") logger:info("Registering " .. groupName .. " as a managed fleet")
local carrierGroup = FleetGroup:new(groupName, database, logger) local carrierGroup = FleetGroup.new(groupName, database, logger)
table.insert(fleetGroups, carrierGroup) table.insert(fleetGroups, carrierGroup)
end end
end end
+31 -9
View File
@@ -15,24 +15,45 @@ MizGroupsManager._groupNames = {}
MizGroupsManager._spawnTemplateData = {} MizGroupsManager._spawnTemplateData = {}
do --init do --init
for coalition_name, coalition_data in pairs(env.mission.coalition) do
local coalition_nr = DcsUtil.stringToCoalition(coalition_name) ---@param coalition_name string
---@return Coalition
local getCoalitionData = function(coalition_name)
return env.mission.coalition[coalition_name]
end
---@param country Country
---@param key string
---@return Groups?
local getGroupsData = function(country, key)
if key == "planes" then return country.plane end
if key == "helicopters" then return country.helicopter end
if key == "ground" then return country.vehicle end
if key == "ships" then return country.ship end
if key == "statics" then return country.static end
return nil
end
for _, coalition_name in ipairs({"neutral", "blue", "red"}) do
local coalition_data = getCoalitionData(coalition_name)
if coalition_data.country then if coalition_data.country then
for country_index, country_data in pairs(coalition_data.country) do for _, country_data in pairs(coalition_data.country) do
for category_name, categorydata in pairs(country_data) do for _, category_name in pairs(DcsUtil.getAllGroupCategoryNames()) do
local category_id = DcsUtil.stringToGroupCategory(category_name) local category_id = DcsUtil.stringToGroupCategory(category_name)
if category_id ~= nil and type(categorydata) == "table" and categorydata.group ~= nil and type(categorydata.group) == "table" then local groups_data = getGroupsData(country_data, category_name)
for group_index, group in pairs(categorydata.group) do
local name = group.name if category_id ~= nil and type(groups_data) == "table" and groups_data.group ~= nil and type(groups_data.group) == "table" then
for _, group in pairs(groups_data.group) do
local name = group.name --[[@as string]]
local skippable = false local skippable = false
local isStatic = false local isStatic = false
if category_id == DcsUtil.GroupCategory.STATIC then if category_id == DcsUtil.GroupCategory.STATIC then
isStatic = true isStatic = true
local unit = group.units[1] local unit = group.units[1] --[[@as table]]
if unit and unit.category == "Heliports" then if unit and unit.category == "Heliports" then
skippable = true skippable = true
elseif unit and unit.name then elseif unit and unit.name then
name = unit.name name = unit.name --[[@as string]]
else else
env.error("Group " .. name .. " has no units, skipping it.") env.error("Group " .. name .. " has no units, skipping it.")
skippable = true skippable = true
@@ -72,6 +93,7 @@ function MizGroupsManager.IsGroupStatic(groupName)
return nil return nil
end end
---@param groupName string
---@return SpawnData? ---@return SpawnData?
function MizGroupsManager.getSpawnTemplateData(groupName) function MizGroupsManager.getSpawnTemplateData(groupName)
return MizGroupsManager._spawnTemplateData[groupName] return MizGroupsManager._spawnTemplateData[groupName]
+19 -25
View File
@@ -16,11 +16,11 @@ SpawnManager.__index = SpawnManager
---@field uncontrolled boolean? Sets the group to be uncontrolled on spawn ---@field uncontrolled boolean? Sets the group to be uncontrolled on spawn
---@param logger Logger ---@param logger Logger
---@return SpawnManager
function SpawnManager.new(logger) function SpawnManager.new(logger)
local self = setmetatable({}, SpawnManager) local self = setmetatable({}, SpawnManager)
self._logger = logger self._logger = logger
self._persistedUnits = {} -- Stores units that should be persisted by name self._persistedUnits = {} -- Stores units that should be persisted by name
return self return self
@@ -33,7 +33,7 @@ end
---@return StaticObject|Group|nil spawned ---@return StaticObject|Group|nil spawned
---@return boolean isStatic ---@return boolean isStatic
function SpawnManager:SpawnGroup(groupName, overrides, isGroupPersistant) function SpawnManager:SpawnGroup(groupName, overrides, isGroupPersistant)
local spawnData = MizGroupsManager.getSpawnTemplateData(groupName) local spawnData = MizGroupsManager.getSpawnTemplateData(groupName)
if spawnData == nil then if spawnData == nil then
@@ -44,13 +44,13 @@ function SpawnManager:SpawnGroup(groupName, overrides, isGroupPersistant)
if spawnData.isStatic == true then if spawnData.isStatic == true then
return self:SpawnStaticInternal(groupName, spawnData, overrides, isGroupPersistant), true return self:SpawnStaticInternal(groupName, spawnData, overrides, isGroupPersistant), true
else else
return self:SpawnGroupInternal(groupName, spawnData, overrides, isGroupPersistant), false return self:SpawnGroupInternal(spawnData, overrides, isGroupPersistant), false
end end
end end
function SpawnManager:DestroyGroup(groupName) function SpawnManager:DestroyGroup(groupName)
if groupName == nil then return end if groupName == nil then return end
if self:IsGroupStatic(groupName) == true then if self:IsGroupStatic(groupName) == true then
local object = StaticObject.getByName(groupName) local object = StaticObject.getByName(groupName)
if object ~= nil then if object ~= nil then
@@ -103,20 +103,20 @@ function SpawnManager:OnUnitLost(object)
end end
end end
---@param groupName string
function SpawnManager:SpawnCorpsesOnly(groupName) function SpawnManager:SpawnCorpsesOnly(groupName)
if groupName == nil then return end
end end
do --- privates do --- privates
---@private ---@private
---@param groupName string
---@param spawnData SpawnData ---@param spawnData SpawnData
---@param override SpawnOverrides? ---@param override SpawnOverrides?
---@param isPersistent boolean? ---@param isPersistent boolean?
---@return Group|nil ---@return Group|nil
function SpawnManager:SpawnGroupInternal(groupName, spawnData, override, isPersistent) function SpawnManager:SpawnGroupInternal(spawnData, override, isPersistent)
if not spawnData then return end if not spawnData then return end
local country = spawnData.country local country = spawnData.country
@@ -125,37 +125,31 @@ do --- privates
end end
local spawnTemplate = Util.deepCopyTable(spawnData.groupTemplate) local spawnTemplate = Util.deepCopyTable(spawnData.groupTemplate)
---@type Array<string>
local removeableUnitNames = {}
--[[ --[[
TODO: Spawn units at "current"/LastKnown position with them going to the next waypoint. TODO: Spawn units at "current"/LastKnown position with them going to the next waypoint.
ONLY when perstable data is found. ONLY when perstable data is found.
]] ]]
if spawnTemplate and spawnTemplate["units"] then if spawnTemplate and spawnTemplate["units"] then
for _, unit in pairs(spawnTemplate["units"]) do local units = spawnTemplate["units"] --[[@as Array<table>]]
local name = unit["name"] for _, unit in pairs(units) do
local name = unit["name"] --[[@as string]]
SpearheadEvents.addOnUnitLostEventListener(name, self) SpearheadEvents.addOnUnitLostEventListener(name, self)
local state = Persistence.UnitState(name)
if state then
if state.isDead == true then
removeableUnitNames[#removeableUnitNames+1] = name
end
end
if override and override.emptyLoadouts == true then if override and override.emptyLoadouts == true then
if unit["payload"] and unit["payload"]["pylons"] then if unit["payload"] and unit["payload"]["pylons"] then
unit["payload"]["pylons"] = {} local payload = unit["payload"] --[[@as table]]
payload["pylons"] = {} --[[@as table]]
end end
end end
if unit["parking"] then if unit["parking"] then
unit["parking_landing"] = unit["parking"] unit["parking_landing"] = unit["parking"] --[[@as number]]
end end
if unit["parking_id"] then if unit["parking_id"] then
unit["parking_landing_id"] = unit["parking_id"] unit["parking_landing_id"] = unit["parking_id"] --[[@as number]]
end end
end end
@@ -169,7 +163,6 @@ do --- privates
local group = coalition.addGroup(country, spawnData.category, spawnTemplate) local group = coalition.addGroup(country, spawnData.category, spawnTemplate)
for _, unit in pairs(group:getUnits()) do for _, unit in pairs(group:getUnits()) do
self:CheckUnitAndReplaceIfPersistentDead(unit) self:CheckUnitAndReplaceIfPersistentDead(unit)
if isPersistent == true then if isPersistent == true then
@@ -181,7 +174,6 @@ do --- privates
return group return group
end end
return nil return nil
end end
@@ -211,9 +203,11 @@ do --- privates
spawnTemplate["x"] = persistentState.pos.x spawnTemplate["x"] = persistentState.pos.x
spawnTemplate["y"] = persistentState.pos.z spawnTemplate["y"] = persistentState.pos.z
if spawnTemplate["units"] and spawnTemplate["units"][1] then if spawnTemplate["units"] and spawnTemplate["units"][1] then
spawnTemplate["units"][1]["x"] = persistentState.pos.x local units = spawnTemplate["units"] --[[@as Array<table>]]
spawnTemplate["units"][1]["y"] = persistentState.pos.z local firstUnit = units[1] --[[@as table]]
spawnTemplate["units"][1]["heading"] = persistentState.heading or 0 firstUnit["x"] = persistentState.pos.x --[[@as number]]
firstUnit["y"] = persistentState.pos.z --[[@as number]]
firstUnit["heading"] = persistentState.heading or 0 --[[@as number]]
end end
end end
end end
+47 -24
View File
@@ -13,7 +13,8 @@ do
---@field unitsStates table<string, UnitState> ---@field unitsStates table<string, UnitState>
---@field random_missions table<string, MissionState> ---@field random_missions table<string, MissionState>
---@field deliveredKilos table<string, number> ---@field deliveredKilos table<string, number>
---@field activeStage integer|nil ---@field activeStage number? required for backwards compatibility
---@field activeStageInStageLane table<string, number>
---@class UnitState ---@class UnitState
---@field isDead boolean ---@field isDead boolean
@@ -33,7 +34,8 @@ do
unitsStates = {}, unitsStates = {},
random_missions = {}, random_missions = {},
deliveredKilos = {}, deliveredKilos = {},
activeStage = nil activeStage = nil, -- Backwards compatibility for previous version without lanes
activeStageInStageLane = {}
} }
local logger = {} local logger = {}
@@ -87,7 +89,7 @@ do
else else
logger:error("Could not load persistence file, using default tables") logger:error("Could not load persistence file, using default tables")
end end
if tables.version == nil then tables.version = version end if tables.version == nil then tables.version = version end
end end
@@ -110,9 +112,9 @@ do
logger:info("Wrote persistence data to file") logger:info("Wrote persistence data to file")
end end
local UpdateContinuous = function(null, time) local UpdateContinuous = function(_, time)
env.info("[Spearhead][Persistence] Checking up on persistence state...") env.info("[Spearhead][Persistence] Checking up on persistence state...")
if Persistence._updateRequired == true then if Persistence._updateRequired == true then
local status, result = pcall(writeToFile) local status, result = pcall(writeToFile)
if status == false then if status == false then
env.error("[Spearhead][Persistence] Could not write state to file: " .. result) env.error("[Spearhead][Persistence] Could not write state to file: " .. result)
@@ -133,20 +135,22 @@ do
end end
local warnForNonPersistenceContinous = function(null, time) local warnForNonPersistenceContinous = function(_, time)
trigger.action.outText("Persistence was enabeld, however, io and lfs are not available and no persistence will be done. Make sure to either disable persistence or fix the issues before continuing.", 10) trigger.action.outText("Persistence was enabeld, however, io and lfs are not available and no persistence will be done. Make sure to either disable persistence or fix the issues before continuing.", 10)
return time + 9 return time + 9
end end
---@param dir string @BaseDirectory ---@param dir string @BaseDirectory
---@return string ---@param startsWith string
---@param default string?
---@return string?
local getLastFileOrDefault = function(dir, startsWith, default) local getLastFileOrDefault = function(dir, startsWith, default)
local latestFile, lastNumber = default, 0 local latestFile, lastNumber = default, 0
for file in lfs.dir(dir) do for file in lfs.dir(dir) do
local split = Util.split_string(file, ".") local split = Util.split_string(file, ".")
local doesStartWith = Util.startswith(file, startsWith, true) local doesStartWith = Util.startsWith(file, startsWith, true)
if split and #split > 0 and split[#split] == EXTENSION and doesStartWith == true then if split and #split > 0 and split[#split] == EXTENSION and doesStartWith == true then
local numberString = split[#split-1] local numberString = split[#split-1]
local number = tonumber(numberString) local number = tonumber(numberString)
@@ -179,11 +183,14 @@ do
local fileName = "Spearhead_Persistence.0.spearhead" local fileName = "Spearhead_Persistence.0.spearhead"
if SpearheadConfig and SpearheadConfig.Persistence then if SpearheadConfig and SpearheadConfig.Persistence then
if SpearheadConfig.Persistence.fileName then if SpearheadConfig.Persistence.fileName then
if type(SpearheadConfig.Persistence.fileName) ~= "string" then
SpearheadConfig.Persistence.fileName = "Spearhead_Persistence.0.spearhead"
end
local userFileName = SpearheadConfig.Persistence.fileName local userFileName = SpearheadConfig.Persistence.fileName
local split = Util.split_string(userFileName, ".") local split = Util.split_string(userFileName, ".")
if not split and #split < 3 then if not split or #split < 3 then
split = split or {}
split[#split+1] = "0" split[#split+1] = "0"
split[#split+1] = "spearhead" split[#split+1] = "spearhead"
end end
@@ -192,11 +199,14 @@ do
split[#split+1] = "0" split[#split+1] = "0"
split[#split+1] = "spearhead" split[#split+1] = "spearhead"
end end
fileName = table.concat(split, ".") fileName = table.concat(split, ".")
end end
if SpearheadConfig.Persistence.directory ~= nil then if SpearheadConfig.Persistence.directory then
if type(SpearheadConfig.Persistence.directory) ~= "string" then
SpearheadConfig.Persistence.directory = lfs.writedir() .. "\\Data"
end
dir = SpearheadConfig.Persistence.directory dir = SpearheadConfig.Persistence.directory
end end
end end
@@ -206,6 +216,10 @@ do
local lastFile = getLastFileOrDefault(dir--[[@as string]], matchingPart, fileName) local lastFile = getLastFileOrDefault(dir--[[@as string]], matchingPart, fileName)
if lastFile == nil then
lastFile = fileName
end
local fileSplit = Util.split_string(lastFile, ".") local fileSplit = Util.split_string(lastFile, ".")
fileSplit[#fileSplit-1] = tostring(tonumber(fileSplit[#fileSplit-1]) + 1) fileSplit[#fileSplit-1] = tostring(tonumber(fileSplit[#fileSplit-1]) + 1)
fileName = table.concat(fileSplit, ".") fileName = table.concat(fileSplit, ".")
@@ -216,12 +230,12 @@ do
logger:info("No previous persistence file found, using default: " .. fileName) logger:info("No previous persistence file found, using default: " .. fileName)
end end
logger:info("New Persistence file name: " .. tostring(fileName)) logger:info("New Persistence file name: " .. tostring(fileName))
local lastPath = dir .. "\\" .. lastFile local lastPath = dir .. "\\" .. lastFile
local path = dir .. "\\" .. fileName local path = dir .. "\\" .. fileName
Persistence._path = path Persistence._path = path
createFileIfNotExists(path) createFileIfNotExists(path)
loadTablesFromFile(lastPath) loadTablesFromFile(lastPath)
timer.scheduleFunction(UpdateContinuous, nil, timer.getTime() + 120) timer.scheduleFunction(UpdateContinuous, nil, timer.getTime() + 120)
@@ -230,12 +244,29 @@ do
end end
---Sets the stage in the persistence table ---Sets the stage in the persistence table
---@param stageLane string?
---@param stageNumber number ---@param stageNumber number
Persistence.SetActiveStage = function(stageNumber) Persistence.SetActiveStage = function(stageLane, stageNumber)
tables.activeStage = stageNumber stageLane = stageLane or "nil"
tables.activeStageInStageLane[stageLane] = stageNumber
Persistence._updateRequired = true Persistence._updateRequired = true
end 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 ---comment
---@param missionName string ---@param missionName string
---@param pickedZone string ---@param pickedZone string
@@ -255,14 +286,6 @@ do
return tables.random_missions[string.lower(missionName)] return tables.random_missions[string.lower(missionName)]
end 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 ---comment
---@param zoneName string ---@param zoneName string
+127 -53
View File
@@ -2,7 +2,7 @@ local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil") local DcsUtil = require("classes.util.DcsUtil")
local MissionEditorWarnings = require("classes.util.MissionEditorWarnings") local MissionEditorWarnings = require("classes.util.MissionEditorWarnings")
local MizGroupsManager = require("classes.helpers.MizGroupsManager") local MizGroupsManager = require("classes.helpers.MizGroupsManager")
local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing") local StageDrawing = require("classes.stageClasses.drawings.StageDrawing")
---@class DatabaseTables ---@class DatabaseTables
@@ -27,7 +27,7 @@ local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
---@field MissionZoneData table<string, MissionZoneData> ---@field MissionZoneData table<string, MissionZoneData>
---@field FarpZoneData table<string,FarpZoneData> ---@field FarpZoneData table<string,FarpZoneData>
---@field missionCodes table<string, boolean> ---@field missionCodes table<string, boolean>
---@field CustomDrawings Array<CustomDrawing> ---@field StageDrawings Array<StageDrawing>
---@class CapRoute ---@class CapRoute
---@field zones Array<SpearheadTriggerZone> ---@field zones Array<SpearheadTriggerZone>
@@ -58,6 +58,7 @@ local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
---@class BlueSamData ---@class BlueSamData
---@field groups Array<string> ---@field groups Array<string>
---@field buildingKilos number? ---@field buildingKilos number?
---@field briefing string?
---@class MissionZoneData ---@class MissionZoneData
---@field ZoneName string ---@field ZoneName string
@@ -68,12 +69,14 @@ local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
---@field descriptionLocation Vec2? ---@field descriptionLocation Vec2?
---@field dependsOn Array<string> ---@field dependsOn Array<string>
---@field completeAt number? ---@field completeAt number?
---@field primaryOverwrite boolean?
---@class FarpZoneData ---@class FarpZoneData
---@field groups Array<string> ---@field groups Array<string>
---@field padNames Array<string> ---@field padNames Array<string>
---@field buildingKilos number? ---@field buildingKilos number?
---@field supplyHubNames Array<string> ---@field supplyHubNames Array<string>
---@field briefing string?
---@class Database ---@class Database
---@field private _tables DatabaseTables ---@field private _tables DatabaseTables
@@ -107,7 +110,7 @@ function Database.New(Logger)
FarpZoneData = {}, FarpZoneData = {},
missionCodes = {}, missionCodes = {},
SupplyHubZones = {}, SupplyHubZones = {},
CustomDrawings = {} StageDrawings = {}
} }
Database.__index = Database Database.__index = Database
@@ -119,7 +122,7 @@ function Database.New(Logger)
self._logger:debug("Initiating tables") self._logger:debug("Initiating tables")
do -- INIT ZONE TABLES do -- INIT ZONE TABLES
for zone_ind, zone_data in pairs(DcsUtil.__trigger_zones) do for _, zone_data in pairs(DcsUtil.__trigger_zones) do
local zone_name = zone_data.name local zone_name = zone_data.name
---@type Vec2 ---@type Vec2
@@ -206,15 +209,18 @@ function Database.New(Logger)
self._logger:debug("initiated zone tables, continuing with descriptions") self._logger:debug("initiated zone tables, continuing with descriptions")
do --load markers do --load markers
if env.mission.drawings and env.mission.drawings.layers then if env.mission.drawings and env.mission.drawings.layers then
for i, layer in pairs(env.mission.drawings.layers) do for _, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then if string.lower(layer.name) == "author" then
for key, layer_object in pairs(layer.objects) do for _, layer_object in pairs(layer.objects) do
if Util.startswith(string.lower(layer_object.name), "buildable", true) == true then if Util.startsWith(string.lower(layer_object.name), "buildable", true) == true then
local airbaseData = self:getAirbaseDataForDrawLayer(layer_object) local airbaseData = self:getAirbaseDataForDrawLayer(layer_object)
if airbaseData then if airbaseData then
self._logger:debug("found airbase data for " .. layer_object.name) self._logger:debug("found airbase data for " .. layer_object.name)
local number = tonumber(layer_object.text) if layer_object.primitiveType == "TextBox" then
airbaseData.buildingKilos = number layer_object = layer_object --[[@as TextBox]]
local number = tonumber(layer_object.text)
airbaseData.buildingKilos = number
end
end end
end end
end end
@@ -226,13 +232,15 @@ function Database.New(Logger)
---@type table<integer, boolean> ---@type table<integer, boolean>
do -- custom drawings do -- custom drawings
if env.mission.drawings and env.mission.drawings.layers then if env.mission.drawings and env.mission.drawings.layers then
for i, layer in pairs(env.mission.drawings.layers) do for _, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then if string.lower(layer.name) == "author" then
for key, layer_object in pairs(layer.objects) do for _, layer_object in pairs(layer.objects) do
if Util.startswith(layer_object.name, "drawing_", true) then if Util.startsWith(layer_object.name, "drawing_", true) then
local object = layer_object --[[@as DrawingObject]] local object = layer_object --[[@as DrawingObject]]
local customDrawing = CustomDrawing.New(object) local stageDrawing = StageDrawing.New(object)
table.insert(self._tables.CustomDrawings, customDrawing) if stageDrawing then
table.insert(self._tables.StageDrawings, stageDrawing)
end
end end
end end
end end
@@ -252,16 +260,19 @@ function Database.New(Logger)
-- Find and add Briefing -- Find and add Briefing
if env.mission.drawings and env.mission.drawings.layers then if env.mission.drawings and env.mission.drawings.layers then
for i, layer in pairs(env.mission.drawings.layers) do for _, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then if string.lower(layer.name) == "author" then
for key, layer_object in pairs(layer.objects) do for _, layer_object in pairs(layer.objects) do
if Util.startswith(string.lower(layer_object.name), "stagebriefing_", true) == true then if Util.startsWith(string.lower(layer_object.name), "stagebriefing_", true) == true then
local zone = DcsUtil.getZoneByName(stageZoneName) local zone = DcsUtil.getZoneByName(stageZoneName)
local vec2 = { x = layer_object.mapX, y = layer_object.mapY } local vec2 = { x = layer_object.mapX, y = layer_object.mapY }
if zone and Util.is2dPointInZone(vec2, zone) == true then if zone and Util.is2dPointInZone(vec2, zone) == true then
local description = layer_object.text if layer_object.primitiveType == "TextBox" then
if description and description ~= "" then layer_object = layer_object --[[@as TextBox]]
stageData.StageBriefing = description local description = layer_object.text
if description and description ~= "" then
stageData.StageBriefing = description
end
end end
end end
end end
@@ -335,14 +346,14 @@ function Database.New(Logger)
end end
-- fill missions -- fill missions
for key, missionZone in pairs(self._tables.MissionZones) do for _, missionZone in pairs(self._tables.MissionZones) do
if DcsUtil.isZoneInZone(missionZone, stageZoneName) == true then if DcsUtil.isZoneInZone(missionZone, stageZoneName) == true then
table.insert(stageData.MissionZones, missionZone) table.insert(stageData.MissionZones, missionZone)
end end
end end
-- fill random missions -- fill random missions
for key, missionZone in pairs(self._tables.RandomMissionZones) do for _, missionZone in pairs(self._tables.RandomMissionZones) do
if DcsUtil.isZoneInZone(missionZone, stageZoneName) == true then if DcsUtil.isZoneInZone(missionZone, stageZoneName) == true then
table.insert(stageData.RandomMissionZones, missionZone) table.insert(stageData.RandomMissionZones, missionZone)
end end
@@ -350,18 +361,6 @@ function Database.New(Logger)
end end
end end
for _, missionZone in pairs(self._tables.MissionZones) do
if self._tables.MissionZoneData[missionZone] == nil or self._tables.MissionZoneData[missionZone].description == nil then
MissionEditorWarnings.Add("Mission with zonename: " .. missionZone .. " does not have a briefing")
end
end
for _, missionZone in pairs(self._tables.RandomMissionZones) do
if self._tables.MissionZoneData[missionZone] == nil or self._tables.MissionZoneData[missionZone].description == nil then
MissionEditorWarnings.Add("Mission with zonename: " .. missionZone .. " does not have a briefing")
end
end
for _, farpZoneName in pairs(self._tables.AllFarpZones) do for _, farpZoneName in pairs(self._tables.AllFarpZones) do
for _, airbase in pairs(world.getAirbases()) do for _, airbase in pairs(world.getAirbases()) do
if airbase:getDesc().category == Airbase.Category.HELIPAD then if airbase:getDesc().category == Airbase.Category.HELIPAD then
@@ -447,6 +446,21 @@ function Database.New(Logger)
end end
end end
-- Checks for missing briefings in mission zones and random mission zones
do
for _, missionZone in pairs(self._tables.MissionZones) do
if self._tables.MissionZoneData[missionZone] == nil or self._tables.MissionZoneData[missionZone].description == nil then
MissionEditorWarnings.Add("Mission with zonename: " .. missionZone .. " does not have a briefing")
end
end
for _, missionZone in pairs(self._tables.RandomMissionZones) do
if self._tables.MissionZoneData[missionZone] == nil or self._tables.MissionZoneData[missionZone].description == nil then
MissionEditorWarnings.Add("Mission with zonename: " .. missionZone .. " does not have a briefing")
end
end
end
if missions == 0 then missions = 1 end if missions == 0 then missions = 1 end
self._logger:info("initiated the database with amount of zones: ") self._logger:info("initiated the database with amount of zones: ")
@@ -461,6 +475,7 @@ function Database.New(Logger)
return self return self
end end
---@type table<string, boolean>
local is_group_taken = {} local is_group_taken = {}
local getAvailableGroups = function() local getAvailableGroups = function()
@@ -477,7 +492,7 @@ end
local getAvailableCAPGroups = function() local getAvailableCAPGroups = function()
local result = {} local result = {}
for name, value in pairs(is_group_taken) do for name, value in pairs(is_group_taken) do
if value == false and Util.startswith(name, "CAP") then if value == false and Util.startsWith(name, "CAP") then
table.insert(result, name) table.insert(result, name)
end end
end end
@@ -580,11 +595,11 @@ function Database:loadCapUnits()
for _, groupName in pairs(groups) do for _, groupName in pairs(groups) do
is_group_taken[groupName] = true is_group_taken[groupName] = true
if Util.startswith(groupName, "CAP_A", true) or Util.startswith(groupName, "CAP_B", true) then if Util.startsWith(groupName, "CAP_A", true) or Util.startsWith(groupName, "CAP_B", true) then
table.insert(baseData.CapGroups, groupName) table.insert(baseData.CapGroups, groupName)
elseif Util.startswith(groupName, "CAP_I", true) then elseif Util.startsWith(groupName, "CAP_I", true) then
table.insert(baseData.InterceptGroups, groupName) table.insert(baseData.InterceptGroups, groupName)
elseif Util.startswith(groupName, "CAP_S", true) then elseif Util.startsWith(groupName, "CAP_S", true) then
table.insert(baseData.SweepGroups, groupName) table.insert(baseData.SweepGroups, groupName)
end end
end end
@@ -607,10 +622,32 @@ function Database:loadBlueSamUnits()
local triggerZone = DcsUtil.getZoneByName(blueSamZone) local triggerZone = DcsUtil.getZoneByName(blueSamZone)
if triggerZone then if triggerZone then
for _, kvPair in pairs(triggerZone.properties) do for _, kvPair in pairs(triggerZone.properties) do
if kvPair.key and Util.startswith(kvPair.key, "buildable") then if kvPair.key and Util.startsWith(kvPair.key, "buildable") then
local number = tonumber(kvPair.value) local number = tonumber(kvPair.value)
if number and number > 0 then if number and number > 0 then
samData.buildingKilos = number samData.buildingKilos = number
if env.mission.drawings and env.mission.drawings.layers then
for _, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then
for _, layer_object in pairs(layer.objects) do
local vec2 = { x = layer_object.mapX, y = layer_object.mapY }
if layer_object.primitiveType == "TextBox" and triggerZone and Util.is2dPointInZone(vec2, triggerZone) then
if layer_object.name and Util.startsWith(layer_object.name, "supplybriefing_", true) then
layer_object = layer_object --[[@as TextBox]]
local description = layer_object.text
if description and description ~= "" then
samData.briefing = description
end
end
end
end
end
end
end
else
MissionEditorWarnings.Add("Buildable number for " .. blueSamZone .. " is invalid")
end end
end end
end end
@@ -618,12 +655,12 @@ function Database:loadBlueSamUnits()
end end
end end
---@return Array<CustomDrawing> ---@return Array<StageDrawing>
function Database:getCustomDrawings() function Database:getStageDrawings()
if self._tables.CustomDrawings == nil then if self._tables.StageDrawings == nil then
return {} return {}
end end
return self._tables.CustomDrawings return self._tables.StageDrawings
end end
---Loads all units, data and briefings ---Loads all units, data and briefings
@@ -642,13 +679,13 @@ function Database:LoadZoneData(missionZoneName)
for _, groupName in pairs(groups) do for _, groupName in pairs(groups) do
if MizGroupsManager.IsGroupStatic(groupName) == true then if MizGroupsManager.IsGroupStatic(groupName) == true then
local object = StaticObject.getByName(groupName) local object = StaticObject.getByName(groupName)
if object and object:getCoalition() == coalition.side.RED then if object and object:getCoalition() == coalition.side.RED then
table.insert(self._tables.MissionZoneData[missionZoneName].RedGroups, groupName) table.insert(self._tables.MissionZoneData[missionZoneName].RedGroups, groupName)
elseif object then elseif object then
table.insert(self._tables.MissionZoneData[missionZoneName].BlueGroups, groupName) table.insert(self._tables.MissionZoneData[missionZoneName].BlueGroups, groupName)
end end
else else
local group = Group.getByName(groupName) local group = Group.getByName(groupName)
if group and group:getCoalition() == coalition.side.RED then if group and group:getCoalition() == coalition.side.RED then
@@ -674,9 +711,9 @@ function Database:LoadZoneData(missionZoneName)
if triggerZone and triggerZone.properties then if triggerZone and triggerZone.properties then
for _, kvPair in pairs(triggerZone.properties) do for _, kvPair in pairs(triggerZone.properties) do
local key = kvPair.key local key = kvPair.key
if Util.startswith(key, "dependson", true) == true then if Util.startsWith(key, "dependson", true) == true then
table.insert(self._tables.MissionZoneData[missionZoneName].dependsOn, kvPair.value) table.insert(self._tables.MissionZoneData[missionZoneName].dependsOn, kvPair.value)
elseif Util.startswith(key, "completeat") == true then elseif Util.startsWith(key, "completeat") == true then
local value = tonumber(kvPair.value) local value = tonumber(kvPair.value)
if value then if value then
if value > 1 and value <= 100 then if value > 1 and value <= 100 then
@@ -686,19 +723,28 @@ function Database:LoadZoneData(missionZoneName)
end end
self._tables.MissionZoneData[missionZoneName].completeAt = value self._tables.MissionZoneData[missionZoneName].completeAt = value
end end
elseif Util.startsWith(key, "primary", true) == true then
if type(kvPair.value) == "string" then
if kvPair.value == "true" then
self._tables.MissionZoneData[missionZoneName].primaryOverwrite = true
elseif kvPair.value == "false" then
self._tables.MissionZoneData[missionZoneName].primaryOverwrite = false
end
end
end end
end end
end end
---Adds Briefings ---Adds Briefings
if env.mission.drawings and env.mission.drawings.layers then if env.mission.drawings and env.mission.drawings.layers then
for i, layer in pairs(env.mission.drawings.layers) do for _, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then if string.lower(layer.name) == "author" then
for key, layer_object in pairs(layer.objects) do for _, layer_object in pairs(layer.objects) do
local vec2 = { x = layer_object.mapX, y = layer_object.mapY } local vec2 = { x = layer_object.mapX, y = layer_object.mapY }
if triggerZone and Util.is2dPointInZone(vec2, triggerZone) then if layer_object.primitiveType == "TextBox" and triggerZone and Util.is2dPointInZone(vec2, triggerZone) then
if layer_object.name and Util.startswith(layer_object.name, "briefing_", true) then if layer_object.name and Util.startsWith(layer_object.name, "briefing_", true) then
layer_object = layer_object --[[@as TextBox]]
local description = layer_object.text local description = layer_object.text
if description and description ~= "" then if description and description ~= "" then
self._tables.MissionZoneData[missionZoneName].description = description self._tables.MissionZoneData[missionZoneName].description = description
@@ -741,10 +787,33 @@ function Database:loadFarpData()
local triggerZone = DcsUtil.getZoneByName(farpZone) local triggerZone = DcsUtil.getZoneByName(farpZone)
if triggerZone then if triggerZone then
for _, kvPair in pairs(triggerZone.properties) do for _, kvPair in pairs(triggerZone.properties) do
if kvPair.key and Util.startswith(kvPair.key, "buildable", true) == true then if kvPair.key and Util.startsWith(kvPair.key, "buildable", true) == true then
local number = tonumber(kvPair.value) local number = tonumber(kvPair.value)
if number and number > 0 then if number and number > 0 then
farpzoneData.buildingKilos = number farpzoneData.buildingKilos = number
-- check briefings
if env.mission.drawings and env.mission.drawings.layers then
for _, layer in pairs(env.mission.drawings.layers) do
if string.lower(layer.name) == "author" then
for _, layer_object in pairs(layer.objects) do
local vec2 = { x = layer_object.mapX, y = layer_object.mapY }
if layer_object.primitiveType == "TextBox" and triggerZone and Util.is2dPointInZone(vec2, triggerZone) then
layer_object = layer_object --[[@as TextBox]]
if layer_object.name and Util.startsWith(layer_object.name, "supplybriefing_", true) then
local description = layer_object.text
if description and description ~= "" then
farpzoneData.briefing = description
end
end
end
end
end
end
end
else
MissionEditorWarnings.Add("Buildable number for " .. farpZone .. " is invalid.")
end end
end end
end end
@@ -900,6 +969,7 @@ function Database:getBriefingForStage(stagename)
return stageZone.StageBriefing or "" return stageZone.StageBriefing or ""
end end
---@param stagename string
---@return Array<string> ---@return Array<string>
function Database:getMissionsForStage(stagename) function Database:getMissionsForStage(stagename)
local stageZone = self._tables.StageZones[stagename] local stageZone = self._tables.StageZones[stagename]
@@ -907,6 +977,7 @@ function Database:getMissionsForStage(stagename)
return stageZone.MissionZones return stageZone.MissionZones
end end
---@param stagename string
---@return Array<string> ---@return Array<string>
function Database:getRandomMissionsForStage(stagename) function Database:getRandomMissionsForStage(stagename)
local stageZone = self._tables.StageZones[stagename] local stageZone = self._tables.StageZones[stagename]
@@ -914,13 +985,14 @@ function Database:getRandomMissionsForStage(stagename)
return stageZone.RandomMissionZones return stageZone.RandomMissionZones
end end
---@param missionZoneName string
---@return MissionZoneData? ---@return MissionZoneData?
function Database:getMissionDataForZone(missionZoneName) function Database:getMissionDataForZone(missionZoneName)
return self._tables.MissionZoneData[missionZoneName] return self._tables.MissionZoneData[missionZoneName]
end end
---@param stageName string ---@param stageName string
---@return table result airbase Names ---@return Array<string> result airbase Names
function Database:getAirbaseNamesInStage(stageName) function Database:getAirbaseNamesInStage(stageName)
local stageData = self._tables.StageZones[stageName] local stageData = self._tables.StageZones[stageName]
if not stageData then return {} end if not stageData then return {} end
@@ -934,6 +1006,7 @@ function Database:getFarpNamesInStage(stageName)
return stageData.FarpZones or {} return stageData.FarpZones or {}
end end
---@param farpZoneName string
---@return FarpZoneData? ---@return FarpZoneData?
function Database:getFarpDataForZone(farpZoneName) function Database:getFarpDataForZone(farpZoneName)
local farpData = self._tables.FarpZoneData[farpZoneName] local farpData = self._tables.FarpZoneData[farpZoneName]
@@ -949,6 +1022,7 @@ function Database:getAirbaseDataForZone(baseName)
return baseData return baseData
end end
---@param stageName string
---@return string? ---@return string?
function Database:getStageBriefingForStage(stageName) function Database:getStageBriefingForStage(stageName)
local stageData = self._tables.StageZones[stageName] local stageData = self._tables.StageZones[stageName]
+95 -52
View File
@@ -29,36 +29,47 @@ do
end end
---@class OnStageChangedListener ---@class OnStageChangedListener
---@field OnStageNumberChanged fun(self:OnStageChangedListener, number:integer) ---@field OnStageNumberChanged fun(self:OnStageChangedListener, number:integer, laneIdentifier:string?)
do -- STAGE NUMBER CHANGED do -- STAGE NUMBER CHANGED
---@type Array<OnStageChangedListener>
local OnStageNumberChangedListeners = {} local OnStageNumberChangedListeners = {}
---@type Array<fun(number:integer, laneIdentifier:string?)>
local OnStageNumberChangedHandlers = {} local OnStageNumberChangedHandlers = {}
---Add a stage zone number changed listener ---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) 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) table.insert(OnStageNumberChangedListeners, listener)
end end
---Add a stage zone number changed listener ---@class OnStageNumberChangeCompleteListener
---@param handler function function(number) ---@field OnStageNumberChangeComplete fun(self:OnStageNumberChangeCompleteListener, number:integer, laneIdentifier:string?)
SpearheadEvents.AddStageNumberChangedHandler = function(handler)
if type(handler) ~= "function" then ---@type Array<OnStageNumberChangeCompleteListener>
warn("Event handler not of type function, did you mean to use listener?") local OnStageNumberChangeCompleteListeners = {}
---@param listener OnStageNumberChangeCompleteListener
SpearheadEvents.AddStageNumberChangeCompleteListener = function(listener)
if type(listener) ~= "table" or type(listener.OnStageNumberChangeComplete) ~= "function" then
warn("Event handler not of type table/object with function OnStageNumberChangeComplete(self, number, laneIdentifier)")
return return
end end
table.insert(OnStageNumberChangedHandlers, handler) table.insert(OnStageNumberChangeCompleteListeners, listener)
end end
---@param newStageNumber number ---@param newStageNumber number
SpearheadEvents.PublishStageNumberChanged = function(newStageNumber) ---@param laneIdentifier string?
SpearheadEvents.PublishStageNumberChanged = function(newStageNumber, laneIdentifier)
pcall(function () pcall(function ()
Persistence.SetActiveStage(newStageNumber) Persistence.SetActiveStage(laneIdentifier, newStageNumber)
end) end)
for _, callable in pairs(OnStageNumberChangedListeners) do for _, callable in pairs(OnStageNumberChangedListeners) do
local succ, err = pcall(function() local _, err = pcall(function()
callable:OnStageNumberChanged(newStageNumber) callable:OnStageNumberChanged(newStageNumber, laneIdentifier)
end) end)
if err then if err then
logError(err) logError(err)
@@ -66,11 +77,21 @@ do
end end
for _, callable in pairs(OnStageNumberChangedHandlers) do for _, callable in pairs(OnStageNumberChangedHandlers) do
local succ, err = pcall(callable, newStageNumber) local _, err = pcall(callable, newStageNumber, laneIdentifier)
if err then if err then
logError(err) logError(err)
end end
end end
for _, callable in pairs(OnStageNumberChangeCompleteListeners) do
local _, err = pcall(function()
callable:OnStageNumberChangeComplete(newStageNumber, laneIdentifier)
end)
if err then
logError(err)
end
end
Logger.new("Events", "INFO"):info("Published stage number changed to: " .. tostring(newStageNumber)) Logger.new("Events", "INFO"):info("Published stage number changed to: " .. tostring(newStageNumber))
end end
end end
@@ -92,9 +113,12 @@ do
table.insert(onWeaponFiredListeners, weaponFiredListener) table.insert(onWeaponFiredListeners, weaponFiredListener)
end end
---@param unit Unit
---@param weapon Weapon
---@param target Unit?
local triggerWeaponFired = function(unit, weapon, target) local triggerWeaponFired = function(unit, weapon, target)
for _, callable in pairs(onWeaponFiredListeners) do for _, callable in pairs(onWeaponFiredListeners) do
local succ, err = pcall(function() local _, err = pcall(function()
callable:OnWeaponFired(unit, weapon, target) callable:OnWeaponFired(unit, weapon, target)
end) end)
@@ -103,12 +127,15 @@ do
end end
end end
end end
---@class OnLandEventListener
---@field OnUnitLanded fun(self:OnLandEventListener, initiatorUnit:Unit, airbase:Airbase)
---@type table<string,Array<OnLandEventListener>>
local onLandEventListeners = {} local onLandEventListeners = {}
---Add an event listener to a specific unit ---Add an event listener to a specific unit
---@param unitName string to call when the unit lands ---@param unitName string to call when the unit lands
---@param landListener table table with function OnUnitLanded(self, initiatorUnit, airbase) ---@param landListener OnLandEventListener table with function OnUnitLanded(self, initiatorUnit, airbase)
SpearheadEvents.addOnUnitLandEventListener = function(unitName, landListener) SpearheadEvents.addOnUnitLandEventListener = function(unitName, landListener)
if type(landListener) ~= "table" then if type(landListener) ~= "table" then
warn("Event handler not of type table/object") warn("Event handler not of type table/object")
@@ -144,6 +171,11 @@ do
end end
do -- ON RTB do -- ON RTB
---@class OnGroupRTBListener
---@field OnGroupRTB fun(self:OnGroupRTBListener, groupName:string)
---@type table<string,Array<OnGroupRTBListener>>
local OnGroupRTBListeners = {} local OnGroupRTBListeners = {}
---Adds a function to the events listener that triggers when a group publishes themselves RTB. ---Adds a function to the events listener that triggers when a group publishes themselves RTB.
---This is only available when a ROUTE is created via the Spearhead.RouteUtil ---This is only available when a ROUTE is created via the Spearhead.RouteUtil
@@ -168,7 +200,7 @@ do
if groupName ~= nil then if groupName ~= nil then
if OnGroupRTBListeners[groupName] then if OnGroupRTBListeners[groupName] then
for _, callable in pairs(OnGroupRTBListeners[groupName]) do for _, callable in pairs(OnGroupRTBListeners[groupName]) do
local succ, err = pcall(function() local _, err = pcall(function()
callable:OnGroupRTB(groupName) callable:OnGroupRTB(groupName)
end) end)
if err then if err then
@@ -179,11 +211,15 @@ do
end end
end end
---@class OnGroupRTBInTenListener
---@field OnGroupRTBInTen fun(self:OnGroupRTBInTenListener, groupName:string)
---@type table<string,Array<OnGroupRTBInTenListener>>
local OnGroupRTBInTenListeners = {} local OnGroupRTBInTenListeners = {}
---Adds a function to the events listener that triggers when a group publishes themselves RTB. ---Adds a function to the events listener that triggers when a group publishes themselves RTB.
---This is only available when a ROUTE is created via the Spearhead.RouteUtil ---This is only available when a ROUTE is created via the Spearhead.RouteUtil
---@param groupName string the groupname to expect ---@param groupName string the groupname to expect
---@param handlingObject table object with OnGroupRTBInTen(self, groupName) ---@param handlingObject OnGroupRTBInTenListener object with OnGroupRTBInTen(self, groupName)
SpearheadEvents.addOnGroupRTBInTenListener = function(groupName, handlingObject) SpearheadEvents.addOnGroupRTBInTenListener = function(groupName, handlingObject)
if type(handlingObject) ~= "table" then if type(handlingObject) ~= "table" then
warn("Event handler not of type table/object") warn("Event handler not of type table/object")
@@ -203,7 +239,7 @@ do
if groupName ~= nil then if groupName ~= nil then
if OnGroupRTBInTenListeners[groupName] then if OnGroupRTBInTenListeners[groupName] then
for _, callable in pairs(OnGroupRTBInTenListeners[groupName]) do for _, callable in pairs(OnGroupRTBInTenListeners[groupName]) do
local succ, err = pcall(function() local _, err = pcall(function()
callable:OnGroupRTBInTen(groupName) callable:OnGroupRTBInTen(groupName)
end) end)
if err then if err then
@@ -216,10 +252,17 @@ do
end end
do -- ON Station do -- ON Station
---@class OnGroupOnStationListener
---@field OnGroupOnStation fun(self:OnGroupOnStationListener, groupName:string)
---@type table<string,Array<OnGroupOnStationListener>>
local OnGroupOnStationListeners = {} local OnGroupOnStationListeners = {}
---Adds a function to the events listener that triggers when a group publishes themselves RTB. ---Adds a function to the events listener that triggers when a group publishes themselves RTB.
---This is only available when a ROUTE is created via the Spearhead.RouteUtil ---This is only available when a ROUTE is created via the Spearhead.RouteUtil
---@param groupName string the groupname to expect ---@param groupName string the groupname to expect
---@param handlingObject OnGroupOnStationListener object with OnGroupOnStation(self, groupName)
SpearheadEvents.addOnGroupOnStationListener = function(groupName, handlingObject) SpearheadEvents.addOnGroupOnStationListener = function(groupName, handlingObject)
if type(handlingObject) ~= "table" then if type(handlingObject) ~= "table" then
warn("Event handler not of type table/object") warn("Event handler not of type table/object")
@@ -239,7 +282,7 @@ do
if groupName ~= nil then if groupName ~= nil then
if OnGroupOnStationListeners[groupName] then if OnGroupOnStationListeners[groupName] then
for _, callable in pairs(OnGroupOnStationListeners[groupName]) do for _, callable in pairs(OnGroupOnStationListeners[groupName]) do
local succ, err = pcall(function() local _, err = pcall(function()
callable:OnGroupOnStation(groupName) callable:OnGroupOnStation(groupName)
end) end)
if err then if err then
@@ -252,15 +295,18 @@ do
end end
do -- PLAYER ENTER UNIT do -- PLAYER ENTER UNIT
---@class OnPlayerEnterUnitListener
---@field OnPlayerEntersUnit fun(self:OnPlayerEnterUnitListener, unit:Unit)
---@type Array<OnPlayerEnterUnitListener>
local playerEnterUnitListeners = {} local playerEnterUnitListeners = {}
---comment ---comment
---@param listener table object with OnPlayerEntersUnit(self, unit) ---@param listener OnPlayerEnterUnitListener object with OnPlayerEntersUnit(self, unit)
SpearheadEvents.AddOnPlayerEnterUnitListener = function(listener) SpearheadEvents.AddOnPlayerEnterUnitListener = function(listener)
if type(listener) ~= "table" then if type(listener) ~= "table" then
warn("Unit lost Event listener not of type table/object") warn("Unit lost Event listener not of type table/object")
return return
end end
table.insert(playerEnterUnitListeners, listener) table.insert(playerEnterUnitListeners, listener)
end end
@@ -268,7 +314,7 @@ do
if unit ~= nil then if unit ~= nil then
if playerEnterUnitListeners then if playerEnterUnitListeners then
for _, callable in pairs(playerEnterUnitListeners) do for _, callable in pairs(playerEnterUnitListeners) do
local succ, err = pcall(function() local _, err = pcall(function()
callable:OnPlayerEntersUnit(unit) callable:OnPlayerEntersUnit(unit)
end) end)
if err then if err then
@@ -281,7 +327,7 @@ do
end end
do -- Ejection events do -- Ejection events
local unitEjectListeners = {} local unitEjectListeners = {}
SpearheadEvents.AddOnUnitEjectedListener = function(listener) SpearheadEvents.AddOnUnitEjectedListener = function(listener)
if type(listener) ~= "table" then if type(listener) ~= "table" then
@@ -297,13 +343,13 @@ do
local e = {} local e = {}
function e:onEvent(event) function e:onEvent(event)
if event.id == world.event.S_EVENT_LAND or event.id == world.event.S_EVENT_RUNWAY_TOUCH then if event.id == world.event.S_EVENT_LAND or event.id == world.event.S_EVENT_RUNWAY_TOUCH then
local unit = event.initiator local unit = event.initiator --[[@as Unit]]
local airbase = event.place local airbase = event.place --[[@as Airbase]]
if unit ~= nil then if unit ~= nil then
local name = unit:getName() local name = unit:getName()
if onLandEventListeners[name] then if onLandEventListeners[name] then
for _, callable in pairs(onLandEventListeners[name]) do for _, callable in pairs(onLandEventListeners[name]) do
local succ, err = pcall(function() local _, err = pcall(function()
callable:OnUnitLanded(unit, airbase) callable:OnUnitLanded(unit, airbase)
end) end)
if err then if err then
@@ -318,15 +364,15 @@ do
event.id == world.event.S_EVENT_CRASH or event.id == world.event.S_EVENT_CRASH or
event.id == world.event.S_EVENT_EJECTION or event.id == world.event.S_EVENT_EJECTION or
event.id == world.event.S_EVENT_UNIT_LOST then event.id == world.event.S_EVENT_UNIT_LOST then
local object = event.initiator local object = event.initiator --[[@as Unit]]
if object and object.getName then if object and object.getName then
logDebug("Receiving death event from: " .. object:getName()) logDebug("Receiving death event from: " .. object:getName())
end end
if object and object.getName and OnUnitLostListeners[object:getName()] then if object and object.getName and OnUnitLostListeners[object:getName()] then
for _, callable in pairs(OnUnitLostListeners[object:getName()]) do for _, callable in pairs(OnUnitLostListeners[object:getName()]) do
local succ, err = pcall(function() local _, err = pcall(function()
callable:OnUnitLost(object) callable:OnUnitLost(object)
end) end)
@@ -337,15 +383,11 @@ do
end end
end end
if event.id == world.event.S_EVENT_EJECTION then
end
if event.id == world.event.S_EVENT_SHOT then if event.id == world.event.S_EVENT_SHOT then
local shooter = event.initiator local shooter = event.initiator --[[@as Unit]]
local weapon = event.weapon local weapon = event.weapon --[[@as Weapon]]
local target = event.target local target = event.target --[[@as Unit?]]
triggerWeaponFired(shooter, weapon, target) triggerWeaponFired(shooter, weapon, target)
end end
@@ -354,28 +396,32 @@ do
Persistence.UpdateNow() Persistence.UpdateNow()
end end
---@type table<string, boolean>
local AI_GROUPS = {} local AI_GROUPS = {}
local function CheckAndTriggerSpawnAsync(unit, time) ---@param unit Unit
---@param _ any
local function isPlayer(unit) local function CheckAndTriggerSpawnAsync(unit, _)
if unit == nil then return false, "unit is nil" end ---@param checkUnit Unit
if unit.getGroup == nil then return false, 'no get group function in unit object, most likely static' end ---@return boolean, string?
if Object.getCategory(unit) ~= Object.Category.UNIT then local function isPlayer(checkUnit)
if checkUnit == nil then return false, "unit is nil" end
if checkUnit.getGroup == nil then return false, 'no get group function in unit object, most likely static' end
if Object.getCategory(checkUnit) ~= Object.Category.UNIT then
return false, "object is not a unit" return false, "object is not a unit"
end end
if unit:isExist() ~= true then return false, "unit does not exist" end if checkUnit:isExist() ~= true then return false, "unit does not exist" end
local group = unit:getGroup() local group = checkUnit:getGroup()
if group ~= nil then if group ~= nil then
if AI_GROUPS[group:getName()] == true then if AI_GROUPS[group:getName()] == true then
return false return false
end end
local players = DcsUtil.getAllPlayerUnits() local players = DcsUtil.getAllPlayerUnits()
local unitName = unit:getName() local unitName = checkUnit:getName()
for i, unit in pairs(players) do for _, playerUnit in pairs(players) do
if unit:getName() == unitName then if playerUnit:getName() == unitName then
return true return true
end end
end end
@@ -385,11 +431,8 @@ do
end end
if isPlayer(unit) == true then if isPlayer(unit) == true then
local groupId = unit:getGroup():getID()
SpearheadEvents.TriggerPlayerEntersUnit(unit) SpearheadEvents.TriggerPlayerEntersUnit(unit)
end end
return nil
end end
if event.id == world.event.S_EVENT_BIRTH then if event.id == world.event.S_EVENT_BIRTH then
+19 -11
View File
@@ -8,6 +8,7 @@ do --setup route util
---@param attackHelos boolean ---@param attackHelos boolean
---@return table ---@return table
local function GetCAPTargetTypes(attackHelos) local function GetCAPTargetTypes(attackHelos)
---@type table<number, string>
local targetTypes = { local targetTypes = {
[1] = "Planes", [1] = "Planes",
} }
@@ -53,8 +54,9 @@ do --setup route util
---comment ---comment
---@param groupName string ---@param groupName string
---@param position table { x, y} ---@param position Vec3
---@param altitude number ---@param altitude number
---@param deviationdistance number
---@param speed number ---@param speed number
---@param duration number ---@param duration number
---@param engageHelos boolean ---@param engageHelos boolean
@@ -92,7 +94,7 @@ do --setup route util
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishOnStation, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishOnStation, \"" .. groupName .. "\")"
} }
} }
} }
@@ -122,7 +124,7 @@ do --setup route util
}, },
stopCondition = { stopCondition = {
duration = durationBefore10, duration = durationBefore10,
condition = "return Spearhead.DcsUtil.IsBingoFuel(\"" .. groupName .. "\", 0.10)", condition = "return GlobalCapCallBacks.IsBingoFuel(\"" .. groupName .. "\", 0.10)",
} }
} }
}, },
@@ -135,7 +137,7 @@ do --setup route util
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishRTBInTen, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishRTBInTen, \"" .. groupName .. "\")"
} }
} }
} }
@@ -156,7 +158,7 @@ do --setup route util
}, },
stopCondition = { stopCondition = {
duration = durationAfter10, duration = durationAfter10,
condition = "return Spearhead.DcsUtil.IsBingoFuel(\"" .. groupName .. "\")", condition = "return GlobalCapCallBacks.IsBingoFuel(\"" .. groupName .. "\", 0.10)",
} }
} }
}, },
@@ -169,7 +171,7 @@ do --setup route util
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" .. groupName .. "\")" command = "pcall(GlobalCapCallBacks.PublishRTB, \"" .. groupName .. "\")"
} }
} }
} }
@@ -181,7 +183,7 @@ do --setup route util
end end
---comment ---comment
---@param position table { x, y} ---@param position Vec3
---@param altitude number ---@param altitude number
---@param speed number ---@param speed number
---@param childTasks table ---@param childTasks table
@@ -210,7 +212,8 @@ do --setup route util
---comment ---comment
---@param groupName string groupName you're creating this route for ---@param groupName string groupName you're creating this route for
---@param airdromeId number airdromeId ---@param airdromeId number airdromeId
---@param capPoint table { x, z } ---@param capPoint Vec3
---@param racetrackSecondPoint Vec3?
---@param altitude number ---@param altitude number
---@param speed number ---@param speed number
---@param durationOnStation number ---@param durationOnStation number
@@ -258,7 +261,8 @@ do --setup route util
if racetrackSecondPoint then orbitType = "Race-Track" end if racetrackSecondPoint then orbitType = "Race-Track" end
local basePoint = base:getPoint() local basePoint = base:getPoint()
local points = {} ---@type Array<table>
local points
if racetrackSecondPoint == nil then if racetrackSecondPoint == nil then
points = { points = {
[1] = FlyToPointTask(capPoint, altitude, speed, additionalFlyOverTasks), [1] = FlyToPointTask(capPoint, altitude, speed, additionalFlyOverTasks),
@@ -304,6 +308,7 @@ do --setup route util
end end
local group = Group.getByName(groupName) local group = Group.getByName(groupName)
---@type Vec3?
local pos; local pos;
local i = 1 local i = 1
if group == nil then if group == nil then
@@ -375,7 +380,7 @@ do --setup route util
action = { action = {
id = "Script", id = "Script",
params = { params = {
command = "pcall(Spearhead.Events.PublishRTB, \"" .. command = "pcall(GlobalCapCallBacks.PublishRTB, \"" ..
groupName .. "\")" groupName .. "\")"
} }
} }
@@ -393,6 +398,9 @@ do --setup route util
}, "" }, ""
end end
---@param pointA Vec3
---@param pointB Vec3
---@return table, string?
ROUTE_UTIL.CreateCarrierRacetrack = function(pointA, pointB) ROUTE_UTIL.CreateCarrierRacetrack = function(pointA, pointB)
return { return {
id = "Mission", id = "Mission",
@@ -461,7 +469,7 @@ do --setup route util
} }
} }
} }
}, "" }, nil
end end
end end
+535 -212
View File
@@ -1,295 +1,618 @@
local Events = require("classes.spearhead_events") local Events = require("classes.spearhead_events")
local Util = require("classes.util.Util") local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil")
local Logger = require("classes.util.Logger") local Logger = require("classes.util.Logger")
local MissionEditorWarnings = require("classes.util.MissionEditorWarnings") local PersistenceConfig = require("classes.configuration.PersistenceConfig")
local ExtraStage = require("classes.stageClasses.Stages.ExtraStage") local ExtraStage = require("classes.stageClasses.Stages.ExtraStage")
local PrimaryStage = require("classes.stageClasses.Stages.PrimaryStage") local PrimaryStage = require("classes.stageClasses.Stages.PrimaryStage")
local WaitingStage = require("classes.stageClasses.Stages.WaitingStage") local WaitingStage = require("classes.stageClasses.Stages.WaitingStage")
local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper")
local StagesByName = {} local StageRepository = require("classes.stageClasses.StageRepository")
local StageLane = require("classes.stageClasses.StageLane")
---@type table<string, Array<Stage>> local Persistence = require("classes.persistence.Persistence")
local StagesByIndex = {}
---@type table<string, Array<Stage>>
local SideStageByIndex = {}
---@type table<string, Array<WaitingStage>>
local WaitingStagesByIndex = {}
local currentStage = -99 local currentStage = -99
---@class GlobalStageManager : StageCompleteListener ---@class GlobalStageManager : StageCompleteListener, OnStageChangedListener, OnStageNumberChangeCompleteListener
---@field private database Database ---@field private database Database
---@field private logger Logger ---@field private logger Logger
---@field private stageConfig StageConfig ---@field private stageConfig StageConfig
---@field private _stageRepository StageRepository
---@field private _missionCommandsHelper MissionCommandsHelper
local GlobalStageManager = {} local GlobalStageManager = {}
GlobalStageManager.__index = GlobalStageManager GlobalStageManager.__index = GlobalStageManager
GlobalStageManager.getCurrentStage = function() return currentStage end GlobalStageManager.getCurrentStage = function() return currentStage end
---@type GlobalStageManager
local singletonInstance = nil
---comment ---comment
---@param database Database ---@param database Database
---@param stageConfig StageConfig ---@param stageConfig StageConfig
---@param logLevel LogLevel ---@param logLevel LogLevel
---@param spawnManager SpawnManager ---@param spawnManager SpawnManager
---@return nil ---@return GlobalStageManager
function GlobalStageManager.NewAndStart(database, stageConfig, logLevel, spawnManager) function GlobalStageManager.new(database, stageConfig, logLevel, spawnManager)
if singletonInstance ~= nil then
return singletonInstance
end
local logger = Logger.new("StageManager", logLevel) local logger = Logger.new("StageManager", logLevel)
logger:info("Using Stage Log Level: " .. logLevel) logger:info("Using Stage Log Level: " .. logLevel)
local self = setmetatable({}, GlobalStageManager) local self = setmetatable({}, GlobalStageManager)
singletonInstance = self
self.database = database self.database = database
self.stageConfig = stageConfig self.stageConfig = stageConfig
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
self._stageRepository = StageRepository.getInstance()
self.logger = logger self.logger = logger
if stageConfig.isAutoStages ~= true then if stageConfig.isAutoStages ~= true then
logger:warn("Spearhead will not automatically progress stages due to the given settings. If you manually have implemented this, please ignore this message") logger:warn(
"Spearhead will not automatically progress stages due to the given settings. If you manually have implemented this, please ignore this message")
end end
---@type OnStageChangedListener Events.AddStageNumberChangedListener(self)
local OnStageNumberChangedListener = {
OnStageNumberChanged = function (self, number)
currentStage = number
end
}
Events.AddStageNumberChangedListener(OnStageNumberChangedListener)
for _, stageName in pairs(database:getStagezoneNames()) do for _, stageName in pairs(database:getStagezoneNames()) do
logger:debug("Found stage zone with name: " .. stageName) logger:debug("Found stage zone with name: " .. stageName)
if Util.startswith(stageName, "missionstage", true) then local parseResult = self:ParseStageName(stageName)
local valid = true if parseResult.isValid == false then
local split = Util.split_string(stageName, "_") logger:warn("Stage zone with name " .. stageName .. " is not valid: " .. parseResult.invalidReason)
if Util.tableLength(split) < 2 then else
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a order number or valid format") local initData = {
valid = false stageZoneName = parseResult.stageZoneName,
end stageNumber = parseResult.orderNumber,
stageDisplayName = parseResult.stageDisplayName,
stageLaneIdentifier = parseResult.stageLaneIdentifier
}
if Util.tableLength(split) < 3 then if parseResult.stageType == "PrimaryStage" then
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a stage name") local stage = PrimaryStage.New(database, stageConfig, logger, initData, spawnManager)
end
local orderNumber = nil stage:AddStageCompleteListener(self)
local isSideStage = false self._stageRepository:AddStage(stage)
if valid == true then elseif parseResult.stageType == "ExtraStage" then
local orderNumberString = string.lower(split[2]) local stage = ExtraStage.New(database, stageConfig, logger, initData, spawnManager)
if Util.startswith(orderNumberString, "x") == true then stage:AddStageCompleteListener(self)
isSideStage = true self._stageRepository:AddStage(stage)
elseif parseResult.stageType == "WaitingStage" then
orderNumberString = string.gsub(orderNumberString, "x", "") local waitingStage = WaitingStage.New(database, stageConfig, logger, initData,
orderNumber = tonumber(orderNumberString) parseResult.waitingStageSeconds, spawnManager)
else waitingStage:AddStageCompleteListener(self)
orderNumber = tonumber(split[2]) self._stageRepository:AddStage(waitingStage)
end
if orderNumber == nil then
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a valid order number : " .. split[2])
valid = false
end
end
local stageDisplayName = split[3]
local stagelogger = Logger.new(stageName, logLevel)
if valid == true and orderNumber then
---@type StageInitData
local initData = {
stageDisplayName = stageDisplayName,
stageNumber = orderNumber,
stageZoneName = stageName,
}
if isSideStage == true then
local stage = ExtraStage.New(database, stageConfig, stagelogger, initData, spawnManager)
stage:AddStageCompleteListener(self)
if SideStageByIndex[tostring(orderNumber)] == nil then SideStageByIndex[tostring(orderNumber)] = {} end
table.insert(SideStageByIndex[tostring(orderNumber)], stage)
else
local stage = PrimaryStage.New(database, stageConfig, stagelogger, initData, spawnManager)
stage:AddStageCompleteListener(self)
if StagesByIndex[tostring(orderNumber)] == nil then StagesByIndex[tostring(orderNumber)] = {} end
table.insert(StagesByIndex[tostring(orderNumber)], stage)
end
end
end
if Util.startswith(stageName, "waitingstage", true) then
local valid = true
local split = Util.split_string(stageName, "_")
if Util.tableLength(split) < 3 then
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a order number or valid format")
valid = false
end
if valid == true then
local stageIndexString = split[2]
local stageIndex = tonumber(stageIndexString)
if not stageIndex then
MissionEditorWarnings.Add("Stage zone with name " .. stageName .. " does not have a valid order number")
valid = false
end
local waitingSecondsString = split[3]
local waitingSeconds = tonumber(waitingSecondsString)
if not waitingSeconds then
MissionEditorWarnings.Add("Waiting Stage zone with name " .. stageName .. " does not have a valid amount of seconds parameter")
valid = false
end
if valid == true then
local stagelogger = Logger.new(stageName, logLevel)
---@type WaitingStageInitData
local initData = {
stageDisplayName = "Waiting Stage " .. stageIndex,
stageNumber = stageIndex or -99,
stageZoneName = stageName,
waitingSeconds = waitingSeconds --[[@as integer]]
}
local waitingStage = WaitingStage.New(database, stageConfig, stagelogger, initData, spawnManager)
if WaitingStagesByIndex[tostring(stageIndex)] == nil then
WaitingStagesByIndex[tostring(stageIndex)] = {}
end
table.insert(WaitingStagesByIndex[tostring(stageIndex)], waitingStage)
waitingStage:AddStageCompleteListener(self)
end
end end
end end
end end
singletonInstance = self
return self return self
end 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) function GlobalStageManager:OnStageComplete(stage)
self.logger:debug("Receiving stage complete event from: " .. stage.zoneName) self.logger:debug("Receiving stage complete event from: " .. stage.zoneName)
local anyIncomplete = false local stageIndex = stage:GetStageIndex()
self.logger:debug("Checking stages for index: " .. tostring(currentStage)) local laneIdentifier = stage:GetStageLaneIdentifier()
for index, stage in pairs(StagesByIndex[tostring(currentStage)]) do
if stage:IsComplete() == false then local stageLane = self._stageRepository:getStageLane(laneIdentifier)
anyIncomplete = true if not stageLane or not stageLane:IsCurrentStageIndexComplete() then
self.logger:debug("Need to wait for Stage " .. stage.zoneName .. " to be completed") self.logger:debug("Stage lane " ..
else (laneIdentifier or "default") .. " is not complete for stage index " .. stageIndex)
self.logger:debug("Stage verified to be completed: " .. stage.zoneName) return
end
local nextStageIndex = stageIndex + 1
Events.PublishStageNumberChanged(nextStageIndex, laneIdentifier)
stageLane:SetActiveStageIndex(nextStageIndex)
if stageLane:IsDefaultStageLane() then
-- COMPLETION IN THE DEFAULT LANE
-- 1. Check if any lane is at status "BetweenChapters" and the next chapter start is "next stage index"
local allLanes = self._stageRepository:getAllStageLanes()
for _, lane in ipairs(allLanes) do
if lane:GetStageLaneIdentifier() ~= StageLane.DefaultLaneKey and lane:GetStageLaneState() == "BetweenChapters" then
local nextChapterStart = lane:GetNextChapterStart()
if nextChapterStart == nextStageIndex then
self.logger:debug("Lane " ..
(lane:GetStageLaneIdentifier() or "default") ..
" is at chapter start for next stage index " .. nextStageIndex)
Events.PublishStageNumberChanged(nextStageIndex, lane:GetStageLaneIdentifier())
lane:SetActiveStageIndex(nextStageIndex)
end
end
end
else
-- COMPLETION IN A SIDE LANE
-- 1. Check if the default lane is at status "BetweenChapters"
local defaultStageLane = self._stageRepository:getStageLane(StageLane.DefaultLaneKey)
if defaultStageLane and defaultStageLane:GetStageLaneState() == "BetweenChapters" then
local nextChapterStart = defaultStageLane:GetNextChapterStart()
if nextChapterStart == nextStageIndex then
-- 2. Check if all side lanes are at or beyond the next chapter starts
local allLanes = self._stageRepository:getAllStageLanes()
local allSideLanesReady = true
for _, lane in ipairs(allLanes) do
if lane:GetStageLaneIdentifier() ~= StageLane.DefaultLaneKey then
-- Check if the lane is at or beyond the next chapter starts when "InChapter" eq "Active"
if lane:GetStageLaneState() == "InChapter" and lane:GetActiveStageIndex() < nextStageIndex then
allSideLanesReady = false
self.logger:debug("Lane " ..
(lane:GetStageLaneIdentifier() or "default") ..
" is not ready for next stage index " .. nextStageIndex)
break
-- When in between chapters, check if the next chapter start is less than the next stage index, meaning there is still stages to be completed before the main lane can be activated again
elseif lane:GetStageLaneState() == "BetweenChapters" then
local nextChapter = lane:GetNextChapterStart()
if nextChapter and nextChapter < nextStageIndex then
allSideLanesReady = false
self.logger:debug("Lane " ..
(lane:GetStageLaneIdentifier() or "default") ..
" is not ready for next stage index " .. nextStageIndex)
break
end
end
end
end
if allSideLanesReady then
self.logger:debug("All side lanes are ready for next stage index " ..
nextStageIndex .. ", activating default lane")
Events.PublishStageNumberChanged(nextStageIndex, nil)
defaultStageLane:SetActiveStageIndex(nextStageIndex)
else
self.logger:debug("Not all side lanes are ready for next stage index " ..
nextStageIndex .. ", default lane will not be activated")
end
end
end
end
end
---@param stageNumber number
---@param stageLaneIdentifier string?
---@return boolean?
function GlobalStageManager:IsStageComplete(stageNumber, stageLaneIdentifier)
local stageLane = self._stageRepository:getStageLane(stageLaneIdentifier)
if not stageLane then
self.logger:warn("Stage lane " .. (stageLaneIdentifier or "default") .. " does not exist")
return nil
end
return stageLane:IsStageIndexComplete(stageNumber)
end
---@public
function GlobalStageManager:OnStageNumberChanged(stageNumber, stageLaneIdentifier)
-- only react on "main" lane changes, ignore other lanes for now
if stageLaneIdentifier ~= nil then return end
self.logger:debug("Stage number changed to: " .. tostring(stageNumber))
self:UpdateDrawings(stageNumber)
end
function GlobalStageManager:OnStageNumberChangeComplete(stageNumber, stageLaneIdentifier)
-- only react on "main" lane changes, ignore other lanes for now
if stageLaneIdentifier ~= nil then return end
self.logger:debug("Stage number change complete to: " .. tostring(stageNumber))
---@type Array<Group>
local groups = {}
for _, player in pairs(DcsUtil.getAllPlayerUnits()) do
local group = player:getGroup()
if group then
table.insert(groups, group)
end end
end end
if anyIncomplete == false and self.stageConfig.isAutoStages == true then for _, group in pairs(groups) do
self._missionCommandsHelper:OverviewToGroup(group:getID())
-- CHECK WAITING STAGES
local nextStage = currentStage + 1
if WaitingStagesByIndex[tostring(nextStage)] then
for _, waitingStage in pairs(WaitingStagesByIndex[tostring(nextStage)]) do
if waitingStage:IsActive() == false then
waitingStage:ActivateStage()
end
end
end
local anyWaiting = false
if WaitingStagesByIndex[tostring(nextStage)] then
for _, waitingStage in pairs(WaitingStagesByIndex[tostring(nextStage)]) do
if waitingStage:IsComplete() == false then
anyWaiting = true
end
end
end
if anyWaiting == false then
local newStageNumber = currentStage + 1
self:UpdateDrawings(newStageNumber)
self.logger:debug("Setting next stage to: " .. tostring(newStageNumber))
Events.PublishStageNumberChanged(newStageNumber)
end
end end
end end
---@private ---@private
function GlobalStageManager:UpdateDrawings(stageNumber) ---@param stageNumber number
local drawings = self.database:getCustomDrawings() ---@param stageLaneIdentifier string?
function GlobalStageManager:UpdateDrawings(stageNumber, stageLaneIdentifier)
self.logger:debug("Updating custom drawings for stage number: " .. tostring(stageNumber))
local drawings = self.database:getStageDrawings()
for _, drawing in pairs(drawings) do for _, drawing in pairs(drawings) do
local startStage, stopStage = drawing:GetStartAndStop() local startStage = drawing:GetStartingStage()
if stageNumber >= startStage and stageNumber < stopStage then local stopStage = drawing:GetRemoveAtStage()
drawing:Draw() local laneIdentifier = drawing:GetStageLaneIdentifier()
else
drawing:Remove() 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 end
end end
function GlobalStageManager:PrintMermaidStage()
local lanes = self._stageRepository:getAllStageLanes()
GlobalStageManager.printFullOverview = function () ---@type table<string, string>
local nodes = {}
local logger = Logger.new("StageOverview", "INFO") ---@type table<string, string>
logger:info("Stage overview:") local edges = {}
---@type table<string, Array<number>>
local stageIndicesByLane = {} -- Track all stage indices per lane
local mainLaneId = "default"
local max = 0 -- Color palette for lanes (26 vibrant colors optimized for dark mode)
local lines = {} local laneColors = {
for stageIndex, stages in pairs(StagesByIndex) do "#FF6B6B", -- 1: Red
"#4ECDC4", -- 2: Teal
local totalStrike = 0 "#45B7D1", -- 3: Blue
local totalbai = 0 "#FFA502", -- 4: Orange
local totaldead = 0 "#95E1D3", -- 5: Mint
local totalMissions = 0 "#F38181", -- 6: Coral
local totalCas = 0 "#AA96DA", -- 7: Purple
"#FCBAD3", -- 8: Pink
"#A8E6CF", -- 9: Light green
"#FFD3B6", -- 10: Peach
"#FFAAA5", -- 11: Light red
"#FF8B94", -- 12: Rose
"#FFEAA7", -- 13: Butter
"#DFE6E9", -- 14: Gray
"#00B894", -- 15: Emerald
"#0984E3", -- 16: Cobalt
"#6C5CE7", -- 17: Indigo
"#A29BFE", -- 18: Lavender
"#FD79A8", -- 19: Magenta
"#FDCB6E", -- 20: Gold
"#6C757D", -- 21: Slate
"#20C997", -- 22: Seafoam
"#E74C3C", -- 23: Scarlet
"#3498DB", -- 24: Dodger blue
"#9B59B6", -- 25: Amethyst
"#1ABC9C", -- 26: Turquoise
}
---@type table<string, string>
local laneColorMap = {} -- Map lane ID to color
---@type number
local colorIndex = 1
for _, stage in pairs(stages) do -- Build nodes and collect all stage indices per lane
for _, lane in ipairs(lanes) do
local strike, dead, bai, cas = stage:GetStageStats() local laneId = lane:GetStageLaneIdentifier() or "default"
local stageIndices = lane:GetAllStageIndices()
stageIndicesByLane[laneId] = stageIndices
totalStrike = totalStrike + strike -- Assign color to this lane
totalbai = totalbai + bai laneColorMap[laneId] = laneColors[colorIndex]
totaldead = totaldead + dead colorIndex = colorIndex + 1
totalCas = totalCas + cas if colorIndex > #laneColors then
totalMissions = totalMissions + strike + dead + bai + cas colorIndex = 1
end end
local index = tonumber(stageIndex) -- Create nodes for each stage index in this lane
if index then for _, stageIndex in ipairs(stageIndices) do
if index > max then local stages = lane:GetStagesAtIndex(stageIndex)
max = index
if stages then
for _, stage in ipairs(stages) do
local nodeId = laneId .. "_" .. stageIndex
local stageType = stage:GetStageType()
local stageName = stage.stageName or stage.zoneName
-- Generate bracket label: [1], [2] for default, [w1], [e2] for other lanes
---@type string
local bracketLabel
if laneId == "default" then
bracketLabel = "[" .. stageIndex .. "]"
else
bracketLabel = "[" .. laneId .. stageIndex .. "]"
end
-- Combined label: [bracket] Name
local label = bracketLabel .. " " .. stageName
-- Different node shapes for different stage types
local nodeShape = "["
local nodeEnd = "]"
if stageType == "ExtraStage" then
nodeShape = "(["
nodeEnd = "])"
elseif stageType == "WaitingStage" then
nodeShape = "[["
nodeEnd = "]]"
end
nodes[nodeId] = string.format(' %s%s"%s"%s',
nodeId, nodeShape, label, nodeEnd)
end
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
end end
for i = 1, max do -- Add edges for sequential stages within the same lane
if lines[i] then for laneId, stageIndices in pairs(stageIndicesByLane) do
logger:info(lines[i]) 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
end end
-- Get main lane reference
local mainStageLane = nil
for _, lane in ipairs(lanes) do
if lane:IsDefaultStageLane() then
mainStageLane = lane
break
end
end
-- Add dependencies from DEFAULT lane to SIDE lanes at chapter starts
-- Side lanes activate when main completes the stage BEFORE the chapter start
if mainStageLane then
for _, lane in ipairs(lanes) do
if lane:IsDefaultStageLane() == false then
local sideId = lane:GetStageLaneIdentifier()
local sideIndices = stageIndicesByLane[sideId] or {} --[[@as Array<number>]]
-- For each stage in the side lane
for _, stageIdx in ipairs(sideIndices) do
-- Check if this is a chapter start in the side lane
if lane:IsChapterStart(stageIdx) then
-- Main lane must complete (stageIdx - 1) to activate side lane at stageIdx
local fromNodeId = mainLaneId .. "_" .. (stageIdx - 1)
local toNodeId = sideId .. "_" .. stageIdx
-- Side lane activates when main lane completes the prior stage
table.insert(edges, string.format(' %s -->|unlock| %s', fromNodeId, toNodeId))
end
end
end
end
end
-- Add dependencies from SIDE lanes gating the MAIN lane at chapter boundaries
-- Main cannot advance to the next chapter until ALL side lanes are at or above that chapter index
if mainStageLane then
local mainIndices = stageIndicesByLane[mainLaneId]
-- For each chapter start in main lane (skip first one)
for i = 2, #mainIndices do
local nextIdx = mainIndices[i]
-- Check if nextIdx is a chapter start (there's a gap before it)
if mainStageLane:IsChapterStart(nextIdx) then
-- Before main can progress to nextIdx, all side lanes must be at >= nextIdx
for _, lane in ipairs(lanes) do
if lane:IsDefaultStageLane() == false then
local sideId = lane:GetStageLaneIdentifier()
local sideIndices = stageIndicesByLane[sideId] or {} --[[@as Array<number>]]
-- Find the appropriate gate node for this side lane
-- Use first stage >= nextIdx if it exists, otherwise use the highest stage
local gateStageIdx = nil
for _, idx in ipairs(sideIndices) do
if idx >= nextIdx then
gateStageIdx = idx
break
end
end
-- If no stage >= nextIdx, use the highest stage in this lane
if gateStageIdx == nil and #sideIndices > 0 then
gateStageIdx = sideIndices[#sideIndices]
end
if gateStageIdx then
local fromNodeId = sideId .. "_" .. gateStageIdx
local toNodeId = mainLaneId .. "_" .. nextIdx
table.insert(edges, string.format(' %s -->|gate| %s', fromNodeId, toNodeId))
end
end
end
end
end
end
-- Build complete Mermaid diagram
local diagramLines = { "graph TD" }
-- Add class definitions for each lane with colors and contrasting text
for laneId, color in pairs(laneColorMap) do
table.insert(diagramLines,
string.format(' classDef lane_%s fill:%s,stroke:#333,stroke-width:2px,color:#000', laneId, color))
end
-- Add nodes
for _, nodeStr in pairs(nodes) do
table.insert(diagramLines, nodeStr)
end
-- Add edges
for _, edgeStr in pairs(edges) do
table.insert(diagramLines, edgeStr)
end
-- Apply classes to nodes
for nodeId in pairs(nodes) do
local laneId = nodeId:match("(.+)_[0-9]+$")
if laneId then
table.insert(diagramLines, string.format(' class %s lane_%s', nodeId, laneId))
end
end
-- Print as single multi-line message
local diagram = "========== STAGE FLOW DIAGRAM ==========\n" ..
table.concat(diagramLines, "\n") .. "\n========== END DIAGRAM =========="
self.logger:info(diagram)
end end
---comment ---comment
---@param stageNumber number ---@param stageNumber number
---@param stageLaneIdentifier string? nil for default lan
---@return boolean | nil ---@return boolean | nil
GlobalStageManager.isStageComplete = function (stageNumber) GlobalStageManager.isStageComplete = function(stageNumber, stageLaneIdentifier)
if singletonInstance == nil then
local stageIndex = tostring(stageNumber) Logger.new("StageManager", "INFO"):warn(
"GlobalStageManager.isStageComplete called before GlobalStageManager was initialized. Returning nil")
if StagesByIndex[stageIndex] == nil then return nil end return nil
for _, stage in ipairs(StagesByIndex[stageIndex]) do
if stage:IsComplete() == false then
return false
end
end end
return true if stageLaneIdentifier then
stageLaneIdentifier = string.lower(stageLaneIdentifier)
end
return singletonInstance:IsStageComplete(stageNumber, stageLaneIdentifier)
end end
return GlobalStageManager return GlobalStageManager
@@ -41,7 +41,7 @@ end
function SpearheadGroup:SpawnCorpsesOnly() function SpearheadGroup:SpawnCorpsesOnly()
if self._isSpawned == true then return end if self._isSpawned == true then return end
self._spawnManager:SpawnCorpsesOnly(self._groupName) self._spawnManager:SpawnCorpsesOnly(self._groupName)
self._isSpawned = true self._isSpawned = true
@@ -57,7 +57,7 @@ function SpearheadGroup:Spawn(lateStart)
uncontrolled = lateStart, uncontrolled = lateStart,
} }
local spawnedObject, isStatic = self._spawnManager:SpawnGroup(self._groupName, overrides, self._isPersistent) local _, isStatic = self._spawnManager:SpawnGroup(self._groupName, overrides, self._isPersistent)
self._isStatic = isStatic self._isStatic = isStatic
self._isSpawned = true self._isSpawned = true
end end
@@ -91,13 +91,13 @@ function SpearheadGroup:GetCoalition()
end end
---comment ---comment
---@return table result list of objects ---@return Array<Unit|StaticObject> result list of objects
function SpearheadGroup:GetObjects() function SpearheadGroup:GetObjects()
---@type Array<Unit|StaticObject>
local result = {} local result = {}
if self._isStatic == true then if self._isStatic == true then
local staticObject = StaticObject.getByName(self._groupName) local staticObject = StaticObject.getByName(self._groupName)
if staticObject then if staticObject then
table.insert(result, staticObject) table.insert(result, staticObject)
end end
else else
@@ -105,7 +105,7 @@ function SpearheadGroup:GetObjects()
if not group then return {} end if not group then return {} end
for _, unit in pairs(group:getUnits()) do for _, unit in pairs(group:getUnits()) do
table.insert(result, unit) table.insert(result, unit)
end end
end end
return result return result
end end
@@ -123,7 +123,7 @@ function SpearheadGroup:GetAsUnits()
if not group then return {} end if not group then return {} end
for _, unit in pairs(group:getUnits()) do for _, unit in pairs(group:getUnits()) do
table.insert(result, unit) table.insert(result, unit)
end end
return result return result
end end
@@ -133,7 +133,7 @@ function SpearheadGroup:GetAllUnitPositions()
local result = {} local result = {}
if self._isStatic == true then if self._isStatic == true then
local staticObject = StaticObject.getByName(self._groupName) local staticObject = StaticObject.getByName(self._groupName)
if staticObject then if staticObject then
table.insert(result, staticObject:getPoint()) table.insert(result, staticObject:getPoint())
end end
else else
@@ -141,7 +141,7 @@ function SpearheadGroup:GetAllUnitPositions()
if not group then return {} end if not group then return {} end
for _, unit in pairs(group:getUnits()) do for _, unit in pairs(group:getUnits()) do
table.insert(result, unit:getPoint()) table.insert(result, unit:getPoint())
end end
end end
return result return result
end end
@@ -50,16 +50,10 @@ function BlueSam.New(database, logger, zoneName, spawnManager)
---@type table<string, Vec3> ---@type table<string, Vec3>
local redUnitsPos = {} local redUnitsPos = {}
local buildable = false
if self._buildableCrateKilos and self._buildableCrateKilos > 0 then
buildable = true
end
for _, groupName in pairs(blueSamData.groups) do for _, groupName in pairs(blueSamData.groups) do
local spearheadGroup = SpearheadGroup.New(groupName, spawnManager, true) local spearheadGroup = SpearheadGroup.New(groupName, spawnManager, true)
if spearheadGroup then if spearheadGroup then
if spearheadGroup:GetCoalition() == 2 or spearheadGroup:GetCoalition() == 0 then if spearheadGroup:GetCoalition() == 2 or spearheadGroup:GetCoalition() == 0 then
table.insert(self._blueGroups, spearheadGroup) table.insert(self._blueGroups, spearheadGroup)
end end
@@ -78,7 +72,7 @@ function BlueSam.New(database, logger, zoneName, spawnManager)
--Cleanup units --Cleanup units
local cleanup_distance = 5 local cleanup_distance = 5
for blueUnitName, blueUnitPos in pairs(blueUnitsPos) do for _, blueUnitPos in pairs(blueUnitsPos) do
for redUnitName, redUnitPos in pairs(redUnitsPos) do for redUnitName, redUnitPos in pairs(redUnitsPos) do
local distance = Util.VectorDistance3d(blueUnitPos, redUnitPos) local distance = Util.VectorDistance3d(blueUnitPos, redUnitPos)
if distance <= cleanup_distance then if distance <= cleanup_distance then
@@ -89,7 +83,7 @@ function BlueSam.New(database, logger, zoneName, spawnManager)
local zone = DcsUtil.getZoneByName(zoneName) local zone = DcsUtil.getZoneByName(zoneName)
if zone then 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 end
return self return self
@@ -140,7 +134,9 @@ end
function BlueSam:SpawnGroups() function BlueSam:SpawnGroups()
for unitName, needsCleanup in pairs(self._cleanupUnits) do for unitName, needsCleanup in pairs(self._cleanupUnits) do
DcsUtil.DestroyUnit(unitName) if needsCleanup then
DcsUtil.DestroyUnit(unitName)
end
end end
for _, group in pairs(self._blueGroups) do for _, group in pairs(self._blueGroups) do
@@ -42,11 +42,11 @@ function FarpZone.New(database, logger, zoneName, spawnManager)
self._groups = {} self._groups = {}
self._padNames = {} self._padNames = {}
self._supplyHubs = {} self._supplyHubs = {}
if farpData then if farpData then
self._padNames = farpData.padNames self._padNames = farpData.padNames
for _, supplyHubName in pairs(farpData.supplyHubNames) do for _, supplyHubName in pairs(farpData.supplyHubNames) do
local supplyHub = SupplyHub.new(database, logger, supplyHubName) local supplyHub = SupplyHub.new(database, logger, supplyHubName)
if supplyHub then if supplyHub then
@@ -55,7 +55,7 @@ function FarpZone.New(database, logger, zoneName, spawnManager)
end end
for _, groupName in pairs(farpData.groups) do for _, groupName in pairs(farpData.groups) do
local group = SpearheadGroup.New(groupName, spawnManager, true) local group = SpearheadGroup.New(groupName, spawnManager, true)
table.insert(self._groups, group) table.insert(self._groups, group)
group:Destroy() group:Destroy()
@@ -64,7 +64,7 @@ function FarpZone.New(database, logger, zoneName, spawnManager)
local zone = DcsUtil.getZoneByName(zoneName) local zone = DcsUtil.getZoneByName(zoneName)
if zone then if zone then
self._logger:debug("Creating Buildable zone: " .. zoneName .. " with " .. (farpData.buildingKilos or "nil") .. " kilos") 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
end end
self:Deactivate() self:Deactivate()
@@ -90,7 +90,7 @@ function StageBase.New(databaseManager, logger, airbaseName, spawnManager)
local cleanup_distance = 5 local cleanup_distance = 5
for blueUnitName, blueUnitPos in pairs(blueUnitsPos) do for _, blueUnitPos in pairs(blueUnitsPos) do
for redUnitName, redUnitPos in pairs(redUnitsPos) do for redUnitName, redUnitPos in pairs(redUnitsPos) do
local distance = Util.VectorDistance3d(blueUnitPos, redUnitPos) local distance = Util.VectorDistance3d(blueUnitPos, redUnitPos)
if distance <= cleanup_distance then if distance <= cleanup_distance then
@@ -113,6 +113,7 @@ end
function StageBase:SpawnRedUnits() function StageBase:SpawnRedUnits()
---comment ---comment
---@param groups Array<SpearheadGroup> ---@param groups Array<SpearheadGroup>
---@return number|nil
local spawnAsync = function(groups) local spawnAsync = function(groups)
for _, group in pairs(groups) do for _, group in pairs(groups) do
group:Spawn() group:Spawn()
@@ -142,6 +143,7 @@ end
function StageBase:SpawnBlueUnits() function StageBase:SpawnBlueUnits()
---comment ---comment
---@param groups Array<SpearheadGroup> ---@param groups Array<SpearheadGroup>
---@return number|nil
local spawnAsync = function(groups) local spawnAsync = function(groups)
for _, group in pairs(groups) do for _, group in pairs(groups) do
group:Spawn() group:Spawn()
@@ -172,7 +174,7 @@ function StageBase:ActivateBlueStage()
else else
self:FinaliseBlueStage() self:FinaliseBlueStage()
end end
end end
function StageBase:FinaliseBlueStage() function StageBase:FinaliseBlueStage()
@@ -2,6 +2,8 @@ local Util = require("classes.util.Util")
local DcsUtil = require("classes.util.DcsUtil") local DcsUtil = require("classes.util.DcsUtil")
local SupplyUnitsTracker = require("classes.stageClasses.helpers.SupplyUnitsTracker") local SupplyUnitsTracker = require("classes.stageClasses.helpers.SupplyUnitsTracker")
local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper") local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionCommandsHelper")
local CustomDrawing = require("classes.stageClasses.drawings.CustomDrawing")
local DrawingHelper = require("classes.stageClasses.drawings.helper.DrawingHelper")
---@class SupplyHub ---@class SupplyHub
---@field private _database Database ---@field private _database Database
@@ -12,7 +14,7 @@ local MissionCommandsHelper = require("classes.stageClasses.helpers.MissionComma
---@field private _isCommmandAdded table<string, boolean> ---@field private _isCommmandAdded table<string, boolean>
---@field private _missionCommandsHelper MissionCommandsHelper ---@field private _missionCommandsHelper MissionCommandsHelper
---@field private _inZone table<string, boolean> ---@field private _inZone table<string, boolean>
---@field private _drawID number ---@field private _customDrawing CustomDrawing?
---@field private _cargoInUnits table<table<string, number>> ---@field private _cargoInUnits table<table<string, number>>
---@field private _activeAtStart boolean ---@field private _activeAtStart boolean
---@field private _active boolean ---@field private _active boolean
@@ -30,6 +32,7 @@ function SupplyHub.new(database, logger, zoneName)
self._database = database self._database = database
self._logger = logger self._logger = logger
self._zoneName = zoneName self._zoneName = zoneName
self._customDrawing = nil
local split = Util.split_string(zoneName, "_") local split = Util.split_string(zoneName, "_")
if string.lower(split[2]) == "a" then if string.lower(split[2]) == "a" then
@@ -39,10 +42,10 @@ function SupplyHub.new(database, logger, zoneName)
end end
self._zone = DcsUtil.getZoneByName(zoneName) self._zone = DcsUtil.getZoneByName(zoneName)
self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate(logger.LogLevel) self._supplyUnitsTracker = SupplyUnitsTracker.getOrCreate()
self._inZone = {} self._inZone = {}
self._missionCommandsHelper = MissionCommandsHelper.getOrCreate(logger.LogLevel) self._missionCommandsHelper = MissionCommandsHelper.getOrCreate()
self._logger:debug("Creating Supply Hub zone: " .. self._zoneName) self._logger:debug("Creating Supply Hub zone: " .. self._zoneName)
@@ -72,13 +75,13 @@ function SupplyHub:Activate()
self._logger:debug("Activating Supply Hub zone: " .. self._zoneName) self._logger:debug("Activating Supply Hub zone: " .. self._zoneName)
local zone = DcsUtil.getZoneByName(self._zoneName) local zone = DcsUtil.getZoneByName(self._zoneName)
if zone and self._drawID == nil then if zone and self._customDrawing == nil then
---@type DrawColor local fillColor = DrawingHelper.ColorTableToColorString({ 0, 1, 0, 0.2 })
local fillColor = { r=0, g=1, b=0, a=0.2 } local lineColor = DrawingHelper.ColorTableToColorString({ 0, 1, 0, 1})
---@type DrawColor
local lineColor = { r=0, g=1, b=0, a=1}
local lineStyle = 1 local lineStyle = 1
self._drawID = DcsUtil.DrawZone(zone, lineColor, fillColor, lineStyle)
self._customDrawing = CustomDrawing.FromZone(zone, lineColor, fillColor, lineStyle, 6)
self._customDrawing:Draw()
end end
self._supplyUnitsTracker:RegisterHub(self) self._supplyUnitsTracker:RegisterHub(self)

Some files were not shown because too many files have changed in this diff Show More