diff --git a/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf b/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf
index 9894313..32dfca8 100644
--- a/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf
+++ b/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf
@@ -4,7 +4,7 @@
* File: fnc_handleUIEvents.sqf
* Author: IDSolutions
* Date: 2026-01-28
- * Last Update: 2026-02-17
+ * Last Update: 2026-03-28
* Public: No
*
* Description:
@@ -35,6 +35,7 @@ switch (_event) do {
case "actor::close::menu": { closeDialog 1; };
case "actor::open::atm": { [true] spawn EFUNC(bank,openUI); };
case "actor::open::bank": { [] spawn EFUNC(bank,openUI); };
+ case "actor::open::cad": { [] spawn EFUNC(cad,openUI); };
case "actor::open::device": { hint "Device interaction is not yet implemented."; };
case "actor::open::garage": { [] spawn EFUNC(garage,openUI); };
case "actor::open::vgarage": { [] spawn EFUNC(garage,openVG); };
diff --git a/arma/client/addons/actor/ui/_site/script.js b/arma/client/addons/actor/ui/_site/script.js
index 62fcf3a..66a283e 100644
--- a/arma/client/addons/actor/ui/_site/script.js
+++ b/arma/client/addons/actor/ui/_site/script.js
@@ -100,6 +100,12 @@ const actions = {
//=============================================================================
const baseMenuItems = [
+ {
+ id: "cad",
+ title: "CAD",
+ description: "Access CAD (Computer Aided Dispatch)",
+ action: "actor::open::cad",
+ },
{
id: "phone",
title: "Phone",
@@ -133,6 +139,12 @@ const actionDefinitions = {
description: "Access your bank account and manage finances",
action: "actor::open::bank",
},
+ cad: {
+ id: "cad",
+ title: "CAD",
+ description: "Access the CAD",
+ action: "actor::open::cad",
+ },
phone: {
id: "phone",
title: "Phone",
diff --git a/arma/client/addons/cad/$PBOPREFIX$ b/arma/client/addons/cad/$PBOPREFIX$
new file mode 100644
index 0000000..4067b98
--- /dev/null
+++ b/arma/client/addons/cad/$PBOPREFIX$
@@ -0,0 +1 @@
+forge\forge_client\addons\cad
diff --git a/arma/client/addons/cad/CfgEventHandlers.hpp b/arma/client/addons/cad/CfgEventHandlers.hpp
new file mode 100644
index 0000000..86e43be
--- /dev/null
+++ b/arma/client/addons/cad/CfgEventHandlers.hpp
@@ -0,0 +1,12 @@
+class Extended_PreInit_EventHandlers {
+ class ADDON {
+ init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
+ clientInit = QUOTE(call COMPILE_SCRIPT(XEH_preInitClient));
+ };
+};
+
+class Extended_PostInit_EventHandlers {
+ class ADDON {
+ clientInit = QUOTE(call COMPILE_SCRIPT(XEH_postInitClient));
+ };
+};
diff --git a/arma/client/addons/cad/MAP_README.md b/arma/client/addons/cad/MAP_README.md
new file mode 100644
index 0000000..157db6b
--- /dev/null
+++ b/arma/client/addons/cad/MAP_README.md
@@ -0,0 +1,214 @@
+# Integrated Map Display System (A3API Pattern)
+
+This system integrates the Arma 3 native map control (`RscMapControl`) within an HTML/CSS/JS UI using Arma's proper WebBrowser control (type 106) and A3API communication pattern.
+
+## How It Works
+
+### Layered Architecture
+
+1. **IFrame Control (type 106)** - Loads HTML content using `ctrlWebBrowserAction`
+2. **Map Control (RscMapControl)** - Native Arma map positioned behind/within the UI
+3. **A3API Communication** - Bidirectional communication between JavaScript and SQF
+
+### Communication Flow
+
+**JavaScript → SQF:**
+```javascript
+// Send alert (no response expected)
+A3API.SendAlert(JSON.stringify({
+ event: "map::zoomIn",
+ data: null
+}));
+
+// Send confirm (expects response via ExecJS)
+A3API.SendConfirm(JSON.stringify({
+ event: "map::getPosition",
+ data: null
+}));
+```
+
+**SQF → JavaScript:**
+```sqf
+_control ctrlWebBrowserAction ["ExecJS", "updateMapState({center: [1000, 2000], scale: 0.5});"];
+```
+
+## File Structure
+
+```
+UI/map/
+├── _site/
+│ ├── index.html # HTML with A3API dynamic loading
+│ ├── script.js # JavaScript using A3API
+│ └── style.css # Styling
+└── MAP_README.md # This file
+
+functions/map/
+├── fn_openMap.sqf # Opens the display
+├── fn_mapHandleUIEvents.sqf # Handles JS events
+├── fn_mapDisplay.sqf # Display initialization
+└── fn_mapDisplayUpdate.sqf # Update loop
+
+UI/MapDisplay.h # Dialog definition
+```
+
+## Usage
+
+### Opening the Map
+
+```sqf
+[] call FORGE_fnc_openMap;
+```
+
+### From Init or Action
+
+```sqf
+// Add player action
+player addAction ["Open Map", {[] call FORGE_fnc_openMap;}];
+
+// In init.sqf
+[] call FORGE_fnc_openMap;
+```
+
+## Key Differences from Standard HTML/CSS/JS
+
+### 1. Dynamic Resource Loading
+
+Instead of ` ` and `
+```
+
+### 2. Event Communication
+
+Use **A3API.SendAlert()** for one-way messages:
+```javascript
+A3API.SendAlert(JSON.stringify({event: "map::action", data: value}));
+```
+
+Use **A3API.SendConfirm()** for messages expecting a response:
+```javascript
+A3API.SendConfirm(JSON.stringify({event: "map::getdata", data: null}));
+```
+
+### 3. Pointer Events
+
+UI elements need `pointer-events: auto` while the body has `pointer-events: none`:
+
+```css
+body {
+ pointer-events: none; /* Allows clicks through to map */
+}
+
+#topBar {
+ pointer-events: auto; /* UI elements catch clicks */
+}
+```
+
+## Dialog Definition Pattern
+
+```cpp
+class RscMapDisplay {
+ idd = 9000;
+ onLoad = "['onLoad', _this] call FORGE_fnc_mapDisplay;";
+
+ class Controls {
+ class Browser: RscText {
+ type = 106; // IFrame control type
+ idc = 9001;
+ x = "safeZoneX";
+ y = "safeZoneY";
+ w = "safeZoneW";
+ h = "safeZoneH";
+ };
+
+ class MapControl: RscMapControl {
+ idc = 9002;
+ // Position to fit within HTML UI
+ };
+ };
+};
+```
+
+## Event Handler Pattern
+
+In `fn_openMap.sqf`:
+```sqf
+private _ctrl = _display displayCtrl 9001;
+
+// Add JSDialog event handler
+_ctrl ctrlAddEventHandler ["JSDialog", {
+ params ["_control", "_isConfirmDialog", "_message"];
+ [_control, _isConfirmDialog, _message] call FORGE_fnc_mapHandleUIEvents;
+}];
+
+// Load HTML file
+_ctrl ctrlWebBrowserAction ["LoadFile", "UI\\map\\_site\\index.html"];
+```
+
+In `fn_mapHandleUIEvents.sqf`:
+```sqf
+params ["_control", "_isConfirmDialog", "_message"];
+
+private _eventData = fromJSON _message;
+private _event = _eventData get "event";
+private _data = _eventData get "data";
+
+switch (_event) do {
+ case "map::ready": {
+ // Initialize
+ };
+ case "map::zoomIn": {
+ // Handle zoom
+ };
+};
+```
+
+## Benefits of This Pattern
+
+1. **Proper Arma Integration** - Uses native WebBrowser control (type 106)
+2. **File System Compatibility** - A3API.RequestFile() works with Arma's file system
+3. **Reliable Communication** - JSDialog event handler is more stable than htmlLoad
+4. **Modular** - CSS and JS in separate files, dynamically loaded
+5. **Consistent** - Matches bank module pattern used in FORGE
+
+## Troubleshooting
+
+**Files not loading:**
+- Check paths use double backslashes: `"UI\\map\\_site\\style.css"`
+- Verify files exist in the correct directory
+- Check .rpt log for file loading errors
+
+**Events not firing:**
+- Verify JSDialog event handler is attached
+- Check JSON formatting in A3API calls
+- Look for JavaScript console errors (use OpenDevConsole)
+
+**Map not showing:**
+- Verify MapControl idc matches (9002)
+- Check map control positioning in MapDisplay.h
+- Ensure map control is rendered after browser control
+
+## Developer Tools
+
+Enable dev console in `fn_openMap.sqf`:
+```sqf
+_ctrl ctrlWebBrowserAction ["OpenDevConsole"];
+```
+
+This opens Chromium dev tools for debugging JavaScript, CSS, and network requests.
diff --git a/arma/client/addons/cad/XEH_PREP.hpp b/arma/client/addons/cad/XEH_PREP.hpp
new file mode 100644
index 0000000..3a2f563
--- /dev/null
+++ b/arma/client/addons/cad/XEH_PREP.hpp
@@ -0,0 +1,5 @@
+PREP(handleUIEvents);
+PREP(initRepository);
+PREP(initUIBridge);
+PREP(initUI);
+PREP(openUI);
diff --git a/arma/client/addons/cad/XEH_postInitClient.sqf b/arma/client/addons/cad/XEH_postInitClient.sqf
new file mode 100644
index 0000000..fdd9bce
--- /dev/null
+++ b/arma/client/addons/cad/XEH_postInitClient.sqf
@@ -0,0 +1,24 @@
+#include "script_component.hpp"
+
+if (isNil QGVAR(CADRepository)) then { call FUNC(initRepository); };
+if (isNil QGVAR(CADUIBridge)) then { call FUNC(initUIBridge); };
+
+[QGVAR(openCAD), {
+ call FUNC(openUI);
+}] call CFUNC(addEventHandler);
+
+[QGVAR(responseTaskCatalog), {
+ params [["_entries", [], [[]]]];
+
+ if !(isNil QGVAR(CADRepository)) then {
+ GVAR(CADRepository) call ["setTaskCatalog", [_entries]];
+ };
+
+ GVAR(CADUIBridge) call ["refreshTaskCatalog", []];
+}] call CFUNC(addEventHandler);
+
+[QGVAR(responseTaskAccept), {
+ params [["_result", createHashMap, [createHashMap]]];
+
+ GVAR(CADUIBridge) call ["handleTaskAcceptResponse", [_result]];
+}] call CFUNC(addEventHandler);
diff --git a/arma/client/addons/cad/XEH_preInit.sqf b/arma/client/addons/cad/XEH_preInit.sqf
new file mode 100644
index 0000000..1f72eca
--- /dev/null
+++ b/arma/client/addons/cad/XEH_preInit.sqf
@@ -0,0 +1,5 @@
+#include "script_component.hpp"
+
+PREP_RECOMPILE_START;
+#include "XEH_PREP.hpp"
+PREP_RECOMPILE_END;
diff --git a/arma/client/addons/cad/XEH_preInitClient.sqf b/arma/client/addons/cad/XEH_preInitClient.sqf
new file mode 100644
index 0000000..421c54b
--- /dev/null
+++ b/arma/client/addons/cad/XEH_preInitClient.sqf
@@ -0,0 +1 @@
+#include "script_component.hpp"
diff --git a/arma/client/addons/cad/config.cpp b/arma/client/addons/cad/config.cpp
new file mode 100644
index 0000000..47de21d
--- /dev/null
+++ b/arma/client/addons/cad/config.cpp
@@ -0,0 +1,21 @@
+#include "script_component.hpp"
+
+class CfgPatches {
+ class ADDON {
+ author = AUTHOR;
+ authors[] = {"IDSolutions"};
+ url = ECSTRING(main,url);
+ name = COMPONENT_NAME;
+ requiredVersion = REQUIRED_VERSION;
+ requiredAddons[] = {
+ "forge_client_main"
+ };
+ units[] = {};
+ weapons[] = {};
+ VERSION_CONFIG;
+ };
+};
+
+#include "CfgEventHandlers.hpp"
+#include "ui\RscCommon.hpp"
+#include "ui\RscMapUI.hpp"
diff --git a/arma/client/addons/cad/functions/fnc_handleUIEvents.sqf b/arma/client/addons/cad/functions/fnc_handleUIEvents.sqf
new file mode 100644
index 0000000..22c93cb
--- /dev/null
+++ b/arma/client/addons/cad/functions/fnc_handleUIEvents.sqf
@@ -0,0 +1,87 @@
+#include "..\script_component.hpp"
+
+/*
+ * File: fnc_handleUIEvents.sqf
+ * Author: IDSolutions
+ * Date: 2026-03-28
+ * Public: No
+ *
+ * Description:
+ * Handles CAD browser UI events.
+ *
+ * Arguments:
+ * 0: Control [CONTROL]
+ * 1: Confirm dialog flag [BOOL]
+ * 2: Browser message [STRING]
+ *
+ * Return Value:
+ * UI event handled [BOOL]
+ *
+ * Example:
+ * [_control, false, _message] call forge_client_cad_fnc_handleUIEvents
+ */
+
+params ["_control", "_isConfirmDialog", "_message"];
+
+private _alert = fromJSON _message;
+private _event = _alert getOrDefault ["event", ""];
+private _data = _alert getOrDefault ["data", nil];
+
+diag_log format ["[FORGE:Client:CAD] Handling UI event: %1", _event];
+
+if (_isConfirmDialog) exitWith { true };
+
+switch (_event) do {
+ case "cad::ready": {
+ GVAR(CADUIBridge) call ["handleReady", [_control, _data]];
+ };
+ case "cad::tasks::refresh": {
+ GVAR(CADUIBridge) call ["requestTaskCatalog", []];
+ };
+ case "cad::tasks::accept": {
+ private _taskID = "";
+ if (_data isEqualType createHashMap) then {
+ _taskID = _data getOrDefault ["taskID", ""];
+ };
+
+ GVAR(CADUIBridge) call ["requestTaskAccept", [_taskID]];
+ };
+ case "map::zoomIn": {
+ private _mapCtrl = uiNamespace getVariable [QGVAR(MapCtrl), controlNull];
+ if (isNull _mapCtrl) exitWith {};
+
+ private _currentZoom = ctrlMapScale _mapCtrl;
+ private _newZoom = (_currentZoom * 0.5) max 0.001;
+ private _center = _mapCtrl ctrlMapScreenToWorld [0.5, 0.5];
+ _mapCtrl ctrlMapAnimAdd [0.3, _newZoom, _center];
+ ctrlMapAnimCommit _mapCtrl;
+ };
+ case "map::zoomOut": {
+ private _mapCtrl = uiNamespace getVariable [QGVAR(MapCtrl), controlNull];
+ if (isNull _mapCtrl) exitWith {};
+
+ private _currentZoom = ctrlMapScale _mapCtrl;
+ private _newZoom = (_currentZoom * 2) min 1;
+ private _center = _mapCtrl ctrlMapScreenToWorld [0.5, 0.5];
+ _mapCtrl ctrlMapAnimAdd [0.3, _newZoom, _center];
+ ctrlMapAnimCommit _mapCtrl;
+ };
+ case "map::search": {
+ private _query = str _data;
+ private _bottomBar = uiNamespace getVariable [QGVAR(BottomBarCtrl), controlNull];
+ if (isNull _bottomBar) exitWith {};
+
+ _bottomBar ctrlWebBrowserAction ["ExecJS", format ["updateStatus('Search not yet implemented: %1');", _query]];
+ };
+ case "map::close": {
+ if !(isNil QGVAR(CADUIBridge)) then {
+ GVAR(CADUIBridge) call ["handleClose", []];
+ };
+ closeDialog 1;
+ };
+ default {
+ diag_log format ["[FORGE:Client:CAD] WARNING: Unhandled UI event: %1", _event];
+ };
+};
+
+true
diff --git a/arma/client/addons/cad/functions/fnc_initRepository.sqf b/arma/client/addons/cad/functions/fnc_initRepository.sqf
new file mode 100644
index 0000000..6cec168
--- /dev/null
+++ b/arma/client/addons/cad/functions/fnc_initRepository.sqf
@@ -0,0 +1,52 @@
+#include "..\script_component.hpp"
+
+/*
+ * File: fnc_initRepository.sqf
+ * Author: IDSolutions
+ * Date: 2026-03-28
+ * Public: No
+ *
+ * Description:
+ * Initializes the CAD repository for lightweight client lifecycle state.
+ *
+ * Arguments:
+ * None
+ *
+ * Return Value:
+ * CAD repository object [HASHMAP OBJECT]
+ *
+ * Example:
+ * call forge_client_cad_fnc_initRepository
+ */
+
+#pragma hemtt ignore_variables ["_self"]
+GVAR(CADRepository) = createHashMapObject [[
+ ["#type", "CADRepository"],
+ ["#create", compileFinal {
+ _self set ["isLoaded", true];
+ _self set ["isOpen", false];
+ _self set ["taskCatalog", []];
+ }],
+ ["pushTaskCatalog", compileFinal {
+ params [["_bridge", createHashMap, [createHashMap]]];
+
+ if (_bridge isEqualTo createHashMap) exitWith { false };
+
+ _bridge call ["sendEvent", ["cad::tasks::hydrate", createHashMapFromArray [
+ ["tasks", +(_self getOrDefault ["taskCatalog", []])]
+ ]]]
+ }],
+ ["setTaskCatalog", compileFinal {
+ params [["_entries", [], [[]]]];
+
+ _self set ["taskCatalog", +_entries];
+ true
+ }],
+ ["setOpen", compileFinal {
+ params [["_isOpen", false, [false]]];
+ _self set ["isOpen", _isOpen];
+ true
+ }]
+]];
+
+GVAR(CADRepository)
diff --git a/arma/client/addons/cad/functions/fnc_initUI.sqf b/arma/client/addons/cad/functions/fnc_initUI.sqf
new file mode 100644
index 0000000..bb84979
--- /dev/null
+++ b/arma/client/addons/cad/functions/fnc_initUI.sqf
@@ -0,0 +1,90 @@
+#include "..\script_component.hpp"
+
+/*
+ * File: fnc_initUI.sqf
+ * Author: IDSolutions
+ * Date: 2026-03-28
+ * Public: No
+ *
+ * Description:
+ * Initializes the CAD map dialog controls and local map event handling.
+ *
+ * Arguments:
+ * 0: Display [DISPLAY]
+ *
+ * Return Value:
+ * UI initialized [BOOL]
+ *
+ * Example:
+ * [_display] call forge_client_cad_fnc_initUI
+ */
+
+params [["_display", displayNull, [displayNull]]];
+
+if (isNull _display) exitWith { false };
+
+private _mapCtrl = _display displayCtrl 1001;
+private _topBarCtrl = _display displayCtrl 1002;
+private _bottomBarCtrl = _display displayCtrl 1003;
+private _sidePanelCtrl = _display displayCtrl 1005;
+
+uiNamespace setVariable [QGVAR(Display), _display];
+uiNamespace setVariable [QGVAR(MapCtrl), _mapCtrl];
+uiNamespace setVariable [QGVAR(TopBarCtrl), _topBarCtrl];
+uiNamespace setVariable [QGVAR(BottomBarCtrl), _bottomBarCtrl];
+uiNamespace setVariable [QGVAR(SidePanelCtrl), _sidePanelCtrl];
+
+private _center = if (isNull player) then {
+ [worldSize / 2, worldSize / 2, 0]
+} else {
+ getPosATL player
+};
+
+_mapCtrl ctrlMapAnimAdd [0, 0.2, _center];
+ctrlMapAnimCommit _mapCtrl;
+
+_mapCtrl ctrlAddEventHandler ["MouseButtonClick", {
+ params ["_ctrl", "_button", "_xPos", "_yPos"];
+
+ private _worldPos = _ctrl ctrlMapScreenToWorld [_xPos, _yPos];
+ private _bottomBar = uiNamespace getVariable [QGVAR(BottomBarCtrl), controlNull];
+ if (isNull _bottomBar) exitWith {};
+
+ private _jsCode = format [
+ "updateStatus('Clicked at: %1, %2');",
+ round (_worldPos # 0),
+ round (_worldPos # 1)
+ ];
+ _bottomBar ctrlWebBrowserAction ["ExecJS", _jsCode];
+}];
+
+_mapCtrl ctrlAddEventHandler ["MouseMoving", {
+ params ["_ctrl", "_xPos", "_yPos"];
+
+ private _worldPos = _ctrl ctrlMapScreenToWorld [_xPos, _yPos];
+ private _topBar = uiNamespace getVariable [QGVAR(TopBarCtrl), controlNull];
+ if (isNull _topBar) exitWith {};
+
+ private _jsCode = format [
+ "updateCoordinates(%1, %2);",
+ _worldPos # 0,
+ _worldPos # 1
+ ];
+ _topBar ctrlWebBrowserAction ["ExecJS", _jsCode];
+}];
+
+[] spawn {
+ while { !isNull (uiNamespace getVariable [QGVAR(Display), displayNull]) } do {
+ private _mapCtrl = uiNamespace getVariable [QGVAR(MapCtrl), controlNull];
+ private _topBar = uiNamespace getVariable [QGVAR(TopBarCtrl), controlNull];
+
+ if (!isNull _mapCtrl && { !isNull _topBar }) then {
+ _topBar ctrlWebBrowserAction ["ExecJS", format ["updateScale(%1);", round (ctrlMapScale _mapCtrl)]];
+ };
+
+ sleep 0.5;
+ };
+};
+
+diag_log "[FORGE:Client:CAD] CAD UI initialized.";
+true
diff --git a/arma/client/addons/cad/functions/fnc_initUIBridge.sqf b/arma/client/addons/cad/functions/fnc_initUIBridge.sqf
new file mode 100644
index 0000000..0205f34
--- /dev/null
+++ b/arma/client/addons/cad/functions/fnc_initUIBridge.sqf
@@ -0,0 +1,84 @@
+#include "..\script_component.hpp"
+
+/*
+ * File: fnc_initUIBridge.sqf
+ * Author: IDSolutions
+ * Date: 2026-03-28
+ * Public: No
+ *
+ * Description:
+ * Initializes the CAD UI bridge for sidepanel browser state and task event routing.
+ *
+ * Arguments:
+ * None
+ *
+ * Return Value:
+ * CAD UI bridge object [HASHMAP OBJECT]
+ *
+ * Example:
+ * call forge_client_cad_fnc_initUIBridge
+ */
+
+#pragma hemtt ignore_variables ["_self"]
+private _webUIDeclarations = call EFUNC(common,initWebUIBridge);
+private _webUIBridgeDeclaration = _webUIDeclarations get "bridgeDeclaration";
+
+GVAR(CADUIBridgeBaseClass) = compileFinal createHashMapFromArray [
+ ["#base", _webUIBridgeDeclaration],
+ ["#type", "CADUIBridgeBaseClass"],
+ ["getActiveBrowserControl", compileFinal {
+ private _display = uiNamespace getVariable [QGVAR(Display), displayNull];
+ if (isNull _display) exitWith {
+ _self call ["setActiveBrowserControl", [controlNull]];
+ controlNull
+ };
+
+ private _control = _display displayCtrl 1005;
+ _self call ["setActiveBrowserControl", [_control]];
+ _control
+ }],
+ ["hasOpenScreen", compileFinal {
+ private _screen = _self call ["getScreen", []];
+ private _control = _self call ["getActiveBrowserControl", []];
+ !(isNull _control) && { _screen call ["isReady", []] }
+ }],
+ ["handleReady", compileFinal {
+ params [["_control", controlNull, [controlNull]], ["_data", createHashMap, [createHashMap]]];
+
+ private _screen = _self call ["getScreen", []];
+ _screen call ["setControl", [_control]];
+ _screen call ["markReady", [true]];
+ _self call ["flushPendingEvents", []];
+
+ _self call ["requestTaskCatalog", []];
+ _self call ["refreshTaskCatalog", []];
+ true
+ }],
+ ["requestTaskCatalog", compileFinal {
+ [SRPC(task,requestTaskCatalog), [getPlayerUID player]] call CFUNC(serverEvent);
+ true
+ }],
+ ["requestTaskAccept", compileFinal {
+ params [["_taskID", "", [""]]];
+
+ if (_taskID isEqualTo "") exitWith { false };
+
+ [SRPC(task,requestAcceptTask), [getPlayerUID player, _taskID]] call CFUNC(serverEvent);
+ true
+ }],
+ ["refreshTaskCatalog", compileFinal {
+ if (isNil QGVAR(CADRepository)) exitWith { false };
+ GVAR(CADRepository) call ["pushTaskCatalog", [_self]]
+ }],
+ ["handleTaskAcceptResponse", compileFinal {
+ params [["_result", createHashMap, [createHashMap]]];
+
+ _self call ["sendEvent", ["cad::tasks::accept::response", createHashMapFromArray [
+ ["message", _result getOrDefault ["message", "Task request processed."]],
+ ["success", _result getOrDefault ["success", false]]
+ ]]]
+ }]
+];
+
+GVAR(CADUIBridge) = createHashMapObject [GVAR(CADUIBridgeBaseClass)];
+GVAR(CADUIBridge)
diff --git a/arma/client/addons/cad/functions/fnc_openUI.sqf b/arma/client/addons/cad/functions/fnc_openUI.sqf
new file mode 100644
index 0000000..0d0804b
--- /dev/null
+++ b/arma/client/addons/cad/functions/fnc_openUI.sqf
@@ -0,0 +1,47 @@
+#include "..\script_component.hpp"
+
+/*
+ * File: fnc_openUI.sqf
+ * Author: IDSolutions
+ * Date: 2026-03-28
+ * Public: No
+ *
+ * Description:
+ * Opens the CAD map interface.
+ *
+ * Arguments:
+ * None
+ *
+ * Return Value:
+ * UI opened [BOOL]
+ *
+ * Example:
+ * call forge_client_cad_fnc_openUI
+ */
+
+private _display = createDialog ["RscMapUI", true];
+if (isNull _display) exitWith {
+ diag_log "[FORGE:Client:CAD] ERROR: Failed to create CAD dialog.";
+ false
+};
+
+private _topBarCtrl = _display displayCtrl 1002;
+private _bottomBarCtrl = _display displayCtrl 1003;
+private _sidePanelCtrl = _display displayCtrl 1005;
+
+{
+ _x ctrlAddEventHandler ["JSDialog", {
+ params ["_control", "_isConfirmDialog", "_message"];
+ [_control, _isConfirmDialog, _message] call FUNC(handleUIEvents);
+ }];
+} forEach [_topBarCtrl, _bottomBarCtrl, _sidePanelCtrl];
+
+_topBarCtrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\topbar.html)];
+_bottomBarCtrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\bottombar.html)];
+_sidePanelCtrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\sidepanel.html)];
+
+if !(isNil QGVAR(CADRepository)) then {
+ GVAR(CADRepository) call ["setOpen", [true]];
+};
+
+true
diff --git a/arma/client/addons/cad/script_component.hpp b/arma/client/addons/cad/script_component.hpp
new file mode 100644
index 0000000..6fb40c2
--- /dev/null
+++ b/arma/client/addons/cad/script_component.hpp
@@ -0,0 +1,9 @@
+#define COMPONENT cad
+#define COMPONENT_BEAUTIFIED CAD
+#include "\forge\forge_client\addons\main\script_mod.hpp"
+
+// #define DEBUG_MODE_FULL
+// #define DISABLE_COMPILE_CACHE
+// #define ENABLE_PERFORMANCE_COUNTERS
+
+#include "\forge\forge_client\addons\main\script_macros.hpp"
diff --git a/arma/client/addons/cad/ui/RscCommon.hpp b/arma/client/addons/cad/ui/RscCommon.hpp
new file mode 100644
index 0000000..4135f3f
--- /dev/null
+++ b/arma/client/addons/cad/ui/RscCommon.hpp
@@ -0,0 +1,6 @@
+// Control types
+#define CT_STATIC 0
+#define CT_MAP 100
+
+class RscText;
+class RscMapControl;
diff --git a/arma/client/addons/cad/ui/RscMapUI.hpp b/arma/client/addons/cad/ui/RscMapUI.hpp
new file mode 100644
index 0000000..37f599d
--- /dev/null
+++ b/arma/client/addons/cad/ui/RscMapUI.hpp
@@ -0,0 +1,90 @@
+class RscMapUI {
+ idd = 1004;
+ movingEnable = 0;
+ enableSimulation = 1;
+ fadein = 0;
+ fadeout = 0;
+ duration = 1e+011;
+ onLoad = "uiNamespace setVariable ['forge_client_cad_Display', _this select 0]; [_this select 0] call forge_client_cad_fnc_initUI;";
+ onUnLoad = "uiNamespace setVariable ['forge_client_cad_Display', nil]; uiNamespace setVariable ['forge_client_cad_MapCtrl', nil]; uiNamespace setVariable ['forge_client_cad_TopBarCtrl', nil]; uiNamespace setVariable ['forge_client_cad_BottomBarCtrl', nil]; uiNamespace setVariable ['forge_client_cad_SidePanelCtrl', nil]; if !(isNil 'forge_client_cad_CADRepository') then { forge_client_cad_CADRepository set ['isOpen', false]; };";
+
+ class controlsBackground {
+ class MapControl: RscMapControl {
+ idc = 1001;
+ x = "safeZoneX + (safeZoneW * 0.1)"; // 10% margin (80% width centered)
+ y = "safeZoneY + (safeZoneH * 0.1) + 0.0926"; // 10% margin + 50px top bar
+ w = "safeZoneW * 0.8"; // 80% width
+ h = "(safeZoneH * 0.8) - 0.0926 - 0.0556"; // 80% height minus top and bottom bars
+
+ // Map specific settings
+ maxSatelliteAlpha = 0.85;
+ alphaFadeStartScale = 0.35;
+ alphaFadeEndScale = 0.4;
+ colorBackground[] = {0.969, 0.957, 0.949, 1};
+ colorSea[] = {0.467, 0.631, 0.851, 0.5};
+ colorForest[] = {0.624, 0.78, 0.388, 0.5};
+ colorRocks[] = {0, 0, 0, 0};
+ colorCountlines[] = {0.572, 0.354, 0.318, 0.25};
+ colorMainCountlines[] = {0.572, 0.354, 0.318, 0.5};
+ colorCountlinesWater[] = {0.491, 0.577, 0.702, 0.3};
+ colorMainCountlinesWater[] = {0.491, 0.577, 0.702, 0.6};
+ colorForestBorder[] = {0, 0, 0, 0};
+ colorRocksBorder[] = {0, 0, 0, 0};
+ colorPowerLines[] = {0.1, 0.1, 0.1, 1};
+ colorRailWay[] = {0.8, 0.2, 0, 1};
+ colorNames[] = {0.1, 0.1, 0.1, 0.9};
+ colorInactive[] = {1, 1, 1, 0.5};
+ colorLevels[] = {0.286, 0.177, 0.094, 0.5};
+ colorTracks[] = {0.84, 0.76, 0.65, 0.15};
+ colorRoads[] = {0.7, 0.7, 0.7, 1};
+ colorMainRoads[] = {0.9, 0.5, 0.3, 1};
+ colorTracksFill[] = {0.84, 0.76, 0.65, 1};
+ colorRoadsFill[] = {1, 1, 1, 1};
+ colorMainRoadsFill[] = {1, 0.6, 0.4, 1};
+ colorGrid[] = {0.1, 0.1, 0.1, 0.6};
+ colorGridMap[] = {0.1, 0.1, 0.1, 0.6};
+ colorText[] = {1, 1, 1, 1};
+ font = "PuristaMedium";
+ sizeEx = 0.04;
+ showCountourInterval = 0;
+ scaleMin = 0.001;
+ scaleMax = 1;
+ scaleDefault = 0.16;
+ };
+ };
+
+ class controls {
+ // Top bar browser
+ class TopBarBrowser: RscText {
+ type = 106;
+ idc = 1002;
+ x = "safeZoneX + (safeZoneW * 0.1)";
+ y = "safeZoneY + (safeZoneH * 0.1)";
+ w = "safeZoneW * 0.8";
+ h = "0.0926"; // 50px
+ colorBackground[] = {0, 0, 0, 0};
+ };
+
+ // Bottom bar browser
+ class BottomBarBrowser: RscText {
+ type = 106;
+ idc = 1003;
+ x = "safeZoneX + (safeZoneW * 0.1)";
+ y = "safeZoneY + (safeZoneH * 0.9) - 0.0556";
+ w = "safeZoneW * 0.8";
+ h = "0.0556"; // 30px
+ colorBackground[] = {0, 0, 0, 0};
+ };
+
+ // Side panel browser (overlays from right side of 80% box)
+ class SidePanelBrowser: RscText {
+ type = 106;
+ idc = 1005;
+ x = "safeZoneX + (safeZoneW * 0.1) + (safeZoneW * 0.8) - 0.4630"; // Right edge of 80% box minus panel width
+ y = "safeZoneY + (safeZoneH * 0.1) + 0.0926"; // Below top bar
+ w = "0.4630"; // ~250px width
+ h = "(safeZoneH * 0.8) - 0.0926 - 0.0556"; // Full height minus bars
+ colorBackground[] = {0, 0, 0, 0};
+ };
+ };
+};
diff --git a/arma/client/addons/cad/ui/_site/bottombar.html b/arma/client/addons/cad/ui/_site/bottombar.html
new file mode 100644
index 0000000..33fb1ec
--- /dev/null
+++ b/arma/client/addons/cad/ui/_site/bottombar.html
@@ -0,0 +1 @@
+
Map Ready
\ No newline at end of file
diff --git a/arma/client/addons/cad/ui/_site/cad-bottombar.css b/arma/client/addons/cad/ui/_site/cad-bottombar.css
new file mode 100644
index 0000000..7133cd2
--- /dev/null
+++ b/arma/client/addons/cad/ui/_site/cad-bottombar.css
@@ -0,0 +1 @@
+body{-webkit-backdrop-filter:blur(18px);background:linear-gradient(90deg,#0e131bf5,#121720ed 55%,#0d1219f5);border-top:1px solid #ffffff24;justify-content:space-between;align-items:center;min-height:36px;padding:0 20px;display:flex;position:absolute;bottom:0;left:0;right:0;overflow:hidden;box-shadow:0 -12px 26px #0000003d}span{color:#f5f8ffcc;text-shadow:0 1px 10px #00000047;font-size:12px}#statusText{color:var(--accent);font-weight:600}
\ No newline at end of file
diff --git a/arma/client/addons/cad/ui/_site/cad-bottombar.js b/arma/client/addons/cad/ui/_site/cad-bottombar.js
new file mode 100644
index 0000000..d39ab4b
--- /dev/null
+++ b/arma/client/addons/cad/ui/_site/cad-bottombar.js
@@ -0,0 +1 @@
+window.CADBottombar=window.CADBottombar||{};
\ No newline at end of file
diff --git a/arma/client/addons/cad/ui/_site/cad-common.css b/arma/client/addons/cad/ui/_site/cad-common.css
new file mode 100644
index 0000000..c2d789e
--- /dev/null
+++ b/arma/client/addons/cad/ui/_site/cad-common.css
@@ -0,0 +1 @@
+:root{--bg:#090c12d1;--panel:#141821e6;--panel2:#11151ed1;--stroke:#ffffff1f;--stroke2:#fff3;--text:#f5f8ffeb;--muted:#f5f8ff9e;--muted2:#f5f8ff6b;--accent:#68c4fff2;--danger:#ff6060f2;--shadow:0 20px 60px #0000008c;--radius:14px;--radius2:10px;--font:ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif}*{box-sizing:border-box;margin:0;padding:0}body{font-family:var(--font);color:var(--text);background:var(--bg);-webkit-backdrop-filter:blur(16px)}.btn{border-radius:var(--radius2);color:var(--text);cursor:pointer;user-select:none;background:#ffffff08;border:1px solid #ffffff1a;padding:8px 16px;font-size:14px;transition:background .16s,border-color .16s,transform .16s}.btn:hover{background:#ffffff12;border-color:#ffffff29}.btn:active{transform:scale(.98)}.btn-close{color:#ffdcdcf2;background:#ff60601a;border-color:#ff606040;font-weight:700}.btn-close:hover{background:#ff606033;border-color:#ff606059}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-thumb{background:#ffffff1a;border:2px solid #0000001a;border-radius:999px}
\ No newline at end of file
diff --git a/arma/client/addons/cad/ui/_site/cad-shared.js b/arma/client/addons/cad/ui/_site/cad-shared.js
new file mode 100644
index 0000000..75008e5
--- /dev/null
+++ b/arma/client/addons/cad/ui/_site/cad-shared.js
@@ -0,0 +1 @@
+window.mapUIState={layersPanelVisible:!0,sidePanelElement:null},window.mapUI={sendEvent(e,t){A3API.SendAlert(JSON.stringify({event:e,data:t}))},updateCoordinates(e,t){const n=document.getElementById("coordsDisplay");n&&(n.textContent=`X: ${Math.round(e).toString().padStart(4,"0")} Y: ${Math.round(t).toString().padStart(4,"0")}`)},updateScale(e){const t=document.getElementById("scaleDisplay");t&&(t.textContent=`Scale: 1:${Math.round(e)}`)},updateStatus(e){const t=document.getElementById("statusText");t&&(t.textContent=e)}},window.updateCoordinates=window.mapUI.updateCoordinates,window.updateScale=window.mapUI.updateScale,window.updateStatus=window.mapUI.updateStatus,window.ForgeBridge=window.ForgeBridge||{_handlers:{},on(e,t){this._handlers[e]=this._handlers[e]||[],this._handlers[e].push(t)},ready:e=>(window.mapUI.sendEvent("cad::ready",e||{}),!0),receive(e){if(!e||"object"!=typeof e)return;(this._handlers[e.event]||[]).forEach(t=>t(e.data||{}))},send:(e,t)=>(window.mapUI.sendEvent(e,t||{}),!0),close:e=>(window.mapUI.sendEvent("map::close",e||{}),!0)};
\ No newline at end of file
diff --git a/arma/client/addons/cad/ui/_site/cad-sidepanel.css b/arma/client/addons/cad/ui/_site/cad-sidepanel.css
new file mode 100644
index 0000000..15d65fb
--- /dev/null
+++ b/arma/client/addons/cad/ui/_site/cad-sidepanel.css
@@ -0,0 +1 @@
+html,body{background:var(--panel);border-left:1px solid var(--stroke);width:100%;height:100%;box-shadow:var(--shadow);-webkit-backdrop-filter:blur(12px);margin:0;padding:0;overflow:hidden}body{opacity:1;visibility:visible}.panel-header{border-bottom:1px solid var(--stroke);background:linear-gradient(#ffffff0d,#0000);justify-content:space-between;align-items:center;padding:14px;display:flex}.panel-header h3{color:var(--accent);text-transform:uppercase;letter-spacing:.8px;font-size:14px;font-weight:650}.panel-content{height:calc(100% - 56px);padding:14px;overflow:auto}.placeholder-message{text-align:center;padding:20px}.placeholder-message p{color:var(--muted);font-size:13px;font-style:italic}.task-toolbar{margin-bottom:10px}.task-toolbar button,.task-accept-btn{color:#f3f6f9;cursor:pointer;background:#1e252be6;border:1px solid #fff3;width:100%;padding:8px 10px}.task-toolbar button:hover,.task-accept-btn:hover{background:#2e3942f2}.task-toolbar button:disabled,.task-accept-btn:disabled{opacity:.55;cursor:default}.task-status-message{color:#cdd6dd;min-height:18px;margin-bottom:10px;font-size:12px}.task-status-message[data-type=success]{color:#79d28a}.task-status-message[data-type=error]{color:#ff8a80}.task-list{flex-direction:column;gap:10px;display:flex}.task-card{background:#0c10149e;border:1px solid #ffffff14;padding:10px}.task-card-header{justify-content:space-between;gap:8px;margin-bottom:8px;display:flex}.task-type{opacity:.7;text-transform:uppercase;font-size:11px}.task-description{margin:0 0 8px;font-size:12px;line-height:1.4}.task-meta{opacity:.8;justify-content:space-between;gap:8px;margin-bottom:8px;font-size:11px;display:flex}
\ No newline at end of file
diff --git a/arma/client/addons/cad/ui/_site/cad-sidepanel.js b/arma/client/addons/cad/ui/_site/cad-sidepanel.js
new file mode 100644
index 0000000..05e57ab
--- /dev/null
+++ b/arma/client/addons/cad/ui/_site/cad-sidepanel.js
@@ -0,0 +1 @@
+window.cadTasks={tasks:[],init(){const s=document.getElementById("refreshTasksBtn");s&&s.addEventListener("click",()=>this.refresh()),window.ForgeBridge.on("cad::tasks::hydrate",s=>{this.setTasks(s.tasks||[])}),window.ForgeBridge.on("cad::tasks::accept::response",s=>{this.handleAcceptResponse(!!s.success,s.message||"")}),window.ForgeBridge.ready({loaded:!0})},setTasks(s){this.tasks=Array.isArray(s)?s:[];const t=document.getElementById("taskStatusMessage");!t||t.dataset.type&&"info"!==t.dataset.type||this.setStatus("",""),this.render()},setStatus(s,t){const e=document.getElementById("taskStatusMessage");e&&(e.textContent=s||"",e.dataset.type=t||"info")},handleAcceptResponse(s,t){this.setStatus(t||(s?"Task accepted.":"Unable to accept task."),s?"success":"error")},refresh(){this.setStatus("Refreshing tasks...","info"),window.mapUI.sendEvent("cad::tasks::refresh",{})},acceptTask(s){this.setStatus("Submitting acceptance...","info"),window.mapUI.sendEvent("cad::tasks::accept",{taskID:s})},render(){const s=document.getElementById("taskList");s&&(this.tasks.length?s.innerHTML=this.tasks.map(s=>{const t=Array.isArray(s.position)?s.position:[0,0,0],e=!!s.accepted,a=e?`Assigned: ${s.orgID||"Unknown"}`:"Available";return`\n \n \n
${s.description||""}
\n
\n ${a} \n X: ${Math.round(t[0]||0)} Y: ${Math.round(t[1]||0)} \n
\n
${e?"Accepted":"Accept"} \n
\n `}).join(""):s.innerHTML='No active tasks are available.
')}},window.cadTasks.init();
\ No newline at end of file
diff --git a/arma/client/addons/cad/ui/_site/cad-topbar.css b/arma/client/addons/cad/ui/_site/cad-topbar.css
new file mode 100644
index 0000000..59129ec
--- /dev/null
+++ b/arma/client/addons/cad/ui/_site/cad-topbar.css
@@ -0,0 +1 @@
+body{-webkit-backdrop-filter:blur(18px);background:linear-gradient(90deg,#10161ff5,#131a24f0 55%,#0f141cf5);border-bottom:1px solid #ffffff24;justify-content:space-between;align-items:center;height:56px;padding:0 20px;display:flex;position:absolute;top:0;left:0;right:0;overflow:hidden;box-shadow:0 14px 28px #00000047}.logo{color:var(--accent);text-transform:uppercase;letter-spacing:.4px;text-shadow:0 1px 12px #00000059;font-size:16px;font-weight:650}.controls{align-items:center;gap:10px;display:flex}.search-input{color:var(--text);background:#ffffff14;border:1px solid #ffffff24;border-radius:999px;outline:none;width:250px;padding:10px 12px;font-size:13px;box-shadow:inset 0 1px #ffffff08}.search-input::placeholder{color:var(--muted2)}.search-input:focus{background:#ffffff1c;border-color:#68c4ff73}.info{color:#f5f8ffd6;font-size:12px;font-family:var(--font);text-shadow:0 1px 10px #00000047;gap:20px;display:flex}
\ No newline at end of file
diff --git a/arma/client/addons/cad/ui/_site/cad-topbar.js b/arma/client/addons/cad/ui/_site/cad-topbar.js
new file mode 100644
index 0000000..91698ea
--- /dev/null
+++ b/arma/client/addons/cad/ui/_site/cad-topbar.js
@@ -0,0 +1 @@
+document.getElementById("btnZoomIn").addEventListener("click",()=>{window.mapUI.sendEvent("map::zoomIn",null)}),document.getElementById("btnZoomOut").addEventListener("click",()=>{window.mapUI.sendEvent("map::zoomOut",null)}),document.getElementById("btnClose").addEventListener("click",()=>{window.mapUI.sendEvent("map::close",null)}),document.getElementById("searchBox").addEventListener("keypress",e=>{"Enter"===e.key&&window.mapUI.sendEvent("map::search",e.target.value)});
\ No newline at end of file
diff --git a/arma/client/addons/cad/ui/_site/sidepanel.html b/arma/client/addons/cad/ui/_site/sidepanel.html
new file mode 100644
index 0000000..3cd718e
--- /dev/null
+++ b/arma/client/addons/cad/ui/_site/sidepanel.html
@@ -0,0 +1 @@
+Refresh Tasks
Loading available tasks...
\ No newline at end of file
diff --git a/arma/client/addons/cad/ui/_site/topbar.html b/arma/client/addons/cad/ui/_site/topbar.html
new file mode 100644
index 0000000..6eed3c9
--- /dev/null
+++ b/arma/client/addons/cad/ui/_site/topbar.html
@@ -0,0 +1 @@
+FORGE OS
+ - X
X: 0000 Y: 0000 Scale: 1:1000
\ No newline at end of file
diff --git a/arma/client/addons/cad/ui/src/bottombar.html b/arma/client/addons/cad/ui/src/bottombar.html
new file mode 100644
index 0000000..b87d3cb
--- /dev/null
+++ b/arma/client/addons/cad/ui/src/bottombar.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+ Map Ready
+
+
+
+
+
diff --git a/arma/client/addons/cad/ui/src/bottombar.js b/arma/client/addons/cad/ui/src/bottombar.js
new file mode 100644
index 0000000..910bfb5
--- /dev/null
+++ b/arma/client/addons/cad/ui/src/bottombar.js
@@ -0,0 +1,6 @@
+/*
+ * Bottombar UI Component
+ * Displays status and selection information.
+ */
+
+window.CADBottombar = window.CADBottombar || {};
diff --git a/arma/client/addons/cad/ui/src/shared.js b/arma/client/addons/cad/ui/src/shared.js
new file mode 100644
index 0000000..204e159
--- /dev/null
+++ b/arma/client/addons/cad/ui/src/shared.js
@@ -0,0 +1,69 @@
+/**
+ * Shared JavaScript for Map UI
+ * Provides common utilities and state management across all UI components
+ */
+
+window.mapUIState = {
+ layersPanelVisible: true,
+ sidePanelElement: null,
+};
+
+window.mapUI = {
+ sendEvent(event, data) {
+ A3API.SendAlert(JSON.stringify({ event: event, data: data }));
+ },
+ updateCoordinates(x, y) {
+ const coordDisplay = document.getElementById("coordsDisplay");
+ if (coordDisplay) {
+ coordDisplay.textContent = `X: ${Math.round(x)
+ .toString()
+ .padStart(4, "0")} Y: ${Math.round(y)
+ .toString()
+ .padStart(4, "0")}`;
+ }
+ },
+ updateScale(scale) {
+ const scaleDisplay = document.getElementById("scaleDisplay");
+ if (scaleDisplay) {
+ scaleDisplay.textContent = `Scale: 1:${Math.round(scale)}`;
+ }
+ },
+ updateStatus(text) {
+ const statusText = document.getElementById("statusText");
+ if (statusText) {
+ statusText.textContent = text;
+ }
+ },
+};
+
+window.updateCoordinates = window.mapUI.updateCoordinates;
+window.updateScale = window.mapUI.updateScale;
+window.updateStatus = window.mapUI.updateStatus;
+
+window.ForgeBridge = window.ForgeBridge || {
+ _handlers: {},
+ on(event, handler) {
+ this._handlers[event] = this._handlers[event] || [];
+ this._handlers[event].push(handler);
+ },
+ ready(payload) {
+ window.mapUI.sendEvent("cad::ready", payload || {});
+ return true;
+ },
+ receive(payload) {
+ if (!payload || typeof payload !== "object") {
+ return;
+ }
+
+ const handlers = this._handlers[payload.event] || [];
+ handlers.forEach((handler) => handler(payload.data || {}));
+ },
+ send(event, data) {
+ window.mapUI.sendEvent(event, data || {});
+ return true;
+ },
+ close(data) {
+ window.mapUI.sendEvent("map::close", data || {});
+ return true;
+ },
+};
diff --git a/arma/client/addons/cad/ui/src/sidepanel.html b/arma/client/addons/cad/ui/src/sidepanel.html
new file mode 100644
index 0000000..8efa438
--- /dev/null
+++ b/arma/client/addons/cad/ui/src/sidepanel.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+ Refresh Tasks
+
+
+
+
+
+
Loading available tasks...
+
+
+
+
+
+
+
diff --git a/arma/client/addons/cad/ui/src/sidepanel.js b/arma/client/addons/cad/ui/src/sidepanel.js
new file mode 100644
index 0000000..bebcd95
--- /dev/null
+++ b/arma/client/addons/cad/ui/src/sidepanel.js
@@ -0,0 +1,94 @@
+window.cadTasks = {
+ tasks: [],
+ init() {
+ const refreshBtn = document.getElementById("refreshTasksBtn");
+ if (refreshBtn) {
+ refreshBtn.addEventListener("click", () => this.refresh());
+ }
+
+ window.ForgeBridge.on("cad::tasks::hydrate", (payload) => {
+ this.setTasks(payload.tasks || []);
+ });
+
+ window.ForgeBridge.on("cad::tasks::accept::response", (payload) => {
+ this.handleAcceptResponse(!!payload.success, payload.message || "");
+ });
+
+ window.ForgeBridge.ready({ loaded: true });
+ },
+ setTasks(tasks) {
+ this.tasks = Array.isArray(tasks) ? tasks : [];
+ const statusEl = document.getElementById("taskStatusMessage");
+ if (
+ statusEl &&
+ (!statusEl.dataset.type || statusEl.dataset.type === "info")
+ ) {
+ this.setStatus("", "");
+ }
+ this.render();
+ },
+ setStatus(message, type) {
+ const statusEl = document.getElementById("taskStatusMessage");
+ if (!statusEl) {
+ return;
+ }
+
+ statusEl.textContent = message || "";
+ statusEl.dataset.type = type || "info";
+ },
+ handleAcceptResponse(success, message) {
+ this.setStatus(
+ message || (success ? "Task accepted." : "Unable to accept task."),
+ success ? "success" : "error",
+ );
+ },
+ refresh() {
+ this.setStatus("Refreshing tasks...", "info");
+ window.mapUI.sendEvent("cad::tasks::refresh", {});
+ },
+ acceptTask(taskID) {
+ this.setStatus("Submitting acceptance...", "info");
+ window.mapUI.sendEvent("cad::tasks::accept", { taskID: taskID });
+ },
+ render() {
+ const listEl = document.getElementById("taskList");
+ if (!listEl) {
+ return;
+ }
+
+ if (!this.tasks.length) {
+ listEl.innerHTML =
+ 'No active tasks are available.
';
+ return;
+ }
+
+ listEl.innerHTML = this.tasks
+ .map((task) => {
+ const position = Array.isArray(task.position)
+ ? task.position
+ : [0, 0, 0];
+ const accepted = !!task.accepted;
+ const ownerLabel = accepted
+ ? `Assigned: ${task.orgID || "Unknown"}`
+ : "Available";
+
+ return `
+
+
+
${task.description || ""}
+
+ ${ownerLabel}
+ X: ${Math.round(position[0] || 0)} Y: ${Math.round(position[1] || 0)}
+
+
${accepted ? "Accepted" : "Accept"}
+
+ `;
+ })
+ .join("");
+ },
+};
+
+window.cadTasks.init();
diff --git a/arma/client/addons/cad/ui/src/styles/bottombar.css b/arma/client/addons/cad/ui/src/styles/bottombar.css
new file mode 100644
index 0000000..99dcfd2
--- /dev/null
+++ b/arma/client/addons/cad/ui/src/styles/bottombar.css
@@ -0,0 +1,33 @@
+body {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ min-height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 20px;
+ background: linear-gradient(
+ 90deg,
+ rgba(14, 19, 27, 0.96),
+ rgba(18, 23, 32, 0.93) 55%,
+ rgba(13, 18, 25, 0.96)
+ );
+ border-top: 1px solid rgba(255, 255, 255, 0.14);
+ box-shadow: 0 -12px 26px rgba(0, 0, 0, 0.24);
+ backdrop-filter: blur(18px);
+ -webkit-backdrop-filter: blur(18px);
+ overflow: hidden;
+}
+
+span {
+ color: rgba(245, 248, 255, 0.8);
+ font-size: 12px;
+ text-shadow: 0 1px 10px rgba(0, 0, 0, 0.28);
+}
+
+#statusText {
+ color: var(--accent);
+ font-weight: 600;
+}
diff --git a/arma/client/addons/cad/ui/src/styles/common.css b/arma/client/addons/cad/ui/src/styles/common.css
new file mode 100644
index 0000000..d674ed5
--- /dev/null
+++ b/arma/client/addons/cad/ui/src/styles/common.css
@@ -0,0 +1,78 @@
+:root {
+ --bg: rgba(9, 12, 18, 0.82);
+ --panel: rgba(20, 24, 33, 0.9);
+ --panel2: rgba(17, 21, 30, 0.82);
+ --stroke: rgba(255, 255, 255, 0.12);
+ --stroke2: rgba(255, 255, 255, 0.2);
+ --text: rgba(245, 248, 255, 0.92);
+ --muted: rgba(245, 248, 255, 0.62);
+ --muted2: rgba(245, 248, 255, 0.42);
+ --accent: rgba(104, 196, 255, 0.95);
+ --danger: rgba(255, 96, 96, 0.95);
+ --shadow: 0 20px 60px rgba(0, 0, 0, 0.55);
+ --radius: 14px;
+ --radius2: 10px;
+ --font:
+ ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial,
+ sans-serif;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: var(--font);
+ color: var(--text);
+ background: var(--bg);
+ backdrop-filter: blur(16px);
+ -webkit-backdrop-filter: blur(16px);
+}
+
+.btn {
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ background: rgba(255, 255, 255, 0.03);
+ padding: 8px 16px;
+ border-radius: var(--radius2);
+ font-size: 14px;
+ color: var(--text);
+ cursor: pointer;
+ transition:
+ background 0.16s ease,
+ border-color 0.16s ease,
+ transform 0.16s ease;
+ user-select: none;
+}
+
+.btn:hover {
+ background: rgba(255, 255, 255, 0.07);
+ border-color: rgba(255, 255, 255, 0.16);
+}
+
+.btn:active {
+ transform: scale(0.98);
+}
+
+.btn-close {
+ background: rgba(255, 96, 96, 0.1);
+ border-color: rgba(255, 96, 96, 0.25);
+ color: rgba(255, 220, 220, 0.95);
+ font-weight: bold;
+}
+
+.btn-close:hover {
+ background: rgba(255, 96, 96, 0.2);
+ border-color: rgba(255, 96, 96, 0.35);
+}
+
+::-webkit-scrollbar {
+ width: 10px;
+}
+
+::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 999px;
+ border: 2px solid rgba(0, 0, 0, 0.1);
+}
diff --git a/arma/client/addons/cad/ui/src/styles/sidepanel.css b/arma/client/addons/cad/ui/src/styles/sidepanel.css
new file mode 100644
index 0000000..0a74484
--- /dev/null
+++ b/arma/client/addons/cad/ui/src/styles/sidepanel.css
@@ -0,0 +1,136 @@
+html,
+body {
+ overflow: hidden;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ background: var(--panel);
+ border-left: 1px solid var(--stroke);
+ box-shadow: var(--shadow);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+}
+
+body {
+ opacity: 1;
+ visibility: visible;
+}
+
+.panel-header {
+ padding: 14px;
+ border-bottom: 1px solid var(--stroke);
+ background: linear-gradient(
+ to bottom,
+ rgba(255, 255, 255, 0.05),
+ transparent
+ );
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.panel-header h3 {
+ color: var(--accent);
+ font-size: 14px;
+ font-weight: 650;
+ text-transform: uppercase;
+ letter-spacing: 0.8px;
+}
+
+.panel-content {
+ padding: 14px;
+ height: calc(100% - 56px);
+ overflow: auto;
+}
+
+.placeholder-message {
+ padding: 20px;
+ text-align: center;
+}
+
+.placeholder-message p {
+ color: var(--muted);
+ font-size: 13px;
+ font-style: italic;
+}
+
+.task-toolbar {
+ margin-bottom: 10px;
+}
+
+.task-toolbar button,
+.task-accept-btn {
+ width: 100%;
+ padding: 8px 10px;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ background: rgba(30, 37, 43, 0.9);
+ color: #f3f6f9;
+ cursor: pointer;
+}
+
+.task-toolbar button:hover,
+.task-accept-btn:hover {
+ background: rgba(46, 57, 66, 0.95);
+}
+
+.task-toolbar button:disabled,
+.task-accept-btn:disabled {
+ opacity: 0.55;
+ cursor: default;
+}
+
+.task-status-message {
+ min-height: 18px;
+ margin-bottom: 10px;
+ font-size: 12px;
+ color: #cdd6dd;
+}
+
+.task-status-message[data-type="success"] {
+ color: #79d28a;
+}
+
+.task-status-message[data-type="error"] {
+ color: #ff8a80;
+}
+
+.task-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.task-card {
+ padding: 10px;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ background: rgba(12, 16, 20, 0.62);
+}
+
+.task-card-header {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 8px;
+}
+
+.task-type {
+ opacity: 0.7;
+ text-transform: uppercase;
+ font-size: 11px;
+}
+
+.task-description {
+ margin: 0 0 8px;
+ font-size: 12px;
+ line-height: 1.4;
+}
+
+.task-meta {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 8px;
+ font-size: 11px;
+ opacity: 0.8;
+}
diff --git a/arma/client/addons/cad/ui/src/styles/topbar.css b/arma/client/addons/cad/ui/src/styles/topbar.css
new file mode 100644
index 0000000..3649d05
--- /dev/null
+++ b/arma/client/addons/cad/ui/src/styles/topbar.css
@@ -0,0 +1,67 @@
+body {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 56px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 20px;
+ background: linear-gradient(
+ 90deg,
+ rgba(16, 22, 31, 0.96),
+ rgba(19, 26, 36, 0.94) 55%,
+ rgba(15, 20, 28, 0.96)
+ );
+ border-bottom: 1px solid rgba(255, 255, 255, 0.14);
+ box-shadow: 0 14px 28px rgba(0, 0, 0, 0.28);
+ backdrop-filter: blur(18px);
+ -webkit-backdrop-filter: blur(18px);
+ overflow: hidden;
+}
+
+.logo {
+ color: var(--accent);
+ font-size: 16px;
+ font-weight: 650;
+ text-transform: uppercase;
+ letter-spacing: 0.4px;
+ text-shadow: 0 1px 12px rgba(0, 0, 0, 0.35);
+}
+
+.controls {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
+
+.search-input {
+ background: rgba(255, 255, 255, 0.08);
+ border: 1px solid rgba(255, 255, 255, 0.14);
+ color: var(--text);
+ padding: 10px 12px;
+ border-radius: 999px;
+ width: 250px;
+ outline: none;
+ font-size: 13px;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
+}
+
+.search-input::placeholder {
+ color: var(--muted2);
+}
+
+.search-input:focus {
+ border-color: rgba(104, 196, 255, 0.45);
+ background: rgba(255, 255, 255, 0.11);
+}
+
+.info {
+ display: flex;
+ gap: 20px;
+ color: rgba(245, 248, 255, 0.84);
+ font-size: 12px;
+ font-family: var(--font);
+ text-shadow: 0 1px 10px rgba(0, 0, 0, 0.28);
+}
diff --git a/arma/client/addons/cad/ui/src/topbar.html b/arma/client/addons/cad/ui/src/topbar.html
new file mode 100644
index 0000000..83f8ffa
--- /dev/null
+++ b/arma/client/addons/cad/ui/src/topbar.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+ FORGE OS
+
+ +
+ -
+
+ X
+
+
+ X: 0000 Y: 0000
+ Scale: 1:1000
+
+
+
+
+
diff --git a/arma/client/addons/cad/ui/src/topbar.js b/arma/client/addons/cad/ui/src/topbar.js
new file mode 100644
index 0000000..30ec5fb
--- /dev/null
+++ b/arma/client/addons/cad/ui/src/topbar.js
@@ -0,0 +1,17 @@
+document.getElementById("btnZoomIn").addEventListener("click", () => {
+ window.mapUI.sendEvent("map::zoomIn", null);
+});
+
+document.getElementById("btnZoomOut").addEventListener("click", () => {
+ window.mapUI.sendEvent("map::zoomOut", null);
+});
+
+document.getElementById("btnClose").addEventListener("click", () => {
+ window.mapUI.sendEvent("map::close", null);
+});
+
+document.getElementById("searchBox").addEventListener("keypress", (event) => {
+ if (event.key === "Enter") {
+ window.mapUI.sendEvent("map::search", event.target.value);
+ }
+});
diff --git a/arma/client/addons/cad/ui/ui.config.mjs b/arma/client/addons/cad/ui/ui.config.mjs
new file mode 100644
index 0000000..47553eb
--- /dev/null
+++ b/arma/client/addons/cad/ui/ui.config.mjs
@@ -0,0 +1,69 @@
+export default {
+ addonName: "cad",
+ title: "FORGE CAD",
+ logLabel: "CAD UI",
+ outputDir: "_site",
+ generateIndex: false,
+ jsBundles: [
+ {
+ name: "CAD shared bridge/runtime",
+ output: "cad-shared.js",
+ sources: ["src/shared.js"],
+ },
+ {
+ name: "CAD topbar app",
+ output: "cad-topbar.js",
+ sources: ["src/topbar.js"],
+ },
+ {
+ name: "CAD sidepanel app",
+ output: "cad-sidepanel.js",
+ sources: ["src/sidepanel.js"],
+ },
+ {
+ name: "CAD bottombar app",
+ output: "cad-bottombar.js",
+ sources: ["src/bottombar.js"],
+ },
+ ],
+ cssBundles: [
+ {
+ name: "CAD common styles",
+ output: "cad-common.css",
+ sources: ["src/styles/common.css"],
+ },
+ {
+ name: "CAD topbar styles",
+ output: "cad-topbar.css",
+ sources: ["src/styles/topbar.css"],
+ },
+ {
+ name: "CAD sidepanel styles",
+ output: "cad-sidepanel.css",
+ sources: ["src/styles/sidepanel.css"],
+ },
+ {
+ name: "CAD bottombar styles",
+ output: "cad-bottombar.css",
+ sources: ["src/styles/bottombar.css"],
+ },
+ ],
+ htmlTemplates: [
+ {
+ name: "CAD topbar page",
+ output: "topbar.html",
+ source: "src/topbar.html",
+ },
+ {
+ name: "CAD sidepanel page",
+ output: "sidepanel.html",
+ source: "src/sidepanel.html",
+ },
+ {
+ name: "CAD bottombar page",
+ output: "bottombar.html",
+ source: "src/bottombar.html",
+ },
+ ],
+ site: {},
+};
diff --git a/arma/server/.hemtt/lints.toml b/arma/server/.hemtt/lints.toml
index 46cef0b..87607ed 100644
--- a/arma/server/.hemtt/lints.toml
+++ b/arma/server/.hemtt/lints.toml
@@ -1,6 +1,6 @@
[sqf.banned_commands]
options.banned = [
- "spawn", # Scheduled should be avoided whenever possible
+ # "spawn", # Scheduled should be avoided whenever possible
"execVM", # Script files should never be run directly, they should be functions
# "remoteExec", # CBA events should be used for networking
]
diff --git a/arma/server/addons/bank/functions/fnc_initStore.sqf b/arma/server/addons/bank/functions/fnc_initStore.sqf
index fdaaefa..b5b62af 100644
--- a/arma/server/addons/bank/functions/fnc_initStore.sqf
+++ b/arma/server/addons/bank/functions/fnc_initStore.sqf
@@ -150,7 +150,7 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
private _finalPatch = _self call ["mset", [GVAR(Registry), "bank:update", _uid, _patch, false]];
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalPatch]];
- GVAR(BankMessenger) call ["sendClientNotification", [_uid, "info", "Bank", format ["Deposited $%1", _amount]]];
+ GVAR(BankMessenger) call ["sendClientNotification", [_uid, "info", "Bank", format ["Deposited $%1", [_amount] call EFUNC(common,formatNumber)]]];
true
}],
["hydrateSession", compileFinal {
@@ -247,7 +247,7 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
private _finalPatch = _self call ["mset", [GVAR(Registry), "bank:update", _uid, _patch, false]];
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalPatch]];
- GVAR(BankMessenger) call ["sendClientNotification", [_uid, "info", "Bank", format ["Paid $%1", _amount]]];
+ GVAR(BankMessenger) call ["sendClientNotification", [_uid, "info", "Bank", format ["Paid $%1", [_amount] call EFUNC(common,formatNumber)]]];
true
}],
["resolveOrgState", compileFinal {
@@ -304,8 +304,8 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
name _player
};
- GVAR(BankMessenger) call ["sendClientNotification", [_uid, "info", "Bank", format ["Transferred $%1 to %2", _amount, _targetName]]];
- GVAR(BankMessenger) call ["sendClientNotification", [_target, "info", "Bank", format ["Received $%1 from %2", _amount, _playerName]]];
+ GVAR(BankMessenger) call ["sendClientNotification", [_uid, "info", "Bank", format ["Transferred $%1 to %2", [_amount] call EFUNC(common,formatNumber), _targetName]]];
+ GVAR(BankMessenger) call ["sendClientNotification", [_target, "info", "Bank", format ["Received $%1 from %2", [_amount] call EFUNC(common,formatNumber), _playerName]]];
true
}],
["withdraw", compileFinal {
@@ -323,7 +323,7 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
private _finalPatch = _self call ["mset", [GVAR(Registry), "bank:update", _uid, _patch, false]];
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalPatch]];
- GVAR(BankMessenger) call ["sendClientNotification", [_uid, "info", "Bank", format ["Withdrew $%1", _amount]]];
+ GVAR(BankMessenger) call ["sendClientNotification", [_uid, "info", "Bank", format ["Withdrew $%1", [_amount] call EFUNC(common,formatNumber)]]];
true
}],
["depositEarnings", compileFinal {
@@ -341,7 +341,7 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
private _finalPatch = _self call ["mset", [GVAR(Registry), "bank:update", _uid, _patch, false]];
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalPatch]];
- GVAR(BankMessenger) call ["sendClientNotification", [_uid, "info", "Bank", format ["Deposited $%1 from earnings", _amount]]];
+ GVAR(BankMessenger) call ["sendClientNotification", [_uid, "info", "Bank", format ["Deposited $%1 from earnings", [_amount] call EFUNC(common,formatNumber)]]];
true
}]
];
diff --git a/arma/server/addons/common/functions/fnc_formatNumber.sqf b/arma/server/addons/common/functions/fnc_formatNumber.sqf
index 30fc559..b0e3132 100644
--- a/arma/server/addons/common/functions/fnc_formatNumber.sqf
+++ b/arma/server/addons/common/functions/fnc_formatNumber.sqf
@@ -20,8 +20,13 @@
#define PX_TH_SEP ","
#define PX_DC_PL 2
+private _value = _this;
+if (_value isEqualType []) then {
+ _value = _value param [0, 0, [0]];
+};
+
private _count = 0;
-private _arr = (_this toFixed PX_DC_PL) splitString ".";
+private _arr = (_value toFixed PX_DC_PL) splitString ".";
private _str = PX_DC_SEP+(_arr select 1);
_arr = toArray(_arr select 0);
diff --git a/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf b/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf
index 1bb6135..830cee0 100644
--- a/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf
+++ b/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf
@@ -47,7 +47,7 @@ GVAR(FEconomyStore) = createHashMapObject [[
private _totalLiters = GETVAR(_target,liters,0);
private _totalCost = _totalLiters * 5;
- private _formattedTotalCost = _totalCost toFixed 2;
+ private _formattedTotalCost = [_totalCost] call EFUNC(common,formatNumber);
private _formattedTotalLiters = _totalLiters toFixed 2;
[CRPC(notifications,recieveNotification), ["info", "Refueling", format ["Refueling complete: %1L Total Cost: $%2", _formattedTotalLiters, _formattedTotalCost]], _player] call CFUNC(targetEvent);
diff --git a/arma/server/addons/economy/functions/fnc_initMEconomyStore.sqf b/arma/server/addons/economy/functions/fnc_initMEconomyStore.sqf
index cbc2d91..7225a09 100644
--- a/arma/server/addons/economy/functions/fnc_initMEconomyStore.sqf
+++ b/arma/server/addons/economy/functions/fnc_initMEconomyStore.sqf
@@ -80,7 +80,7 @@ GVAR(MEconomyStore) = createHashMapObject [[
private _newBalance = 0;
if (_bank < _healCost && _cash < _healCost) exitWith {
- [CRPC(notifications,recieveNotification), ["danger", "Insufficient Funds", format ["Insufficient funds for %1. Bank: %2, Cash: %3, Required: %4", (name _unit), _bank, _cash, _healCost]], _unit] call CFUNC(targetEvent);
+ [CRPC(notifications,recieveNotification), ["danger", "Insufficient Funds", format ["Insufficient funds for %1. Bank: $%2, Cash: $%3, Required: $%4", (name _unit), [_bank] call EFUNC(common,formatNumber), [_cash] call EFUNC(common,formatNumber), [_healCost] call EFUNC(common,formatNumber)]], _unit] call CFUNC(targetEvent);
};
if (_bank >= _healCost) then {
diff --git a/arma/server/addons/locker/functions/fnc_initVAStore.sqf b/arma/server/addons/locker/functions/fnc_initVAStore.sqf
index 809fa95..385b120 100644
--- a/arma/server/addons/locker/functions/fnc_initVAStore.sqf
+++ b/arma/server/addons/locker/functions/fnc_initVAStore.sqf
@@ -4,7 +4,7 @@
* File: fnc_initVAStore.sqf
* Author: IDSolutions
* Date: 2025-12-17
- * Last Update: 2026-02-13
+ * Last Update: 2026-03-27
* Public: No
*
* Description:
@@ -28,7 +28,7 @@ GVAR(VArsenalModel) = compileFinal createHashMapObject [[
private _vArsenal = createHashMap;
_vArsenal set ["backpacks", ["B_AssaultPack_rgr"]];
- _vArsenal set ["items", ["FirstAidKit", "G_Combat", "H_Cap_blk_ION", "H_HelmetB", "ItemCompass", "ItemGPS", "ItemMap", "ItemRadio", "ItemWatch", "U_IG_Guerrilla_6_1", "V_TacVest_oli"]];
+ _vArsenal set ["items", ["FirstAidKit", "G_Combat", "H_Cap_blk_ION", "H_HelmetB", "ItemCompass", "ItemGPS", "ItemMap", "ItemRadio", "ItemWatch", "U_IG_Guerrilla_6_1", "V_TacVest_oli", "ACE_EarPlugs"]];
_vArsenal set ["magazines", ["16Rnd_9x21_Mag", "30Rnd_65x39_caseless_black_mag", "Chemlight_blue", "Chemlight_green", "Chemlight_red", "Chemlight_yellow", "HandGrenade", "SmokeShell", "SmokeShellBlue", "SmokeShellGreen", "SmokeShellOrange", "SmokeShellPurple", "SmokeShellRed", "SmokeShellYellow"]];
_vArsenal set ["weapons", ["arifle_MX_F", "hgun_P07_F"]];
diff --git a/arma/server/addons/org/XEH_preInit.sqf b/arma/server/addons/org/XEH_preInit.sqf
index 807e24b..0d56d12 100644
--- a/arma/server/addons/org/XEH_preInit.sqf
+++ b/arma/server/addons/org/XEH_preInit.sqf
@@ -127,7 +127,7 @@ PREP_RECOMPILE_END;
private _index = GVAR(IndexRegistry) get _uid;
private _key = _index get "orgID";
- GVAR(OrgStore) call ["save", [GVAR(Registry), "org:update", _key]];
+ GVAR(OrgStore) call ["saveById", [_key]];
}] call CFUNC(addEventHandler);
[QGVAR(requestRemoveOrg), {
diff --git a/arma/server/addons/org/functions/fnc_initOrgStore.sqf b/arma/server/addons/org/functions/fnc_initOrgStore.sqf
index b55cf35..038d6d4 100644
--- a/arma/server/addons/org/functions/fnc_initOrgStore.sqf
+++ b/arma/server/addons/org/functions/fnc_initOrgStore.sqf
@@ -50,6 +50,42 @@ GVAR(OrgModel) = compileFinal createHashMapObject [[
if !(_x in _org) then { _org set [_x, _y]; };
} forEach _defaults;
+ private _assets = _org getOrDefault ["assets", createHashMap];
+ if !(_assets isEqualType createHashMap) then {
+ _assets = createHashMap;
+ };
+
+ private _migratedAssets = createHashMap;
+ {
+ private _categoryKey = _x;
+ private _value = _y;
+
+ if (_value isEqualType createHashMap) then {
+ private _categoryMap = createHashMap;
+
+ if (_categoryKey find ":" >= 0) then {
+ private _legacyAsset = +_value;
+ private _category = toLowerANSI (_legacyAsset getOrDefault ["type", "items"]);
+ private _className = _legacyAsset getOrDefault ["classname", ""];
+ if (_className isNotEqualTo "") then {
+ _categoryMap = +(_migratedAssets getOrDefault [_category, createHashMap]);
+ _categoryMap set [_className, _legacyAsset];
+ _migratedAssets set [_category, _categoryMap];
+ };
+ } else {
+ {
+ if (_y isEqualType createHashMap) then {
+ _categoryMap set [_x, +_y];
+ };
+ } forEach _value;
+
+ _migratedAssets set [toLowerANSI _categoryKey, _categoryMap];
+ };
+ };
+ } forEach _assets;
+
+ _org set ["assets", _migratedAssets];
+
_org
}],
["validate", compileFinal {
@@ -121,6 +157,13 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["org:create", ["default", _defaultJson]] call EFUNC(extension,extCall);
};
+ _defaultOrg = GVAR(OrgModel) call ["migrate", [_defaultOrg]];
+ private _defaultAssets = _self call ["fetch", ["org:assets:get", "default"]];
+ if !(_defaultAssets isEqualType createHashMap) then { _defaultAssets = createHashMap; };
+ _defaultOrg set ["assets", _defaultAssets];
+ private _defaultFleet = _self call ["fetch", ["org:fleet:get", "default"]];
+ if !(_defaultFleet isEqualType createHashMap) then { _defaultFleet = createHashMap; };
+ _defaultOrg set ["fleet", _defaultFleet];
GVAR(Registry) set ["default", _defaultOrg];
}],
["verifyMember", compileFinal {
@@ -231,12 +274,30 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
private _assetsList = [];
{
- private _assetData = _y;
- _assetsList pushBack (createHashMapFromArray [
- ["name", _assetData getOrDefault ["name", "Unknown Asset"]],
- ["type", _assetData getOrDefault ["type", "items"]],
- ["quantity", str (_assetData getOrDefault ["quantity", 0])]
- ]);
+ private _category = _x;
+ {
+ private _assetData = _y;
+ private _className = _assetData getOrDefault ["classname", ""];
+ private _displayName = _className;
+ {
+ private _cfg = _x >> _className;
+ if (isClass _cfg) exitWith {
+ private _resolvedName = getText (_cfg >> "displayName");
+ if (_resolvedName isNotEqualTo "") then { _displayName = _resolvedName; };
+ };
+ } forEach [
+ configFile >> "CfgWeapons",
+ configFile >> "CfgMagazines",
+ configFile >> "CfgVehicles",
+ configFile >> "CfgGlasses"
+ ];
+
+ _assetsList pushBack (createHashMapFromArray [
+ ["name", _displayName],
+ ["type", _assetData getOrDefault ["type", _category]],
+ ["quantity", str (_assetData getOrDefault ["quantity", 0])]
+ ]);
+ } forEach _y;
} forEach _assetsRaw;
private _fleetList = [];
@@ -291,8 +352,112 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["chargeCheckout", compileFinal {
GVAR(OrgTreasuryService) call ["chargeCheckout", _this]
}],
+ ["saveById", compileFinal {
+ params [["_orgID", "", [""]]];
+
+ if (_orgID isEqualTo "") exitWith { createHashMap };
+
+ private _org = GVAR(Registry) getOrDefault [_orgID, createHashMap];
+ if (_org isEqualTo createHashMap) then {
+ _org = _self call ["loadById", [_orgID]];
+ };
+ if (_org isEqualTo createHashMap) exitWith { createHashMap };
+
+ private _coreOrg = createHashMapFromArray [
+ ["id", _org getOrDefault ["id", _orgID]],
+ ["owner", _org getOrDefault ["owner", ""]],
+ ["name", _org getOrDefault ["name", ""]],
+ ["funds", _org getOrDefault ["funds", 0]],
+ ["reputation", _org getOrDefault ["reputation", 0]],
+ ["credit_lines", _org getOrDefault ["credit_lines", createHashMap]]
+ ];
+
+ private _coreJson = _self call ["toJSON", [_coreOrg]];
+ ["org:update", [_orgID, _coreJson]] call EFUNC(extension,extCall);
+
+ private _assets = _org getOrDefault ["assets", createHashMap];
+ private _assetsJson = _self call ["toJSON", [_assets]];
+ ["org:assets:update", [_orgID, _assetsJson]] call EFUNC(extension,extCall);
+
+ private _fleet = _org getOrDefault ["fleet", createHashMap];
+ private _fleetJson = _self call ["toJSON", [_fleet]];
+ ["org:fleet:update", [_orgID, _fleetJson]] call EFUNC(extension,extCall);
+
+ _org
+ }],
+ ["addAssets", compileFinal {
+ params [["_requesterUid", "", [""]], ["_assets", [], [[]]], ["_commit", false, [false]], ["_orgID", "", [""]]];
+
+ private _result = createHashMapFromArray [
+ ["success", false],
+ ["message", "Unable to update organization assets."],
+ ["patch", createHashMap],
+ ["memberUids", []]
+ ];
+
+ if (_assets isEqualTo []) exitWith {
+ _result set ["success", true];
+ _result set ["message", ""];
+ _result
+ };
+
+ private _resolvedOrgID = _orgID;
+ if (_resolvedOrgID isEqualTo "") then {
+ private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
+ _resolvedOrgID = _requesterActor getOrDefault ["organization", "default"];
+ };
+ if (_resolvedOrgID isEqualTo "") then { _resolvedOrgID = "default"; };
+
+ private _org = GVAR(Registry) getOrDefault [_resolvedOrgID, createHashMap];
+ if (_org isEqualTo createHashMap) then {
+ _org = _self call ["loadById", [_resolvedOrgID]];
+ };
+ if (_org isEqualTo createHashMap) exitWith {
+ _result set ["message", "Organization data is unavailable for asset updates."];
+ _result
+ };
+
+ private _assetMap = +(_org getOrDefault ["assets", createHashMap]);
+
+ {
+ private _className = _x getOrDefault ["classname", ""];
+ private _category = toLowerANSI (_x getOrDefault ["category", "items"]);
+ private _quantity = floor ((_x getOrDefault ["quantity", 0]) max 0);
+ if (_className isEqualTo "" || { _quantity <= 0 }) then { continue; };
+
+ private _categoryMap = +(_assetMap getOrDefault [_category, createHashMap]);
+ private _assetEntry = +(_categoryMap getOrDefault [_className, createHashMap]);
+
+ private _existingQuantity = _assetEntry getOrDefault ["quantity", 0];
+ _categoryMap set [_className, createHashMapFromArray [
+ ["classname", _className],
+ ["type", _category],
+ ["quantity", (_existingQuantity + _quantity)]
+ ]];
+ _assetMap set [_category, _categoryMap];
+ } forEach _assets;
+
+ private _patch = _self call ["mset", [
+ GVAR(Registry),
+ "org:update",
+ _resolvedOrgID,
+ createHashMapFromArray [["assets", _assetMap]],
+ false
+ ]];
+
+ if (_commit) then {
+ private _assetJson = _self call ["toJSON", [_assetMap]];
+ ["org:assets:update", [_resolvedOrgID, _assetJson]] call EFUNC(extension,extCall);
+ };
+
+ _result set ["success", true];
+ _result set ["message", ""];
+ _result set ["patch", _patch];
+ _result set ["memberUids", GVAR(OrgTreasuryService) call ["resolveOrgMemberUids", [_org, _requesterUid]]];
+ _result
+ }],
["addFleetVehicles", compileFinal {
- params [["_requesterUid", "", [""]], ["_vehicles", [], [[]]], ["_commit", false, [false]]];
+ params [["_requesterUid", "", [""]], ["_vehicles", [], [[]]], ["_commit", false, [false]], ["_orgID", "", [""]]];
private _result = createHashMapFromArray [
["success", false],
@@ -301,17 +466,23 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["memberUids", []]
];
- if (_requesterUid isEqualTo "" || { _vehicles isEqualTo [] }) exitWith {
+ if (_vehicles isEqualTo []) exitWith {
_result set ["success", true];
_result set ["message", ""];
_result
};
- private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
- private _orgID = _requesterActor getOrDefault ["organization", "default"];
- if (_orgID isEqualTo "") then { _orgID = "default"; };
+ private _resolvedOrgID = _orgID;
+ if (_resolvedOrgID isEqualTo "") then {
+ private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
+ _resolvedOrgID = _requesterActor getOrDefault ["organization", "default"];
+ };
+ if (_resolvedOrgID isEqualTo "") then { _resolvedOrgID = "default"; };
- private _org = GVAR(Registry) getOrDefault [_orgID, createHashMap];
+ private _org = GVAR(Registry) getOrDefault [_resolvedOrgID, createHashMap];
+ if (_org isEqualTo createHashMap) then {
+ _org = _self call ["loadById", [_resolvedOrgID]];
+ };
if (_org isEqualTo createHashMap) exitWith {
_result set ["message", "Organization data is unavailable for fleet updates."];
_result
@@ -347,9 +518,17 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_fleetIndex = _fleetIndex + 1;
} forEach _vehicles;
- private _patch = createHashMapFromArray [["fleet", _fleet]];
+ private _patch = _self call ["mset", [
+ GVAR(Registry),
+ "org:update",
+ _resolvedOrgID,
+ createHashMapFromArray [["fleet", _fleet]],
+ false
+ ]];
+
if (_commit) then {
- _patch = _self call ["mset", [GVAR(Registry), "org:update", _orgID, _patch, false]];
+ private _fleetJson = _self call ["toJSON", [_fleet]];
+ ["org:fleet:update", [_resolvedOrgID, _fleetJson]] call EFUNC(extension,extCall);
};
_result set ["success", true];
@@ -373,6 +552,16 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
if (_org isEqualTo createHashMap) exitWith { _org };
_org = GVAR(OrgModel) call ["migrate", [_org]];
+ private _assets = _self call ["fetch", ["org:assets:get", _orgID]];
+ if !(_assets isEqualType createHashMap) then {
+ _assets = createHashMap;
+ };
+ _org set ["assets", _assets];
+ private _fleet = _self call ["fetch", ["org:fleet:get", _orgID]];
+ if !(_fleet isEqualType createHashMap) then {
+ _fleet = createHashMap;
+ };
+ _org set ["fleet", _fleet];
private _memberRows = _self call ["fetch", ["org:members:get", _orgID]];
if !(_memberRows isEqualType []) then {
@@ -438,8 +627,6 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["funds", 0],
["reputation", 0],
["credit_lines", createHashMap],
- ["assets", createHashMap],
- ["fleet", createHashMap],
["members", createHashMap]
];
diff --git a/arma/server/addons/org/functions/fnc_treasuryService.sqf b/arma/server/addons/org/functions/fnc_treasuryService.sqf
index 33cffe8..dfc46d2 100644
--- a/arma/server/addons/org/functions/fnc_treasuryService.sqf
+++ b/arma/server/addons/org/functions/fnc_treasuryService.sqf
@@ -17,7 +17,9 @@ GVAR(OrgTreasuryServiceBase) = compileFinal createHashMapFromArray [
params [["_org", createHashMap, [createHashMap]], ["_requesterUid", "", [""]]];
private _memberUids = keys (_org getOrDefault ["members", createHashMap]);
- if !(_requesterUid in _memberUids) then { _memberUids pushBack _requesterUid; };
+ if (_requesterUid isNotEqualTo "" && { !(_requesterUid in _memberUids) }) then {
+ _memberUids pushBack _requesterUid;
+ };
_memberUids
}],
@@ -88,7 +90,7 @@ GVAR(OrgTreasuryServiceBase) = compileFinal createHashMapFromArray [
private _memberUids = _self call ["resolveOrgMemberUids", [_org, _requesterUid]];
_result set ["success", true];
- _result set ["message", format ["Credit line of $%1 assigned to %2.", [_amount] call BIS_fnc_numberText, _resolvedMemberName]];
+ _result set ["message", format ["Credit line of $%1 assigned to %2.", [_amount] call EFUNC(common,formatNumber), _resolvedMemberName]];
_result set ["patch", _patch];
_result set ["memberUids", _memberUids];
_result
diff --git a/arma/server/addons/store/functions/fnc_initCatalogService.sqf b/arma/server/addons/store/functions/fnc_initCatalogService.sqf
index 2e81ba4..8eb6b52 100644
--- a/arma/server/addons/store/functions/fnc_initCatalogService.sqf
+++ b/arma/server/addons/store/functions/fnc_initCatalogService.sqf
@@ -20,7 +20,7 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
["formatCurrency", compileFinal {
params [["_amount", 0, [0]]];
- format ["$%1", [_amount max 0] call BIS_fnc_numberText]
+ format ["$%1", [_amount max 0] call EFUNC(common,formatNumber)]
}],
["isVisibleConfig", compileFinal {
params [["_cfg", configNull, [configNull]]];
diff --git a/arma/server/addons/store/functions/fnc_initStoreStore.sqf b/arma/server/addons/store/functions/fnc_initStoreStore.sqf
index 7526eb6..b7019dd 100644
--- a/arma/server/addons/store/functions/fnc_initStoreStore.sqf
+++ b/arma/server/addons/store/functions/fnc_initStoreStore.sqf
@@ -158,7 +158,7 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
["formatCurrency", compileFinal {
params [["_amount", 0, [0]]];
- format ["$%1", [_amount max 0] call BIS_fnc_numberText]
+ format ["$%1", [_amount max 0] call EFUNC(common,formatNumber)]
}],
["applyPaymentPatch", compileFinal {
params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_paymentMethod", "cash", [""]], ["_total", 0, [0]], ["_commit", false, [false]]];
diff --git a/arma/server/addons/task/$PBOPREFIX$ b/arma/server/addons/task/$PBOPREFIX$
new file mode 100644
index 0000000..429c994
--- /dev/null
+++ b/arma/server/addons/task/$PBOPREFIX$
@@ -0,0 +1 @@
+forge\forge_server\addons\task
diff --git a/arma/server/addons/task/CfgEventHandlers.hpp b/arma/server/addons/task/CfgEventHandlers.hpp
new file mode 100644
index 0000000..f6503c2
--- /dev/null
+++ b/arma/server/addons/task/CfgEventHandlers.hpp
@@ -0,0 +1,17 @@
+class Extended_PreStart_EventHandlers {
+ class ADDON {
+ init = QUOTE(call COMPILE_SCRIPT(XEH_preStart));
+ };
+};
+
+class Extended_PreInit_EventHandlers {
+ class ADDON {
+ init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
+ };
+};
+
+class Extended_PostInit_EventHandlers {
+ class ADDON {
+ init = QUOTE(call COMPILE_SCRIPT(XEH_postInit));
+ };
+};
diff --git a/arma/server/addons/task/CfgFactionClasses.hpp b/arma/server/addons/task/CfgFactionClasses.hpp
new file mode 100644
index 0000000..84782dd
--- /dev/null
+++ b/arma/server/addons/task/CfgFactionClasses.hpp
@@ -0,0 +1,6 @@
+class CfgFactionClasses {
+ class NO_CATEGORY;
+ class FORGE_Modules: NO_CATEGORY {
+ displayName = "FORGE";
+ };
+};
diff --git a/arma/server/addons/task/CfgMissions.hpp b/arma/server/addons/task/CfgMissions.hpp
new file mode 100644
index 0000000..33f7c12
--- /dev/null
+++ b/arma/server/addons/task/CfgMissions.hpp
@@ -0,0 +1,269 @@
+// TODO: Move to mission template and provide documentation
+class CfgMissions {
+ // Global settings
+ maxConcurrentMissions = 3;
+ missionInterval = 300; // 5 minutes between mission generation
+
+ // Mission type weights
+ class MissionWeights {
+ attack = 0.2;
+ defend = 0.2;
+ hostage = 0.2;
+ hvt = 0.15;
+ defuse = 0.15;
+ delivery = 0.1;
+ };
+
+ // Mission locations
+ class Locations {
+ class CityOne {
+ position[] = {1000, 1000, 0};
+ type = "city";
+ radius = 300;
+ suitable[] = {"attack", "defend", "hostage"};
+ };
+ class MilitaryBase {
+ position[] = {2000, 2000, 0};
+ type = "military";
+ radius = 500;
+ suitable[] = {"hvt", "defend", "attack"};
+ };
+ class Industrial {
+ position[] = {3000, 3000, 0};
+ type = "industrial";
+ radius = 200;
+ suitable[] = {"delivery", "defuse"};
+ };
+ };
+
+ // AI Groups configuration
+ class AIGroups {
+ class Infantry {
+ side = "EAST";
+ class Units {
+ class Unit0 {
+ vehicle = "O_Soldier_TL_F";
+ rank = "SERGEANT";
+ position[] = {0, 0, 0};
+ };
+ class Unit1 {
+ vehicle = "O_Soldier_AR_F";
+ rank = "CORPORAL";
+ position[] = {5, -5, 0};
+ };
+ class Unit2 {
+ vehicle = "O_Soldier_LAT_F";
+ rank = "PRIVATE";
+ position[] = {-5, -5, 0};
+ };
+ };
+ suitable[] = {"attack", "defend", "hostage"};
+ };
+ class Assault {
+ side = "EAST";
+ class Units {
+ class Unit0 {
+ vehicle = "O_Soldier_SL_F";
+ rank = "SERGEANT";
+ position[] = {0, 0, 0};
+ };
+ class Unit1 {
+ vehicle = "O_Soldier_GL_F";
+ rank = "CORPORAL";
+ position[] = {4, -3, 0};
+ };
+ class Unit2 {
+ vehicle = "O_Soldier_AR_F";
+ rank = "CORPORAL";
+ position[] = {-4, -3, 0};
+ };
+ class Unit3 {
+ vehicle = "O_medic_F";
+ rank = "PRIVATE";
+ position[] = {7, -6, 0};
+ };
+ };
+ suitable[] = {"attack", "defend"};
+ };
+ class MotorizedPatrol {
+ side = "EAST";
+ class Units {
+ class Unit0 {
+ vehicle = "O_Soldier_TL_F";
+ rank = "SERGEANT";
+ position[] = {0, 0, 0};
+ };
+ class Unit1 {
+ vehicle = "O_Soldier_LAT_F";
+ rank = "CORPORAL";
+ position[] = {5, -4, 0};
+ };
+ class Unit2 {
+ vehicle = "O_Soldier_F";
+ rank = "PRIVATE";
+ position[] = {-5, -4, 0};
+ };
+ class Unit3 {
+ vehicle = "O_Soldier_A_F";
+ rank = "PRIVATE";
+ position[] = {8, -7, 0};
+ };
+ };
+ suitable[] = {"attack", "defend"};
+ };
+ class SpecOps {
+ side = "EAST";
+ class Units {
+ class Unit0 {
+ vehicle = "O_recon_TL_F";
+ rank = "SERGEANT";
+ position[] = {0, 0, 0};
+ };
+ class Unit1 {
+ vehicle = "O_recon_M_F";
+ rank = "CORPORAL";
+ position[] = {5, -5, 0};
+ };
+ };
+ suitable[] = {"hvt", "hostage"};
+ };
+ class ReconRaid {
+ side = "EAST";
+ class Units {
+ class Unit0 {
+ vehicle = "O_recon_TL_F";
+ rank = "SERGEANT";
+ position[] = {0, 0, 0};
+ };
+ class Unit1 {
+ vehicle = "O_recon_M_F";
+ rank = "CORPORAL";
+ position[] = {4, -4, 0};
+ };
+ class Unit2 {
+ vehicle = "O_recon_LAT_F";
+ rank = "CORPORAL";
+ position[] = {-4, -4, 0};
+ };
+ class Unit3 {
+ vehicle = "O_recon_medic_F";
+ rank = "PRIVATE";
+ position[] = {7, -7, 0};
+ };
+ };
+ suitable[] = {"attack", "hvt", "hostage"};
+ };
+ };
+
+ // TODO: Continue to refine mission types and their specific settings
+ // Mission type specific settings
+ class MissionTypes {
+ class Attack {
+ minUnits = 4;
+ maxUnits = 8;
+ class Rewards {
+ money[] = {25000, 60000};
+ reputation[] = {6, 14};
+ equipment[] = {{"ItemGPS", 0.5}, {"ItemCompass", 0.3}};
+ supplies[] = {{"FirstAidKit", 0.2}, {"Medikit", 0.1}};
+ weapons[] = {{"arifle_MX_F", 0.3}, {"arifle_Katiba_F", 0.2}};
+ vehicles[] = {{"B_MRAP_01_F", 0.1}, {"B_APC_Wheeled_01_cannon_F", 0.05}};
+ special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
+ };
+ penalty[] = {-8, -3};
+ timeLimit[] = {900, 1800}; // 15-30 minutes
+ };
+
+ class Defend {
+ minWaves = 3;
+ maxWaves = 8;
+ unitsPerWave[] = {4, 8};
+ waveCooldown = 300;
+ class Rewards {
+ money[] = {40000, 90000};
+ reputation[] = {8, 18};
+ equipment[] = {{"ItemGPS", 0.5}, {"ItemCompass", 0.3}};
+ supplies[] = {{"FirstAidKit", 0.2}, {"Medikit", 0.1}};
+ weapons[] = {{"arifle_MX_F", 0.3}, {"arifle_Katiba_F", 0.2}};
+ vehicles[] = {{"B_MRAP_01_F", 0.1}, {"B_APC_Wheeled_01_cannon_F", 0.05}};
+ special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
+ };
+ penalty[] = {-12, -4};
+ timeLimit[] = {1800, 3600}; // 30-60 minutes
+ };
+
+ class Hostage {
+ class Hostages {
+ civilian[] = {"C_man_1", "C_man_polo_1_F"};
+ military[] = {"B_Pilot_F", "B_officer_F"};
+ };
+ class Rewards {
+ money[] = {60000, 140000};
+ reputation[] = {12, 25};
+ equipment[] = {{"ItemGPS", 0.5}, {"ItemCompass", 0.3}};
+ supplies[] = {{"FirstAidKit", 0.2}, {"Medikit", 0.1}};
+ weapons[] = {{"arifle_MX_F", 0.3}, {"arifle_Katiba_F", 0.2}};
+ vehicles[] = {{"B_MRAP_01_F", 0.1}, {"B_APC_Wheeled_01_cannon_F", 0.05}};
+ special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
+ };
+ penalty[] = {-16, -6};
+ timeLimit[] = {600, 900}; // 10-15 minutes
+ };
+
+ class HVT {
+ class Targets {
+ officer[] = {"O_officer_F"};
+ sniper[] = {"O_sniper_F"};
+ };
+ escorts = 4;
+ class Rewards {
+ money[] = {50000, 120000};
+ reputation[] = {10, 22};
+ equipment[] = {{"ItemGPS", 0.5}, {"ItemCompass", 0.3}};
+ supplies[] = {{"FirstAidKit", 0.2}, {"Medikit", 0.1}};
+ weapons[] = {{"arifle_MX_F", 0.3}, {"arifle_Katiba_F", 0.2}};
+ vehicles[] = {{"B_MRAP_01_F", 0.1}, {"B_APC_Wheeled_01_cannon_F", 0.05}};
+ special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
+ };
+ penalty[] = {-14, -5};
+ timeLimit[] = {900, 1800}; // 15-30 minutes
+ };
+
+ class Defuse {
+ class Devices {
+ small[] = {"DemoCharge_Remote_Mag"};
+ large[] = {"SatchelCharge_Remote_Mag"};
+ };
+ maxDevices = 3;
+ class Rewards {
+ money[] = {20000, 50000};
+ reputation[] = {5, 12};
+ equipment[] = {{"ItemGPS", 0.5}, {"ItemCompass", 0.3}};
+ supplies[] = {{"FirstAidKit", 0.2}, {"Medikit", 0.1}};
+ weapons[] = {{"arifle_MX_F", 0.3}, {"arifle_Katiba_F", 0.2}};
+ vehicles[] = {{"B_MRAP_01_F", 0.1}, {"B_APC_Wheeled_01_cannon_F", 0.05}};
+ special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
+ };
+ penalty[] = {-9, -3};
+ timeLimit[] = {600, 900}; // 10-15 minutes
+ };
+
+ class Delivery {
+ class Cargo {
+ supplies[] = {"Land_CargoBox_V1_F"};
+ vehicles[] = {"B_MRAP_01_F", "B_Truck_01_transport_F"};
+ };
+ class Rewards {
+ money[] = {10000, 30000};
+ reputation[] = {3, 8};
+ equipment[] = {{"ItemGPS", 0.5}, {"ItemCompass", 0.3}};
+ supplies[] = {{"FirstAidKit", 0.2}, {"Medikit", 0.1}};
+ weapons[] = {{"arifle_MX_F", 0.3}, {"arifle_Katiba_F", 0.2}};
+ vehicles[] = {{"B_MRAP_01_F", 0.1}, {"B_APC_Wheeled_01_cannon_F", 0.05}};
+ special[] = {{"B_UAV_01_F", 0.05}, {"B_Heli_Light_01_F", 0.02}};
+ };
+ penalty[] = {-6, -2};
+ timeLimit[] = {900, 1800}; // 15-30 minutes
+ };
+ };
+};
diff --git a/arma/server/addons/task/CfgVehicles.hpp b/arma/server/addons/task/CfgVehicles.hpp
new file mode 100644
index 0000000..06a1a39
--- /dev/null
+++ b/arma/server/addons/task/CfgVehicles.hpp
@@ -0,0 +1,782 @@
+class CfgVehicles {
+ class Logic;
+ class Module_F: Logic {
+ class AttributesBase {
+ class Edit;
+ class Combo;
+ };
+ class ModuleDescription {};
+ };
+
+ class FORGE_Module_Attack: Module_F {
+ scope = 2;
+ displayName = "Attack Task";
+ // icon = "\a3\ui_f\data\IGUI\Cfg\simpleTasks\types\default_ca.paa";
+ category = "FORGE_Modules";
+
+ function = QFUNC(attackModule);
+ functionPriority = 1;
+ isGlobal = 1;
+ isTriggerActivated = 1;
+ isDisposable = 1;
+ is3DEN = 0;
+
+ canSetArea = 0;
+ canSetAreaShape = 0;
+ canSetAreaHeight = 0;
+
+ class AttributeValues {};
+ class Attributes: AttributesBase {
+ class TaskID: Edit {
+ property = "FORGE_Module_Attack_TaskID";
+ displayName = "Task ID";
+ tooltip = "Unique identifier for this task";
+ typeName = "STRING";
+ // defaultValue = """";
+ };
+ class LimitFail: Edit {
+ property = "FORGE_Module_Attack_LimitFail";
+ displayName = "Fail Limit";
+ tooltip = "Number of targets that escape to fail the task";
+ typeName = "NUMBER";
+ defaultValue = -1;
+ };
+ class LimitSuccess: Edit {
+ property = "FORGE_Module_Attack_LimitSuccess";
+ displayName = "Success Limit";
+ tooltip = "Number of targets that need to be eliminated to succeed the task";
+ typeName = "NUMBER";
+ defaultValue = -1;
+ };
+ class CompanyFunds: Edit {
+ property = "FORGE_Module_Attack_CompanyFunds";
+ displayName = "Reward Funds";
+ tooltip = "Amount of funds awarded on success";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ class RatingFail: Edit {
+ property = "FORGE_Module_Attack_RatingFail";
+ displayName = "Rating Loss";
+ tooltip = "Amount of rating lost on failure";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ class RatingSuccess: Edit {
+ property = "FORGE_Module_Attack_RatingSuccess";
+ displayName = "Rating Gain";
+ tooltip = "Amount of rating gained on success";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ class EndSuccess: Combo {
+ property = "FORGE_Module_Attack_EndSuccess";
+ displayName = "End on Success";
+ tooltip = "End mission when task is completed successfully";
+ typeName = "BOOL";
+ defaultValue = 0;
+
+ class Values {
+ class EnableEndSuccess { name = "Enable"; value = 1; };
+ class DisableEndSuccess { name = "Disable"; value = 0; };
+ };
+ };
+ class EndFail: Combo {
+ property = "FORGE_Module_Attack_EndFail";
+ displayName = "End on Failure";
+ tooltip = "End mission when task fails";
+ typeName = "BOOL";
+ defaultValue = 0;
+
+ class Values {
+ class EnableEndFail { name = "Enable"; value = 1; };
+ class DisableEndFail { name = "Disable"; value = 0; };
+ };
+ };
+ class TimeLimit: Edit {
+ property = "FORGE_Module_Attack_TimeLimit";
+ displayName = "Time Limit";
+ tooltip = "Time in seconds before targets escape (0 for no limit)";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ };
+
+ class ModuleDescription: ModuleDescription {
+ description = "Creates an attack task with configurable parameters";
+ sync[] = { "Anything" };
+
+ class Anything {
+ description[] = {
+ "Attack task module",
+ "Sync with units/vehicles to mark as targets"
+ };
+ position = 1;
+ direction = 1;
+ optional = 1;
+ duplicate = 1;
+ };
+ };
+ };
+
+ class FORGE_Module_Explosives: Module_F {
+ scope = 2;
+ displayName = "Explosive Entities";
+ // icon = "\a3\ui_f\data\IGUI\Cfg\simpleTasks\types\default_ca.paa";
+ category = "FORGE_Modules";
+
+ function = QFUNC(explosivesModule);
+ functionPriority = 1;
+ isGlobal = 1;
+ isTriggerActivated = 0;
+ isDisposable = 1;
+ is3DEN = 0;
+
+ canSetArea = 0;
+ canSetAreaShape = 0;
+ canSetAreaHeight = 0;
+
+ class AttributeValues {};
+ class Attributes: AttributesBase {};
+ class ModuleDescription: ModuleDescription {
+ description = "Module for explosive entities that need to be defused";
+ sync[] = { "Anything" };
+
+ class Anything {
+ description[] = {
+ "Explosive entities module",
+ "Sync with objects to mark as explosives",
+ "Those objects will be processed as defusal targets"
+ };
+ position = 1;
+ direction = 1;
+ optional = 1;
+ duplicate = 1;
+ };
+ };
+ };
+
+ class FORGE_Module_Hostages: Module_F {
+ scope = 2;
+ displayName = "Hostage Entities";
+ // icon = "\a3\ui_f\data\IGUI\Cfg\simpleTasks\types\default_ca.paa";
+ category = "FORGE_Modules";
+
+ function = QFUNC(hostagesModule);
+ functionPriority = 1;
+ isGlobal = 1;
+ isTriggerActivated = 0;
+ isDisposable = 1;
+ is3DEN = 0;
+
+ canSetArea = 0;
+ canSetAreaShape = 0;
+ canSetAreaHeight = 0;
+
+ class AttributeValues {};
+ class Attributes: AttributesBase {};
+ class ModuleDescription: ModuleDescription {
+ description = "Module for hostage entities that need to be rescued";
+ sync[] = { "Anything" };
+
+ class Anything {
+ description[] = {
+ "Hostage entities module",
+ "Sync with units to mark as hostages",
+ "Those objects will be processed as rescue targets"
+ };
+ position = 1;
+ direction = 1;
+ optional = 1;
+ duplicate = 1;
+ };
+ };
+ };
+
+ class FORGE_Module_Shooters: Module_F {
+ scope = 2;
+ displayName = "Shooter Entities";
+ // icon = "\a3\ui_f\data\IGUI\Cfg\simpleTasks\types\default_ca.paa";
+ category = "FORGE_Modules";
+
+ function = QFUNC(shootersModule);
+ functionPriority = 1;
+ isGlobal = 1;
+ isTriggerActivated = 0;
+ isDisposable = 1;
+ is3DEN = 0;
+
+ canSetArea = 0;
+ canSetAreaShape = 0;
+ canSetAreaHeight = 0;
+
+ class AttributeValues {};
+ class Attributes: AttributesBase {};
+ class ModuleDescription: ModuleDescription {
+ description = "Module for shooter entities that need to be eliminated";
+ sync[] = { "AnyBrain" };
+
+ class AnyBrain {
+ description[] = {
+ "Shooter entities module",
+ "Sync with units to mark as shooters",
+ "Those objects will be processed as elimination targets"
+ };
+ position = 1;
+ direction = 1;
+ optional = 1;
+ duplicate = 1;
+ };
+ };
+ };
+
+ class FORGE_Module_Protected: Module_F {
+ scope = 2;
+ displayName = "Protected Entities";
+ // icon = "\a3\ui_f\data\IGUI\Cfg\simpleTasks\types\default_ca.paa";
+ category = "FORGE_Modules";
+
+ function = QFUNC(protectedModule);
+ functionPriority = 1;
+ isGlobal = 1;
+ isTriggerActivated = 0;
+ isDisposable = 1;
+ is3DEN = 0;
+
+ canSetArea = 0;
+ canSetAreaShape = 0;
+ canSetAreaHeight = 0;
+
+ class AttributeValues {};
+ class Attributes: AttributesBase {};
+ class ModuleDescription: ModuleDescription {
+ description = "Module for protected entities that need to be protected";
+ sync[] = { "Anything" };
+
+ class Anything {
+ description[] = {
+ "Protected entities module",
+ "Sync with objects to mark as protected entities",
+ "Those objects will be processed as protected targets"
+ };
+ position = 1;
+ direction = 1;
+ optional = 1;
+ duplicate = 1;
+ };
+ };
+ };
+
+ class FORGE_Module_Defuse: Module_F {
+ scope = 2;
+ displayName = "Defuse Task";
+ // icon = "\a3\ui_f\data\IGUI\Cfg\simpleTasks\types\default_ca.paa";
+ category = "FORGE_Modules";
+
+ function = QFUNC(defuseModule);
+ functionPriority = 1;
+ isGlobal = 1;
+ isTriggerActivated = 1;
+ isDisposable = 1;
+ is3DEN = 0;
+
+ canSetArea = 0;
+ canSetAreaShape = 0;
+ canSetAreaHeight = 0;
+
+ class AttributeValues {};
+ class Attributes: AttributesBase {
+ class TaskID: Edit {
+ property = "FORGE_Module_Defuse_TaskID";
+ displayName = "Task ID";
+ tooltip = "Unique identifier for this task";
+ typeName = "STRING";
+ // defaultValue = """";
+ };
+ class LimitFail: Edit {
+ property = "FORGE_Module_Defuse_LimitFail";
+ displayName = "Fail Limit";
+ tooltip = "Number of protected entities destroyed to fail the task";
+ typeName = "NUMBER";
+ defaultValue = -1;
+ };
+ class LimitSuccess: Edit {
+ property = "FORGE_Module_Defuse_LimitSuccess";
+ displayName = "Success Limit";
+ tooltip = "Number of entities that need to be defused to complete the task";
+ typeName = "NUMBER";
+ defaultValue = -1;
+ };
+ class CompanyFunds: Edit {
+ property = "FORGE_Module_Defuse_CompanyFunds";
+ displayName = "Reward Funds";
+ tooltip = "Amount of funds awarded on success";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ class RatingFail: Edit {
+ property = "FORGE_Module_Defuse_RatingFail";
+ displayName = "Rating Loss";
+ tooltip = "Amount of rating lost on failure";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ class RatingSuccess: Edit {
+ property = "FORGE_Module_Defuse_RatingSuccess";
+ displayName = "Rating Gain";
+ tooltip = "Amount of rating gained on success";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ class EndSuccess: Combo {
+ property = "FORGE_Module_Defuse_EndSuccess";
+ displayName = "End on Success";
+ tooltip = "End mission when task is completed successfully";
+ typeName = "BOOL";
+ defaultValue = 0;
+
+ class Values {
+ class EnableEndSuccess { name = "Enable"; value = 1; };
+ class DisableEnSuccess { name = "Disable"; value = 0; };
+ };
+ };
+ class EndFail: Combo {
+ property = "FORGE_Module_Defuse_EndFail";
+ displayName = "End on Failure";
+ tooltip = "End mission when task fails";
+ typeName = "BOOL";
+ defaultValue = 0;
+
+ class Values {
+ class EnableEndFail { name = "Enable"; value = 1; };
+ class DisableEndFail { name = "Disable"; value = 0; };
+ };
+ };
+ class TimeLimit: Edit {
+ property = "FORGE_Module_Defuse_TimeLimit";
+ displayName = "Time Limit";
+ tooltip = "Time in seconds before detenation (0 for no limit)";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ };
+
+ class ModuleDescription: ModuleDescription {
+ description = "Creates a defuse task with configurable parameters";
+ sync[] = { "Anything" };
+
+ class Anything {
+ description[] = {
+ "Defuse task module",
+ "Sync with entities to mark as explosives and protected entities",
+ };
+ position = 1;
+ direction = 1;
+ optional = 1;
+ duplicate = 1;
+ };
+ };
+ };
+
+ class FORGE_Module_Destroy: Module_F {
+ scope = 2;
+ displayName = "Destroy Task";
+ // icon = "\a3\ui_f\data\IGUI\Cfg\simpleTasks\types\default_ca.paa";
+ category = "FORGE_Modules";
+
+ function = QFUNC(destroyModule);
+ functionPriority = 1;
+ isGlobal = 1;
+ isTriggerActivated = 1;
+ isDisposable = 1;
+ is3DEN = 0;
+
+ canSetArea = 0;
+ canSetAreaShape = 0;
+ canSetAreaHeight = 0;
+
+ class AttributeValues {};
+ class Attributes: AttributesBase {
+ class TaskID: Edit {
+ property = "FORGE_Module_Destroy_TaskID";
+ displayName = "Task ID";
+ tooltip = "Unique identifier for this task";
+ typeName = "STRING";
+ // defaultValue = """";
+ };
+ class LimitFail: Edit {
+ property = "FORGE_Module_Destroy_LimitFail";
+ displayName = "Fail Limit";
+ tooltip = "Number of targets that can escape before failing";
+ typeName = "NUMBER";
+ defaultValue = -1;
+ };
+ class LimitSuccess: Edit {
+ property = "FORGE_Module_Destroy_LimitSuccess";
+ displayName = "Success Limit";
+ tooltip = "Number of targets that need to be destroyed";
+ typeName = "NUMBER";
+ defaultValue = -1;
+ };
+ class CompanyFunds: Edit {
+ property = "FORGE_Module_Destroy_CompanyFunds";
+ displayName = "Reward Funds";
+ tooltip = "Amount of funds awarded on success";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ class RatingFail: Edit {
+ property = "FORGE_Module_Destroy_RatingFail";
+ displayName = "Rating Loss";
+ tooltip = "Amount of rating lost on failure";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ class RatingSuccess: Edit {
+ property = "FORGE_Module_Destroy_RatingSuccess";
+ displayName = "Rating Gain";
+ tooltip = "Amount of rating gained on success";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ class EndSuccess: Combo {
+ property = "FORGE_Module_Destroy_EndSuccess";
+ displayName = "End on Success";
+ tooltip = "End mission when task is completed successfully";
+ typeName = "BOOL";
+ defaultValue = 0;
+
+ class Values {
+ class EnableEndSuccess { name = "Enable"; value = 1; };
+ class DisableEndSuccess { name = "Disable"; value = 0; };
+ };
+ };
+ class EndFail: Combo {
+ property = "FORGE_Module_Destroy_EndFail";
+ displayName = "End on Failure";
+ tooltip = "End mission when task fails";
+ typeName = "BOOL";
+ defaultValue = 0;
+
+ class Values {
+ class EnableEndFail { name = "Enable"; value = 1; };
+ class DisableEndFail { name = "Disable"; value = 0; };
+ };
+ };
+ class TimeLimit: Edit {
+ property = "FORGE_Module_Destroy_TimeLimit";
+ displayName = "Time Limit";
+ tooltip = "Time in seconds before targets escape (0 for no limit)";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ };
+
+ class ModuleDescription: ModuleDescription {
+ description = "Creates a destroy task with configurable parameters";
+ sync[] = { "Anything" };
+
+ class Anything {
+ description[] = {
+ "Destroy task module",
+ "Sync with units and/or vehicles to mark as targets"
+ };
+ position = 1;
+ direction = 1;
+ optional = 1;
+ duplicate = 1;
+ };
+ };
+ };
+
+ class FORGE_Module_Hostage: Module_F {
+ scope = 2;
+ displayName = "Hostage Task";
+ // icon = "\a3\ui_f\data\IGUI\Cfg\simpleTasks\types\default_ca.paa";
+ category = "FORGE_Modules";
+
+ function = QFUNC(hostageModule);
+ functionPriority = 1;
+ isGlobal = 1;
+ isTriggerActivated = 1;
+ isDisposable = 1;
+ is3DEN = 0;
+
+ canSetArea = 0;
+ canSetAreaShape = 0;
+ canSetAreaHeight = 0;
+
+ class AttributeValues {};
+ class Attributes: AttributesBase {
+ class TaskID: Edit {
+ property = "FORGE_Module_Hostage_TaskID";
+ displayName = "Task ID";
+ tooltip = "Unique identifier for this task";
+ typeName = "STRING";
+ // defaultValue = """";
+ };
+ class LimitFail: Edit {
+ property = "FORGE_Module_Hostage_LimitFail";
+ displayName = "Fail Limit";
+ tooltip = "Number of hostages KIA before failing";
+ typeName = "NUMBER";
+ defaultValue = -1;
+ };
+ class LimitSuccess: Edit {
+ property = "FORGE_Module_Hostage_LimitSuccess";
+ displayName = "Success Limit";
+ tooltip = "Number of hostages rescued before succeeding";
+ typeName = "NUMBER";
+ defaultValue = -1;
+ };
+ class ExtZone: Edit {
+ property = "FORGE_Module_Hostage_ExtZone";
+ displayName = "Extraction Zone";
+ tooltip = "Unique marker name for the extraction zone";
+ typeName = "STRING";
+ // defaultValue = """";
+ };
+ class CompanyFunds: Edit {
+ property = "FORGE_Module_Hostage_CompanyFunds";
+ displayName = "Reward Funds";
+ tooltip = "Amount of funds awarded on success";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ class RatingFail: Edit {
+ property = "FORGE_Module_Hostage_RatingFail";
+ displayName = "Rating Loss";
+ tooltip = "Amount of rating lost on failure";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ class RatingSuccess: Edit {
+ property = "FORGE_Module_Hostage_RatingSuccess";
+ displayName = "Rating Gain";
+ tooltip = "Amount of rating gained on success";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ class CBRN: Combo {
+ property = "FORGE_Module_Hostage_CBRN";
+ displayName = "CBRN Attack";
+ tooltip = "CBRN Attack instead of execution";
+ typeName = "BOOL";
+ defaultValue = 0;
+
+ class Values {
+ class TrueCBRN { name = "True"; value = 1; };
+ class FalseCBRN { name = "False"; value = 0; };
+ };
+ };
+ class Execution: Combo {
+ property = "FORGE_Module_Hostage_Execution";
+ displayName = "Execution";
+ tooltip = "Execution instead of CBRN Attack";
+ typeName = "BOOL";
+ defaultValue = 0;
+
+ class Values {
+ class TrueExecution { name = "True"; value = 1; };
+ class FalseExecution { name = "False"; value = 0; };
+ };
+ };
+ class EndSuccess: Combo {
+ property = "FORGE_Module_Hostage_EndSuccess";
+ displayName = "End on Success";
+ tooltip = "End mission when task is completed successfully";
+ typeName = "BOOL";
+ defaultValue = 0;
+
+ class Values {
+ class EnableEndSuccess { name = "Enable"; value = 1; };
+ class DisableEndSuccess { name = "Disable"; value = 0; };
+ };
+ };
+ class EndFail: Combo {
+ property = "FORGE_Module_Hostage_EndFail";
+ displayName = "End on Failure";
+ tooltip = "End mission when task fails";
+ typeName = "BOOL";
+ defaultValue = 0;
+
+ class Values {
+ class EnableEndFail { name = "Enable"; value = 1; };
+ class DisableEndFail { name = "Disable"; value = 0; };
+ };
+ };
+ class TimeLimit: Edit {
+ property = "FORGE_Module_Hostage_TimeLimit";
+ displayName = "Time Limit";
+ tooltip = "Time in seconds before HVTs escape (0 for no limit)";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ class CBRNZone: Edit {
+ property = "FORGE_Module_Hostage_CBRNZone";
+ displayName = "CBRN Zone";
+ tooltip = "Unique marker name for the CBRN zone";
+ typeName = "STRING";
+ // defaultValue = """";
+ };
+ };
+
+ class ModuleDescription: ModuleDescription {
+ description = "Creates a Hostage task with configurable parameters";
+ sync[] = { "Anything" };
+
+ class Anything {
+ description[] = {
+ "Hostage task module",
+ "Sync with hostage and shooter module to register the entities to the task"
+ };
+ position = 1;
+ direction = 1;
+ optional = 1;
+ duplicate = 1;
+ };
+ };
+ };
+
+ class FORGE_Module_HVT: Module_F {
+ scope = 2;
+ displayName = "HVT Task";
+ // icon = "\a3\ui_f\data\IGUI\Cfg\simpleTasks\types\default_ca.paa";
+ category = "FORGE_Modules";
+
+ function = QFUNC(hvtModule);
+ functionPriority = 1;
+ isGlobal = 1;
+ isTriggerActivated = 1;
+ isDisposable = 1;
+ is3DEN = 0;
+
+ canSetArea = 0;
+ canSetAreaShape = 0;
+ canSetAreaHeight = 0;
+
+ class AttributeValues {};
+ class Attributes: AttributesBase {
+ class TaskID: Edit {
+ property = "FORGE_Module_HVT_TaskID";
+ displayName = "Task ID";
+ tooltip = "Unique identifier for this task";
+ typeName = "STRING";
+ // defaultValue = """";
+ };
+ class LimitFail: Edit {
+ property = "FORGE_Module_HVT_LimitFail";
+ displayName = "Fail Limit";
+ tooltip = "Number of hvts that can escape or KIA before failing";
+ typeName = "NUMBER";
+ defaultValue = -1;
+ };
+ class LimitSuccess: Edit {
+ property = "FORGE_Module_HVT_LimitSuccess";
+ displayName = "Success Limit";
+ tooltip = "Number of hvts that need to be captured or KIA";
+ typeName = "NUMBER";
+ defaultValue = -1;
+ };
+ class ExtZone: Edit {
+ property = "FORGE_Module_HVT_ExtZone";
+ displayName = "Extraction Zone";
+ tooltip = "Unique marker name for the extraction zone";
+ typeName = "STRING";
+ // defaultValue = """";
+ };
+ class CompanyFunds: Edit {
+ property = "FORGE_Module_HVT_CompanyFunds";
+ displayName = "Reward Funds";
+ tooltip = "Amount of funds awarded on success";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ class RatingFail: Edit {
+ property = "FORGE_Module_HVT_RatingFail";
+ displayName = "Rating Loss";
+ tooltip = "Amount of rating lost on failure";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ class RatingSuccess: Edit {
+ property = "FORGE_Module_HVT_RatingSuccess";
+ displayName = "Rating Gain";
+ tooltip = "Amount of rating gained on success";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ class CaptureHVT: Combo {
+ property = "FORGE_Module_HVT_CaptureHVT";
+ displayName = "Capture HVT";
+ tooltip = "Capture HVT instead of eliminating";
+ typeName = "BOOL";
+ defaultValue = 1;
+
+ class Values {
+ class TrueCapture { name = "True"; value = 1; };
+ class FalseCapture { name = "False"; value = 0; };
+ };
+ };
+ class EliminateHVT: Combo {
+ property = "FORGE_Module_HVT_EliminateHVT";
+ displayName = "Eliminate HVT";
+ tooltip = "Eliminate HVT instead of capturing";
+ typeName = "BOOL";
+ defaultValue = 0;
+
+ class Values {
+ class TrueEliminate { name = "True"; value = 1; };
+ class FalseEliminate { name = "False"; value = 0; };
+ };
+ };
+ class EndSuccess: Combo {
+ property = "FORGE_Module_HVT_EndSuccess";
+ displayName = "End on Success";
+ tooltip = "End mission when task is completed successfully";
+ typeName = "BOOL";
+ defaultValue = 0;
+
+ class Values {
+ class EnableEndSuccess { name = "Enable"; value = 1; };
+ class DisableEndSuccess { name = "Disable"; value = 0; };
+ };
+ };
+ class EndFail: Combo {
+ property = "FORGE_Module_HVT_EndFail";
+ displayName = "End on Failure";
+ tooltip = "End mission when task fails";
+ typeName = "BOOL";
+ defaultValue = 0;
+
+ class Values {
+ class EnableEndFail { name = "Enable"; value = 1; };
+ class DisableEndFail { name = "Disable"; value = 0; };
+ };
+ };
+ class TimeLimit: Edit {
+ property = "FORGE_Module_HVT_TimeLimit";
+ displayName = "Time Limit";
+ tooltip = "Time in seconds before HVTs escape (0 for no limit)";
+ typeName = "NUMBER";
+ defaultValue = 0;
+ };
+ };
+
+ class ModuleDescription: ModuleDescription {
+ description = "Creates a HVT task with configurable parameters";
+ sync[] = { "Anything" };
+
+ class Anything {
+ description[] = {
+ "HVT task module",
+ "Sync with units to mark as HVTs"
+ };
+ position = 1;
+ direction = 1;
+ optional = 1;
+ duplicate = 1;
+ };
+ };
+ };
+};
diff --git a/arma/server/addons/task/README.md b/arma/server/addons/task/README.md
new file mode 100644
index 0000000..3ea76e3
--- /dev/null
+++ b/arma/server/addons/task/README.md
@@ -0,0 +1,104 @@
+# Forge Task Module
+
+## Overview
+The task addon is a server-owned mission/task system for Forge. It manages task execution, task-owned state, participant tracking, contribution-based player earnings, and org-owned rewards.
+
+## Responsibilities
+- spawn and monitor task flows on the server
+- track per-task entities through `TaskStore`
+- track task participants and engine-rating contribution
+- award player earnings through the bank module
+- award org funds, reputation, assets, and fleet rewards
+- notify task participants and sync org updates to online members
+
+## Dependencies
+- `forge_server_common`
+- `forge_server_actor`
+- `forge_server_bank`
+- `forge_server_org`
+- `forge_client_notifications`
+
+## Main Components
+
+### Task Flows
+- `fnc_attack.sqf`
+- `fnc_defend.sqf`
+- `fnc_defuse.sqf`
+- `fnc_delivery.sqf`
+- `fnc_destroy.sqf`
+- `fnc_hostage.sqf`
+- `fnc_hvt.sqf`
+
+### TaskStore
+`fnc_initTaskStore.sqf` initializes `TaskStore`, which owns:
+- task ownership bindings
+- participant snapshots
+- defuse progress
+- per-task entity registries for cargo, hostages, HVTs, IEDs, protected entities, shooters, and targets
+
+### Reward Handling
+`fnc_handleTaskRewards.sqf` applies org-owned rewards:
+- `funds` -> org funds
+- `equipment`, `supplies`, `weapons`, `special` -> org assets
+- `vehicles` -> org fleet
+
+Player `earnings` and org `reputation` from task outcomes are distributed separately through `TaskStore.applyRatingOutcome` using Arma engine `rating` deltas.
+
+## Task Ownership
+Tasks are bound to an owner org when they are started through `fnc_handler.sqf`.
+
+- if a requester UID is provided, the task is owned by that requester's org
+- if no requester UID is available, the task is bound to the `default` org
+
+Org rewards always go to the bound owner org. Player earnings still use per-player contribution.
+
+## Usage
+
+### Start Through The Handler
+Use the handler when you want reputation gating and task ownership binding.
+
+```sqf
+["attack", ["task_attack_1", 1, 2, 1500000, -75, 375, false, false], 250, getPlayerUID player] call forge_server_task_fnc_handler;
+["delivery", ["task_delivery_1", 1, 3, "delivery_zone", 250000, -75, 300, false, false, 900], 0, getPlayerUID player] call forge_server_task_fnc_handler;
+```
+
+Arguments:
+- `0`: task type
+- `1`: task-specific argument array
+- `2`: minimum org reputation required to start the task
+- `3`: requester UID used for ownership binding
+
+### Start Task Functions Directly
+Direct task calls still work, but they do not provide a requester UID. That means task ownership falls back to the `default` org.
+
+Use direct starts only when that behavior is intended, such as:
+- mission-authored tasks
+- editor-placed tasks
+- server-owned/random tasks
+
+If you want the accepting player's org to own the task rewards, use `fnc_handler.sqf` instead.
+
+```sqf
+["task_attack_1", 1, 2, 1500000, -75, 375, false, false] spawn forge_server_task_fnc_attack;
+["task_hostage_1", 1, 2, "extract_marker", 1500000, -75, 500, [false, true], false, false] spawn forge_server_task_fnc_hostage;
+```
+
+## Event Hooks
+- `XEH_preInit.sqf`
+ - compiles functions
+ - initializes `TaskStore`
+- `XEH_postInit.sqf`
+ - registers the ACE defuse event hook
+ - starts the attack-only mission manager on the server
+
+## Notes
+- the dynamic mission manager in `fnc_missionManager.sqf` is now limited to attack missions only
+- it starts server-owned tasks through `fnc_handler.sqf` and binds them to the `default` org
+- task lifecycle for the mission manager is tracked through `TaskStore` status entries
+- task rewards are org-owned, not player-owned
+- participant notifications are sent through the notifications module, not through local server UI
+
+## Authors
+- J. Schmidt
+- Creedcoder
+- IDSolutions
diff --git a/arma/server/addons/task/XEH_PREP.hpp b/arma/server/addons/task/XEH_PREP.hpp
new file mode 100644
index 0000000..1fb187a
--- /dev/null
+++ b/arma/server/addons/task/XEH_PREP.hpp
@@ -0,0 +1,31 @@
+PREP(attack);
+PREP(attackModule);
+PREP(defend);
+PREP(defendModule);
+PREP(defuse);
+PREP(defuseModule);
+PREP(delivery);
+PREP(deliveryModule);
+PREP(destroy);
+PREP(destroyModule);
+PREP(explosivesModule);
+PREP(handler);
+PREP(handleTaskRewards);
+PREP(heartBeat);
+PREP(hostage);
+PREP(hostageModule);
+PREP(hostagesModule);
+PREP(hvt);
+PREP(hvtModule);
+PREP(makeCargo);
+PREP(makeHostage);
+PREP(makeHVT);
+PREP(makeIED);
+PREP(makeObject);
+PREP(makeShooter);
+PREP(makeTarget);
+PREP(missionManager);
+PREP(initTaskStore);
+PREP(protectedModule);
+PREP(shootersModule);
+PREP(spawnEnemyWave);
diff --git a/arma/server/addons/task/XEH_postInit.sqf b/arma/server/addons/task/XEH_postInit.sqf
new file mode 100644
index 0000000..5dcafcd
--- /dev/null
+++ b/arma/server/addons/task/XEH_postInit.sqf
@@ -0,0 +1,16 @@
+#include "script_component.hpp"
+
+if (isServer) then { [] call FUNC(missionManager); };
+
+["ace_explosives_defuse", {
+ private _taskID = "";
+ {
+ if (_x isEqualType objNull && { !isNull _x }) then {
+ _taskID = _x getVariable ["assignedTask", ""];
+ if (_taskID isNotEqualTo "") exitWith {};
+ };
+ } forEach _this;
+
+ if (_taskID isEqualTo "") exitWith {};
+ GVAR(TaskStore) call ["incrementDefuseCount", [_taskID]];
+}] call CFUNC(addEventHandler);
diff --git a/arma/server/addons/task/XEH_preInit.sqf b/arma/server/addons/task/XEH_preInit.sqf
new file mode 100644
index 0000000..40139e4
--- /dev/null
+++ b/arma/server/addons/task/XEH_preInit.sqf
@@ -0,0 +1,35 @@
+#include "script_component.hpp"
+
+PREP_RECOMPILE_START;
+#include "XEH_PREP.hpp"
+PREP_RECOMPILE_END;
+
+call FUNC(initTaskStore);
+
+[QGVAR(requestTaskCatalog), {
+ params [["_uid", "", [""]]];
+
+ if (_uid isEqualTo "") exitWith {
+ ["WARNING", "Task catalog request received with empty UID."] call EFUNC(common,log);
+ };
+
+ private _player = [_uid] call EFUNC(common,getPlayer);
+ if (_player isEqualTo objNull) exitWith {};
+
+ [CRPC(cad,responseTaskCatalog), [GVAR(TaskStore) call ["getActiveTaskCatalog", []]], _player] call CFUNC(targetEvent);
+}] call CFUNC(addEventHandler);
+
+[QGVAR(requestAcceptTask), {
+ params [["_uid", "", [""]], ["_taskID", "", [""]]];
+
+ if (_uid isEqualTo "" || { _taskID isEqualTo "" }) exitWith {
+ ["WARNING", "Invalid task accept request payload."] call EFUNC(common,log);
+ };
+
+ private _player = [_uid] call EFUNC(common,getPlayer);
+ if (_player isEqualTo objNull) exitWith {};
+
+ private _result = GVAR(TaskStore) call ["acceptTask", [_taskID, _uid]];
+ [CRPC(cad,responseTaskAccept), [_result], _player] call CFUNC(targetEvent);
+ [CRPC(cad,responseTaskCatalog), [GVAR(TaskStore) call ["getActiveTaskCatalog", []]], _player] call CFUNC(targetEvent);
+}] call CFUNC(addEventHandler);
diff --git a/arma/server/addons/task/XEH_preStart.sqf b/arma/server/addons/task/XEH_preStart.sqf
new file mode 100644
index 0000000..a51262a
--- /dev/null
+++ b/arma/server/addons/task/XEH_preStart.sqf
@@ -0,0 +1,2 @@
+#include "script_component.hpp"
+#include "XEH_PREP.hpp"
diff --git a/arma/server/addons/task/config.cpp b/arma/server/addons/task/config.cpp
new file mode 100644
index 0000000..fa97541
--- /dev/null
+++ b/arma/server/addons/task/config.cpp
@@ -0,0 +1,23 @@
+#include "script_component.hpp"
+
+class CfgPatches {
+ class ADDON {
+ author = AUTHOR;
+ authors[] = {"J.Schmidt"};
+ url = ECSTRING(main,url);
+ name = COMPONENT_NAME;
+ requiredVersion = REQUIRED_VERSION;
+ requiredAddons[] = {
+ "forge_server_main",
+ "forge_server_common"
+ };
+ units[] = {};
+ weapons[] = {};
+ VERSION_CONFIG;
+ };
+};
+
+#include "CfgEventHandlers.hpp"
+#include "CfgFactionClasses.hpp"
+#include "CfgVehicles.hpp"
+#include "CfgMissions.hpp"
diff --git a/arma/server/addons/task/functions/fnc_attack.sqf b/arma/server/addons/task/functions/fnc_attack.sqf
new file mode 100644
index 0000000..ef91a6e
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_attack.sqf
@@ -0,0 +1,116 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Registers an attack task
+ *
+ * Arguments:
+ * 0: ID of the task
+ * 1: Amount of targets escaped to fail the task
+ * 2: Amount of targets eliminated to complete the task
+ * 3: Amount of funds the company recieves if the task is successful (default: 0)
+ * 4: Amount of rating the company and player lose if the task is failed (default: 0)
+ * 5: Amount of rating the company and player recieve if the task is successful (default: 0)
+ * 6: Should the mission end (MissionSuccess) if the task is successful (default: false)
+ * 7: Should the mission end (MissionFailed) if the task is failed (default: false)
+ * 8: Amount of time before target(s) escape (default: -1)
+ * 9: Equipment rewards (default: [])
+ * 10: Supply rewards (default: [])
+ * 11: Weapon rewards (default: [])
+ * 12: Vehicle rewards (default: [])
+ * 13: Special rewards (default: [])
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * ["task_name", 1, 2, 1500000, -75, 375, false, false] spawn forge_server_task_fnc_attack;
+ * ["task_name", 1, 2, 1500000, -75, 375, false, false, 45] spawn forge_server_task_fnc_attack;
+ *
+ * Public: Yes
+ */
+
+params [
+ ["_taskID", "", [""]],
+ ["_limitFail", -1, [0]],
+ ["_limitSuccess", -1, [0]],
+ ["_companyFunds", 0, [0]],
+ ["_ratingFail", 0, [0]],
+ ["_ratingSuccess", 0, [0]],
+ ["_endSuccess", false, [false]],
+ ["_endFail", false, [false]],
+ ["_time", -1, [0]],
+ ["_equipmentRewards", [], [[]]],
+ ["_supplyRewards", [], [[]]],
+ ["_weaponRewards", [], [[]]],
+ ["_vehicleRewards", [], [[]]],
+ ["_specialRewards", [], [[]]]
+];
+
+private _result = 0;
+private _targets = [];
+
+waitUntil {
+ sleep 1;
+ _targets = GVAR(TaskStore) call ["getTaskEntities", ["targets", _taskID]];
+ GVAR(TaskStore) call ["trackParticipants", [_taskID, _targets, "", 300]];
+ count _targets > 0
+};
+
+_targets = GVAR(TaskStore) call ["getTaskEntities", ["targets", _taskID]];
+private _startTime = if (!isNil "_time") then { floor(time) } else { nil };
+
+waitUntil {
+ sleep 1;
+ GVAR(TaskStore) call ["trackParticipants", [_taskID, _targets, "", 300]];
+
+ private _targetsKilled = ({ !alive _x } count _targets);
+
+ if (_time isNotEqualTo -1) then {
+ private _timeExpired = (floor time - _startTime >= _time);
+
+ if (_targetsKilled < _limitSuccess && _timeExpired) then { _result = 1; };
+
+ (_result == 1) or (_targetsKilled >= _limitSuccess)
+ } else {
+ (_targetsKilled >= _limitSuccess)
+ };
+};
+
+if (_result == 1) then {
+ { deleteVehicle _x } forEach _targets;
+
+ [_taskID, "FAILED"] call BFUNC(taskSetState);
+ GVAR(TaskStore) call ["setTaskStatus", [_taskID, "failed"]];
+
+ sleep 1;
+
+ GVAR(TaskStore) call ["notifyParticipants", [_taskID, "warning", "Tasks", format ["Task failed: %1 reputation", _ratingFail]]];
+ GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingFail]];
+ GVAR(TaskStore) call ["clearTask", [_taskID]];
+
+ if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
+} else {
+ { deleteVehicle _x } forEach _targets;
+
+ private _rewards = createHashMap;
+ _rewards set ["funds", _companyFunds];
+
+ if (_equipmentRewards isNotEqualTo []) then { _rewards set ["equipment", _equipmentRewards]; };
+ if (_supplyRewards isNotEqualTo []) then { _rewards set ["supplies", _supplyRewards]; };
+ if (_weaponRewards isNotEqualTo []) then { _rewards set ["weapons", _weaponRewards]; };
+ if (_vehicleRewards isNotEqualTo []) then { _rewards set ["vehicles", _vehicleRewards]; };
+ if (_specialRewards isNotEqualTo []) then { _rewards set ["special", _specialRewards]; };
+
+ [_taskID, _rewards] call FUNC(handleTaskRewards);
+ [_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
+ GVAR(TaskStore) call ["setTaskStatus", [_taskID, "succeeded"]];
+
+ sleep 1;
+
+ GVAR(TaskStore) call ["notifyParticipants", [_taskID, "success", "Tasks", format ["Task completed: %1 reputation, $%2 funds", _ratingSuccess, [_companyFunds] call EFUNC(common,formatNumber)]]];
+ GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingSuccess]];
+ GVAR(TaskStore) call ["clearTask", [_taskID]];
+
+ if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
+};
diff --git a/arma/server/addons/task/functions/fnc_attackModule.sqf b/arma/server/addons/task/functions/fnc_attackModule.sqf
new file mode 100644
index 0000000..7a364d6
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_attackModule.sqf
@@ -0,0 +1,51 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Initializes the attack module
+ *
+ * Arguments:
+ * 0: Logic - The logic object
+ * 1: Units - The array of units
+ * 2: Activated - Whether the module is activated
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * [logicObject, [unit1, unit2], true] call forge_server_task_fnc_attackModule;
+ *
+ * Public: No
+ */
+
+params [["_logic", objNull, [objNull]], ["_units", [], [[]]], ["_activated", true, [true]]];
+
+if !(_activated) exitWith {};
+
+private _taskID = _logic getVariable ["TaskID", ""];
+private _limitFail = _logic getVariable ["LimitFail", -1];
+private _limitSuccess = _logic getVariable ["LimitSuccess", -1];
+private _companyFunds = _logic getVariable ["CompanyFunds", 0];
+private _ratingFail = _logic getVariable ["RatingFail", 0];
+private _ratingSuccess = _logic getVariable ["RatingSuccess", 0];
+private _endSuccess = _logic getVariable ["EndSuccess", false];
+private _endFail = _logic getVariable ["EndFail", false];
+private _timeLimit = _logic getVariable ["TimeLimit", 0];
+
+["INFO", format ["Attack Module Parameters: TaskID: %1, LimitFail: %2, LimitSuccess: %3, Funds: %4, RatingFail: %5, RatingSuccess: %6, EndSuccess: %7, EndFail: %8, Time: %9", _taskID, _limitFail, _limitSuccess, _companyFunds, _ratingFail, _ratingSuccess, _endSuccess, _endFail, _timeLimit]] call EFUNC(common,log);
+
+private _syncedEntities = synchronizedObjects _logic;
+["INFO", format ["Attack Module Synced Entities: %1", _syncedEntities]] call EFUNC(common,log);
+
+{
+ [_x, _taskID] spawn FUNC(makeTarget);
+} forEach _syncedEntities;
+
+private _params = [_taskID, _limitFail, _limitSuccess, _companyFunds, _ratingFail, _ratingSuccess, _endSuccess, _endFail];
+if (_timeLimit != 0) then {
+ _params pushBack _timeLimit;
+};
+
+["attack", _params, 0, ""] spawn FUNC(handler);
+
+deleteVehicle _logic;
diff --git a/arma/server/addons/task/functions/fnc_defend.sqf b/arma/server/addons/task/functions/fnc_defend.sqf
new file mode 100644
index 0000000..c1e7b20
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_defend.sqf
@@ -0,0 +1,126 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Registers a defend task where players must hold a zone marked by a marker
+ *
+ * Arguments:
+ * 0: ID of the task
+ * 1: Defense zone marker name
+ * 2: Time to defend in seconds
+ * 3: Amount of funds the company receives if the task is successful (default: 0)
+ * 4: Amount of rating the company and player lose if the task is failed (default: 0)
+ * 5: Amount of rating the company and player receive if the task is successful (default: 0)
+ * 6: Should the mission end (MissionSuccess) if the task is successful (default: false)
+ * 7: Should the mission end (MissionFailed) if the task is failed (default: false)
+ * 8: Enemy wave count (default: 3)
+ * 9: Time between waves in seconds (default: 300)
+ * 10: Minimum BLUFOR units required in zone (default: 1)
+ * 11: Equipment rewards (default: [])
+ * 12: Supply rewards (default: [])
+ * 13: Weapon rewards (default: [])
+ * 14: Vehicle rewards (default: [])
+ * 15: Special rewards (default: [])
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * ["defend_zone_1", "defend_marker", 900, 500000, -100, 400, false, false, 3, 300, 1, ["ItemGPS"], ["FirstAidKit"], ["arifle_MX_F"], ["B_MRAP_01_F"], ["B_UAV_01_F"]] spawn forge_server_task_fnc_defend;
+ *
+ * Public: Yes
+ */
+
+params [
+ ["_taskID", "", [""]],
+ ["_defenseZone", "", [""]],
+ ["_defendTime", 600, [0]],
+ ["_companyFunds", 0, [0]],
+ ["_ratingFail", 0, [0]],
+ ["_ratingSuccess", 0, [0]],
+ ["_endSuccess", false, [false]],
+ ["_endFail", false, [false]],
+ ["_waveCount", 3, [0]],
+ ["_waveCooldown", 300, [0]],
+ ["_minBlufor", 1, [0]],
+ ["_equipmentRewards", [], [[]]],
+ ["_supplyRewards", [], [[]]],
+ ["_weaponRewards", [], [[]]],
+ ["_vehicleRewards", [], [[]]],
+ ["_specialRewards", [], [[]]]
+];
+
+if (_defenseZone == "" || !(markerShape _defenseZone in ["RECTANGLE", "ELLIPSE"])) exitWith {
+ ["ERROR", format ["Invalid defense zone marker: %1", _defenseZone]] call EFUNC(common,log);
+};
+
+private _result = 0;
+private _startTime = time;
+private _nextWaveTime = _startTime;
+private _currentWave = 0;
+private _zoneEmptyCounter = 0;
+private _warningIssued = false;
+
+waitUntil {
+ sleep 1;
+ GVAR(TaskStore) call ["trackParticipants", [_taskID, [], _defenseZone, 0]];
+ private _bluforInZone = count (allUnits select { _x isKindOf "CAManBase" && { side _x == west } && { alive _x }} inAreaArray _defenseZone);
+ private _timeElapsed = time - _startTime;
+
+ if (_bluforInZone < _minBlufor) then {
+ _zoneEmptyCounter = _zoneEmptyCounter + 1;
+
+ if (_zoneEmptyCounter == 15 && !_warningIssued) then {
+ GVAR(TaskStore) call ["notifyParticipants", [_taskID, "warning", "Tasks", "Defense zone is empty. Return immediately."]];
+ _warningIssued = true;
+ };
+ } else {
+ _zoneEmptyCounter = 0;
+ _warningIssued = false;
+ };
+
+ if (_currentWave < _waveCount && time >= _nextWaveTime) then {
+ [_defenseZone, _taskID, _currentWave] call FUNC(spawnEnemyWave);
+
+ _currentWave = _currentWave + 1;
+ _nextWaveTime = time + _waveCooldown;
+
+ GVAR(TaskStore) call ["notifyParticipants", [_taskID, "info", "Tasks", format ["Enemy forces approaching. Wave %1 of %2.", _currentWave, _waveCount]]];
+ };
+
+ if (_zoneEmptyCounter >= 30) then { _result = 1; };
+
+ (_result == 1) or ((_bluforInZone >= _minBlufor) && (_timeElapsed >= _defendTime) && (_currentWave >= _waveCount));
+};
+
+if (_result == 1) then {
+ [_taskID, "FAILED"] call BFUNC(taskSetState);
+
+ sleep 1;
+
+ GVAR(TaskStore) call ["notifyParticipants", [_taskID, "warning", "Tasks", format ["Task failed: %1 reputation", _ratingFail]]];
+ GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingFail]];
+ GVAR(TaskStore) call ["clearTask", [_taskID]];
+
+ if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
+} else {
+ private _rewards = createHashMap;
+ _rewards set ["funds", _companyFunds];
+
+ if (_equipmentRewards isNotEqualTo []) then { _rewards set ["equipment", _equipmentRewards]; };
+ if (_supplyRewards isNotEqualTo []) then { _rewards set ["supplies", _supplyRewards]; };
+ if (_weaponRewards isNotEqualTo []) then { _rewards set ["weapons", _weaponRewards]; };
+ if (_vehicleRewards isNotEqualTo []) then { _rewards set ["vehicles", _vehicleRewards]; };
+ if (_specialRewards isNotEqualTo []) then { _rewards set ["special", _specialRewards]; };
+
+ [_taskID, _rewards] call FUNC(handleTaskRewards);
+ [_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
+
+ sleep 1;
+
+ GVAR(TaskStore) call ["notifyParticipants", [_taskID, "success", "Tasks", format ["Task completed: %1 reputation, $%2 funds", _ratingSuccess, [_companyFunds] call EFUNC(common,formatNumber)]]];
+ GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingSuccess]];
+ GVAR(TaskStore) call ["clearTask", [_taskID]];
+
+ if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
+};
diff --git a/arma/server/addons/task/functions/fnc_defendModule.sqf b/arma/server/addons/task/functions/fnc_defendModule.sqf
new file mode 100644
index 0000000..8d8dbe3
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_defendModule.sqf
@@ -0,0 +1,61 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Creates a defend task module
+ *
+ * Arguments:
+ * None
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * call forge_server_task_fnc_defendModule;
+ *
+ * Public: No
+ */
+
+// Module category
+private _category = "Forge Tasks";
+private _subCategory = "Defense Tasks";
+
+// Create the module
+private _module = createDialog "RscDisplayAttributes";
+_module setVariable ["category", _category];
+_module setVariable ["subcategory", _subCategory];
+_module setVariable ["description", "Configure a defend task"];
+
+// Add fields for task configuration
+[_module, "Task ID", "taskID", "", true] call BIS_fnc_addAttribute;
+[_module, "Defense Zone Marker", "defenseZone", "", true] call BIS_fnc_addAttribute;
+[_module, "Defense Time (seconds)", "defendTime", "600", true] call BIS_fnc_addAttribute;
+[_module, "Min BLUFOR in Zone", "minBlufor", "1", true] call BIS_fnc_addAttribute;
+[_module, "Company Funds Reward", "companyFunds", "500000", true] call BIS_fnc_addAttribute;
+[_module, "Rating Loss on Fail", "ratingFail", "-100", true] call BIS_fnc_addAttribute;
+[_module, "Rating Gain on Success", "ratingSuccess", "400", true] call BIS_fnc_addAttribute;
+[_module, "End Mission on Success", "endSuccess", "false", false] call BIS_fnc_addAttribute;
+[_module, "End Mission on Fail", "endFail", "false", false] call BIS_fnc_addAttribute;
+[_module, "Enemy Wave Count", "waveCount", "3", false] call BIS_fnc_addAttribute;
+[_module, "Time Between Waves (seconds)", "waveCooldown", "300", false] call BIS_fnc_addAttribute;
+
+// Add confirm button handler
+_module setVariable ["onConfirm", {
+ params ["_module"];
+ private _taskID = _module getVariable ["taskID", ""];
+ private _defenseZone = _module getVariable ["defenseZone", ""];
+ private _defendTime = parseNumber (_module getVariable ["defendTime", "600"]);
+ private _companyFunds = parseNumber (_module getVariable ["companyFunds", "500000"]);
+ private _ratingFail = parseNumber (_module getVariable ["ratingFail", "-100"]);
+ private _ratingSuccess = parseNumber (_module getVariable ["ratingSuccess", "400"]);
+ private _endSuccess = _module getVariable ["endSuccess", "false"] == "true";
+ private _endFail = _module getVariable ["endFail", "false"] == "true";
+ private _waveCount = parseNumber (_module getVariable ["waveCount", "3"]);
+ private _waveCooldown = parseNumber (_module getVariable ["waveCooldown", "300"]);
+ private _minBlufor = parseNumber (_module getVariable ["minBlufor", "1"]);
+
+ // Create the task
+ private _params = [_taskID, _defenseZone, _defendTime, _companyFunds, _ratingFail, _ratingSuccess, _endSuccess, _endFail, _waveCount, _waveCooldown, _minBlufor];
+ private _requesterUid = ["", getPlayerUID player] select hasInterface;
+ ["defend", _params, 0, _requesterUid] spawn FUNC(handler);
+}];
diff --git a/arma/server/addons/task/functions/fnc_defuse.sqf b/arma/server/addons/task/functions/fnc_defuse.sqf
new file mode 100644
index 0000000..1c4d6b0
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_defuse.sqf
@@ -0,0 +1,114 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Registers a defuse task
+ *
+ * Arguments:
+ * 0: ID of the task
+ * 1: Amount of entities destroyed to fail the task
+ * 2: Amount of ieds defused to complete the task
+ * 3: Amount of funds the company recieves if the task is successful (default: 0)
+ * 4: Amount of rating the company and player lose if the task is failed (default: 0)
+ * 5: Amount of rating the company and player recieve if the task is successful (default: 0)
+ * 6: Should the mission end (MissionSuccess) if the task is successful (default: false)
+ * 7: Should the mission end (MissionFailed) if the task is failed (default: false)
+ * 8: Equipment rewards (default: [])
+ * 9: Supply rewards (default: [])
+ * 10: Weapon rewards (default: [])
+ * 11: Vehicle rewards (default: [])
+ * 12: Special rewards (default: [])
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * ["task_name", 2, 3, 375000, -75, 300, false, false] spawn forge_server_task_fnc_defuse;
+ *
+ * Public: Yes
+ */
+
+params [
+ ["_taskID", "", [""]],
+ ["_limitFail", -1, [0]],
+ ["_limitSuccess", -1, [0]],
+ ["_companyFunds", 0, [0]],
+ ["_ratingFail", 0, [0]],
+ ["_ratingSuccess", 0, [0]],
+ ["_endSuccess", false, [false]],
+ ["_endFail", false, [false]],
+ ["_equipmentRewards", [], [[]]],
+ ["_supplyRewards", [], [[]]],
+ ["_weaponRewards", [], [[]]],
+ ["_vehicleRewards", [], [[]]],
+ ["_specialRewards", [], [[]]]
+];
+
+private _result = 0;
+private _ieds = [];
+private _entities = [];
+
+waitUntil {
+ sleep 1;
+ _ieds = GVAR(TaskStore) call ["getTaskEntities", ["ieds", _taskID]];
+ count _ieds > 0
+};
+
+waitUntil {
+ sleep 1;
+ _entities = GVAR(TaskStore) call ["getTaskEntities", ["entities", _taskID]];
+ GVAR(TaskStore) call ["trackParticipants", [_taskID, _ieds + _entities, "", 250]];
+ count _entities > 0
+};
+
+_ieds = GVAR(TaskStore) call ["getTaskEntities", ["ieds", _taskID]];
+_entities = GVAR(TaskStore) call ["getTaskEntities", ["entities", _taskID]];
+
+waitUntil {
+ sleep 1;
+ GVAR(TaskStore) call ["trackParticipants", [_taskID, _ieds + _entities, "", 250]];
+
+ private _entitiesDestroyed = ({ !alive _x } count _entities);
+
+ if (_entitiesDestroyed >= _limitFail) then { _result = 1; };
+
+ (_result == 1) or ((GVAR(TaskStore) call ["getDefuseCount", [_taskID]]) >= _limitSuccess && (_entitiesDestroyed < _limitFail))
+};
+
+if (_result == 1) then {
+ { deleteVehicle _x } forEach _ieds;
+ { deleteVehicle _x } forEach _entities;
+
+ [_taskID, "FAILED"] call BFUNC(taskSetState);
+
+ sleep 1;
+
+ GVAR(TaskStore) call ["notifyParticipants", [_taskID, "warning", "Tasks", format ["Task failed: %1 reputation", _ratingFail]]];
+ GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingFail]];
+
+ if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
+} else {
+ { deleteVehicle _x } forEach _ieds;
+ { deleteVehicle _x } forEach _entities;
+
+ private _rewards = createHashMap;
+ _rewards set ["funds", _companyFunds];
+
+ if (_equipmentRewards isNotEqualTo []) then { _rewards set ["equipment", _equipmentRewards]; };
+ if (_supplyRewards isNotEqualTo []) then { _rewards set ["supplies", _supplyRewards]; };
+ if (_weaponRewards isNotEqualTo []) then { _rewards set ["weapons", _weaponRewards]; };
+ if (_vehicleRewards isNotEqualTo []) then { _rewards set ["vehicles", _vehicleRewards]; };
+ if (_specialRewards isNotEqualTo []) then { _rewards set ["special", _specialRewards]; };
+
+ [_taskID, _rewards] call FUNC(handleTaskRewards);
+ [_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
+
+ sleep 1;
+
+ GVAR(TaskStore) call ["notifyParticipants", [_taskID, "success", "Tasks", format ["Task completed: %1 reputation, $%2 funds", _ratingSuccess, [_companyFunds] call EFUNC(common,formatNumber)]]];
+ GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingSuccess]];
+
+ if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
+};
+
+GVAR(TaskStore) call ["clearTask", [_taskID]];
diff --git a/arma/server/addons/task/functions/fnc_defuseModule.sqf b/arma/server/addons/task/functions/fnc_defuseModule.sqf
new file mode 100644
index 0000000..759c62b
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_defuseModule.sqf
@@ -0,0 +1,64 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Initializes the defuse module
+ *
+ * Arguments:
+ * 0: Logic - The logic object
+ * 1: Units - The array of units
+ * 2: Activated - Whether the module is activated
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * [logicObject, [unit1, unit2], true] call forge_server_task_fnc_defuseModule;
+ *
+ * Public: No
+ */
+
+params [["_logic", objNull, [objNull]], ["_units", [], [[]]], ["_activated", true, [true]]];
+
+if !(_activated) exitWith {};
+
+private _taskID = _logic getVariable ["TaskID", ""];
+private _limitFail = _logic getVariable ["LimitFail", -1];
+private _limitSuccess = _logic getVariable ["LimitSuccess", -1];
+private _companyFunds = _logic getVariable ["CompanyFunds", 0];
+private _ratingFail = _logic getVariable ["RatingFail", 0];
+private _ratingSuccess = _logic getVariable ["RatingSuccess", 0];
+private _endSuccess = _logic getVariable ["EndSuccess", false];
+private _endFail = _logic getVariable ["EndFail", false];
+private _timeLimit = _logic getVariable ["TimeLimit", 0];
+
+["INFO", format ["Defuse Module Parameters: TaskID: %1, LimitFail: %2, LimitSuccess: %3, Funds: %4, RatingFail: %5, RatingSuccess: %6, EndSuccess: %7, EndFail: %8, Time: %9", _taskID, _limitFail, _limitSuccess, _companyFunds, _ratingFail, _ratingSuccess, _endSuccess, _endFail, _timeLimit]] call EFUNC(common,log);
+
+private _syncedModules = synchronizedObjects _logic;
+["INFO", format ["Defuse Module Synced Modules: %1", _syncedModules]] call EFUNC(common,log);
+
+private _iedModule = (_syncedModules select { typeOf _x == "FORGE_Module_Explosives" }) select 0;
+private _protectedModule = (_syncedModules select { typeOf _x == "FORGE_Module_Protected" }) select 0;
+
+private _explosiveEntities = synchronizedObjects _iedModule;
+["INFO", format ["Defuse Module Explosive Entites: %1", _explosiveEntities]] call EFUNC(common,log);
+
+private _protectedEntities = synchronizedObjects _protectedModule;
+["INFO", format ["Defuse Module Protected Entities: %1", _protectedEntities]] call EFUNC(common,log);
+
+{
+ if (!isNull _x) then {
+ [_x, _taskID, _timeLimit] spawn FUNC(makeIED);
+ };
+} forEach _explosiveEntities;
+
+{
+ if (!isNull _x) then {
+ [_x, _taskID] spawn FUNC(makeObject);
+ };
+} forEach _protectedEntities;
+
+private _params = [_taskID, _limitFail, _limitSuccess, _companyFunds, _ratingFail, _ratingSuccess, _endSuccess, _endFail];
+["defuse", _params, 0, ""] spawn FUNC(handler);
+
+deleteVehicle _logic;
diff --git a/arma/server/addons/task/functions/fnc_delivery.sqf b/arma/server/addons/task/functions/fnc_delivery.sqf
new file mode 100644
index 0000000..1088e0f
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_delivery.sqf
@@ -0,0 +1,120 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Registers a delivery task
+ *
+ * Arguments:
+ * 0: ID of the task
+ * 1: Amount of damaged cargo to fail the task
+ * 2: Amount of cargo delivered to complete the task
+ * 3: Marker name for the delivery zone
+ * 4: Amount of funds the company receives if the task is successful (default: 0)
+ * 5: Amount of rating the company and player lose if the task is failed (default: 0)
+ * 6: Amount of rating the company and player receive if the task is successful (default: 0)
+ * 7: Should the mission end (MissionSuccess) if the task is successful (default: false)
+ * 8: Should the mission end (MissionFailed) if the task is failed (default: false)
+ * 9: Amount of time to complete delivery (default: -1)
+ * 10: Equipment rewards (default: [])
+ * 11: Supply rewards (default: [])
+ * 12: Weapon rewards (default: [])
+ * 13: Vehicle rewards (default: [])
+ * 14: Special rewards (default: [])
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * ["delivery_1", 1, 3, "delivery_zone", 250000, -75, 300, false, false] spawn forge_server_task_fnc_delivery;
+ * ["delivery_1", 1, 3, "delivery_zone", 250000, -75, 300, false, false, 900] spawn forge_server_task_fnc_delivery;
+ *
+ * Public: Yes
+ */
+
+params [
+ ["_taskID", "", [""]],
+ ["_limitFail", -1, [0]],
+ ["_limitSuccess", -1, [0]],
+ ["_deliveryZone", "", [""]],
+ ["_companyFunds", 0, [0]],
+ ["_ratingFail", 0, [0]],
+ ["_ratingSuccess", 0, [0]],
+ ["_endSuccess", false, [false]],
+ ["_endFail", false, [false]],
+ ["_time", -1, [0]],
+ ["_equipmentRewards", [], [[]]],
+ ["_supplyRewards", [], [[]]],
+ ["_weaponRewards", [], [[]]],
+ ["_vehicleRewards", [], [[]]],
+ ["_specialRewards", [], [[]]]
+];
+
+private _result = 0;
+private _cargo = [];
+
+waitUntil {
+ sleep 1;
+ _cargo = GVAR(TaskStore) call ["getTaskEntities", ["cargo", _taskID]];
+ GVAR(TaskStore) call ["trackParticipants", [_taskID, _cargo, _deliveryZone, 125]];
+ count _cargo > 0
+};
+
+_cargo = GVAR(TaskStore) call ["getTaskEntities", ["cargo", _taskID]];
+private _startTime = if (_time isNotEqualTo -1) then { floor(time) } else { nil };
+
+waitUntil {
+ sleep 1;
+ GVAR(TaskStore) call ["trackParticipants", [_taskID, _cargo, _deliveryZone, 125]];
+
+ private _cargoDelivered = ({ _x inArea _deliveryZone && (damage _x) < 0.7 } count _cargo);
+ private _cargoDamaged = ({ damage _x >= 0.7 } count _cargo);
+
+ if (_time isNotEqualTo -1) then {
+ private _timeExpired = (floor time - _startTime >= _time);
+
+ if (_cargoDamaged >= _limitFail) then { _result = 1; };
+ if (_cargoDelivered < _limitSuccess && _timeExpired) then { _result = 1; };
+
+ (_result == 1) or ((_cargoDelivered >= _limitSuccess) && (_cargoDamaged < _limitFail))
+ } else {
+ if (_cargoDamaged >= _limitFail) then { _result = 1; };
+
+ (_result == 1) or ((_cargoDelivered >= _limitSuccess) && (_cargoDamaged < _limitFail))
+ };
+};
+
+if (_result == 1) then {
+ { deleteVehicle _x } forEach _cargo;
+
+ [_taskID, "FAILED"] call BFUNC(taskSetState);
+
+ sleep 1;
+
+ GVAR(TaskStore) call ["notifyParticipants", [_taskID, "warning", "Tasks", format ["Task failed: %1 reputation", _ratingFail]]];
+ GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingFail]];
+ GVAR(TaskStore) call ["clearTask", [_taskID]];
+
+ if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
+} else {
+ { deleteVehicle _x } forEach _cargo;
+
+ private _rewards = createHashMap;
+ _rewards set ["funds", _companyFunds];
+
+ if (_equipmentRewards isNotEqualTo []) then { _rewards set ["equipment", _equipmentRewards]; };
+ if (_supplyRewards isNotEqualTo []) then { _rewards set ["supplies", _supplyRewards]; };
+ if (_weaponRewards isNotEqualTo []) then { _rewards set ["weapons", _weaponRewards]; };
+ if (_vehicleRewards isNotEqualTo []) then { _rewards set ["vehicles", _vehicleRewards]; };
+ if (_specialRewards isNotEqualTo []) then { _rewards set ["special", _specialRewards]; };
+
+ [_taskID, _rewards] call FUNC(handleTaskRewards);
+ [_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
+
+ sleep 1;
+
+ GVAR(TaskStore) call ["notifyParticipants", [_taskID, "success", "Tasks", format ["Task completed: %1 reputation, $%2 funds", _ratingSuccess, [_companyFunds] call EFUNC(common,formatNumber)]]];
+ GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingSuccess]];
+ GVAR(TaskStore) call ["clearTask", [_taskID]];
+
+ if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
+};
diff --git a/arma/server/addons/task/functions/fnc_deliveryModule.sqf b/arma/server/addons/task/functions/fnc_deliveryModule.sqf
new file mode 100644
index 0000000..19ead22
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_deliveryModule.sqf
@@ -0,0 +1,67 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Creates a delivery task module
+ *
+ * Arguments:
+ * None
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * call forge_server_task_fnc_deliveryModule;
+ *
+ * Public: No
+ */
+
+// Module category
+private _category = "Forge Tasks";
+private _subCategory = "Delivery Tasks";
+
+// Create the module
+private _module = createDialog "RscDisplayAttributes";
+_module setVariable ["category", _category];
+_module setVariable ["subcategory", _subCategory];
+_module setVariable ["description", "Configure a delivery task"];
+
+// Add fields for task configuration
+[_module, "Task ID", "taskID", "", true] call BIS_fnc_addAttribute;
+[_module, "Fail Limit", "limitFail", "1", true] call BIS_fnc_addAttribute;
+[_module, "Success Count", "limitSuccess", "3", true] call BIS_fnc_addAttribute;
+[_module, "Delivery Zone", "deliveryZone", "", true] call BIS_fnc_addAttribute;
+[_module, "Company Funds Reward", "companyFunds", "250000", true] call BIS_fnc_addAttribute;
+[_module, "Rating Loss on Fail", "ratingFail", "-75", true] call BIS_fnc_addAttribute;
+[_module, "Rating Gain on Success", "ratingSuccess", "300", true] call BIS_fnc_addAttribute;
+[_module, "End Mission on Success", "endSuccess", "false", false] call BIS_fnc_addAttribute;
+[_module, "End Mission on Fail", "endFail", "false", false] call BIS_fnc_addAttribute;
+[_module, "Time Limit (seconds)", "timeLimit", "", false] call BIS_fnc_addAttribute;
+
+// Add confirm button handler
+_module setVariable ["onConfirm", {
+ params ["_module"];
+
+ private _taskID = _module getVariable ["taskID", ""];
+ private _limitFail = parseNumber (_module getVariable ["limitFail", "1"]);
+ private _limitSuccess = parseNumber (_module getVariable ["limitSuccess", "3"]);
+ private _deliveryZone = _module getVariable ["deliveryZone", ""];
+ private _companyFunds = parseNumber (_module getVariable ["companyFunds", "250000"]);
+ private _ratingFail = parseNumber (_module getVariable ["ratingFail", "-75"]);
+ private _ratingSuccess = parseNumber (_module getVariable ["ratingSuccess", "300"]);
+ private _endSuccess = _module getVariable ["endSuccess", "false"] == "true";
+ private _endFail = _module getVariable ["endFail", "false"] == "true";
+ private _timeLimit = _module getVariable ["timeLimit", ""];
+
+ // Convert time limit to number or nil
+ private _timeLimitNum = if (_timeLimit == "") then { nil } else { parseNumber _timeLimit };
+
+ // Create the task
+ private _params = [_taskID, _limitFail, _limitSuccess, _deliveryZone, _companyFunds, _ratingFail, _ratingSuccess, _endSuccess, _endFail];
+ if (!isNil "_timeLimitNum") then {
+ _params pushBack _timeLimitNum;
+ };
+
+ private _requesterUid = ["", getPlayerUID player] select hasInterface;
+ ["delivery", _params, 0, _requesterUid] spawn FUNC(handler);
+}];
diff --git a/arma/server/addons/task/functions/fnc_destroy.sqf b/arma/server/addons/task/functions/fnc_destroy.sqf
new file mode 100644
index 0000000..dcb1013
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_destroy.sqf
@@ -0,0 +1,114 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Registers an destroy task
+ *
+ * Arguments:
+ * 0: ID of the task
+ * 1: Amount of targets escaped to fail the task
+ * 2: Amount of targets eliminated to complete the task
+ * 3: Amount of funds the company recieves if the task is successful (default: 0)
+ * 4: Amount of rating the company and player lose if the task is failed (default: 0)
+ * 5: Amount of rating the company and player recieve if the task is successful (default: 0)
+ * 6: Should the mission end (MissionSuccess) if the task is successful (default: false)
+ * 7: Should the mission end (MissionFailed) if the task is failed (default: false)
+ * 8: Amount of time before target(s) escape (default: -1)
+ * 9: Equipment rewards (default: [])
+ * 10: Supply rewards (default: [])
+ * 11: Weapon rewards (default: [])
+ * 12: Vehicle rewards (default: [])
+ * 13: Special rewards (default: [])
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * ["task_name", 1, 2, 250000, -75, 300, false, false] spawn forge_server_task_fnc_destroy;
+ * ["task_name", 1, 2, 250000, -75, 300, false, false, 45] spawn forge_server_task_fnc_destroy;
+ *
+ * Public: Yes
+ */
+
+params [
+ ["_taskID", "", [""]],
+ ["_limitFail", -1, [0]],
+ ["_limitSuccess", -1, [0]],
+ ["_companyFunds", 0, [0]],
+ ["_ratingFail", 0, [0]],
+ ["_ratingSuccess", 0, [0]],
+ ["_endSuccess", false, [false]],
+ ["_endFail", false, [false]],
+ ["_time", -1, [0]],
+ ["_equipmentRewards", [], [[]]],
+ ["_supplyRewards", [], [[]]],
+ ["_weaponRewards", [], [[]]],
+ ["_vehicleRewards", [], [[]]],
+ ["_specialRewards", [], [[]]]
+];
+
+private _result = 0;
+private _targets = [];
+
+waitUntil {
+ sleep 1;
+ _targets = GVAR(TaskStore) call ["getTaskEntities", ["targets", _taskID]];
+ GVAR(TaskStore) call ["trackParticipants", [_taskID, _targets, "", 300]];
+ count _targets > 0
+};
+
+_targets = GVAR(TaskStore) call ["getTaskEntities", ["targets", _taskID]];
+private _startTime = if (!isNil "_time") then { floor(time) } else { nil };
+
+waitUntil {
+ sleep 1;
+ GVAR(TaskStore) call ["trackParticipants", [_taskID, _targets, "", 300]];
+
+ private _targetsDestroyed = ({ !alive _x } count _targets);
+
+ if (!isNil "_time") then {
+ private _timeExpired = (floor time - _startTime >= _time);
+
+ if (_targetsDestroyed < _limitSuccess && _timeExpired) then { _result = 1; };
+
+ (_result == 1) or (_targetsDestroyed >= _limitSuccess)
+ } else {
+ (_targetsDestroyed >= _limitSuccess)
+ };
+};
+
+if (_result == 1) then {
+ { deleteVehicle _x } forEach _targets;
+
+ [_taskID, "FAILED"] call BFUNC(taskSetState);
+
+ sleep 1;
+
+ GVAR(TaskStore) call ["notifyParticipants", [_taskID, "warning", "Tasks", format ["Task failed: %1 reputation", _ratingFail]]];
+ GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingFail]];
+ GVAR(TaskStore) call ["clearTask", [_taskID]];
+
+ if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
+} else {
+ { deleteVehicle _x } forEach _targets;
+
+ private _rewards = createHashMap;
+ _rewards set ["funds", _companyFunds];
+
+ if (_equipmentRewards isNotEqualTo []) then { _rewards set ["equipment", _equipmentRewards]; };
+ if (_supplyRewards isNotEqualTo []) then { _rewards set ["supplies", _supplyRewards]; };
+ if (_weaponRewards isNotEqualTo []) then { _rewards set ["weapons", _weaponRewards]; };
+ if (_vehicleRewards isNotEqualTo []) then { _rewards set ["vehicles", _vehicleRewards]; };
+ if (_specialRewards isNotEqualTo []) then { _rewards set ["special", _specialRewards]; };
+
+ [_taskID, _rewards] call FUNC(handleTaskRewards);
+ [_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
+
+ sleep 1;
+
+ GVAR(TaskStore) call ["notifyParticipants", [_taskID, "success", "Tasks", format ["Task completed: %1 reputation, $%2 funds", _ratingSuccess, [_companyFunds] call EFUNC(common,formatNumber)]]];
+ GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingSuccess]];
+ GVAR(TaskStore) call ["clearTask", [_taskID]];
+
+ if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
+};
diff --git a/arma/server/addons/task/functions/fnc_destroyModule.sqf b/arma/server/addons/task/functions/fnc_destroyModule.sqf
new file mode 100644
index 0000000..eaac010
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_destroyModule.sqf
@@ -0,0 +1,51 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Initializes the destroy module.
+ *
+ * Arguments:
+ * 0: Logic - The logic object
+ * 1: Units - The array of units
+ * 2: Activated - Whether the module is activated
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * [logicObject, [unit1, unit2], true] call forge_server_task_fnc_destroyModule;
+ *
+ * Public: No
+ */
+
+params [["_logic", objNull, [objNull]], ["_units", [], [[]]], ["_activated", true, [true]]];
+
+if !(_activated) exitWith {};
+
+private _taskID = _logic getVariable ["TaskID", ""];
+private _limitFail = _logic getVariable ["LimitFail", -1];
+private _limitSuccess = _logic getVariable ["LimitSuccess", -1];
+private _companyFunds = _logic getVariable ["CompanyFunds", 0];
+private _ratingFail = _logic getVariable ["RatingFail", 0];
+private _ratingSuccess = _logic getVariable ["RatingSuccess", 0];
+private _endSuccess = _logic getVariable ["EndSuccess", false];
+private _endFail = _logic getVariable ["EndFail", false];
+private _timeLimit = _logic getVariable ["TimeLimit", 0];
+
+["INFO", format ["Destroy Module Parameters: TaskID: %1, LimitFail: %2, LimitSuccess: %3, Funds: %4, RatingFail: %5, RatingSuccess: %6, EndSuccess: %7, EndFail: %8, Time: %9", _taskID, _limitFail, _limitSuccess, _companyFunds, _ratingFail, _ratingSuccess, _endSuccess, _endFail, _timeLimit]] call EFUNC(common,log);
+
+private _syncedEntities = synchronizedObjects _logic;
+["INFO", format ["Destroy Module Synced Entities: %1", _syncedEntities]] call EFUNC(common,log);
+
+{
+ [_x, _taskID] spawn FUNC(makeTarget);
+} forEach _syncedEntities;
+
+private _params = [_taskID, _limitFail, _limitSuccess, _companyFunds, _ratingFail, _ratingSuccess, _endSuccess, _endFail];
+if (_timeLimit != 0) then {
+ _params pushBack _timeLimit;
+};
+
+["destroy", _params, 0, ""] spawn FUNC(handler);
+
+deleteVehicle _logic;
diff --git a/arma/server/addons/task/functions/fnc_explosivesModule.sqf b/arma/server/addons/task/functions/fnc_explosivesModule.sqf
new file mode 100644
index 0000000..6725b17
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_explosivesModule.sqf
@@ -0,0 +1,23 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Initializes the explosives module
+ *
+ * Arguments:
+ * 0: Logic - The logic object
+ * 1: Units - The array of units
+ * 2: Activated - Whether the module is activated
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * [logicObject, [unit1, unit2], true] call forge_server_task_fnc_explosivesModule;
+ *
+ * Public: No
+ */
+
+params [["_logic", objNull, [objNull]], ["_units", [], [[]]], ["_activated", true, [true]]];
+
+if !(_activated) exitWith {};
diff --git a/arma/server/addons/task/functions/fnc_handleTaskRewards.sqf b/arma/server/addons/task/functions/fnc_handleTaskRewards.sqf
new file mode 100644
index 0000000..0812355
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_handleTaskRewards.sqf
@@ -0,0 +1,223 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Handles task completion rewards for organizations.
+ *
+ * Arguments:
+ * 0: Task ID
+ * 1: Reward Data
+ * - funds: Amount of money to award
+ * - equipment: Array of equipment classnames to award
+ * - supplies: Array of supply classnames to award
+ * - weapons: Array of weapon classnames to award
+ * - vehicles: Array of vehicle classnames to award
+ * - special: Array of special item classnames to award
+ *
+ * Return Value:
+ * Success
+ *
+ * Example:
+ * private _rewards = createHashMapFromArray [
+ * ["funds", 10000],
+ * ["reputation", 50],
+ * ["equipment", ["ItemGPS", "ItemCompass"]],
+ * ["supplies", ["FirstAidKit", "Medikit"]],
+ * ["weapons", ["arifle_MX_F"]],
+ * ["vehicles", ["B_MRAP_01_F"]],
+ * ["special", ["B_UAV_01_F"]]
+ * ];
+ * ["task_1", _rewards] call forge_server_task_fnc_handleTaskRewards;
+ *
+ * Public: No
+ */
+
+params [["_taskID", ""], ["_rewards", createHashMap]];
+
+if (_taskID == "") exitWith {
+ ["ERROR", "No task ID provided for rewards"] call EFUNC(common,log);
+ false
+};
+
+private _rewardContext = GVAR(TaskStore) call ["resolveRewardContext", [_taskID]];
+private _requesterUid = _rewardContext getOrDefault ["requesterUid", ""];
+private _orgID = _rewardContext getOrDefault ["orgID", ""];
+private _memberUids = _rewardContext getOrDefault ["memberUids", []];
+if (_orgID isEqualTo "") exitWith {
+ ["ERROR", format ["No organization reward context found for task %1.", _taskID]] call EFUNC(common,log);
+ false
+};
+
+private _success = true;
+private _funds = _rewards getOrDefault ["funds", 0];
+private _rewardMessages = [];
+
+private _resolveRewardLabel = {
+ params [["_className", "", [""]]];
+
+ if (_className isEqualTo "") exitWith { "" };
+
+ {
+ private _cfg = _x >> _className;
+ if (isClass _cfg) exitWith {
+ private _displayName = getText (_cfg >> "displayName");
+ [_displayName, _className] select (_displayName isEqualTo "");
+ };
+ } forEach [
+ configFile >> "CfgWeapons",
+ configFile >> "CfgMagazines",
+ configFile >> "CfgVehicles",
+ configFile >> "CfgGlasses"
+ ];
+
+ _className
+};
+
+private _notifyMembers = {
+ params [["_type", "info", [""]], ["_title", "Tasks", [""]], ["_message", "", [""]]];
+
+ if (_message isEqualTo "") exitWith {};
+ {
+ private _player = [_x] call EFUNC(common,getPlayer);
+ if (isNull _player) then { continue; };
+ [CRPC(notifications,recieveNotification), [_type, _title, _message], _player] call CFUNC(targetEvent);
+ } forEach _memberUids;
+};
+
+private _syncOrgPatch = {
+ params [["_patch", createHashMap, [createHashMap]]];
+
+ if (_patch isEqualTo createHashMap) exitWith {};
+ {
+ private _player = [_x] call EFUNC(common,getPlayer);
+ if (isNull _player) then { continue; };
+ [CRPC(org,responseSyncOrg), [_patch], _player] call CFUNC(targetEvent);
+ } forEach _memberUids;
+};
+
+if (_funds > 0) then {
+ private _org = EGVAR(org,Registry) getOrDefault [_orgID, createHashMap];
+ if (_org isEqualTo createHashMap) then {
+ _org = EGVAR(org,OrgStore) call ["loadById", [_orgID]];
+ };
+
+ if (_org isEqualTo createHashMap) then {
+ ["ERROR", format ["Failed to load organization %1 for task %2 funds reward.", _orgID, _taskID]] call EFUNC(common,log);
+ _success = false;
+ } else {
+ private _patch = EGVAR(org,OrgStore) call [
+ "set",
+ [
+ EGVAR(org,Registry),
+ "org:update",
+ _orgID,
+ "funds",
+ ((_org getOrDefault ["funds", 0]) + _funds),
+ false
+ ]
+ ];
+
+ [_patch] call _syncOrgPatch;
+ _rewardMessages pushBack format ["$%1 org funds", [_funds] call EFUNC(common,formatNumber)];
+ };
+};
+
+private _grantOrgAssets = {
+ params [["_category", "items", [""]], ["_items", [], [[]]]];
+
+ if (_items isEqualTo []) exitWith {};
+
+ private _assetEntries = _items apply {
+ createHashMapFromArray [
+ ["classname", _x],
+ ["category", _category],
+ ["quantity", 1]
+ ]
+ };
+
+ private _grantResult = EGVAR(org,OrgStore) call ["addAssets", [_requesterUid, _assetEntries, false, _orgID]];
+ if !(_grantResult getOrDefault ["success", false]) then {
+ ["ERROR", format ["Failed to award %1 assets for task %2: %3", _category, _taskID, _grantResult getOrDefault ["message", "Unknown error."]]] call EFUNC(common,log);
+ _success = false;
+ } else {
+ [_grantResult getOrDefault ["patch", createHashMap]] call _syncOrgPatch;
+ private _labels = _items apply { [_x] call _resolveRewardLabel };
+ _rewardMessages pushBack format ["%1: %2", _category, _labels joinString ", "];
+ };
+};
+
+private _grantOrgFleet = {
+ params [["_vehicles", [], [[]]]];
+
+ if (_vehicles isEqualTo []) exitWith {};
+
+ private _vehicleEntries = _vehicles apply {
+ private _category = "other";
+ if (_x isKindOf "Car") then { _category = "cars"; };
+ if (_x isKindOf "Tank") then { _category = "armor"; };
+ if (_x isKindOf "Helicopter") then { _category = "helis"; };
+ if (_x isKindOf "Plane") then { _category = "planes"; };
+ if (_x isKindOf "Ship") then { _category = "naval"; };
+
+ createHashMapFromArray [
+ ["classname", _x],
+ ["category", _category]
+ ]
+ };
+
+ private _fleetResult = EGVAR(org,OrgStore) call ["addFleetVehicles", [_requesterUid, _vehicleEntries, false, _orgID]];
+ if !(_fleetResult getOrDefault ["success", false]) then {
+ ["ERROR", format ["Failed to award vehicle rewards for task %2: %1", _fleetResult getOrDefault ["message", "Unknown error."], _taskID]] call EFUNC(common,log);
+ _success = false;
+ } else {
+ [_fleetResult getOrDefault ["patch", createHashMap]] call _syncOrgPatch;
+ private _labels = _vehicles apply { [_x] call _resolveRewardLabel };
+ _rewardMessages pushBack format ["vehicles: %1", _labels joinString ", "];
+ };
+};
+
+private _equipment = _rewards getOrDefault ["equipment", []];
+if (count _equipment > 0) then {
+ ["equipment", _equipment] call _grantOrgAssets;
+};
+
+private _supplies = _rewards getOrDefault ["supplies", []];
+if (count _supplies > 0) then {
+ ["supplies", _supplies] call _grantOrgAssets;
+};
+
+private _weapons = _rewards getOrDefault ["weapons", []];
+if (count _weapons > 0) then {
+ ["weapons", _weapons] call _grantOrgAssets;
+};
+
+private _special = _rewards getOrDefault ["special", []];
+if (count _special > 0) then {
+ ["special", _special] call _grantOrgAssets;
+};
+
+private _vehicles = _rewards getOrDefault ["vehicles", []];
+if (count _vehicles > 0) then {
+ [_vehicles] call _grantOrgFleet;
+};
+
+if (_success) then {
+ private _orgName = "";
+ private _org = EGVAR(org,Registry) getOrDefault [_orgID, createHashMap];
+ if (_org isNotEqualTo createHashMap) then {
+ _orgName = _org getOrDefault ["name", _orgID];
+ };
+ if (_orgName isEqualTo "") then { _orgName = _orgID; };
+
+ private _message = format ["Task rewards added to %1.", _orgName];
+ if (_rewardMessages isNotEqualTo []) then {
+ _message = format ["%1 %2", _message, _rewardMessages joinString ", "];
+ };
+
+ ["INFO", _message] call EFUNC(common,log);
+ ["success", "Tasks", _message] call _notifyMembers;
+} else {
+ ["warning", "Tasks", format ["Task %1 completed, but one or more org rewards failed to apply.", _taskID]] call _notifyMembers;
+};
+
+_success
diff --git a/arma/server/addons/task/functions/fnc_handler.sqf b/arma/server/addons/task/functions/fnc_handler.sqf
new file mode 100644
index 0000000..73416f9
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_handler.sqf
@@ -0,0 +1,108 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Server side task handler/spawner
+ *
+ * Arguments:
+ * 0: Type of task
+ * 1: Arguments for task
+ * 2: Minimum org reputation for task (default: 0)
+ * 3: Requester UID (default: "")
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * ["task_type", [_reward, _punish, _time, etc.....], minReputation, requesterUid] call forge_server_task_fnc_handler;
+ *
+ * Public: Yes
+ */
+
+params [["_taskType", "", [""]], ["_args", [], [[]]], ["_minRating", 0, [0]], ["_requesterUid", "", [""]]];
+
+private _taskID = "";
+
+if (_minRating > 0) then {
+ if (_requesterUid isEqualTo "") then {
+ ["WARNING", format ["Task %1 requires minimum reputation %2 but no requester UID was provided, skipping reputation gate.", _taskType, _minRating]] call EFUNC(common,log);
+ } else {
+ private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
+ if (_requesterActor isEqualTo createHashMap) then {
+ _requesterActor = EGVAR(actor,ActorStore) call ["init", [_requesterUid]];
+ };
+
+ private _orgID = _requesterActor getOrDefault ["organization", "default"];
+ if (_orgID isEqualTo "") then { _orgID = "default"; };
+
+ private _org = EGVAR(org,Registry) getOrDefault [_orgID, createHashMap];
+ if (_org isEqualTo createHashMap) then {
+ _org = EGVAR(org,OrgStore) call ["loadById", [_orgID]];
+ };
+
+ private _orgReputation = _org getOrDefault ["reputation", 0];
+ if (_orgReputation < _minRating) exitWith {
+ private _message = format ["Organization reputation of %1 does not meet the minimum required reputation of %2.", _orgReputation, _minRating];
+ ["WARNING", format ["Task %1 blocked: %2", _taskType, _message]] call EFUNC(common,log);
+
+ private _player = [_requesterUid] call EFUNC(common,getPlayer);
+ if (isNull _player) exitWith {};
+
+ [CRPC(notifications,recieveNotification), ["warning", "Tasks", _message], _player] call CFUNC(targetEvent);
+ };
+ };
+};
+
+if (_args isNotEqualTo [] && { (_args select 0) isEqualType "" }) then {
+ _taskID = _args select 0;
+};
+
+if (_taskID isNotEqualTo "") then {
+ private _ownershipResult = GVAR(TaskStore) call ["bindTaskOwnership", [_taskID, _requesterUid]];
+ if !(_ownershipResult getOrDefault ["success", false]) then {
+ ["WARNING", format [
+ "Failed to bind task ownership for %1 (%2): %3",
+ _taskID,
+ _taskType,
+ _ownershipResult getOrDefault ["message", "Unknown error."]
+ ]] call EFUNC(common,log);
+ };
+
+ GVAR(TaskStore) call ["setTaskStatus", [_taskID, "active"]];
+};
+
+switch (_taskType) do {
+ case "attack": {
+ private _thread = _args spawn FUNC(attack);
+ waitUntil { sleep 2; scriptDone _thread };
+ };
+ case "defuse": {
+ private _thread = _args spawn FUNC(defuse);
+ waitUntil { sleep 2; scriptDone _thread };
+ };
+ case "destroy": {
+ private _thread = _args spawn FUNC(destroy);
+ waitUntil { sleep 2; scriptDone _thread };
+ };
+ case "delivery": {
+ private _thread = _args spawn FUNC(delivery);
+ waitUntil { sleep 2; scriptDone _thread };
+ };
+ case "defend": {
+ private _thread = _args spawn FUNC(defend);
+ waitUntil { sleep 2; scriptDone _thread };
+ };
+ case "hostage": {
+ private _thread = _args spawn FUNC(hostage);
+ waitUntil { sleep 2; scriptDone _thread };
+ };
+ case "hvt": {
+ private _thread = _args spawn FUNC(hvt);
+ waitUntil { sleep 2; scriptDone _thread };
+ };
+ default {
+ ["ERROR", format ["Unknown Contract Type: %1", _taskType]] call EFUNC(common,log);
+ };
+};
+
+["INFO", "Mission Handler Done"] call EFUNC(common,log);
diff --git a/arma/server/addons/task/functions/fnc_heartBeat.sqf b/arma/server/addons/task/functions/fnc_heartBeat.sqf
new file mode 100644
index 0000000..7a57941
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_heartBeat.sqf
@@ -0,0 +1,68 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Registers Entity and starts heartbeat
+ *
+ * Arguments:
+ * 0: The entity
+ * 1: Type of the entity
+ * 2: The countdown timer
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * [_entity, "entity_type", 30] spawn FUNC(heartBeat);
+ *
+ * Public: Yes
+ */
+
+params [["_entity", nil, [objNull, 0, [], sideUnknown, grpNull, ""]], ["_typeOf", "", [""]], ["_time", 0, [0]]];
+
+private _nearPlayers = [];
+
+switch (_typeOf) do {
+ case "hostage": {
+ _entity setCaptive true;
+ _entity enableAIFeature ["MOVE", false];
+ _entity playMove "acts_executionvictim_loop";
+
+ waitUntil {
+ sleep 1;
+ _nearPlayers = allPlayers inAreaArray [ASLToAGL getPosASL _entity, 2, 2, 0, false, 2];
+ count _nearPlayers > 0
+ };
+
+ private _nearPlayer = _nearPlayers select 0;
+
+ [_entity] joinSilent (group _nearPlayer);
+
+ _entity setCaptive false;
+ _entity enableAIFeature ["MOVE", true];
+ _entity playMove "acts_executionvictim_unbow";
+ };
+ case "hvt": {
+ waitUntil {
+ sleep 1;
+ _nearPlayers = allPlayers inAreaArray [ASLToAGL getPosASL _entity, 2, 2, 0, false, 2];
+ count _nearPlayers > 0
+ };
+
+ _entity setCaptive true;
+ doStop _entity;
+ };
+ case "ied": {
+ while { alive _entity && _time > 0} do {
+ if (_time > 10) then { _entity say3D "FORGE_timerBeep" };
+ if (_time <= 10 && _time > 5) then { _entity say3D "FORGE_timerBeepShort" };
+ if (_time <= 5) then { _entity say3D "FORGE_timerEnd" };
+ if (_time <= 0) exitWith { _entity setDamage 1 };
+
+ _time = _time -1;
+ sleep 1;
+ };
+
+ if (alive _entity && _time <= 0) then { _entity setDamage 1 };
+ };
+};
diff --git a/arma/server/addons/task/functions/fnc_hostage.sqf b/arma/server/addons/task/functions/fnc_hostage.sqf
new file mode 100644
index 0000000..ead6b2a
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_hostage.sqf
@@ -0,0 +1,173 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Registers an hostage task
+ *
+ * Arguments:
+ * 0: ID of the task
+ * 1: Amount of hostages KIA to fail the task
+ * 2: Amount of hostages rescued to complete the task
+ * 3: Marker name for the extraction zone
+ * 4: Amount of funds the company recieves if the task is successful (default: 0)
+ * 5: Amount of rating the company and player lose if the task is failed (default: 0)
+ * 6: Amount of rating the company and player recieve if the task is successful (default: 0)
+ * 7: Subcategory of task (default: [false, true])
+ * 8: Should the mission end (MissionSuccess) if the task is successful (default: false)
+ * 9: Should the mission end (MissionFailed) if the task is failed (default: false)
+ * 10: Amount of time before hostage(s) die (default: -1)
+ * 11: Marker name for the cbrn zone (default: "")
+ * 12: Equipment rewards (default: [])
+ * 13: Supply rewards (default: [])
+ * 14: Weapon rewards (default: [])
+ * 15: Vehicle rewards (default: [])
+ * 16: Special rewards (default: [])
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * ["task_name", 1, 2, "marker_name", 1500000, -75, 500, [false, true], false, false] spawn forge_server_task_fnc_hostage;
+ * ["task_name", 1, 2, "marker_name", 1500000, -75, 500, [false, true], false, false, 45] spawn forge_server_task_fnc_hostage;
+ * ["task_name", 1, 2, "marker_name", 1500000, -75, 500, [true, false], false, false, 45, "marker_name"] spawn forge_server_task_fnc_hostage;
+ *
+ * Public: Yes
+ */
+
+params [
+ ["_taskID", ""],
+ ["_limitFail", -1],
+ ["_limitSuccess", -1],
+ ["_extZone", ""],
+ ["_companyFunds", 0],
+ ["_ratingFail", 0],
+ ["_ratingSuccess", 0],
+ ["_type", [["_cbrn", false, [false]], ["_hostage", true, [false]]]],
+ ["_endSuccess", false, [false]],
+ ["_endFail", false, [false]],
+ ["_time", -1, [0]],
+ ["_cbrnZone", "", [""]],
+ ["_equipmentRewards", [], [[]]],
+ ["_supplyRewards", [], [[]]],
+ ["_weaponRewards", [], [[]]],
+ ["_vehicleRewards", [], [[]]],
+ ["_specialRewards", [], [[]]]
+];
+
+private _cbrn = (_this select 7) select 0;
+private _hostage = (_this select 7) select 1;
+private _result = 0;
+private _hostages = [];
+private _shooters = [];
+
+waitUntil {
+ sleep 1;
+ _hostages = GVAR(TaskStore) call ["getTaskEntities", ["hostages", _taskID]];
+ count _hostages > 0
+};
+
+waitUntil {
+ sleep 1;
+ _shooters = GVAR(TaskStore) call ["getTaskEntities", ["shooters", _taskID]];
+ GVAR(TaskStore) call ["trackParticipants", [_taskID, _hostages + _shooters, _extZone, 250]];
+ count _shooters > 0
+};
+
+_hostages = GVAR(TaskStore) call ["getTaskEntities", ["hostages", _taskID]];
+_shooters = GVAR(TaskStore) call ["getTaskEntities", ["shooters", _taskID]];
+private _startTime = if (_time isNotEqualTo -1) then { floor(time) } else { nil };
+
+waitUntil {
+ sleep 1;
+ GVAR(TaskStore) call ["trackParticipants", [_taskID, _hostages + _shooters, _extZone, 250]];
+
+ private _hostagesFreed = ({ !captive _x } count _hostages);
+ private _hostagesInZone = ({ _x inArea _extZone } count _hostages);
+ private _hostagesKilled = ({ !alive _x } count _hostages);
+ private _shootersAlive = ({ alive _x } count _shooters);
+
+ if (_time isNotEqualTo -1) then {
+ private _timeExpired = (floor time - _startTime >= _time);
+
+ if (_hostagesFreed < _limitSuccess && _timeExpired) then { _result = 1; };
+ if (_hostagesKilled >= _limitFail) then { _result = 1; };
+
+ (_result == 1) or
+ ((_hostagesInZone >= _limitSuccess) && (_hostagesKilled < _limitFail)) or
+ ((!isNil "_shooters") && (_shootersAlive <= 0) && (_hostagesInZone >= _limitSuccess) && (_hostagesKilled < _limitFail))
+ } else {
+ if (_hostagesKilled >= _limitFail) then { _result = 1; };
+
+ (_result == 1) or
+ ((_hostagesInZone >= _limitSuccess) && (_hostagesKilled < _limitFail)) or
+ ((!isNil "_shooters") && (_shootersAlive <= 0) && (_hostagesInZone >= _limitSuccess) && (_hostagesKilled < _limitFail))
+ };
+};
+
+if (_result == 1) then {
+ if (_cbrn) then {
+ "SmokeShellYellow" createVehicle getMarkerPos _cbrnZone;
+
+ sleep 5;
+
+ {
+ if (captive _x) then {
+ _x setDamage 0.9;
+ _x playMove "acts_executionvictim_kill_end";
+
+ sleep 2.75;
+
+ _x setDamage 1;
+ }
+ } forEach _hostages;
+ };
+
+ if (_hostage) then {
+ {
+ _x enableAIFeature ["MOVE", true];
+ _x playMove "";
+ } forEach _shooters;
+
+ sleep 1;
+
+ { _x setCaptive false; } forEach _hostages;
+
+ sleep 5;
+ };
+
+ { deleteVehicle _x } forEach _hostages;
+ { deleteVehicle _x } forEach _shooters;
+
+ [_taskID, "FAILED"] call BFUNC(taskSetState);
+
+ sleep 1;
+
+ GVAR(TaskStore) call ["notifyParticipants", [_taskID, "warning", "Tasks", format ["Task failed: %1 reputation", _ratingFail]]];
+ GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingFail]];
+ GVAR(TaskStore) call ["clearTask", [_taskID]];
+
+ if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
+} else {
+ { deleteVehicle _x } forEach _hostages;
+ { deleteVehicle _x } forEach _shooters;
+
+ private _rewards = createHashMap;
+ _rewards set ["funds", _companyFunds];
+
+ if (_equipmentRewards isNotEqualTo []) then { _rewards set ["equipment", _equipmentRewards]; };
+ if (_supplyRewards isNotEqualTo []) then { _rewards set ["supplies", _supplyRewards]; };
+ if (_weaponRewards isNotEqualTo []) then { _rewards set ["weapons", _weaponRewards]; };
+ if (_vehicleRewards isNotEqualTo []) then { _rewards set ["vehicles", _vehicleRewards]; };
+ if (_specialRewards isNotEqualTo []) then { _rewards set ["special", _specialRewards]; };
+
+ [_taskID, _rewards] call FUNC(handleTaskRewards);
+ [_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
+
+ sleep 1;
+
+ GVAR(TaskStore) call ["notifyParticipants", [_taskID, "success", "Tasks", format ["Task completed: %1 reputation, $%2 funds", _ratingSuccess, [_companyFunds] call EFUNC(common,formatNumber)]]];
+ GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingSuccess]];
+ GVAR(TaskStore) call ["clearTask", [_taskID]];
+
+ if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
+};
diff --git a/arma/server/addons/task/functions/fnc_hostageModule.sqf b/arma/server/addons/task/functions/fnc_hostageModule.sqf
new file mode 100644
index 0000000..b5d2a9a
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_hostageModule.sqf
@@ -0,0 +1,76 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Initializes the hostage module
+ *
+ * Arguments:
+ * 0: Logic - The logic object
+ * 1: Units - The array of units
+ * 2: Activated - Whether the module is activated
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * [logicObject, [unit1, unit2], true] call forge_server_task_fnc_hostageModule;
+ *
+ * Public: No
+ */
+
+params [["_logic", objNull, [objNull]], ["_units", [], [[]]], ["_activated", true, [true]]];
+
+if !(_activated) exitWith {};
+
+private _taskID = _logic getVariable ["TaskID", ""];
+private _limitFail = _logic getVariable ["LimitFail", -1];
+private _limitSuccess = _logic getVariable ["LimitSuccess", -1];
+private _extraction = _logic getVariable ["ExtZone", ""];
+private _companyFunds = _logic getVariable ["CompanyFunds", 0];
+private _ratingFail = _logic getVariable ["RatingFail", 0];
+private _ratingSuccess = _logic getVariable ["RatingSuccess", 0];
+private _cbrn = _logic getVariable ["CBRN", false];
+private _execution = _logic getVariable ["Execution", false];
+private _endSuccess = _logic getVariable ["EndSuccess", false];
+private _endFail = _logic getVariable ["EndFail", false];
+private _timeLimit = _logic getVariable ["TimeLimit", 0];
+private _cbrnZone = _logic getVariable ["CBRNZone", ""];
+
+["INFO", format [
+ "Hostage Module Parameters: TaskID: %1, LimitFail: %2, LimitSuccess: %3, ExtractionZone: %4, Funds: %5, RatingFail: %6, RatingSuccess: %7, CBRN: %8, Execution: %9, EndSuccess: %10, EndFail: %11, Time: %12, CBRNZone: %13",
+ _taskID, _limitFail, _limitSuccess, _extraction, _companyFunds, _ratingFail, _ratingSuccess, _cbrn, _execution, _endSuccess, _endFail, _timeLimit, _cbrnZone
+]] call EFUNC(common,log);
+
+private _syncedModules = synchronizedObjects _logic;
+["INFO", format ["Hostage Module Synced Entities: %1", _syncedModules]] call EFUNC(common,log);
+
+private _hostageModule = (_syncedModules select { typeOf _x == "FORGE_Module_Hostages" }) select 0;
+private _shooterModule = (_syncedModules select { typeOf _x == "FORGE_Module_Shooters" }) select 0;
+
+private _hostageEntities = synchronizedObjects _hostageModule;
+["INFO", format ["Hostage Module Hostage Entities: %1", _hostageEntities]] call EFUNC(common,log);
+
+private _shooterEntities = synchronizedObjects _shooterModule;
+["INFO", format ["Hostage Module Shooter Entities: %1", _shooterEntities]] call EFUNC(common,log);
+
+{
+ if (!isNull _x && (_x isNotEqualTo str objNull)) then {
+ [_x, _taskID] spawn FUNC(makeHostage);
+ };
+} forEach _hostageEntities;
+
+{
+ if (!isNull _x && (_x isNotEqualTo str objNull)) then {
+ [_x, _taskID] spawn FUNC(makeShooter);
+ };
+} forEach _shooterEntities;
+
+private _params = [_taskID, _limitFail, _limitSuccess, _extraction, _companyFunds, _ratingFail, _ratingSuccess, [_cbrn, _execution], _endSuccess, _endFail];
+if (_timeLimit != 0) then {
+ _params pushBack _timeLimit;
+ _params pushBack _cbrnZone;
+};
+
+["hostage", _params, 0, ""] spawn FUNC(handler);
+
+deleteVehicle _logic;
diff --git a/arma/server/addons/task/functions/fnc_hostagesModule.sqf b/arma/server/addons/task/functions/fnc_hostagesModule.sqf
new file mode 100644
index 0000000..80b7b00
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_hostagesModule.sqf
@@ -0,0 +1,23 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Initializes the hostage module
+ *
+ * Arguments:
+ * 0: Logic - The logic object
+ * 1: Units - The array of units
+ * 2: Activated - Whether the module is activated
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * [logicObject, [unit1, unit2], true] call forge_server_task_fnc_hostagesModule;
+ *
+ * Public: No
+ */
+
+params [["_logic", objNull, [objNull]], ["_units", [], [[]]], ["_activated", true, [true]]];
+
+if !(_activated) exitWith {};
diff --git a/arma/server/addons/task/functions/fnc_hvt.sqf b/arma/server/addons/task/functions/fnc_hvt.sqf
new file mode 100644
index 0000000..f763300
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_hvt.sqf
@@ -0,0 +1,128 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Registers an hvt task
+ *
+ * Arguments:
+ * 0: ID of the task
+ * 1: Amount of HVTs KIA to fail the task
+ * 2: Amount of HVTs Captured or KIA to complete the task
+ * 3: Marker name for the extraction zone
+ * 4: Amount of funds the company recieves if the task is successful (default: 0)
+ * 5: Amount of rating the company and player lose if the task is failed (default: 0)
+ * 6: Amount of rating the company and player recieve if the task is successful (default: 0)
+ * 7: Subcategory of task (default: [true, false])
+ * 8: Should the mission end (MissionSuccess) if the task is successful (default: false)
+ * 9: Should the mission end (MissionFailed) if the task is failed (default: false)
+ * 10: Amount of time before hvt(s) die (default: -1)
+ * 11: Equipment rewards (default: [])
+ * 12: Supply rewards (default: [])
+ * 13: Weapon rewards (default: [])
+ * 14: Vehicle rewards (default: [])
+ * 15: Special rewards (default: [])
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * ["task_name", 1, 1, "marker_name", 500000, -75, 300, [true, false], false, false] spawn forge_server_task_fnc_hvt;
+ * ["task_name", -1, 1, "", 500000, -75, 300, [false, true], false, false] spawn forge_server_task_fnc_hvt;
+ * ["task_name", 1, 1, "marker_name", 500000, -75, 300, [true, false], false, false, 45] spawn forge_server_task_fnc_hvt;
+ * ["task_name", -1, 1, "", 500000, -75, 300, [false, true], false, false, 45] spawn forge_server_task_fnc_hvt;
+ *
+ * Public: Yes
+ */
+
+params [
+ ["_taskID", "", [""]],
+ ["_limitFail", -1, [0]],
+ ["_limitSuccess", -1, [0]],
+ ["_extZone", "", [""]],
+ ["_companyFunds", 0, [0]],
+ ["_ratingFail", 0, [0]],
+ ["_ratingSuccess", 0, [0]],
+ ["_type", [["_capture", true, [false]], ["_eliminate", false, [false]]]],
+ ["_endSuccess", false, [false]],
+ ["_endFail", false, [false]],
+ ["_time", -1, [0]],
+ ["_equipmentRewards", [], [[]]],
+ ["_supplyRewards", [], [[]]],
+ ["_weaponRewards", [], [[]]],
+ ["_vehicleRewards", [], [[]]],
+ ["_specialRewards", [], [[]]]
+];
+
+private _capture = (_this select 7) select 0;
+private _eliminate = (_this select 7) select 1;
+private _result = 0;
+private _hvts = [];
+
+waitUntil {
+ sleep 1;
+ _hvts = GVAR(TaskStore) call ["getTaskEntities", ["hvts", _taskID]];
+ GVAR(TaskStore) call ["trackParticipants", [_taskID, _hvts, _extZone, 250]];
+ count _hvts > 0
+};
+
+_hvts = GVAR(TaskStore) call ["getTaskEntities", ["hvts", _taskID]];
+private _startTime = if (!isNil "_time") then { floor(time) } else { nil };
+
+waitUntil {
+ sleep 1;
+ GVAR(TaskStore) call ["trackParticipants", [_taskID, _hvts, _extZone, 250]];
+
+ private _hvtsCaptive = ({ captive _x } count _hvts);
+ private _hvtsKilled = ({ !alive _x } count _hvts);
+ private _hvtsInZone = ({ _x inArea _extZone } count _hvts);
+
+ if (!isNil "_time") then {
+ private _timeExpired = (floor time - _startTime >= _time);
+
+ if (_capture && _hvtsKilled >= _limitFail) then { _result = 1; };
+ if (_capture && _hvtsCaptive < _limitSuccess && _timeExpired) then { _result = 1; };
+ if (_eliminate && _hvtsKilled < _limitSuccess && _timeExpired) then { _result = 1; };
+
+ (_result == 1) or (_capture && (_hvtsInZone >= _limitSuccess) && (_hvtsKilled < _limitFail)) or (_eliminate && (_hvtsKilled >= _limitSuccess))
+ } else {
+ if (_capture && (_hvtsKilled >= _limitFail)) then { _result = 1; };
+
+ (_result == 1) or (_capture && (_hvtsInZone >= _limitSuccess) && (_hvtsKilled < _limitFail)) or (_eliminate && (_hvtsKilled >= _limitSuccess))
+ };
+};
+
+if (_result == 1) then {
+ { deleteVehicle _x } forEach _hvts;
+
+ [_taskID, "FAILED"] call BFUNC(taskSetState);
+
+ sleep 1;
+
+ GVAR(TaskStore) call ["notifyParticipants", [_taskID, "warning", "Tasks", format ["Task failed: %1 reputation", _ratingFail]]];
+ GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingFail]];
+ GVAR(TaskStore) call ["clearTask", [_taskID]];
+
+ if (_endFail) then { ["MissionFail", false] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
+} else {
+ { deleteVehicle _x } forEach _hvts;
+
+ private _rewards = createHashMap;
+ _rewards set ["funds", _companyFunds];
+
+ if (_equipmentRewards isNotEqualTo []) then { _rewards set ["equipment", _equipmentRewards]; };
+ if (_supplyRewards isNotEqualTo []) then { _rewards set ["supplies", _supplyRewards]; };
+ if (_weaponRewards isNotEqualTo []) then { _rewards set ["weapons", _weaponRewards]; };
+ if (_vehicleRewards isNotEqualTo []) then { _rewards set ["vehicles", _vehicleRewards]; };
+ if (_specialRewards isNotEqualTo []) then { _rewards set ["special", _specialRewards]; };
+
+ [_taskID, _rewards] call FUNC(handleTaskRewards);
+ [_taskID, "SUCCEEDED"] call BFUNC(taskSetState);
+
+ sleep 1;
+
+ GVAR(TaskStore) call ["notifyParticipants", [_taskID, "success", "Tasks", format ["Task completed: %1 reputation, $%2 funds", _ratingSuccess, [_companyFunds] call EFUNC(common,formatNumber)]]];
+ GVAR(TaskStore) call ["applyRatingOutcome", [_taskID, _ratingSuccess]];
+ GVAR(TaskStore) call ["clearTask", [_taskID]];
+
+ if (_endSuccess) then { ["MissionSuccess", true] remoteExecCall ["BIS_fnc_endMission", playerSide]; };
+};
diff --git a/arma/server/addons/task/functions/fnc_hvtModule.sqf b/arma/server/addons/task/functions/fnc_hvtModule.sqf
new file mode 100644
index 0000000..2ed59b0
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_hvtModule.sqf
@@ -0,0 +1,59 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Initializes the hvt module
+ *
+ * Arguments:
+ * 0: Logic - The logic object
+ * 1: Units - The array of units
+ * 2: Activated - Whether the module is activated
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * [logicObject, [unit1, unit2], true] call forge_server_task_fnc_hvtModule;
+ *
+ * Public: No
+ */
+
+params [["_logic", objNull, [objNull]], ["_units", [], [[]]], ["_activated", true, [true]]];
+
+if !(_activated) exitWith {};
+
+private _taskID = _logic getVariable ["TaskID", ""];
+private _limitFail = _logic getVariable ["LimitFail", -1];
+private _limitSuccess = _logic getVariable ["LimitSuccess", -1];
+private _extraction = _logic getVariable ["ExtZone", ""];
+private _companyFunds = _logic getVariable ["CompanyFunds", 0];
+private _ratingFail = _logic getVariable ["RatingFail", 0];
+private _ratingSuccess = _logic getVariable ["RatingSuccess", 0];
+private _capture = _logic getVariable ["CaptureHVT", true];
+private _eliminate = _logic getVariable ["EliminateHVT", false];
+private _endSuccess = _logic getVariable ["EndSuccess", false];
+private _endFail = _logic getVariable ["EndFail", false];
+private _timeLimit = _logic getVariable ["TimeLimit", 0];
+
+["INFO", format [
+ "HVT Module Parameters: TaskID: %1, LimitFail: %2, LimitSuccess: %3, ExtractionZone: %4, Funds: %5, RatingFail: %6, RatingSuccess: %7, CaptureHvt: %8, EliminateHvt: %9, EndSuccess: %10, EndFail: %11, Time: %12",
+ _taskID, _limitFail, _limitSuccess, _extraction, _companyFunds, _ratingFail, _ratingSuccess, _capture, _eliminate, _endSuccess, _endFail, _timeLimit
+]] call EFUNC(common,log);
+
+private _syncedEntities = synchronizedObjects _logic;
+["INFO", format ["HVT Module Synced Entities: %1", _syncedEntities]] call EFUNC(common,log);
+
+{
+ if (!isNull _x && (_x isNotEqualTo str objNull)) then {
+ [_x, _taskID] spawn FUNC(makeHVT);
+ };
+} forEach _syncedEntities;
+
+private _params = [_taskID, _limitFail, _limitSuccess, _extraction, _companyFunds, _ratingFail, _ratingSuccess, [_capture, _eliminate], _endSuccess, _endFail];
+if (_timeLimit != 0) then {
+ _params pushBack _timeLimit;
+};
+
+["hvt", _params, 0, ""] spawn FUNC(handler);
+
+deleteVehicle _logic;
diff --git a/arma/server/addons/task/functions/fnc_initTaskStore.sqf b/arma/server/addons/task/functions/fnc_initTaskStore.sqf
new file mode 100644
index 0000000..2ba7a07
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_initTaskStore.sqf
@@ -0,0 +1,545 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Initializes the task store for task entity tracking, participant
+ * contribution tracking, and task outcome application.
+ *
+ * Arguments:
+ * None
+ *
+ * Return Value:
+ * Task store object [HASHMAP OBJECT]
+ *
+ * Example:
+ * call forge_server_task_fnc_initTaskStore
+ *
+ * Public: No
+ */
+
+#pragma hemtt ignore_variables ["_self"]
+GVAR(TaskStore) = createHashMapObject [[
+ ["#type", "TaskStore"],
+ ["#create", compileFinal {
+ _self set ["participantRegistry", createHashMap];
+ _self set ["defuseRegistry", createHashMap];
+ _self set ["taskOwnershipRegistry", createHashMap];
+ _self set ["taskStatusRegistry", createHashMap];
+ _self set ["completedTaskStatusRegistry", createHashMap];
+ _self set ["taskCatalogRegistry", createHashMap];
+ _self set ["taskEntityRegistries", createHashMapFromArray [
+ ["cargo", createHashMap],
+ ["hostages", createHashMap],
+ ["hvts", createHashMap],
+ ["ieds", createHashMap],
+ ["entities", createHashMap],
+ ["shooters", createHashMap],
+ ["targets", createHashMap]
+ ]];
+ }],
+ ["bindTaskOwnership", compileFinal {
+ params [["_taskID", "", [""]], ["_requesterUid", "", [""]]];
+
+ private _result = createHashMapFromArray [
+ ["success", false],
+ ["requesterUid", _requesterUid],
+ ["orgID", "default"],
+ ["message", ""]
+ ];
+
+ if (_taskID isEqualTo "") exitWith {
+ _result set ["message", "Missing task ID."];
+ _result
+ };
+
+ if (_requesterUid isEqualTo "") exitWith {
+ private _taskOwnershipRegistry = _self getOrDefault ["taskOwnershipRegistry", createHashMap];
+ _taskOwnershipRegistry set [_taskID, createHashMapFromArray [
+ ["requesterUid", ""],
+ ["orgID", "default"]
+ ]];
+ _self set ["taskOwnershipRegistry", _taskOwnershipRegistry];
+
+ _result set ["success", true];
+ _result set ["message", "No requester UID provided. Bound task to default organization."];
+ _result
+ };
+
+ private _actor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
+ if (_actor isEqualTo createHashMap) then {
+ _actor = EGVAR(actor,ActorStore) call ["init", [_requesterUid]];
+ };
+
+ if (_actor isEqualTo createHashMap) exitWith {
+ _result set ["message", format ["Failed to load actor for %1.", _requesterUid]];
+ _result
+ };
+
+ private _orgID = _actor getOrDefault ["organization", ""];
+ if (_orgID isEqualTo "") then { _orgID = "default"; };
+
+ private _taskOwnershipRegistry = _self getOrDefault ["taskOwnershipRegistry", createHashMap];
+ _taskOwnershipRegistry set [_taskID, createHashMapFromArray [
+ ["requesterUid", _requesterUid],
+ ["orgID", _orgID]
+ ]];
+ _self set ["taskOwnershipRegistry", _taskOwnershipRegistry];
+
+ private _taskCatalogRegistry = _self getOrDefault ["taskCatalogRegistry", createHashMap];
+ private _catalogEntry = +(_taskCatalogRegistry getOrDefault [_taskID, createHashMap]);
+ if (_catalogEntry isNotEqualTo createHashMap) then {
+ _catalogEntry set ["requesterUid", _requesterUid];
+ _catalogEntry set ["orgID", _orgID];
+ _catalogEntry set ["accepted", true];
+ _taskCatalogRegistry set [_taskID, _catalogEntry];
+ _self set ["taskCatalogRegistry", _taskCatalogRegistry];
+ };
+
+ _result set ["success", true];
+ _result set ["orgID", _orgID];
+ _result
+ }],
+ ["registerTaskCatalogEntry", compileFinal {
+ params [["_taskID", "", [""]], ["_entry", createHashMap, [createHashMap]]];
+
+ if (_taskID isEqualTo "" || { _entry isEqualTo createHashMap }) exitWith { false };
+
+ private _taskCatalogRegistry = _self getOrDefault ["taskCatalogRegistry", createHashMap];
+ _taskCatalogRegistry set [_taskID, +_entry];
+ _self set ["taskCatalogRegistry", _taskCatalogRegistry];
+ true
+ }],
+ ["getActiveTaskCatalog", compileFinal {
+ private _taskCatalogRegistry = _self getOrDefault ["taskCatalogRegistry", createHashMap];
+ private _taskStatusRegistry = _self getOrDefault ["taskStatusRegistry", createHashMap];
+ private _entries = [];
+
+ {
+ if ((_taskStatusRegistry getOrDefault [_x, ""]) isNotEqualTo "active") then { continue; };
+
+ private _entry = +_y;
+ _entry set ["taskID", _x];
+ _entry set ["status", "active"];
+ _entries pushBack _entry;
+ } forEach _taskCatalogRegistry;
+
+ _entries
+ }],
+ ["acceptTask", compileFinal {
+ params [["_taskID", "", [""]], ["_requesterUid", "", [""]]];
+
+ private _result = createHashMapFromArray [
+ ["success", false],
+ ["message", "Unable to accept task."],
+ ["entry", createHashMap]
+ ];
+
+ if (_taskID isEqualTo "" || { _requesterUid isEqualTo "" }) exitWith {
+ _result set ["message", "Missing task ID or requester UID."];
+ _result
+ };
+
+ if ((_self call ["getTaskStatus", [_taskID]]) isNotEqualTo "active") exitWith {
+ _result set ["message", "Task is no longer active."];
+ _result
+ };
+
+ private _taskCatalogRegistry = _self getOrDefault ["taskCatalogRegistry", createHashMap];
+ private _entry = +(_taskCatalogRegistry getOrDefault [_taskID, createHashMap]);
+ if (_entry isEqualTo createHashMap) exitWith {
+ _result set ["message", "Task does not exist."];
+ _result
+ };
+
+ private _taskOwnershipRegistry = _self getOrDefault ["taskOwnershipRegistry", createHashMap];
+ private _ownership = _taskOwnershipRegistry getOrDefault [_taskID, createHashMap];
+ private _currentRequesterUid = _ownership getOrDefault ["requesterUid", ""];
+
+ if (_currentRequesterUid isNotEqualTo "" && { _currentRequesterUid isNotEqualTo _requesterUid }) exitWith {
+ _result set ["message", "Task has already been accepted."];
+ _result set ["entry", _entry];
+ _result
+ };
+
+ private _bindResult = _self call ["bindTaskOwnership", [_taskID, _requesterUid]];
+ if !(_bindResult getOrDefault ["success", false]) exitWith {
+ _result set ["message", _bindResult getOrDefault ["message", "Failed to bind task ownership."]];
+ _result
+ };
+
+ private _updatedTaskCatalogRegistry = _self getOrDefault ["taskCatalogRegistry", createHashMap];
+ private _updatedEntry = +(_updatedTaskCatalogRegistry getOrDefault [_taskID, _entry]);
+ _updatedEntry set ["accepted", true];
+ _updatedEntry set ["requesterUid", _requesterUid];
+ _updatedEntry set ["orgID", _bindResult getOrDefault ["orgID", "default"]];
+ _updatedTaskCatalogRegistry set [_taskID, _updatedEntry];
+ _self set ["taskCatalogRegistry", _updatedTaskCatalogRegistry];
+
+ _result set ["success", true];
+ _result set ["message", "Task accepted."];
+ _result set ["entry", _updatedEntry];
+ _result
+ }],
+ ["setTaskStatus", compileFinal {
+ params [["_taskID", "", [""]], ["_status", "", [""]]];
+
+ if (_taskID isEqualTo "" || { _status isEqualTo "" }) exitWith { false };
+
+ private _taskStatusRegistry = _self getOrDefault ["taskStatusRegistry", createHashMap];
+ private _completedTaskStatusRegistry = _self getOrDefault ["completedTaskStatusRegistry", createHashMap];
+ _taskStatusRegistry set [_taskID, _status];
+ if (_status in ["succeeded", "failed"]) then {
+ _completedTaskStatusRegistry set [_taskID, _status];
+ } else {
+ _completedTaskStatusRegistry deleteAt _taskID;
+ };
+ _self set ["taskStatusRegistry", _taskStatusRegistry];
+ _self set ["completedTaskStatusRegistry", _completedTaskStatusRegistry];
+ true
+ }],
+ ["getTaskStatus", compileFinal {
+ params [["_taskID", "", [""]]];
+
+ if (_taskID isEqualTo "") exitWith { "" };
+
+ private _taskStatusRegistry = _self getOrDefault ["taskStatusRegistry", createHashMap];
+ private _status = _taskStatusRegistry getOrDefault [_taskID, ""];
+ if (_status isNotEqualTo "") exitWith { _status };
+
+ private _completedTaskStatusRegistry = _self getOrDefault ["completedTaskStatusRegistry", createHashMap];
+ _completedTaskStatusRegistry getOrDefault [_taskID, ""]
+ }],
+ ["clearTaskStatus", compileFinal {
+ params [["_taskID", "", [""]]];
+
+ if (_taskID isEqualTo "") exitWith { false };
+
+ private _taskStatusRegistry = _self getOrDefault ["taskStatusRegistry", createHashMap];
+ private _completedTaskStatusRegistry = _self getOrDefault ["completedTaskStatusRegistry", createHashMap];
+ _taskStatusRegistry deleteAt _taskID;
+ _completedTaskStatusRegistry deleteAt _taskID;
+ _self set ["taskStatusRegistry", _taskStatusRegistry];
+ _self set ["completedTaskStatusRegistry", _completedTaskStatusRegistry];
+ true
+ }],
+ ["registerTaskEntity", compileFinal {
+ params [["_registryKey", "", [""]], ["_taskID", "", [""]], ["_entity", objNull, [objNull]]];
+
+ if (_registryKey isEqualTo "" || { _taskID isEqualTo "" } || { isNull _entity }) exitWith { false };
+
+ private _taskEntityRegistries = _self getOrDefault ["taskEntityRegistries", createHashMap];
+ private _registry = +(_taskEntityRegistries getOrDefault [_registryKey, createHashMap]);
+ private _entities = +(_registry getOrDefault [_taskID, []]);
+ _entities pushBackUnique _entity;
+ _registry set [_taskID, _entities];
+ _taskEntityRegistries set [_registryKey, _registry];
+ _self set ["taskEntityRegistries", _taskEntityRegistries];
+
+ true
+ }],
+ ["getTaskEntities", compileFinal {
+ params [["_registryKey", "", [""]], ["_taskID", "", [""]]];
+
+ if (_registryKey isEqualTo "" || { _taskID isEqualTo "" }) exitWith { [] };
+
+ private _taskEntityRegistries = _self getOrDefault ["taskEntityRegistries", createHashMap];
+ private _registry = _taskEntityRegistries getOrDefault [_registryKey, createHashMap];
+
+ +(_registry getOrDefault [_taskID, []])
+ }],
+ ["clearTaskEntities", compileFinal {
+ params [["_taskID", "", [""]]];
+
+ if (_taskID isEqualTo "") exitWith { false };
+
+ private _taskEntityRegistries = _self getOrDefault ["taskEntityRegistries", createHashMap];
+
+ {
+ private _registry = +_y;
+ _registry deleteAt _taskID;
+ _taskEntityRegistries set [_x, _registry];
+ } forEach _taskEntityRegistries;
+
+ _self set ["taskEntityRegistries", _taskEntityRegistries];
+ true
+ }],
+ ["trackParticipants", compileFinal {
+ params [["_taskID", "", [""]], ["_entities", [], [[]]], ["_marker", "", [""]], ["_radius", 300, [0]]];
+
+ if (_taskID isEqualTo "") exitWith { createHashMap };
+
+ private _participantRegistry = _self getOrDefault ["participantRegistry", createHashMap];
+ private _participantSnapshots = +(_participantRegistry getOrDefault [_taskID, createHashMap]);
+ private _activePlayers = allPlayers select {
+ alive _x
+ && { side group _x isEqualTo west }
+ };
+
+ if (_marker isNotEqualTo "" && { markerShape _marker in ["RECTANGLE", "ELLIPSE"] }) then {
+ {
+ private _uid = getPlayerUID _x;
+ if (_uid isNotEqualTo "" && { _x inArea _marker }) then {
+ if !(_uid in _participantSnapshots) then {
+ _participantSnapshots set [_uid, createHashMapFromArray [
+ ["startRating", rating _x]
+ ]];
+ };
+ };
+ } forEach _activePlayers;
+ };
+
+ if (_radius > 0 && { _entities isNotEqualTo [] }) then {
+ {
+ private _entity = _x;
+ if (isNull _entity) then { continue; };
+
+ {
+ private _uid = getPlayerUID _x;
+ if (_uid isNotEqualTo "" && { (_x distance2D _entity) <= _radius }) then {
+ if !(_uid in _participantSnapshots) then {
+ _participantSnapshots set [_uid, createHashMapFromArray [
+ ["startRating", rating _x]
+ ]];
+ };
+ };
+ } forEach _activePlayers;
+ } forEach _entities;
+ };
+
+ _participantRegistry set [_taskID, _participantSnapshots];
+ _self set ["participantRegistry", _participantRegistry];
+
+ _participantSnapshots
+ }],
+ ["resolveRewardContext", compileFinal {
+ params [["_taskID", "", [""]]];
+
+ private _result = createHashMapFromArray [
+ ["requesterUid", ""],
+ ["orgID", ""],
+ ["memberUids", []]
+ ];
+
+ if (_taskID isEqualTo "") exitWith { _result };
+
+ private _taskOwnershipRegistry = _self getOrDefault ["taskOwnershipRegistry", createHashMap];
+ private _ownership = _taskOwnershipRegistry getOrDefault [_taskID, createHashMap];
+ if (_ownership isEqualTo createHashMap) exitWith { _result };
+
+ private _requesterUid = _ownership getOrDefault ["requesterUid", ""];
+ private _resolvedOrgID = _ownership getOrDefault ["orgID", ""];
+ if (_resolvedOrgID isEqualTo "") exitWith { _result };
+
+ private _org = EGVAR(org,Registry) getOrDefault [_resolvedOrgID, createHashMap];
+ if (_org isEqualTo createHashMap) then {
+ _org = EGVAR(org,OrgStore) call ["loadById", [_resolvedOrgID]];
+ };
+
+ private _memberUids = [];
+ if (_org isNotEqualTo createHashMap) then {
+ _memberUids = EGVAR(org,OrgTreasuryService) call ["resolveOrgMemberUids", [_org, _requesterUid]];
+ };
+
+ _result set ["requesterUid", _requesterUid];
+ _result set ["orgID", _resolvedOrgID];
+ _result set ["memberUids", _memberUids];
+ _result
+ }],
+ ["incrementDefuseCount", compileFinal {
+ params [["_taskID", "", [""]]];
+
+ if (_taskID isEqualTo "") exitWith { 0 };
+
+ private _defuseRegistry = _self getOrDefault ["defuseRegistry", createHashMap];
+ private _nextCount = 1 + (_defuseRegistry getOrDefault [_taskID, 0]);
+ _defuseRegistry set [_taskID, _nextCount];
+ _self set ["defuseRegistry", _defuseRegistry];
+
+ _nextCount
+ }],
+ ["getDefuseCount", compileFinal {
+ params [["_taskID", "", [""]]];
+
+ if (_taskID isEqualTo "") exitWith { 0 };
+
+ private _defuseRegistry = _self getOrDefault ["defuseRegistry", createHashMap];
+ _defuseRegistry getOrDefault [_taskID, 0]
+ }],
+ ["notifyParticipants", compileFinal {
+ params [
+ ["_taskID", "", [""]],
+ ["_type", "info", [""]],
+ ["_title", "Tasks", [""]],
+ ["_message", "", [""]]
+ ];
+
+ if (_taskID isEqualTo "" || { _message isEqualTo "" }) exitWith { false };
+
+ private _participantRegistry = _self getOrDefault ["participantRegistry", createHashMap];
+ private _participantSnapshots = +(_participantRegistry getOrDefault [_taskID, createHashMap]);
+ if (_participantSnapshots isEqualTo createHashMap) exitWith { false };
+
+ {
+ private _player = [_x] call EFUNC(common,getPlayer);
+ if (isNull _player) then { continue; };
+ [CRPC(notifications,recieveNotification), [_type, _title, _message], _player] call CFUNC(targetEvent);
+ } forEach (keys _participantSnapshots);
+
+ true
+ }],
+ ["clearTask", compileFinal {
+ params [["_taskID", "", [""]]];
+
+ if (_taskID isEqualTo "") exitWith { false };
+
+ private _participantRegistry = _self getOrDefault ["participantRegistry", createHashMap];
+ private _defuseRegistry = _self getOrDefault ["defuseRegistry", createHashMap];
+ private _taskOwnershipRegistry = _self getOrDefault ["taskOwnershipRegistry", createHashMap];
+ private _taskStatusRegistry = _self getOrDefault ["taskStatusRegistry", createHashMap];
+ private _taskCatalogRegistry = _self getOrDefault ["taskCatalogRegistry", createHashMap];
+
+ _participantRegistry deleteAt _taskID;
+ _defuseRegistry deleteAt _taskID;
+ _taskOwnershipRegistry deleteAt _taskID;
+ _taskStatusRegistry deleteAt _taskID;
+ _taskCatalogRegistry deleteAt _taskID;
+
+ _self set ["participantRegistry", _participantRegistry];
+ _self set ["defuseRegistry", _defuseRegistry];
+ _self set ["taskOwnershipRegistry", _taskOwnershipRegistry];
+ _self set ["taskStatusRegistry", _taskStatusRegistry];
+ _self set ["taskCatalogRegistry", _taskCatalogRegistry];
+ _self call ["clearTaskEntities", [_taskID]];
+ true
+ }],
+ ["applyRatingOutcome", compileFinal {
+ params [["_taskID", "", [""]], ["_delta", 0, [0]]];
+
+ private _result = createHashMapFromArray [
+ ["participantUids", []],
+ ["orgIds", []],
+ ["contributions", createHashMap]
+ ];
+
+ if (_taskID isEqualTo "" || { _delta isEqualTo 0 }) exitWith { _result };
+
+ private _participantRegistry = _self getOrDefault ["participantRegistry", createHashMap];
+ private _participantSnapshots = +(_participantRegistry getOrDefault [_taskID, createHashMap]);
+ if (_participantSnapshots isEqualTo createHashMap) exitWith { _result };
+
+ private _participantUids = keys _participantSnapshots;
+ if (_participantUids isEqualTo []) exitWith { _result };
+
+ private _orgIds = [];
+ private _contributions = createHashMap;
+ private _totalContribution = 0;
+
+ {
+ private _uid = _x;
+ private _player = [_uid] call EFUNC(common,getPlayer);
+ if (isNull _player) then { continue; };
+
+ private _snapshot = _participantSnapshots getOrDefault [_uid, createHashMap];
+ private _startRating = _snapshot getOrDefault ["startRating", rating _player];
+ private _ratingDelta = (rating _player) - _startRating;
+ private _contribution = _ratingDelta max 0;
+
+ if (_delta < 0) then {
+ _contribution = (0 - _ratingDelta) max 0;
+ };
+
+ if (_contribution <= 0) then { continue; };
+
+ _contributions set [_uid, _contribution];
+ _totalContribution = _totalContribution + _contribution;
+ } forEach _participantUids;
+
+ if (_totalContribution <= 0) exitWith {
+ _self call ["clearTask", [_taskID]];
+ _result
+ };
+
+ {
+ private _uid = _x;
+ private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
+ if (_actor isEqualTo createHashMap) then {
+ _actor = EGVAR(actor,ActorStore) call ["init", [_uid]];
+ };
+
+ private _orgID = _actor getOrDefault ["organization", ""];
+ if (_orgID isNotEqualTo "") then {
+ _orgIds pushBackUnique _orgID;
+ };
+
+ if (_delta > 0) then {
+ private _contribution = _contributions getOrDefault [_uid, 0];
+ if (_contribution <= 0) then { continue; };
+
+ private _account = EGVAR(bank,Registry) getOrDefault [_uid, createHashMap];
+ if (_account isEqualTo createHashMap) then {
+ _account = EGVAR(bank,BankStore) call ["init", [_uid]];
+ };
+
+ if (_account isNotEqualTo createHashMap) then {
+ private _earnings = _account getOrDefault ["earnings", 0];
+ private _earningsDelta = round ((_delta * _contribution) / _totalContribution);
+ if (_earningsDelta <= 0) then { continue; };
+
+ private _patch = EGVAR(bank,BankStore) call [
+ "mset",
+ [
+ EGVAR(bank,Registry),
+ "bank:update",
+ _uid,
+ createHashMapFromArray [["earnings", (_earnings + _earningsDelta)]],
+ false
+ ]
+ ];
+
+ EGVAR(bank,BankMessenger) call ["sendAccountSync", [_uid, _patch]];
+ };
+ };
+ } forEach _participantUids;
+
+ private _rewardContext = _self call ["resolveRewardContext", [_taskID]];
+ private _ownerOrgID = _rewardContext getOrDefault ["orgID", ""];
+ if (_ownerOrgID isNotEqualTo "") then {
+ private _org = EGVAR(org,Registry) getOrDefault [_ownerOrgID, createHashMap];
+ if (_org isEqualTo createHashMap) then {
+ _org = EGVAR(org,OrgStore) call ["loadById", [_ownerOrgID]];
+ };
+
+ if (_org isNotEqualTo createHashMap) then {
+ private _reputation = _org getOrDefault ["reputation", 0];
+ private _nextReputation = round (_reputation + _delta);
+ private _patch = EGVAR(org,OrgStore) call [
+ "set",
+ [
+ EGVAR(org,Registry),
+ "org:update",
+ _ownerOrgID,
+ "reputation",
+ _nextReputation,
+ false
+ ]
+ ];
+
+ private _memberUids = _rewardContext getOrDefault ["memberUids", []];
+ {
+ private _player = [_x] call EFUNC(common,getPlayer);
+ if (isNull _player) then { continue; };
+ [CRPC(org,responseSyncOrg), [_patch], _player] call CFUNC(targetEvent);
+ } forEach _memberUids;
+
+ _orgIds = [_ownerOrgID];
+ };
+ };
+
+ _result set ["participantUids", _participantUids];
+ _result set ["orgIds", _orgIds];
+ _result set ["contributions", _contributions];
+ _result
+ }]
+]];
+
+GVAR(TaskStore)
diff --git a/arma/server/addons/task/functions/fnc_makeCargo.sqf b/arma/server/addons/task/functions/fnc_makeCargo.sqf
new file mode 100644
index 0000000..098b6ea
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_makeCargo.sqf
@@ -0,0 +1,41 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Assigns cargo to a task for delivery
+ *
+ * Arguments:
+ * 0: Object to convert to delivery cargo
+ * 1: Task ID to assign the cargo to
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * [_cargoObject, "delivery_1"] call forge_server_task_fnc_makeCargo;
+ *
+ * Public: Yes
+ */
+
+params [["_cargo", objNull, [objNull]], ["_taskID", "", [""]]];
+
+["INFO", format ["Make Cargo: %1", _this]] call EFUNC(common,log);
+
+if (isNull _cargo) exitWith { ["ERROR", "Attempt to create cargo from null object"] call EFUNC(common,log); };
+if (_taskID == "") exitWith { ["ERROR", "No task ID provided for cargo"] call EFUNC(common,log); };
+
+SETPVAR(_cargo,assignedTask,_taskID);
+GVAR(TaskStore) call ["registerTaskEntity", ["cargo", _taskID, _cargo]];
+
+_cargo addEventHandler ["Dammaged", {
+ params ["_unit", "_hitSelection", "_damage", "_hitPartIndex", "_hitPoint", "_shooter", "_projectile"];
+
+ if (damage _unit >= 0.7) then {
+ private _taskID = GETVAR(_unit,assignedTask,"");
+ if (_taskID isEqualTo "") exitWith {};
+ if (_unit getVariable [QGVAR(cargoDamageWarned), false]) exitWith {};
+
+ _unit setVariable [QGVAR(cargoDamageWarned), true];
+ GVAR(TaskStore) call ["notifyParticipants", [_taskID, "warning", "Tasks", format ["Cargo for task %1 has been severely damaged.", _taskID]]];
+ };
+}];
diff --git a/arma/server/addons/task/functions/fnc_makeHVT.sqf b/arma/server/addons/task/functions/fnc_makeHVT.sqf
new file mode 100644
index 0000000..8091035
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_makeHVT.sqf
@@ -0,0 +1,30 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Assigns an AI unit to a task as a hvt
+ *
+ * Arguments:
+ * 0: The AI unit
+ * 1: ID of the task
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * [this, "task_name"] spawn forge_server_task_fnc_makeHVT;
+ *
+ * Public: Yes
+ */
+
+params [["_entity", objNull, [objNull, grpNull]], ["_taskID", "", [""]]];
+
+if (isNull _entity) exitWith { ["ERROR", "Attempt to create entity from null object"] call EFUNC(common,log); };
+if (_taskID == "") exitWith { ["ERROR", "No task ID provided for entity"] call EFUNC(common,log); };
+
+["INFO", format ["Make HVT: %1", _this]] call EFUNC(common,log);
+
+SETVAR(_entity,assignedTask,_taskID);
+GVAR(TaskStore) call ["registerTaskEntity", ["hvts", _taskID, _entity]];
+
+if (alive _entity) then { [_entity, "hvt"] spawn FUNC(heartBeat); };
diff --git a/arma/server/addons/task/functions/fnc_makeHostage.sqf b/arma/server/addons/task/functions/fnc_makeHostage.sqf
new file mode 100644
index 0000000..4644ac8
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_makeHostage.sqf
@@ -0,0 +1,30 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Assigns an AI unit to a task as a hostage
+ *
+ * Arguments:
+ * 0: The AI unit
+ * 1: ID of the task
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * [this, "task_name"] spawn forge_server_task_fnc_makeHostage;
+ *
+ * Public: Yes
+ */
+
+params [["_entity", objNull, [objNull, grpNull]], ["_taskID", "", [""]]];
+
+if (isNull _entity) exitWith { ["ERROR", "Attempt to create entity from null object"] call EFUNC(common,log); };
+if (_taskID == "") exitWith { ["ERROR", "No task ID provided for entity"] call EFUNC(common,log); };
+
+["INFO", format ["Make Hostage: %1", _this]] call EFUNC(common,log);
+
+SETVAR(_entity,assignedTask,_taskID);
+GVAR(TaskStore) call ["registerTaskEntity", ["hostages", _taskID, _entity]];
+
+if (alive _entity) then { [_entity, "hostage"] spawn FUNC(heartBeat); };
diff --git a/arma/server/addons/task/functions/fnc_makeIED.sqf b/arma/server/addons/task/functions/fnc_makeIED.sqf
new file mode 100644
index 0000000..a4489c6
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_makeIED.sqf
@@ -0,0 +1,32 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Assigns an IED to a task and starts countdown timer
+ *
+ * Arguments:
+ * 0: The object
+ * 1: ID of the task
+ * 2: The Countdown Timer
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * [this, "task_name", 30] spawn forge_server_task_fnc_makeIED;
+ *
+ * Public: Yes
+ */
+
+params [["_entity", objNull, [objNull]], ["_taskID", "", [""]], ["_time", 0, [0]]];
+
+if (isNull _entity) exitWith { ["ERROR", "Attempt to create entity from null object"] call EFUNC(common,log); };
+if (_taskID == "") exitWith { ["ERROR", "No task ID provided for entity"] call EFUNC(common,log); };
+if (_time < 0) exitWith { ["ERROR", "Invalid time provided for IED"] call EFUNC(common,log); };
+
+["INFO", format ["Make IED: %1", _this]] call EFUNC(common,log);
+
+SETVAR(_entity,assignedTask,_taskID);
+GVAR(TaskStore) call ["registerTaskEntity", ["ieds", _taskID, _entity]];
+
+if (alive _entity) then { [_entity, "ied", _time] spawn FUNC(heartBeat); };
diff --git a/arma/server/addons/task/functions/fnc_makeObject.sqf b/arma/server/addons/task/functions/fnc_makeObject.sqf
new file mode 100644
index 0000000..72c1d83
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_makeObject.sqf
@@ -0,0 +1,28 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Assigns an object to a task as a protected target
+ *
+ * Arguments:
+ * 0: The object
+ * 1: ID of the task
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * [this, "task_name"] spawn forge_server_task_fnc_makeObject;
+ *
+ * Public: Yes
+ */
+
+params [["_entity", objNull, [objNull]], ["_taskID", "", [""]]];
+
+if (isNull _entity) exitWith { ["ERROR", "Attempt to create entity from null object"] call EFUNC(common,log); };
+if (_taskID == "") exitWith { ["ERROR", "No task ID provided for entity"] call EFUNC(common,log); };
+
+["INFO", format ["Make Object: %1", _this]] call EFUNC(common,log);
+
+SETPVAR(_entity,assignedTask,_taskID);
+GVAR(TaskStore) call ["registerTaskEntity", ["entities", _taskID, _entity]];
diff --git a/arma/server/addons/task/functions/fnc_makeShooter.sqf b/arma/server/addons/task/functions/fnc_makeShooter.sqf
new file mode 100644
index 0000000..ffce942
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_makeShooter.sqf
@@ -0,0 +1,28 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Assigns an AI unit to a task as a shooter
+ *
+ * Arguments:
+ * 0: The AI unit
+ * 1: ID of the task
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * [this, "task_name"] spawn forge_server_task_fnc_makeShooter;
+ *
+ * Public: Yes
+ */
+
+params [["_entity", objNull, [objNull, grpNull]], ["_taskID", "", [""]]];
+
+if (isNull _entity) exitWith { ["ERROR", "Attempt to create entity from null object"] call EFUNC(common,log); };
+if (_taskID == "") exitWith { ["ERROR", "No task ID provided for entity"] call EFUNC(common,log); };
+
+["INFO", format ["Make Shooter: %1", _this]] call EFUNC(common,log);
+
+SETVAR(_entity,assignedTask,_taskID);
+GVAR(TaskStore) call ["registerTaskEntity", ["shooters", _taskID, _entity]];
diff --git a/arma/server/addons/task/functions/fnc_makeTarget.sqf b/arma/server/addons/task/functions/fnc_makeTarget.sqf
new file mode 100644
index 0000000..284ce4e
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_makeTarget.sqf
@@ -0,0 +1,28 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Assigns an object to a task as a target
+ *
+ * Arguments:
+ * 0: The object
+ * 1: ID of the task
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * [this, "task_name"] spawn forge_server_task_fnc_makeTarget;
+ *
+ * Public: Yes
+ */
+
+params [["_entity", objNull, [objNull, grpNull]], ["_taskID", "", [""]]];
+
+if (isNull _entity) exitWith { ["ERROR", "Attempt to create entity from null object"] call EFUNC(common,log); };
+if (_taskID == "") exitWith { ["ERROR", "No task ID provided for entity"] call EFUNC(common,log); };
+
+["INFO", format ["Make Target: %1", _this]] call EFUNC(common,log);
+
+SETVAR(_entity,assignedTask,_taskID);
+GVAR(TaskStore) call ["registerTaskEntity", ["targets", _taskID, _entity]];
diff --git a/arma/server/addons/task/functions/fnc_missionManager.sqf b/arma/server/addons/task/functions/fnc_missionManager.sqf
new file mode 100644
index 0000000..a1aec02
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_missionManager.sqf
@@ -0,0 +1,369 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Manages attack-only dynamic mission generation.
+ *
+ * Arguments:
+ * None
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * [] call forge_server_task_fnc_missionManager
+ *
+ * Public: No
+ */
+
+#pragma hemtt ignore_variables ["_self"]
+GVAR(MissionManagerBaseClass) = compileFinal createHashMapFromArray [
+ ["#type", "MissionManagerBaseClass"],
+ ["#create", compileFinal {
+ private _missionConfig = missionConfigFile >> "CfgMissions";
+ _self set ["missionConfig", _missionConfig];
+ _self set ["locationsConfig", (_missionConfig >> "Locations")];
+ _self set ["aiGroupsConfig", (_missionConfig >> "AIGroups")];
+ _self set ["attackConfig", (_missionConfig >> "MissionTypes" >> "Attack")];
+ _self set ["maxConcurrentMissions", getNumber (_missionConfig >> "maxConcurrentMissions")];
+ _self set ["missionInterval", getNumber (_missionConfig >> "missionInterval")];
+ _self set ["recentLocationRegistry", createHashMap];
+ _self set ["activeMissionRegistry", createHashMap];
+ }],
+ ["getMissionInterval", compileFinal {
+ private _interval = _self getOrDefault ["missionInterval", 300];
+ if (_interval <= 0) then { _interval = 300; };
+ _interval
+ }],
+ ["getMaxConcurrentMissions", compileFinal {
+ private _maxConcurrent = _self getOrDefault ["maxConcurrentMissions", 1];
+ if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
+ _maxConcurrent
+ }],
+ ["getLocationReuseCooldown", compileFinal {
+ private _missionConfig = _self getOrDefault ["missionConfig", configNull];
+ private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
+ if (_cooldown <= 0) then { _cooldown = 900; };
+ _cooldown
+ }],
+ ["getActiveMissionIds", compileFinal {
+ private _activeMissionRegistry = _self getOrDefault ["activeMissionRegistry", createHashMap];
+ keys _activeMissionRegistry
+ }],
+ ["getActiveLocationKeys", compileFinal {
+ private _activeMissionRegistry = _self getOrDefault ["activeMissionRegistry", createHashMap];
+ private _locationKeys = [];
+ {
+ private _locationKey = _y getOrDefault ["locationKey", ""];
+ if (_locationKey isNotEqualTo "") then {
+ _locationKeys pushBackUnique _locationKey;
+ };
+ } forEach _activeMissionRegistry;
+ _locationKeys
+ }],
+ ["buildAttackSpawnPosition", compileFinal {
+ params [["_locationConfig", configNull, [configNull]]];
+
+ if (isNull _locationConfig) exitWith { [0, 0, 0] };
+
+ private _center = getArray (_locationConfig >> "position");
+ private _radius = getNumber (_locationConfig >> "radius");
+ if (_radius <= 0) exitWith { _center };
+
+ private _spawnPosition = +_center;
+ private _attempts = 0;
+ while { _attempts < 8 } do {
+ private _angle = random 360;
+ private _distance = (_radius * 0.2) + random (_radius * 0.65);
+ private _candidate = [
+ (_center # 0) + ((sin _angle) * _distance),
+ (_center # 1) + ((cos _angle) * _distance),
+ _center param [2, 0]
+ ];
+
+ if !(surfaceIsWater _candidate) exitWith {
+ _spawnPosition = _candidate;
+ };
+
+ _attempts = _attempts + 1;
+ };
+
+ _spawnPosition
+ }],
+ ["selectAttackLocation", compileFinal {
+ private _locationsConfig = _self getOrDefault ["locationsConfig", configNull];
+ private _locations = [];
+ private _recentLocationRegistry = _self getOrDefault ["recentLocationRegistry", createHashMap];
+ private _activeLocationKeys = _self call ["getActiveLocationKeys", []];
+ private _reuseCooldown = _self call ["getLocationReuseCooldown", []];
+ private _now = serverTime;
+
+ {
+ private _locationKey = configName _x;
+ private _lastUsed = _recentLocationRegistry getOrDefault [_locationKey, -1];
+ private _isCoolingDown = (_lastUsed >= 0) && { (_now - _lastUsed) < _reuseCooldown };
+
+ if (
+ "attack" in getArray (_x >> "suitable")
+ && { !(_locationKey in _activeLocationKeys) }
+ && { !_isCoolingDown }
+ ) then {
+ _locations pushBack _x;
+ };
+ } forEach ("true" configClasses _locationsConfig);
+
+ if (_locations isEqualTo []) then {
+ {
+ if ("attack" in getArray (_x >> "suitable") && { !(configName _x in _activeLocationKeys) }) then {
+ _locations pushBack _x;
+ };
+ } forEach ("true" configClasses _locationsConfig);
+ };
+
+ if (_locations isEqualTo []) exitWith { createHashMap };
+
+ private _location = selectRandom _locations;
+ createHashMapFromArray [
+ ["config", _location],
+ ["key", configName _location],
+ ["position", _self call ["buildAttackSpawnPosition", [_location]]]
+ ]
+ }],
+ ["spawnAttackGroup", compileFinal {
+ params [["_position", [0, 0, 0], [[]]]];
+
+ private _aiGroupsConfig = _self getOrDefault ["aiGroupsConfig", configNull];
+ private _attackConfig = _self getOrDefault ["attackConfig", configNull];
+ private _groups = [];
+ {
+ if ("attack" in getArray (_x >> "suitable")) then {
+ _groups pushBack _x;
+ };
+ } forEach ("true" configClasses _aiGroupsConfig);
+
+ if (_groups isEqualTo []) exitWith { grpNull };
+
+ private _groupConfig = selectRandom _groups;
+ private _side = getText (_groupConfig >> "side");
+ private _group = createGroup (call compile _side);
+ private _minUnits = getNumber (_attackConfig >> "minUnits");
+ private _maxUnits = getNumber (_attackConfig >> "maxUnits");
+ if (_minUnits <= 0) then { _minUnits = 4; };
+ if (_maxUnits < _minUnits) then { _maxUnits = _minUnits; };
+ private _targetUnitCount = floor random [ _minUnits, ceil ((_minUnits + _maxUnits) / 2), _maxUnits + 1 ];
+
+ private _unitPool = [];
+ {
+ if ((getText (_x >> "side")) isNotEqualTo _side) then { continue; };
+
+ {
+ _unitPool pushBack createHashMapFromArray [
+ ["vehicle", getText (_x >> "vehicle")],
+ ["rank", getText (_x >> "rank")],
+ ["position", getArray (_x >> "position")]
+ ];
+ } forEach ("true" configClasses (_x >> "Units"));
+ } forEach _groups;
+
+ if (_unitPool isEqualTo []) exitWith {
+ deleteGroup _group;
+ grpNull
+ };
+
+ private _leaderPool = _unitPool select {
+ toUpperANSI (_x getOrDefault ["rank", "PRIVATE"]) in ["SERGEANT", "LIEUTENANT", "CAPTAIN", "MAJOR", "COLONEL"]
+ };
+ if (_leaderPool isEqualTo []) then { _leaderPool = +_unitPool; };
+
+ private _spawnDefs = [selectRandom _leaderPool];
+ for "_i" from 1 to (_targetUnitCount - 1) do {
+ _spawnDefs pushBack (selectRandom _unitPool);
+ };
+
+ {
+ private _unitClass = _x getOrDefault ["vehicle", ""];
+ if (_unitClass isEqualTo "") then { continue; };
+
+ private _unitOffset = +(_x getOrDefault ["position", [0, 0, 0]]);
+ if (count _unitOffset < 3) then { _unitOffset resize 3; };
+ _unitOffset set [0, (_unitOffset # 0) + (random 6 - 3)];
+ _unitOffset set [1, (_unitOffset # 1) + (random 6 - 3)];
+
+ private _unit = _group createUnit [_unitClass, _position vectorAdd _unitOffset, [], 0, "NONE"];
+ _unit setRank (_x getOrDefault ["rank", "PRIVATE"]);
+ } forEach _spawnDefs;
+
+ _group
+ }],
+ ["rollRewards", compileFinal {
+ private _attackConfig = _self getOrDefault ["attackConfig", configNull];
+ private _equipmentRewards = [];
+ private _supplyRewards = [];
+ private _weaponRewards = [];
+ private _vehicleRewards = [];
+ private _specialRewards = [];
+
+ {
+ private _category = _x;
+ {
+ _x params ["_item", "_chance"];
+ if (random 1 < _chance) then {
+ switch (_category) do {
+ case "equipment": { _equipmentRewards pushBack _item; };
+ case "supplies": { _supplyRewards pushBack _item; };
+ case "weapons": { _weaponRewards pushBack _item; };
+ case "vehicles": { _vehicleRewards pushBack _item; };
+ case "special": { _specialRewards pushBack _item; };
+ };
+ };
+ } forEach (getArray (_attackConfig >> "Rewards" >> _category));
+ } forEach ["equipment", "supplies", "weapons", "vehicles", "special"];
+
+ createHashMapFromArray [
+ ["equipment", _equipmentRewards],
+ ["supplies", _supplyRewards],
+ ["weapons", _weaponRewards],
+ ["vehicles", _vehicleRewards],
+ ["special", _specialRewards]
+ ]
+ }],
+ ["createAttackTask", compileFinal {
+ params [
+ ["_taskID", "", [""]],
+ ["_position", [0, 0, 0], [[]]],
+ ["_locationConfig", configNull, [configNull]]
+ ];
+
+ if (_taskID isEqualTo "" || { isNull _locationConfig }) exitWith { false };
+
+ private _locationKey = configName _locationConfig;
+ private _locationType = getText (_locationConfig >> "type");
+ if (_locationType isEqualTo "") then { _locationType = "area"; };
+
+ [
+ west,
+ _taskID,
+ [
+ format ["Eliminate hostile forces operating near %1.", _locationKey],
+ format ["Attack: %1", _locationKey],
+ _locationType
+ ],
+ _position,
+ "CREATED",
+ 1,
+ true,
+ "attack"
+ ] call BFUNC(taskCreate);
+
+ true
+ }],
+ ["startAttackMission", compileFinal {
+ private _attackConfig = _self getOrDefault ["attackConfig", configNull];
+ private _locationData = _self call ["selectAttackLocation"];
+ if (_locationData isEqualTo createHashMap) exitWith { "" };
+
+ private _location = _locationData getOrDefault ["config", configNull];
+ private _locationKey = _locationData getOrDefault ["key", ""];
+ private _position = _locationData getOrDefault ["position", [0, 0, 0]];
+ private _group = _self call ["spawnAttackGroup", [_position]];
+ if (isNull _group) exitWith { "" };
+
+ private _units = units _group;
+ if (_units isEqualTo []) exitWith {
+ deleteGroup _group;
+ ""
+ };
+
+ private _taskID = format ["task_attack_%1", round (diag_tickTime * 1000)];
+ {
+ [_x, _taskID] call FUNC(makeTarget);
+ } forEach _units;
+
+ _self call ["createAttackTask", [_taskID, _position, _location]];
+ GVAR(TaskStore) call ["registerTaskCatalogEntry", [_taskID, createHashMapFromArray [
+ ["type", "attack"],
+ ["title", format ["Attack: %1", _locationKey]],
+ ["description", format ["Eliminate hostile forces operating near %1.", _locationKey]],
+ ["position", _position],
+ ["locationKey", _locationKey],
+ ["accepted", false],
+ ["requesterUid", ""],
+ ["orgID", "default"],
+ ["source", "mission_manager"]
+ ]]];
+
+ private _rewardRange = getArray (_attackConfig >> "Rewards" >> "money");
+ private _reputationRange = getArray (_attackConfig >> "Rewards" >> "reputation");
+ private _penaltyRange = getArray (_attackConfig >> "penalty");
+ private _timeRange = getArray (_attackConfig >> "timeLimit");
+ private _rewards = _self call ["rollRewards"];
+
+ private _params = [
+ _taskID,
+ 0,
+ count _units,
+ _rewardRange call BFUNC(randomNum),
+ _penaltyRange call BFUNC(randomNum),
+ _reputationRange call BFUNC(randomNum),
+ false,
+ false,
+ _timeRange call BFUNC(randomNum),
+ _rewards get "equipment",
+ _rewards get "supplies",
+ _rewards get "weapons",
+ _rewards get "vehicles",
+ _rewards get "special"
+ ];
+
+ private _activeMissionRegistry = _self getOrDefault ["activeMissionRegistry", createHashMap];
+ _activeMissionRegistry set [_taskID, createHashMapFromArray [
+ ["locationKey", _locationKey],
+ ["startedAt", serverTime]
+ ]];
+ _self set ["activeMissionRegistry", _activeMissionRegistry];
+
+ ["attack", _params, 0, ""] spawn FUNC(handler);
+ _taskID
+ }],
+ ["completeMission", compileFinal {
+ params [["_taskID", "", [""]]];
+
+ if (_taskID isEqualTo "") exitWith { false };
+
+ private _activeMissionRegistry = _self getOrDefault ["activeMissionRegistry", createHashMap];
+ private _missionRecord = _activeMissionRegistry getOrDefault [_taskID, createHashMap];
+ private _locationKey = _missionRecord getOrDefault ["locationKey", ""];
+
+ _activeMissionRegistry deleteAt _taskID;
+ _self set ["activeMissionRegistry", _activeMissionRegistry];
+
+ if (_locationKey isNotEqualTo "") then {
+ private _recentLocationRegistry = _self getOrDefault ["recentLocationRegistry", createHashMap];
+ _recentLocationRegistry set [_locationKey, serverTime];
+ _self set ["recentLocationRegistry", _recentLocationRegistry];
+ };
+
+ true
+ }]
+];
+
+GVAR(MissionManager) = createHashMapObject [GVAR(MissionManagerBaseClass)];
+
+[{
+ {
+ private _status = GVAR(TaskStore) call ["getTaskStatus", [_x]];
+ if (_status in ["succeeded", "failed"]) then {
+ GVAR(MissionManager) call ["completeMission", [_x]];
+ GVAR(TaskStore) call ["clearTaskStatus", [_x]];
+ };
+ } forEach (GVAR(MissionManager) call ["getActiveMissionIds", []]);
+
+ if (count (GVAR(MissionManager) call ["getActiveMissionIds", []]) >= (GVAR(MissionManager) call ["getMaxConcurrentMissions", []])) exitWith {};
+
+ private _taskID = GVAR(MissionManager) call ["startAttackMission", []];
+ if (_taskID isEqualTo "") exitWith {
+ ["WARNING", "Mission manager failed to start an attack mission."] call EFUNC(common,log);
+ };
+
+ ["INFO", format ["Mission manager started attack mission %1.", _taskID]] call EFUNC(common,log);
+}, GVAR(MissionManager) call ["getMissionInterval", []], []] call CFUNC(addPerFrameHandler);
diff --git a/arma/server/addons/task/functions/fnc_protectedModule.sqf b/arma/server/addons/task/functions/fnc_protectedModule.sqf
new file mode 100644
index 0000000..cd80fd0
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_protectedModule.sqf
@@ -0,0 +1,23 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Initializes the protected module
+ *
+ * Arguments:
+ * 0: Logic - The logic object
+ * 1: Units - The array of units
+ * 2: Activated - Whether the module is activated
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * [logicObject, [unit1, unit2], true] call forge_server_task_fnc_protectedModule;
+ *
+ * Public: No
+ */
+
+params [["_logic", objNull, [objNull]], ["_units", [], [[]]], ["_activated", true, [true]]];
+
+if !(_activated) exitWith {};
diff --git a/arma/server/addons/task/functions/fnc_shootersModule.sqf b/arma/server/addons/task/functions/fnc_shootersModule.sqf
new file mode 100644
index 0000000..610f89b
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_shootersModule.sqf
@@ -0,0 +1,23 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Initializes the shooters module
+ *
+ * Arguments:
+ * 0: Logic - The logic object
+ * 1: Units - The array of units
+ * 2: Activated - Whether the module is activated
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * [logicObject, [unit1, unit2], true] call forge_server_task_fnc_shootersModule;
+ *
+ * Public: No
+ */
+
+params [["_logic", objNull, [objNull]], ["_units", [], [[]]], ["_activated", true, [true]]];
+
+if !(_activated) exitWith {};
diff --git a/arma/server/addons/task/functions/fnc_spawnEnemyWave.sqf b/arma/server/addons/task/functions/fnc_spawnEnemyWave.sqf
new file mode 100644
index 0000000..eac719a
--- /dev/null
+++ b/arma/server/addons/task/functions/fnc_spawnEnemyWave.sqf
@@ -0,0 +1,83 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Spawns an enemy wave for a defense task
+ *
+ * Arguments:
+ * 0: Defense zone marker name
+ * 1: Task ID
+ * 2: Wave number (0-based)
+ *
+ * Return Value:
+ * None
+ *
+ * Example:
+ * ["defend_marker", "defend_1", 0] call forge_server_task_fnc_spawnEnemyWave;
+ *
+ * Public: No
+ */
+
+params [["_defenseZone", "", [""]], ["_taskID", "", [""]], ["_waveNumber", 0, [0]]];
+
+if (_defenseZone == "") exitWith { ["ERROR", "No defense zone provided for enemy wave spawn"] call EFUNC(common,log); };
+
+// TODO: Add unit types to mission config
+private _basicTypes = ["O_Soldier_F", "O_Soldier_AR_F", "O_Soldier_GL_F", "O_medic_F"];
+private _specialTypes = ["O_Soldier_LAT_F", "O_soldier_M_F", "O_Soldier_TL_F", "O_Soldier_SL_F"];
+private _eliteTypes = ["O_Soldier_HAT_F", "O_Soldier_AA_F", "O_engineer_F", "O_Sharpshooter_F"];
+
+private _unitCount = 6 + (_waveNumber * 2); // TODO: Make this configurable in mission config
+private _specialChance = 0.2 + (_waveNumber * 0.1); // TODO: Make this configurable in mission config
+private _eliteChance = (_waveNumber * 0.05); // TODO: Make this configurable in mission config
+
+private _center = getMarkerPos _defenseZone;
+private _radius = (getMarkerSize _defenseZone select 0) max (getMarkerSize _defenseZone select 1);
+private _spawnRadius = _radius + 150;
+private _spawnPositions = [];
+
+for "_i" from 0 to 3 do {
+ private _angle = _i * 90;
+ private _variance = 45;
+ private _spawnAngle = _angle + (random (_variance * 2) - _variance);
+ private _spawnDist = _spawnRadius + (random 50 - 25);
+
+ private _spawnX = (_center select 0) + (_spawnDist * cos _spawnAngle);
+ private _spawnY = (_center select 1) + (_spawnDist * sin _spawnAngle);
+ private _spawnPos = [_spawnX, _spawnY, 0];
+
+ private _safePos = _spawnPos findEmptyPosition [0, 50, "O_Soldier_F"];
+ if (count _safePos > 0) then {
+ _spawnPositions pushBack _safePos;
+ };
+};
+
+private _groups = [];
+{
+ private _groupSize = ceil(_unitCount / (count _spawnPositions));
+ private _group = createGroup east;
+ _groups pushBack _group;
+
+ for "_i" from 1 to _groupSize do {
+ private _unitType = _basicTypes select (floor random count _basicTypes);
+ private _roll = random 1;
+
+ if (_roll < _eliteChance) then {
+ _unitType = _eliteTypes select (floor random count _eliteTypes);
+ } else {
+ if (_roll < _specialChance) then {
+ _unitType = _specialTypes select (floor random count _specialTypes);
+ };
+ };
+
+ private _unit = _group createUnit [_unitType, _x, [], 0, "NONE"];
+ _unit setVariable ["assignedTask", _taskID, true];
+ _unit setBehaviour "AWARE";
+ _unit setSpeedMode "NORMAL";
+ _unit enableDynamicSimulation true;
+ };
+
+ [_group, _center, _radius * 0.75] call CBA_fnc_taskDefend;
+} forEach _spawnPositions;
+
+["INFO", format ["Spawned defense wave %1 for task %2 with %3 units", _waveNumber + 1, _taskID, _unitCount]] call EFUNC(common,log);
diff --git a/arma/server/addons/task/script_component.hpp b/arma/server/addons/task/script_component.hpp
new file mode 100644
index 0000000..c90c053
--- /dev/null
+++ b/arma/server/addons/task/script_component.hpp
@@ -0,0 +1,9 @@
+#define COMPONENT task
+#define COMPONENT_BEAUTIFIED Task
+#include "\forge\forge_server\addons\main\script_mod.hpp"
+
+// #define DEBUG_MODE_FULL
+// #define DISABLE_COMPILE_CACHE
+// #define ENABLE_PERFORMANCE_COUNTERS
+
+#include "\forge\forge_server\addons\main\script_macros.hpp"
diff --git a/arma/server/addons/task/stringtable.xml b/arma/server/addons/task/stringtable.xml
new file mode 100644
index 0000000..ea8a314
--- /dev/null
+++ b/arma/server/addons/task/stringtable.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ Task
+
+
+
diff --git a/arma/server/extension/src/org.rs b/arma/server/extension/src/org.rs
index f73ef8e..2f4d7f4 100644
--- a/arma/server/extension/src/org.rs
+++ b/arma/server/extension/src/org.rs
@@ -31,6 +31,18 @@ pub fn group() -> Group {
.command("update", update_org)
.command("exists", org_exists)
.command("delete", delete_org)
+ .group(
+ "assets",
+ Group::new()
+ .command("get", get_assets)
+ .command("update", update_assets),
+ )
+ .group(
+ "fleet",
+ Group::new()
+ .command("get", get_fleet)
+ .command("update", update_fleet),
+ )
.group(
"members",
Group::new()
@@ -162,6 +174,56 @@ pub fn delete_org(key: String) -> String {
}
}
+pub fn get_assets(key: String) -> String {
+ match ORG_SERVICE.get_assets(key) {
+ Ok(assets) => match serde_json::to_string(&assets) {
+ Ok(json) => json,
+ Err(e) => format!("Error: Failed to serialize org assets: {}", e),
+ },
+ Err(e) => format!("Error: {}", e),
+ }
+}
+
+pub fn update_assets(key: String, json_update: String) -> String {
+ let assets_value: serde_json::Value = match serde_json::from_str(&json_update) {
+ Ok(value) => value,
+ Err(e) => return format!("Error: Invalid JSON: {}", e),
+ };
+
+ match ORG_SERVICE.update_assets(key, assets_value) {
+ Ok(assets) => match serde_json::to_string(&assets) {
+ Ok(json) => json,
+ Err(e) => format!("Error: Failed to serialize org assets: {}", e),
+ },
+ Err(e) => format!("Error: {}", e),
+ }
+}
+
+pub fn get_fleet(key: String) -> String {
+ match ORG_SERVICE.get_fleet(key) {
+ Ok(fleet) => match serde_json::to_string(&fleet) {
+ Ok(json) => json,
+ Err(e) => format!("Error: Failed to serialize org fleet: {}", e),
+ },
+ Err(e) => format!("Error: {}", e),
+ }
+}
+
+pub fn update_fleet(key: String, json_update: String) -> String {
+ let fleet_value: serde_json::Value = match serde_json::from_str(&json_update) {
+ Ok(value) => value,
+ Err(e) => return format!("Error: Invalid JSON: {}", e),
+ };
+
+ match ORG_SERVICE.update_fleet(key, fleet_value) {
+ Ok(fleet) => match serde_json::to_string(&fleet) {
+ Ok(json) => json,
+ Err(e) => format!("Error: Failed to serialize org fleet: {}", e),
+ },
+ Err(e) => format!("Error: {}", e),
+ }
+}
+
// ============================================================================
// Member Operations
// ============================================================================
diff --git a/arma/server/extension/src/redis/hash.rs b/arma/server/extension/src/redis/hash.rs
index 8b53240..399ba45 100644
--- a/arma/server/extension/src/redis/hash.rs
+++ b/arma/server/extension/src/redis/hash.rs
@@ -39,13 +39,10 @@ pub fn hash_get(key: String, field: String) -> String {
pub fn hash_get_all(key: String) -> String {
redis_operation!(conn => {
match conn.hgetall::<_, HashMap>(&key).await {
- Ok(hash_map) => {
- let formatted_pairs: Vec = hash_map
- .iter()
- .map(|(k, v)| format!("{}, {}", k, v))
- .collect();
- formatted_pairs.join(", ")
- }
+ Ok(hash_map) => match serde_json::to_string(&hash_map) {
+ Ok(json) => json,
+ Err(e) => format!("Error: Failed to serialize hash map: {}", e),
+ },
Err(e) => format!("Error: {}", e),
}
})
diff --git a/lib/models/src/lib.rs b/lib/models/src/lib.rs
index b710d4f..1a149e7 100644
--- a/lib/models/src/lib.rs
+++ b/lib/models/src/lib.rs
@@ -10,6 +10,6 @@ pub use actor::Actor;
pub use bank::Bank;
pub use garage::{Garage, HitPoints, Vehicle};
pub use locker::{Item, Locker};
-pub use org::{CreditLineSummary, MemberSummary, Org};
+pub use org::{CreditLineSummary, MemberSummary, Org, OrgAssetEntry, OrgFleetEntry};
pub use v_garage::{VGarage, VehicleCategory};
pub use v_locker::{EquipmentCategory, VLocker};
diff --git a/lib/models/src/org.rs b/lib/models/src/org.rs
index b4da3fa..7683b29 100644
--- a/lib/models/src/org.rs
+++ b/lib/models/src/org.rs
@@ -10,6 +10,24 @@ pub struct CreditLineSummary {
pub amount: f64,
}
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct OrgAssetEntry {
+ pub classname: String,
+ #[serde(rename = "type")]
+ pub asset_type: String,
+ pub quantity: i64,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct OrgFleetEntry {
+ pub classname: String,
+ pub name: String,
+ #[serde(rename = "type")]
+ pub fleet_type: String,
+ pub status: String,
+ pub damage: String,
+}
+
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Org {
pub id: String,
@@ -62,6 +80,12 @@ impl Org {
return Err(OrgValidationError::NegativeFunds);
}
+ if self.reputation < 0 {
+ return Err(OrgValidationError::InvalidName(
+ "Organization reputation cannot be negative".to_string(),
+ ));
+ }
+
if !self.id.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err(OrgValidationError::InvalidId(self.id.clone()));
}
diff --git a/lib/repositories/src/actor.rs b/lib/repositories/src/actor.rs
index c20fcd8..c576a3f 100644
--- a/lib/repositories/src/actor.rs
+++ b/lib/repositories/src/actor.rs
@@ -98,21 +98,14 @@ impl ActorRepository for RedisActorRepository {
return Ok(None);
}
- // Parse comma-separated field-value pairs
- let parts: Vec<&str> = actor_string.split(", ").collect();
+ let redis_map: std::collections::HashMap =
+ serde_json::from_str(&actor_string)
+ .map_err(|e| format!("Failed to parse actor hash response: {}", e))?;
let mut json_map = serde_json::Map::new();
- let mut i = 0;
- // Process pairs of field names and values
- while i + 1 < parts.len() {
- let key = parts[i];
- let value = parts[i + 1];
-
- // Convert Redis string value back to proper JSON type
- let json_value = parse_redis_value(value);
- json_map.insert(key.to_string(), json_value);
-
- i += 2; // Move to next field-value pair
+ for (key, value) in redis_map {
+ let json_value = parse_redis_value(&value);
+ json_map.insert(key, json_value);
}
// Reconstruct Actor from JSON object
diff --git a/lib/repositories/src/bank.rs b/lib/repositories/src/bank.rs
index 11940d6..0189c94 100644
--- a/lib/repositories/src/bank.rs
+++ b/lib/repositories/src/bank.rs
@@ -98,21 +98,14 @@ impl BankRepository for RedisBankRepository {
return Ok(None);
}
- // Parse comma-separated field-value pairs
- let parts: Vec<&str> = bank_string.split(", ").collect();
+ let redis_map: std::collections::HashMap =
+ serde_json::from_str(&bank_string)
+ .map_err(|e| format!("Failed to parse bank hash response: {}", e))?;
let mut json_map = serde_json::Map::new();
- let mut i = 0;
- // Process pairs of field names and values
- while i + 1 < parts.len() {
- let key = parts[i];
- let value = parts[i + 1];
-
- // Convert Redis string value back to proper JSON type
- let json_value = parse_redis_value(value);
- json_map.insert(key.to_string(), json_value);
-
- i += 2; // Move to next field-value pair
+ for (key, value) in redis_map {
+ let json_value = parse_redis_value(&value);
+ json_map.insert(key, json_value);
}
// Reconstruct Bank from JSON object
diff --git a/lib/repositories/src/org.rs b/lib/repositories/src/org.rs
index 60340cd..cd854f9 100644
--- a/lib/repositories/src/org.rs
+++ b/lib/repositories/src/org.rs
@@ -5,8 +5,9 @@
//!
//! For full documentation and examples, see the [crate README](../README.md).
-use forge_models::{MemberSummary, Org};
+use forge_models::{MemberSummary, Org, OrgAssetEntry, OrgFleetEntry};
use forge_shared::{RedisClient, parse_json_value, parse_redis_value};
+use std::collections::HashMap;
/// Repository trait defining the contract for organization data operations.
///
@@ -37,6 +38,29 @@ pub trait OrgRepository: Send + Sync {
/// Removes a specific member from an organization.
fn remove_member(&self, org_id: &str, member_uid: &str) -> Result<(), String>;
+
+ /// Retrieves all organization assets grouped by category and classname.
+ fn get_assets(
+ &self,
+ org_id: &str,
+ ) -> Result>, String>;
+
+ /// Replaces the organization asset hash with the provided grouped assets.
+ fn update_assets(
+ &self,
+ org_id: &str,
+ assets: &HashMap>,
+ ) -> Result<(), String>;
+
+ /// Retrieves all organization fleet entries.
+ fn get_fleet(&self, org_id: &str) -> Result, String>;
+
+ /// Replaces the organization fleet hash with the provided fleet entries.
+ fn update_fleet(
+ &self,
+ org_id: &str,
+ fleet: &HashMap,
+ ) -> Result<(), String>;
}
/// Redis-based implementation of the OrgRepository trait.
@@ -109,21 +133,14 @@ impl OrgRepository for RedisOrgRepository {
return Ok(None);
}
- // Parse comma-separated field-value pairs
- let parts: Vec<&str> = org_string.split(", ").collect();
+ let redis_map: std::collections::HashMap =
+ serde_json::from_str(&org_string)
+ .map_err(|e| format!("Failed to parse org hash response: {}", e))?;
let mut json_map = serde_json::Map::new();
- let mut i = 0;
- // Process pairs of field names and values
- while i + 1 < parts.len() {
- let key = parts[i];
- let value = parts[i + 1];
-
- // Convert Redis string value back to proper JSON type
- let json_value = parse_redis_value(value);
- json_map.insert(key.to_string(), json_value);
-
- i += 2; // Move to next field-value pair
+ for (key, value) in redis_map {
+ let json_value = parse_redis_value(&value);
+ json_map.insert(key, json_value);
}
// Reconstruct Org from JSON object
@@ -261,4 +278,100 @@ impl OrgRepository for RedisOrgRepository {
// Remove the UID from the set using SREM
self.client.set_del(redis_key, member_uid.to_string())
}
+
+ fn get_assets(
+ &self,
+ org_id: &str,
+ ) -> Result>, String> {
+ let redis_key = format!("org:{}:assets", org_id);
+ let assets_string = self.client.hash_get_all(redis_key)?;
+
+ if assets_string.is_empty() {
+ return Ok(HashMap::new());
+ }
+
+ let redis_map: HashMap = serde_json::from_str(&assets_string)
+ .map_err(|e| format!("Failed to parse org asset hash response: {}", e))?;
+
+ let mut assets = HashMap::new();
+ for (category, value) in redis_map {
+ let json_value = parse_redis_value(&value);
+ let category_assets =
+ serde_json::from_value::>(json_value)
+ .map_err(|e| format!("Failed to parse asset category '{}': {}", category, e))?;
+ assets.insert(category, category_assets);
+ }
+
+ Ok(assets)
+ }
+
+ fn update_assets(
+ &self,
+ org_id: &str,
+ assets: &HashMap>,
+ ) -> Result<(), String> {
+ let redis_key = format!("org:{}:assets", org_id);
+
+ if assets.is_empty() {
+ return self.client.delete_key(redis_key);
+ }
+
+ let fields: Vec<(String, String)> = assets
+ .iter()
+ .map(|(category, value)| {
+ let json_value = serde_json::to_value(value)
+ .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
+ (category.clone(), parse_json_value(&json_value))
+ })
+ .collect();
+
+ self.client.delete_key(redis_key.clone())?;
+ self.client.hash_mset(redis_key, fields)
+ }
+
+ fn get_fleet(&self, org_id: &str) -> Result, String> {
+ let redis_key = format!("org:{}:fleet", org_id);
+ let fleet_string = self.client.hash_get_all(redis_key)?;
+
+ if fleet_string.is_empty() {
+ return Ok(HashMap::new());
+ }
+
+ let redis_map: HashMap = serde_json::from_str(&fleet_string)
+ .map_err(|e| format!("Failed to parse org fleet hash response: {}", e))?;
+
+ let mut fleet = HashMap::new();
+ for (fleet_key, value) in redis_map {
+ let json_value = parse_redis_value(&value);
+ let fleet_entry = serde_json::from_value::(json_value)
+ .map_err(|e| format!("Failed to parse fleet entry '{}': {}", fleet_key, e))?;
+ fleet.insert(fleet_key, fleet_entry);
+ }
+
+ Ok(fleet)
+ }
+
+ fn update_fleet(
+ &self,
+ org_id: &str,
+ fleet: &HashMap,
+ ) -> Result<(), String> {
+ let redis_key = format!("org:{}:fleet", org_id);
+
+ if fleet.is_empty() {
+ return self.client.delete_key(redis_key);
+ }
+
+ let fields: Vec<(String, String)> = fleet
+ .iter()
+ .map(|(fleet_key, value)| {
+ let json_value = serde_json::to_value(value)
+ .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
+ (fleet_key.clone(), parse_json_value(&json_value))
+ })
+ .collect();
+
+ self.client.delete_key(redis_key.clone())?;
+ self.client.hash_mset(redis_key, fields)
+ }
}
diff --git a/lib/repositories/src/v_garage.rs b/lib/repositories/src/v_garage.rs
index e9e0e00..1a3751e 100644
--- a/lib/repositories/src/v_garage.rs
+++ b/lib/repositories/src/v_garage.rs
@@ -98,21 +98,14 @@ impl VGarageRepository for RedisVGarageRepository {
return Ok(None);
}
- // Parse comma-separated field-value pairs
- let parts: Vec<&str> = garage_string.split(", ").collect();
+ let redis_map: std::collections::HashMap =
+ serde_json::from_str(&garage_string)
+ .map_err(|e| format!("Failed to parse virtual garage hash response: {}", e))?;
let mut json_map = serde_json::Map::new();
- let mut i = 0;
- // Process pairs of field names and values
- while i + 1 < parts.len() {
- let key = parts[i];
- let value = parts[i + 1];
-
- // Convert Redis string value back to proper JSON type
- let json_value = parse_redis_value(value);
- json_map.insert(key.to_string(), json_value);
-
- i += 2; // Move to next field-value pair
+ for (key, value) in redis_map {
+ let json_value = parse_redis_value(&value);
+ json_map.insert(key, json_value);
}
// Reconstruct VLocker from JSON object
diff --git a/lib/repositories/src/v_locker.rs b/lib/repositories/src/v_locker.rs
index 54382d9..23bb442 100644
--- a/lib/repositories/src/v_locker.rs
+++ b/lib/repositories/src/v_locker.rs
@@ -94,21 +94,14 @@ impl VLockerRepository for RedisVLockerRepository {
return Ok(None);
}
- // Parse comma-separated field-value pairs
- let parts: Vec<&str> = locker_string.split(", ").collect();
+ let redis_map: std::collections::HashMap =
+ serde_json::from_str(&locker_string)
+ .map_err(|e| format!("Failed to parse virtual locker hash response: {}", e))?;
let mut json_map = serde_json::Map::new();
- let mut i = 0;
- // Process pairs of field names and values
- while i + 1 < parts.len() {
- let key = parts[i];
- let value = parts[i + 1];
-
- // Convert Redis string value back to proper JSON type
- let json_value = parse_redis_value(value);
- json_map.insert(key.to_string(), json_value);
-
- i += 2; // Move to next field-value pair
+ for (key, value) in redis_map {
+ let json_value = parse_redis_value(&value);
+ json_map.insert(key, json_value);
}
// Reconstruct VLocker from JSON object
diff --git a/lib/services/src/org.rs b/lib/services/src/org.rs
index 05bddee..d2855c8 100644
--- a/lib/services/src/org.rs
+++ b/lib/services/src/org.rs
@@ -5,7 +5,7 @@
//!
//! For full documentation, architecture, and examples, see the [crate README](../README.md).
-use forge_models::{CreditLineSummary, MemberSummary, Org};
+use forge_models::{CreditLineSummary, MemberSummary, Org, OrgAssetEntry, OrgFleetEntry};
use forge_repositories::OrgRepository;
use std::collections::HashMap;
@@ -237,4 +237,76 @@ impl OrgService {
// Delegate member removal to repository layer
self.repository.remove_member(&key, &member_uid)
}
+
+ pub fn get_assets(
+ &self,
+ key: String,
+ ) -> Result>, String> {
+ if !self.repository.exists(&key)? {
+ return Err(format!("Organization with ID '{}' not found", key));
+ }
+
+ self.repository.get_assets(&key)
+ }
+
+ pub fn update_assets(
+ &self,
+ key: String,
+ mut assets_update: serde_json::Value,
+ ) -> Result>, String> {
+ if !self.repository.exists(&key)? {
+ return Err(format!("Organization with ID '{}' not found", key));
+ }
+
+ if matches!(&assets_update, serde_json::Value::Array(lines) if lines.is_empty()) {
+ assets_update = serde_json::Value::Object(serde_json::Map::new());
+ }
+
+ let assets = if assets_update.is_null() {
+ HashMap::new()
+ } else {
+ serde_json::from_value::>>(assets_update)
+ .map_err(|e| {
+ format!(
+ "Assets must be an object of category maps keyed by classname: {}",
+ e
+ )
+ })?
+ };
+
+ self.repository.update_assets(&key, &assets)?;
+ Ok(assets)
+ }
+
+ pub fn get_fleet(&self, key: String) -> Result, String> {
+ if !self.repository.exists(&key)? {
+ return Err(format!("Organization with ID '{}' not found", key));
+ }
+
+ self.repository.get_fleet(&key)
+ }
+
+ pub fn update_fleet(
+ &self,
+ key: String,
+ mut fleet_update: serde_json::Value,
+ ) -> Result, String> {
+ if !self.repository.exists(&key)? {
+ return Err(format!("Organization with ID '{}' not found", key));
+ }
+
+ if matches!(&fleet_update, serde_json::Value::Array(lines) if lines.is_empty()) {
+ fleet_update = serde_json::Value::Object(serde_json::Map::new());
+ }
+
+ let fleet = if fleet_update.is_null() {
+ HashMap::new()
+ } else {
+ serde_json::from_value::>(fleet_update)
+ .map_err(|e| format!("Fleet must be an object of fleet entries: {}", e))?
+ };
+
+ self.repository.update_fleet(&key, &fleet)?;
+ Ok(fleet)
+ }
}
diff --git a/tools/build-webui.mjs b/tools/build-webui.mjs
index 900d143..0103401 100644
--- a/tools/build-webui.mjs
+++ b/tools/build-webui.mjs
@@ -191,6 +191,21 @@ async function buildHtmlPage({ name, output, title, siteConfig }) {
console.log(`Built ${output}`);
}
+async function buildHtmlTemplate({ name, output, source }) {
+ const html = await readSource(source);
+ const minifiedHtml = await minifyHtml(html, {
+ collapseBooleanAttributes: true,
+ collapseWhitespace: true,
+ minifyCSS: true,
+ minifyJS: true,
+ removeComments: true,
+ removeRedundantAttributes: true,
+ });
+
+ await writeBundle(output, minifiedHtml);
+ console.log(`Built ${output}`);
+}
+
async function pathExists(absolutePath) {
try {
await stat(absolutePath);
@@ -297,22 +312,38 @@ async function loadUiConfig(absoluteConfigPath) {
resolveFromConfigDir(configDir, source),
),
}));
- const htmlPage = {
- name: `${config.addonName} UI index`,
- output: resolveFromConfigDir(configDir, path.join(config.outputDir, "index.html")),
- title: config.title,
- siteConfig: {
- addonName: config.addonName,
- logLabel: config.logLabel || `${config.addonName} UI`,
- ...config.site,
- },
- };
+ const htmlPages = [];
+ if (config.generateIndex !== false) {
+ htmlPages.push({
+ kind: "generated",
+ name: `${config.addonName} UI index`,
+ output: resolveFromConfigDir(configDir, path.join(config.outputDir, "index.html")),
+ title: config.title,
+ siteConfig: {
+ addonName: config.addonName,
+ logLabel: config.logLabel || `${config.addonName} UI`,
+ ...config.site,
+ },
+ });
+ }
+
+ for (const page of config.htmlTemplates || []) {
+ htmlPages.push({
+ kind: "template",
+ name: page.name || `${config.addonName} UI template`,
+ output: resolveFromConfigDir(
+ configDir,
+ path.join(config.outputDir, page.output),
+ ),
+ source: resolveFromConfigDir(configDir, page.source),
+ });
+ }
return {
outputDir,
jsBundles,
cssBundles,
- htmlPage,
+ htmlPages,
formatSourceTargets,
};
}
@@ -325,7 +356,7 @@ async function collectUiBuildArtifacts() {
outputDirs: uiConfigs.map((config) => config.outputDir),
jsBundles: uiConfigs.flatMap((config) => config.jsBundles),
cssBundles: uiConfigs.flatMap((config) => config.cssBundles),
- htmlPages: uiConfigs.map((config) => config.htmlPage),
+ htmlPages: uiConfigs.flatMap((config) => config.htmlPages),
formatSourceTargets: uiConfigs.flatMap(
(config) => config.formatSourceTargets,
),
@@ -348,7 +379,11 @@ async function build() {
...uiArtifacts.jsBundles.map(buildJsBundle),
]);
await Promise.all(uiArtifacts.cssBundles.map(buildCssBundle));
- await Promise.all(uiArtifacts.htmlPages.map(buildHtmlPage));
+ await Promise.all(
+ uiArtifacts.htmlPages.map((page) =>
+ page.kind === "template" ? buildHtmlTemplate(page) : buildHtmlPage(page),
+ ),
+ );
}
build().catch((error) => {