From 45a4f7460a6ae63a798b84cdccaf549b1b9196b8 Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Sat, 28 Mar 2026 02:20:34 -0500 Subject: [PATCH] Integrate task contracts and CAD UI pipeline - add the imported server task addon to the current framework with task ownership, task catalog, mission-manager attack generation, org-owned reward routing, participant notifications, and reputation syncing - restructure org persistence so core org data, assets, fleet, and members are handled through the current Redis/extension model with matching Rust repository and service updates - wire the client CAD addon into the framework, actor device action, shared web UI bridge pattern, and task listing/acceptance flow - add a source-driven CAD web UI layout with ui.config.mjs and extend the shared web UI builder to support custom HTML template pages for multi-surface UIs --- .../actor/functions/fnc_handleUIEvents.sqf | 3 +- arma/client/addons/actor/ui/_site/script.js | 12 + arma/client/addons/cad/$PBOPREFIX$ | 1 + arma/client/addons/cad/CfgEventHandlers.hpp | 12 + arma/client/addons/cad/MAP_README.md | 214 +++++ arma/client/addons/cad/XEH_PREP.hpp | 5 + arma/client/addons/cad/XEH_postInitClient.sqf | 24 + arma/client/addons/cad/XEH_preInit.sqf | 5 + arma/client/addons/cad/XEH_preInitClient.sqf | 1 + arma/client/addons/cad/config.cpp | 21 + .../cad/functions/fnc_handleUIEvents.sqf | 87 ++ .../cad/functions/fnc_initRepository.sqf | 52 ++ .../addons/cad/functions/fnc_initUI.sqf | 90 ++ .../addons/cad/functions/fnc_initUIBridge.sqf | 84 ++ .../addons/cad/functions/fnc_openUI.sqf | 47 ++ arma/client/addons/cad/script_component.hpp | 9 + arma/client/addons/cad/ui/RscCommon.hpp | 6 + arma/client/addons/cad/ui/RscMapUI.hpp | 90 ++ .../client/addons/cad/ui/_site/bottombar.html | 1 + .../addons/cad/ui/_site/cad-bottombar.css | 1 + .../addons/cad/ui/_site/cad-bottombar.js | 1 + .../client/addons/cad/ui/_site/cad-common.css | 1 + arma/client/addons/cad/ui/_site/cad-shared.js | 1 + .../addons/cad/ui/_site/cad-sidepanel.css | 1 + .../addons/cad/ui/_site/cad-sidepanel.js | 1 + .../client/addons/cad/ui/_site/cad-topbar.css | 1 + arma/client/addons/cad/ui/_site/cad-topbar.js | 1 + .../client/addons/cad/ui/_site/sidepanel.html | 1 + arma/client/addons/cad/ui/_site/topbar.html | 1 + arma/client/addons/cad/ui/src/bottombar.html | 49 ++ arma/client/addons/cad/ui/src/bottombar.js | 6 + arma/client/addons/cad/ui/src/shared.js | 69 ++ arma/client/addons/cad/ui/src/sidepanel.html | 63 ++ arma/client/addons/cad/ui/src/sidepanel.js | 94 +++ .../addons/cad/ui/src/styles/bottombar.css | 33 + .../addons/cad/ui/src/styles/common.css | 78 ++ .../addons/cad/ui/src/styles/sidepanel.css | 136 +++ .../addons/cad/ui/src/styles/topbar.css | 67 ++ arma/client/addons/cad/ui/src/topbar.html | 63 ++ arma/client/addons/cad/ui/src/topbar.js | 17 + arma/client/addons/cad/ui/ui.config.mjs | 69 ++ arma/server/.hemtt/lints.toml | 2 +- .../addons/bank/functions/fnc_initStore.sqf | 12 +- .../common/functions/fnc_formatNumber.sqf | 7 +- .../functions/fnc_initFEconomyStore.sqf | 2 +- .../functions/fnc_initMEconomyStore.sqf | 2 +- .../locker/functions/fnc_initVAStore.sqf | 4 +- arma/server/addons/org/XEH_preInit.sqf | 2 +- .../addons/org/functions/fnc_initOrgStore.sqf | 219 ++++- .../org/functions/fnc_treasuryService.sqf | 6 +- .../functions/fnc_initCatalogService.sqf | 2 +- .../store/functions/fnc_initStoreStore.sqf | 2 +- arma/server/addons/task/$PBOPREFIX$ | 1 + arma/server/addons/task/CfgEventHandlers.hpp | 17 + arma/server/addons/task/CfgFactionClasses.hpp | 6 + arma/server/addons/task/CfgMissions.hpp | 269 ++++++ arma/server/addons/task/CfgVehicles.hpp | 782 ++++++++++++++++++ arma/server/addons/task/README.md | 104 +++ arma/server/addons/task/XEH_PREP.hpp | 31 + arma/server/addons/task/XEH_postInit.sqf | 16 + arma/server/addons/task/XEH_preInit.sqf | 35 + arma/server/addons/task/XEH_preStart.sqf | 2 + arma/server/addons/task/config.cpp | 23 + .../addons/task/functions/fnc_attack.sqf | 116 +++ .../task/functions/fnc_attackModule.sqf | 51 ++ .../addons/task/functions/fnc_defend.sqf | 126 +++ .../task/functions/fnc_defendModule.sqf | 61 ++ .../addons/task/functions/fnc_defuse.sqf | 114 +++ .../task/functions/fnc_defuseModule.sqf | 64 ++ .../addons/task/functions/fnc_delivery.sqf | 120 +++ .../task/functions/fnc_deliveryModule.sqf | 67 ++ .../addons/task/functions/fnc_destroy.sqf | 114 +++ .../task/functions/fnc_destroyModule.sqf | 51 ++ .../task/functions/fnc_explosivesModule.sqf | 23 + .../task/functions/fnc_handleTaskRewards.sqf | 223 +++++ .../addons/task/functions/fnc_handler.sqf | 108 +++ .../addons/task/functions/fnc_heartBeat.sqf | 68 ++ .../addons/task/functions/fnc_hostage.sqf | 173 ++++ .../task/functions/fnc_hostageModule.sqf | 76 ++ .../task/functions/fnc_hostagesModule.sqf | 23 + arma/server/addons/task/functions/fnc_hvt.sqf | 128 +++ .../addons/task/functions/fnc_hvtModule.sqf | 59 ++ .../task/functions/fnc_initTaskStore.sqf | 545 ++++++++++++ .../addons/task/functions/fnc_makeCargo.sqf | 41 + .../addons/task/functions/fnc_makeHVT.sqf | 30 + .../addons/task/functions/fnc_makeHostage.sqf | 30 + .../addons/task/functions/fnc_makeIED.sqf | 32 + .../addons/task/functions/fnc_makeObject.sqf | 28 + .../addons/task/functions/fnc_makeShooter.sqf | 28 + .../addons/task/functions/fnc_makeTarget.sqf | 28 + .../task/functions/fnc_missionManager.sqf | 369 +++++++++ .../task/functions/fnc_protectedModule.sqf | 23 + .../task/functions/fnc_shootersModule.sqf | 23 + .../task/functions/fnc_spawnEnemyWave.sqf | 83 ++ arma/server/addons/task/script_component.hpp | 9 + arma/server/addons/task/stringtable.xml | 8 + arma/server/extension/src/org.rs | 62 ++ arma/server/extension/src/redis/hash.rs | 11 +- lib/models/src/lib.rs | 2 +- lib/models/src/org.rs | 24 + lib/repositories/src/actor.rs | 19 +- lib/repositories/src/bank.rs | 19 +- lib/repositories/src/org.rs | 141 +++- lib/repositories/src/v_garage.rs | 19 +- lib/repositories/src/v_locker.rs | 19 +- lib/services/src/org.rs | 74 +- tools/build-webui.mjs | 61 +- 107 files changed, 6435 insertions(+), 122 deletions(-) create mode 100644 arma/client/addons/cad/$PBOPREFIX$ create mode 100644 arma/client/addons/cad/CfgEventHandlers.hpp create mode 100644 arma/client/addons/cad/MAP_README.md create mode 100644 arma/client/addons/cad/XEH_PREP.hpp create mode 100644 arma/client/addons/cad/XEH_postInitClient.sqf create mode 100644 arma/client/addons/cad/XEH_preInit.sqf create mode 100644 arma/client/addons/cad/XEH_preInitClient.sqf create mode 100644 arma/client/addons/cad/config.cpp create mode 100644 arma/client/addons/cad/functions/fnc_handleUIEvents.sqf create mode 100644 arma/client/addons/cad/functions/fnc_initRepository.sqf create mode 100644 arma/client/addons/cad/functions/fnc_initUI.sqf create mode 100644 arma/client/addons/cad/functions/fnc_initUIBridge.sqf create mode 100644 arma/client/addons/cad/functions/fnc_openUI.sqf create mode 100644 arma/client/addons/cad/script_component.hpp create mode 100644 arma/client/addons/cad/ui/RscCommon.hpp create mode 100644 arma/client/addons/cad/ui/RscMapUI.hpp create mode 100644 arma/client/addons/cad/ui/_site/bottombar.html create mode 100644 arma/client/addons/cad/ui/_site/cad-bottombar.css create mode 100644 arma/client/addons/cad/ui/_site/cad-bottombar.js create mode 100644 arma/client/addons/cad/ui/_site/cad-common.css create mode 100644 arma/client/addons/cad/ui/_site/cad-shared.js create mode 100644 arma/client/addons/cad/ui/_site/cad-sidepanel.css create mode 100644 arma/client/addons/cad/ui/_site/cad-sidepanel.js create mode 100644 arma/client/addons/cad/ui/_site/cad-topbar.css create mode 100644 arma/client/addons/cad/ui/_site/cad-topbar.js create mode 100644 arma/client/addons/cad/ui/_site/sidepanel.html create mode 100644 arma/client/addons/cad/ui/_site/topbar.html create mode 100644 arma/client/addons/cad/ui/src/bottombar.html create mode 100644 arma/client/addons/cad/ui/src/bottombar.js create mode 100644 arma/client/addons/cad/ui/src/shared.js create mode 100644 arma/client/addons/cad/ui/src/sidepanel.html create mode 100644 arma/client/addons/cad/ui/src/sidepanel.js create mode 100644 arma/client/addons/cad/ui/src/styles/bottombar.css create mode 100644 arma/client/addons/cad/ui/src/styles/common.css create mode 100644 arma/client/addons/cad/ui/src/styles/sidepanel.css create mode 100644 arma/client/addons/cad/ui/src/styles/topbar.css create mode 100644 arma/client/addons/cad/ui/src/topbar.html create mode 100644 arma/client/addons/cad/ui/src/topbar.js create mode 100644 arma/client/addons/cad/ui/ui.config.mjs create mode 100644 arma/server/addons/task/$PBOPREFIX$ create mode 100644 arma/server/addons/task/CfgEventHandlers.hpp create mode 100644 arma/server/addons/task/CfgFactionClasses.hpp create mode 100644 arma/server/addons/task/CfgMissions.hpp create mode 100644 arma/server/addons/task/CfgVehicles.hpp create mode 100644 arma/server/addons/task/README.md create mode 100644 arma/server/addons/task/XEH_PREP.hpp create mode 100644 arma/server/addons/task/XEH_postInit.sqf create mode 100644 arma/server/addons/task/XEH_preInit.sqf create mode 100644 arma/server/addons/task/XEH_preStart.sqf create mode 100644 arma/server/addons/task/config.cpp create mode 100644 arma/server/addons/task/functions/fnc_attack.sqf create mode 100644 arma/server/addons/task/functions/fnc_attackModule.sqf create mode 100644 arma/server/addons/task/functions/fnc_defend.sqf create mode 100644 arma/server/addons/task/functions/fnc_defendModule.sqf create mode 100644 arma/server/addons/task/functions/fnc_defuse.sqf create mode 100644 arma/server/addons/task/functions/fnc_defuseModule.sqf create mode 100644 arma/server/addons/task/functions/fnc_delivery.sqf create mode 100644 arma/server/addons/task/functions/fnc_deliveryModule.sqf create mode 100644 arma/server/addons/task/functions/fnc_destroy.sqf create mode 100644 arma/server/addons/task/functions/fnc_destroyModule.sqf create mode 100644 arma/server/addons/task/functions/fnc_explosivesModule.sqf create mode 100644 arma/server/addons/task/functions/fnc_handleTaskRewards.sqf create mode 100644 arma/server/addons/task/functions/fnc_handler.sqf create mode 100644 arma/server/addons/task/functions/fnc_heartBeat.sqf create mode 100644 arma/server/addons/task/functions/fnc_hostage.sqf create mode 100644 arma/server/addons/task/functions/fnc_hostageModule.sqf create mode 100644 arma/server/addons/task/functions/fnc_hostagesModule.sqf create mode 100644 arma/server/addons/task/functions/fnc_hvt.sqf create mode 100644 arma/server/addons/task/functions/fnc_hvtModule.sqf create mode 100644 arma/server/addons/task/functions/fnc_initTaskStore.sqf create mode 100644 arma/server/addons/task/functions/fnc_makeCargo.sqf create mode 100644 arma/server/addons/task/functions/fnc_makeHVT.sqf create mode 100644 arma/server/addons/task/functions/fnc_makeHostage.sqf create mode 100644 arma/server/addons/task/functions/fnc_makeIED.sqf create mode 100644 arma/server/addons/task/functions/fnc_makeObject.sqf create mode 100644 arma/server/addons/task/functions/fnc_makeShooter.sqf create mode 100644 arma/server/addons/task/functions/fnc_makeTarget.sqf create mode 100644 arma/server/addons/task/functions/fnc_missionManager.sqf create mode 100644 arma/server/addons/task/functions/fnc_protectedModule.sqf create mode 100644 arma/server/addons/task/functions/fnc_shootersModule.sqf create mode 100644 arma/server/addons/task/functions/fnc_spawnEnemyWave.sqf create mode 100644 arma/server/addons/task/script_component.hpp create mode 100644 arma/server/addons/task/stringtable.xml 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.title||s.taskID}\n ${s.type||"task"}\n
\n

${s.description||""}

\n
\n ${a}\n X: ${Math.round(t[0]||0)} Y: ${Math.round(t[1]||0)}\n
\n \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 @@ +

CAD System

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 @@ +
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 @@ + + + + + + +
+

CAD System

+
+
+
+ +
+
+
+
+

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.title || task.taskID} + ${task.type || "task"} +
+

${task.description || ""}

+
+ ${ownerLabel} + X: ${Math.round(position[0] || 0)} Y: ${Math.round(position[1] || 0)} +
+ +
+ `; + }) + .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 @@ + + + + + + + +
+ + + + +
+
+ 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) => {