From ff7ff0c4e599b0f723c95ef15c49741945fb9fb0 Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Thu, 2 Apr 2026 16:50:38 -0500 Subject: [PATCH] Implement org credit line debt and bank repayment flow (#2) ## Summary This finishes the org credit line workflow so it behaves like reserved treasury-backed credit instead of a simple member allowance. ## What changed - reserve org funds immediately when a credit line is assigned - track credit lines with: - approved amount - available amount - outstanding principal - interest rate - amount due - consume reserved credit during store checkout without charging org funds a second time - add credit line repayment through the bank app - sync richer credit line state into org and bank payloads/UI - keep legacy `amount` compatibility mapped to available credit for older consumers ## User-facing behavior - assigning a credit line now reduces available org funds immediately - spending on `credit_line` reduces available credit and creates debt with interest - the bank app now shows outstanding credit debt and allows repayment from personal bank funds - the org treasury view now shows reserved credit and outstanding due totals ## Validation - `cargo fmt` - `npm run build:webui` - `cargo test -p forge-services --quiet` - `cargo test -p forge-server --quiet` ## Follow-up checks - validate in-game that assigning a credit line reduces org funds immediately - validate store checkout with `credit_line` updates available credit and debt correctly - validate bank repayment decreases player bank balance, increases org funds, and reduces amount due Co-authored-by: Jacob Schmidt Reviewed-on: http://gitea.innovativedevsolutions.org/IDSolutions/forge/pulls/2 --- arma/client/addons/actor/XEH_PREP.hpp | 2 +- .../addons/actor/XEH_postInitClient.sqf | 12 +- arma/client/addons/actor/XEH_preStart.sqf | 1 - .../actor/functions/fnc_handleUIEvents.sqf | 5 +- ...tActorClass.sqf => fnc_initRepository.sqf} | 35 +- arma/client/addons/actor/ui/_site/script.js | 12 + arma/client/addons/bank/XEH_PREP.hpp | 3 +- .../client/addons/bank/XEH_postInitClient.sqf | 31 +- arma/client/addons/bank/XEH_preStart.sqf | 1 - .../bank/functions/fnc_handleUIEvents.sqf | 10 + .../addons/bank/functions/fnc_initClass.sqf | 62 - .../bank/functions/fnc_initRepository.sqf | 44 + .../bank/functions/fnc_initSessionService.sqf | 80 -- .../bank/functions/fnc_initUIBridge.sqf | 101 +- arma/client/addons/bank/ui/_site/bank-ui.js | 2 +- arma/client/addons/bank/ui/src/bridge.js | 14 +- arma/client/addons/bank/ui/src/data.js | 16 +- .../addons/bank/ui/src/pages/BankView.js | 67 + .../addons/bank/ui/src/registry/events.js | 110 +- .../addons/bank/ui/src/registry/store.js | 25 +- arma/client/addons/cad/$PBOPREFIX$ | 1 + arma/client/addons/cad/CfgEventHandlers.hpp | 11 + arma/client/addons/cad/MAP_README.md | 214 +++ arma/client/addons/cad/XEH_PREP.hpp | 5 + arma/client/addons/cad/XEH_postInitClient.sqf | 40 + 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 | 229 +++ .../cad/functions/fnc_initRepository.sqf | 105 ++ .../addons/cad/functions/fnc_initUI.sqf | 51 + .../addons/cad/functions/fnc_initUIBridge.sqf | 450 ++++++ .../addons/cad/functions/fnc_openUI.sqf | 49 + arma/client/addons/cad/script_component.hpp | 9 + arma/client/addons/cad/ui/RscCommon.hpp | 6 + arma/client/addons/cad/ui/RscMapUI.hpp | 109 ++ .../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 + .../addons/cad/ui/_site/cad-dispatcher.css | 1 + .../addons/cad/ui/_site/cad-dispatcher.js | 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 + .../addons/cad/ui/_site/dispatcher.html | 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 | 7 + arma/client/addons/cad/ui/src/dispatcher.html | 372 +++++ .../cad/ui/src/dispatcher/formatters.js | 120 ++ .../addons/cad/ui/src/dispatcher/index.js | 274 ++++ .../addons/cad/ui/src/dispatcher/modals.js | 269 ++++ .../addons/cad/ui/src/dispatcher/render.js | 325 +++++ arma/client/addons/cad/ui/src/shared.js | 74 + arma/client/addons/cad/ui/src/sidepanel.html | 190 +++ arma/client/addons/cad/ui/src/sidepanel.js | 1238 +++++++++++++++++ .../addons/cad/ui/src/styles/bottombar.css | 40 + .../addons/cad/ui/src/styles/common.css | 78 ++ .../addons/cad/ui/src/styles/dispatcher.css | 562 ++++++++ .../addons/cad/ui/src/styles/sidepanel.css | 554 ++++++++ .../addons/cad/ui/src/styles/topbar.css | 296 ++++ arma/client/addons/cad/ui/src/topbar.html | 132 ++ arma/client/addons/cad/ui/src/topbar.js | 162 +++ arma/client/addons/cad/ui/ui.config.mjs | 89 ++ arma/client/addons/common/XEH_preStart.sqf | 1 - arma/client/addons/garage/XEH_PREP.hpp | 10 +- .../addons/garage/XEH_postInitClient.sqf | 30 +- .../garage/functions/fnc_handleUIEvents.sqf | 8 +- .../functions/fnc_initActionService.sqf | 133 ++ .../functions/fnc_initContextService.sqf | 146 ++ ...gService.sqf => fnc_initHelperService.sqf} | 23 +- .../functions/fnc_initPayloadService.sqf | 44 + ...c_initClass.sqf => fnc_initRepository.sqf} | 37 +- .../functions/fnc_initSessionService.sqf | 298 ---- .../garage/functions/fnc_initUIBridge.sqf | 172 +-- ...itVGClass.sqf => fnc_initVGRepository.sqf} | 38 +- .../addons/garage/functions/fnc_openVG.sqf | 2 +- arma/client/addons/locker/XEH_PREP.hpp | 4 +- .../addons/locker/XEH_postInitClient.sqf | 20 +- ...LockerClass.sqf => fnc_initRepository.sqf} | 52 +- ...itVAClass.sqf => fnc_initVARepository.sqf} | 27 +- arma/client/addons/notifications/XEH_PREP.hpp | 2 +- .../notifications/XEH_postInitClient.sqf | 6 +- .../addons/notifications/XEH_preStart.sqf | 1 - .../functions/fnc_handleUIEvents.sqf | 2 +- ...ificationClass.sqf => fnc_initService.sqf} | 22 +- arma/client/addons/org/XEH_PREP.hpp | 2 +- arma/client/addons/org/XEH_postInitClient.sqf | 17 +- arma/client/addons/org/XEH_preStart.sqf | 1 - .../org/functions/fnc_handleUIEvents.sqf | 15 +- .../addons/org/functions/fnc_initClass.sqf | 181 --- .../org/functions/fnc_initRepository.sqf | 44 + .../addons/org/functions/fnc_initUIBridge.sqf | 57 +- .../addons/org/functions/fnc_openUI.sqf | 11 +- arma/client/addons/org/ui/_site/org-ui.js | 2 +- arma/client/addons/org/ui/src/bridge.js | 12 +- .../ui/src/components/portal/treasuryCard.js | 86 +- arma/client/addons/org/ui/src/portal/data.js | 54 +- arma/client/addons/org/ui/src/portal/store.js | 51 +- arma/client/addons/phone.7z | Bin 0 -> 691770 bytes arma/client/addons/store/XEH_PREP.hpp | 2 - .../addons/store/XEH_postInitClient.sqf | 7 +- .../store/functions/fnc_buildUIPayload.sqf | 125 -- .../addons/store/functions/fnc_initClass.sqf | 42 - .../store/functions/fnc_initUIBridge.sqf | 37 +- arma/server/.hemtt/lints.toml | 2 +- .../actor/functions/fnc_initActorStore.sqf | 222 ++- arma/server/addons/bank/XEH_PREP.hpp | 6 +- arma/server/addons/bank/XEH_preInit.sqf | 80 +- .../bank/functions/fnc_initBankStore.sqf | 326 ----- .../bank/functions/fnc_initMessenger.sqf | 75 + .../addons/bank/functions/fnc_initModel.sqf | 67 + .../bank/functions/fnc_initPayloadBuilder.sqf | 151 ++ .../bank/functions/fnc_initSessionManager.sqf | 102 ++ .../addons/bank/functions/fnc_initStore.sqf | 495 +++++++ arma/server/addons/cad/$PBOPREFIX$ | 1 + arma/server/addons/cad/CfgEventHandlers.hpp | 5 + arma/server/addons/cad/XEH_PREP.hpp | 7 + arma/server/addons/cad/XEH_preInit.sqf | 219 +++ arma/server/addons/cad/config.cpp | 23 + .../functions/fnc_initActivityRepository.sqf | 93 ++ .../fnc_initAssignmentRepository.sqf | 549 ++++++++ .../addons/cad/functions/fnc_initCadStore.sqf | 250 ++++ .../cad/functions/fnc_initGroupRepository.sqf | 341 +++++ .../functions/fnc_initPermissionService.sqf | 48 + .../functions/fnc_initPersistenceService.sqf | 209 +++ .../functions/fnc_initRequestRepository.sqf | 208 +++ arma/server/addons/cad/script_component.hpp | 9 + .../common/functions/fnc_formatNumber.sqf | 7 +- .../functions/fnc_initFEconomyStore.sqf | 2 +- .../functions/fnc_initMEconomyStore.sqf | 9 +- arma/server/addons/extension/XEH_PREP.hpp | 1 + .../extension/functions/fnc_extCall.sqf | 172 ++- .../extension/functions/fnc_transport.sqf | 115 ++ arma/server/addons/garage/XEH_preInit.sqf | 103 +- .../garage/functions/fnc_initGarageStore.sqf | 88 +- .../garage/functions/fnc_initVGStore.sqf | 133 +- arma/server/addons/locker/XEH_preInit.sqf | 89 +- .../locker/functions/fnc_initLockerStore.sqf | 146 +- .../locker/functions/fnc_initVAStore.sqf | 129 +- arma/server/addons/main/XEH_PREP.hpp | 1 + arma/server/addons/main/XEH_preInit.sqf | 22 + .../addons/main/functions/fnc_initStores.sqf | 7 +- .../main/functions/fnc_saveHotState.sqf | 84 ++ arma/server/addons/org/XEH_PREP.hpp | 3 +- arma/server/addons/org/XEH_preInit.sqf | 73 +- .../addons/org/functions/fnc_initOrgStore.sqf | 752 +++++++--- .../org/functions/fnc_initPayloadBuilder.sqf | 222 +++ .../org/functions/fnc_memberService.sqf | 243 ---- .../org/functions/fnc_treasuryService.sqf | 164 --- arma/server/addons/phone.7z | Bin 0 -> 6025 bytes arma/server/addons/store/XEH_preInit.sqf | 20 + .../functions/fnc_initCatalogService.sqf | 43 +- .../store/functions/fnc_initStoreStore.sqf | 339 +++-- 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 | 7 + 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 | 225 +++ .../addons/task/functions/fnc_handler.sqf | 104 ++ .../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 | 563 ++++++++ .../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/actor.rs | 106 +- arma/server/extension/src/bank.rs | 323 ++++- arma/server/extension/src/cad.rs | 183 +++ arma/server/extension/src/garage.rs | 168 ++- arma/server/extension/src/lib.rs | 25 +- arma/server/extension/src/locker.rs | 102 +- arma/server/extension/src/org.rs | 279 +++- arma/server/extension/src/redis/hash.rs | 11 +- arma/server/extension/src/store.rs | 37 + arma/server/extension/src/task.rs | 123 ++ arma/server/extension/src/transport.rs | 998 +++++++++++++ arma/server/extension/src/v_garage.rs | 204 ++- arma/server/extension/src/v_locker.rs | 128 +- build-arma.ps1 | 16 +- lib/models/Cargo.toml | 3 +- lib/models/src/bank.rs | 45 + lib/models/src/cad.rs | 237 ++++ lib/models/src/lib.rs | 29 +- lib/models/src/org.rs | 275 +++- lib/models/src/store.rs | 72 + lib/models/src/task.rs | 57 + lib/models/src/v_locker.rs | 1 + lib/repositories/src/actor.rs | 63 +- lib/repositories/src/bank.rs | 63 +- lib/repositories/src/cad.rs | 236 ++++ lib/repositories/src/garage.rs | 43 + lib/repositories/src/lib.rs | 28 +- lib/repositories/src/locker.rs | 43 + lib/repositories/src/org.rs | 184 ++- lib/repositories/src/task.rs | 204 +++ lib/repositories/src/v_garage.rs | 63 +- lib/repositories/src/v_locker.rs | 63 +- lib/services/src/actor.rs | 60 +- lib/services/src/bank.rs | 402 +++++- lib/services/src/cad.rs | 1170 ++++++++++++++++ lib/services/src/garage.rs | 90 +- lib/services/src/lib.rs | 20 +- lib/services/src/locker.rs | 63 +- lib/services/src/org.rs | 803 ++++++++++- lib/services/src/store.rs | 699 ++++++++++ lib/services/src/task.rs | 379 +++++ lib/services/src/v_garage.rs | 96 +- lib/services/src/v_locker.rs | 72 +- tools/build-webui.mjs | 61 +- 246 files changed, 24542 insertions(+), 3164 deletions(-) rename arma/client/addons/actor/functions/{fnc_initActorClass.sqf => fnc_initRepository.sqf} (88%) delete mode 100644 arma/client/addons/bank/functions/fnc_initClass.sqf create mode 100644 arma/client/addons/bank/functions/fnc_initRepository.sqf delete mode 100644 arma/client/addons/bank/functions/fnc_initSessionService.sqf 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-dispatcher.css create mode 100644 arma/client/addons/cad/ui/_site/cad-dispatcher.js 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/dispatcher.html 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/dispatcher.html create mode 100644 arma/client/addons/cad/ui/src/dispatcher/formatters.js create mode 100644 arma/client/addons/cad/ui/src/dispatcher/index.js create mode 100644 arma/client/addons/cad/ui/src/dispatcher/modals.js create mode 100644 arma/client/addons/cad/ui/src/dispatcher/render.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/dispatcher.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/client/addons/garage/functions/fnc_initActionService.sqf create mode 100644 arma/client/addons/garage/functions/fnc_initContextService.sqf rename arma/client/addons/garage/functions/{fnc_initCatalogService.sqf => fnc_initHelperService.sqf} (91%) create mode 100644 arma/client/addons/garage/functions/fnc_initPayloadService.sqf rename arma/client/addons/garage/functions/{fnc_initClass.sqf => fnc_initRepository.sqf} (58%) delete mode 100644 arma/client/addons/garage/functions/fnc_initSessionService.sqf rename arma/client/addons/garage/functions/{fnc_initVGClass.sqf => fnc_initVGRepository.sqf} (74%) rename arma/client/addons/locker/functions/{fnc_initLockerClass.sqf => fnc_initRepository.sqf} (86%) rename arma/client/addons/locker/functions/{fnc_initVAClass.sqf => fnc_initVARepository.sqf} (79%) rename arma/client/addons/notifications/functions/{fnc_initNotificationClass.sqf => fnc_initService.sqf} (66%) delete mode 100644 arma/client/addons/org/functions/fnc_initClass.sqf create mode 100644 arma/client/addons/org/functions/fnc_initRepository.sqf create mode 100644 arma/client/addons/phone.7z delete mode 100644 arma/client/addons/store/functions/fnc_buildUIPayload.sqf delete mode 100644 arma/client/addons/store/functions/fnc_initClass.sqf delete mode 100644 arma/server/addons/bank/functions/fnc_initBankStore.sqf create mode 100644 arma/server/addons/bank/functions/fnc_initMessenger.sqf create mode 100644 arma/server/addons/bank/functions/fnc_initModel.sqf create mode 100644 arma/server/addons/bank/functions/fnc_initPayloadBuilder.sqf create mode 100644 arma/server/addons/bank/functions/fnc_initSessionManager.sqf create mode 100644 arma/server/addons/bank/functions/fnc_initStore.sqf create mode 100644 arma/server/addons/cad/$PBOPREFIX$ create mode 100644 arma/server/addons/cad/CfgEventHandlers.hpp create mode 100644 arma/server/addons/cad/XEH_PREP.hpp create mode 100644 arma/server/addons/cad/XEH_preInit.sqf create mode 100644 arma/server/addons/cad/config.cpp create mode 100644 arma/server/addons/cad/functions/fnc_initActivityRepository.sqf create mode 100644 arma/server/addons/cad/functions/fnc_initAssignmentRepository.sqf create mode 100644 arma/server/addons/cad/functions/fnc_initCadStore.sqf create mode 100644 arma/server/addons/cad/functions/fnc_initGroupRepository.sqf create mode 100644 arma/server/addons/cad/functions/fnc_initPermissionService.sqf create mode 100644 arma/server/addons/cad/functions/fnc_initPersistenceService.sqf create mode 100644 arma/server/addons/cad/functions/fnc_initRequestRepository.sqf create mode 100644 arma/server/addons/cad/script_component.hpp create mode 100644 arma/server/addons/extension/functions/fnc_transport.sqf create mode 100644 arma/server/addons/main/functions/fnc_saveHotState.sqf create mode 100644 arma/server/addons/org/functions/fnc_initPayloadBuilder.sqf delete mode 100644 arma/server/addons/org/functions/fnc_memberService.sqf delete mode 100644 arma/server/addons/org/functions/fnc_treasuryService.sqf create mode 100644 arma/server/addons/phone.7z 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 create mode 100644 arma/server/extension/src/cad.rs create mode 100644 arma/server/extension/src/store.rs create mode 100644 arma/server/extension/src/task.rs create mode 100644 arma/server/extension/src/transport.rs create mode 100644 lib/models/src/cad.rs create mode 100644 lib/models/src/store.rs create mode 100644 lib/models/src/task.rs create mode 100644 lib/repositories/src/cad.rs create mode 100644 lib/repositories/src/task.rs create mode 100644 lib/services/src/cad.rs create mode 100644 lib/services/src/store.rs create mode 100644 lib/services/src/task.rs diff --git a/arma/client/addons/actor/XEH_PREP.hpp b/arma/client/addons/actor/XEH_PREP.hpp index 0dcc312..97e1950 100644 --- a/arma/client/addons/actor/XEH_PREP.hpp +++ b/arma/client/addons/actor/XEH_PREP.hpp @@ -1,3 +1,3 @@ PREP(handleUIEvents); -PREP(initActorClass); +PREP(initRepository); PREP(openUI); diff --git a/arma/client/addons/actor/XEH_postInitClient.sqf b/arma/client/addons/actor/XEH_postInitClient.sqf index 12c69b1..483c48c 100644 --- a/arma/client/addons/actor/XEH_postInitClient.sqf +++ b/arma/client/addons/actor/XEH_postInitClient.sqf @@ -23,17 +23,17 @@ player addEventHandler ["Respawn", { [SRPC(economy,onRespawn), [_unit, _corpse, _uid]] call CFUNC(serverEvent); }]; -if (isNil QGVAR(ActorClass)) then { call FUNC(initActorClass); }; +if (isNil QGVAR(ActorRepository)) then { call FUNC(initRepository); }; [QGVAR(initActor), { - GVAR(ActorClass) call ["init", []]; + GVAR(ActorRepository) call ["init", []]; }] call CFUNC(addEventHandler); [QGVAR(onActorRespawn), { params [["_loadout", [], [[]]], ["_medSpawnPos", [0,0,0], [[]]], ["_medSpawnDir", 0, [0]]]; private _message = ["warning", "Medical Alert", "You have been revived at a medical facility.", 5000]; - EGVAR(notifications,NotificationClass) call ["create", _message]; + EGVAR(notifications,NotificationService) call ["create", _message]; player setUnitLoadout _loadout; player setPosATL _medSpawnPos; @@ -53,14 +53,14 @@ if (isNil QGVAR(ActorClass)) then { call FUNC(initActorClass); }; [QGVAR(responseInitActor), { params [["_data", createHashMap, [createHashMap]]]; - GVAR(ActorClass) call ["sync", [_data, true]]; + GVAR(ActorRepository) call ["sync", [_data, true]]; cutText ["", "PLAIN", 1]; }] call CFUNC(addEventHandler); [QGVAR(responseSyncActor), { params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]]; - GVAR(ActorClass) call ["sync", [_data, _jip]]; + GVAR(ActorRepository) call ["sync", [_data, _jip]]; }] call CFUNC(addEventHandler); [QGVAR(initActor), []] call CFUNC(localEvent); @@ -68,6 +68,6 @@ if (isNil QGVAR(ActorClass)) then { call FUNC(initActorClass); }; [{ GETVAR(player,FORGE_isLoaded,false) }, { - private _holster = GVAR(ActorClass) call ["get", ["holster", true]]; + private _holster = GVAR(ActorRepository) call ["get", ["holster", true]]; if (_holster) then { [player] call AFUNC(weaponselect,putWeaponAway); }; }] call CFUNC(waitUntilAndExecute); diff --git a/arma/client/addons/actor/XEH_preStart.sqf b/arma/client/addons/actor/XEH_preStart.sqf index 0228885..a51262a 100644 --- a/arma/client/addons/actor/XEH_preStart.sqf +++ b/arma/client/addons/actor/XEH_preStart.sqf @@ -1,3 +1,2 @@ #include "script_component.hpp" - #include "XEH_PREP.hpp" diff --git a/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf b/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf index 869b624..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: @@ -31,10 +31,11 @@ private _data = _alert get "data"; diag_log format ["[FORGE:Client:Actor] Handling UI event: %1 with data: %2", _event, _data]; switch (_event) do { - case "actor::get::actions": { GVAR(ActorClass) call ["getNearbyActions", [_control]]; }; + case "actor::get::actions": { GVAR(ActorRepository) call ["getNearbyActions", [_control]]; }; 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/functions/fnc_initActorClass.sqf b/arma/client/addons/actor/functions/fnc_initRepository.sqf similarity index 88% rename from arma/client/addons/actor/functions/fnc_initActorClass.sqf rename to arma/client/addons/actor/functions/fnc_initRepository.sqf index 729b790..71cdd8a 100644 --- a/arma/client/addons/actor/functions/fnc_initActorClass.sqf +++ b/arma/client/addons/actor/functions/fnc_initRepository.sqf @@ -1,29 +1,27 @@ #include "..\script_component.hpp" /* - * File: fnc_initActorClass.sqf + * File: fnc_initRepository.sqf * Author: IDSolutions - * Date: 2026-01-28 - * Last Update: 2026-02-17 - * Public: Yes + * Date: 2026-03-27 + * Public: No * * Description: - * Initializes the actor class for managing player data. - * Provides methods for saving, loading, and applying actor data. + * Initializes the actor repository for managing player actor data. * * Arguments: * None * * Return Value: - * Actor class object [HASHMAP OBJECT] + * Actor repository object [HASHMAP OBJECT] * * Example: - * call forge_client_actor_fnc_initActorClass + * call forge_client_actor_fnc_initRepository; */ #pragma hemtt ignore_variables ["_self"] -GVAR(ActorBaseClass) = compileFinal createHashMapFromArray [ - ["#type", "ActorBaseClass"], +GVAR(ActorRepositoryBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "ActorRepositoryBaseClass"], ["#create", compileFinal { _self set ["uid", getPlayerUID player]; _self set ["actor", createHashMap]; @@ -33,9 +31,10 @@ GVAR(ActorBaseClass) = compileFinal createHashMapFromArray [ ["init", compileFinal { private _uid = _self get "uid"; [SRPC(actor,requestInitActor), [_uid]] call CFUNC(serverEvent); + _self set ["lastSave", time]; - systemChat format ["Actor loaded for %1", (name player)]; - diag_log "[FORGE:Client:Actor] Actor Class Initialized!"; + systemChat format ["Actor loaded for %1", name player]; + diag_log "[FORGE:Client:Actor] Actor Repository Initialized!"; }], ["save", compileFinal { params [["_sync", false, [false]]]; @@ -68,29 +67,23 @@ GVAR(ActorBaseClass) = compileFinal createHashMapFromArray [ _self set ["actor", _actor]; SETPVAR(player,FORGE_isLoaded,true); - if !(_isLoaded) then { _self set ["isLoaded", true]; }; diag_log "[FORGE:Client:Actor] Sync completed"; }], ["get", compileFinal { params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]]; - private _actor = _self get "actor"; _actor getOrDefault [_key, _default]; }], ["applyPosition", compileFinal { private _position = _self call ["get", ["position", [0, 0, 0]]]; - if (GVAR(enableLoc)) then { player setPosASL _position; - private _pAlt = ((getPosATLVisual player) select 2); private _pVelZ = ((velocity player) select 2); - if (_pAlt > 5 && _pVelZ < 0) then { player setVelocity [0, 0, 0]; player setPosATL [((getPosATLVisual player) select 0), ((getPosATLVisual player) select 1), 1]; - hint "You logged off mid air. You were moved to a safe position on the ground"; }; }; @@ -113,9 +106,7 @@ GVAR(ActorBaseClass) = compileFinal createHashMapFromArray [ }], ["getNearbyActions", compileFinal { params [["_control", controlNull, [controlNull]]]; - private _nearbyActions = []; - { private _storeType = _x getVariable ["storeType", ""]; private _isAtm = _x getVariable ["isAtm", false]; @@ -140,5 +131,5 @@ GVAR(ActorBaseClass) = compileFinal createHashMapFromArray [ }] ]; -GVAR(ActorClass) = createHashMapObject [GVAR(ActorBaseClass)]; -GVAR(ActorClass) +GVAR(ActorRepository) = createHashMapObject [GVAR(ActorRepositoryBaseClass)]; +GVAR(ActorRepository) 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/bank/XEH_PREP.hpp b/arma/client/addons/bank/XEH_PREP.hpp index f1a55dc..fb83b48 100644 --- a/arma/client/addons/bank/XEH_PREP.hpp +++ b/arma/client/addons/bank/XEH_PREP.hpp @@ -1,5 +1,4 @@ PREP(handleUIEvents); -PREP(initClass); -PREP(initSessionService); +PREP(initRepository); PREP(initUIBridge); PREP(openUI); diff --git a/arma/client/addons/bank/XEH_postInitClient.sqf b/arma/client/addons/bank/XEH_postInitClient.sqf index a4cf8d6..4a79659 100644 --- a/arma/client/addons/bank/XEH_postInitClient.sqf +++ b/arma/client/addons/bank/XEH_postInitClient.sqf @@ -1,33 +1,48 @@ #include "script_component.hpp" -if (isNil QGVAR(BankClass)) then { call FUNC(initClass); }; -if (isNil QGVAR(BankSessionService)) then { call FUNC(initSessionService); }; +if (isNil QGVAR(BankRepository)) then { call FUNC(initRepository); }; if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); }; [QGVAR(initBank), { - GVAR(BankClass) call ["init", []]; + GVAR(BankRepository) call ["init", []]; }] call CFUNC(addEventHandler); [QGVAR(responseInitBank), { params [["_data", createHashMap, [createHashMap]]]; - GVAR(BankClass) call ["sync", [_data, true]]; + GVAR(BankRepository) call ["markLoaded", []]; if !(isNil QGVAR(BankUIBridge)) then { - GVAR(BankUIBridge) call ["refreshSession", []]; + GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]]; }; }] call CFUNC(addEventHandler); [QGVAR(responseSyncBank), { params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]]; - GVAR(BankClass) call ["sync", [_data, _jip]]; + GVAR(BankRepository) call ["markLoaded", []]; if !(isNil QGVAR(BankUIBridge)) then { - GVAR(BankUIBridge) call ["refreshSession", []]; + GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]]; + }; +}] call CFUNC(addEventHandler); + +[QGVAR(responseHydrateBank), { + params [["_data", createHashMap, [createHashMap]]]; + + if !(isNil QGVAR(BankUIBridge)) then { + GVAR(BankUIBridge) call ["handleHydrateResponse", [_data, "bank::hydrate"]]; + }; +}] call CFUNC(addEventHandler); + +[QGVAR(responseBankNotice), { + params [["_type", "error", [""]], ["_message", "", [""]]]; + + if !(isNil QGVAR(BankUIBridge)) then { + GVAR(BankUIBridge) call ["handleNoticeResponse", [_type, _message]]; }; }] call CFUNC(addEventHandler); [{ - EGVAR(org,OrgClass) get "isLoaded"; + EGVAR(actor,ActorRepository) get "isLoaded"; }, { [QGVAR(initBank), []] call CFUNC(localEvent); }] call CFUNC(waitUntilAndExecute); diff --git a/arma/client/addons/bank/XEH_preStart.sqf b/arma/client/addons/bank/XEH_preStart.sqf index 0228885..a51262a 100644 --- a/arma/client/addons/bank/XEH_preStart.sqf +++ b/arma/client/addons/bank/XEH_preStart.sqf @@ -1,3 +1,2 @@ #include "script_component.hpp" - #include "XEH_PREP.hpp" diff --git a/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf b/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf index b2fcd53..4d5982b 100644 --- a/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf +++ b/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf @@ -68,6 +68,16 @@ switch (_event) do { GVAR(BankUIBridge) call ["handleDepositEarningsRequest", [_data]]; }; }; + case "bank::repayCreditLine::request": { + if !(isNil QGVAR(BankUIBridge)) then { + GVAR(BankUIBridge) call ["handleRepayCreditLineRequest", [_data]]; + }; + }; + case "bank::pin::request": { + if !(isNil QGVAR(BankUIBridge)) then { + GVAR(BankUIBridge) call ["handleSubmitPinRequest", [_data]]; + }; + }; default { hint format ["Unhandled bank UI event: %1", _event]; }; diff --git a/arma/client/addons/bank/functions/fnc_initClass.sqf b/arma/client/addons/bank/functions/fnc_initClass.sqf deleted file mode 100644 index ede4cc8..0000000 --- a/arma/client/addons/bank/functions/fnc_initClass.sqf +++ /dev/null @@ -1,62 +0,0 @@ -#include "..\script_component.hpp" - -/* - * File: fnc_initClass.sqf - * Author: IDSolutions - * Public: No - * - * Description: - * Initializes the bank class for account sync and access helpers. - */ - -#pragma hemtt ignore_variables ["_self"] -GVAR(BankBaseClass) = compileFinal createHashMapFromArray [ - ["#type", "BankBaseClass"], - ["#create", compileFinal { - _self set ["uid", getPlayerUID player]; - _self set ["account", createHashMapFromArray [ - ["bank", 0], - ["cash", 0], - ["earnings", 0], - ["pin", 1234], - ["transactions", []] - ]]; - _self set ["isLoaded", false]; - _self set ["lastSave", time]; - }], - ["getAccountState", compileFinal { - _self getOrDefault ["account", createHashMap] - }], - ["get", compileFinal { - params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]]; - - private _account = _self getOrDefault ["account", createHashMap]; - _account getOrDefault [_key, _default] - }], - ["init", compileFinal { - [SRPC(bank,requestInitBank), [getPlayerUID player]] call CFUNC(serverEvent); - _self set ["lastSave", time]; - }], - ["save", compileFinal { - [SRPC(bank,requestSaveBank), [getPlayerUID player]] call CFUNC(serverEvent); - _self set ["lastSave", time]; - }], - ["sync", compileFinal { - params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]]; - - private _account = _self getOrDefault ["account", createHashMap]; - { - _account set [_x, _y]; - } forEach _data; - - _self set ["account", _account]; - if !(_self getOrDefault ["isLoaded", false]) then { - _self set ["isLoaded", true]; - }; - - true - }] -]; - -GVAR(BankClass) = createHashMapObject [GVAR(BankBaseClass)]; -GVAR(BankClass) diff --git a/arma/client/addons/bank/functions/fnc_initRepository.sqf b/arma/client/addons/bank/functions/fnc_initRepository.sqf new file mode 100644 index 0000000..a3efc61 --- /dev/null +++ b/arma/client/addons/bank/functions/fnc_initRepository.sqf @@ -0,0 +1,44 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initRepository.sqf + * Author: IDSolutions + * Date: 2026-03-27 + * Public: No + * + * Description: + * Initializes the bank repository for client bank lifecycle state. + * + * Arguments: + * None + * + * Return Value: + * Bank repository object [HASHMAP OBJECT] + * + * Example: + * call forge_client_bank_fnc_initRepository; + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(BankRepositoryBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "BankRepositoryBaseClass"], + ["#create", compileFinal { + _self set ["uid", getPlayerUID player]; + _self set ["isLoaded", false]; + _self set ["lastSave", time]; + }], + ["init", compileFinal { + [SRPC(bank,requestInitBank), [getPlayerUID player]] call CFUNC(serverEvent); + _self set ["lastSave", time]; + + systemChat format ["Bank loaded for %1", name player]; + diag_log "[FORGE:Client:Bank] Bank Repository Initialized!"; + }], + ["markLoaded", compileFinal { + if !(_self getOrDefault ["isLoaded", false]) then { _self set ["isLoaded", true]; }; + true + }] +]; + +GVAR(BankRepository) = createHashMapObject [GVAR(BankRepositoryBaseClass)]; +GVAR(BankRepository) diff --git a/arma/client/addons/bank/functions/fnc_initSessionService.sqf b/arma/client/addons/bank/functions/fnc_initSessionService.sqf deleted file mode 100644 index 155652b..0000000 --- a/arma/client/addons/bank/functions/fnc_initSessionService.sqf +++ /dev/null @@ -1,80 +0,0 @@ -#include "..\script_component.hpp" - -/* - * File: fnc_initSessionService.sqf - * Author: IDSolutions - * Public: No - * - * Description: - * Initializes the bank session service that shapes the browser payload. - */ - -#pragma hemtt ignore_variables ["_self"] -GVAR(BankSessionServiceBaseClass) = compileFinal createHashMapFromArray [ - ["#type", "BankSessionServiceBaseClass"], - ["buildTransferTargets", compileFinal { - private _targets = []; - - { - if (isNull _x || { _x isEqualTo player }) then { - continue; - }; - - private _uid = getPlayerUID _x; - private _name = name _x; - if (_uid isEqualTo "" || { _name isEqualTo "" }) then { - continue; - }; - - _targets pushBack (createHashMapFromArray [ - ["name", _name], - ["uid", _uid] - ]); - } forEach allPlayers; - - private _targetPairs = _targets apply { - [toLowerANSI (_x getOrDefault ["name", ""]), _x] - }; - _targetPairs sort true; - _targetPairs apply { - _x param [1, createHashMap] - } - }], - ["buildPayload", compileFinal { - params [["_mode", "bank", [""]]]; - - private _account = if (isNil QGVAR(BankClass)) then { - createHashMap - } else { - GVAR(BankClass) call ["getAccountState", []] - }; - - private _orgFunds = 0; - private _orgName = ""; - if !(isNil QEGVAR(org,OrgClass)) then { - _orgFunds = EGVAR(org,OrgClass) call ["get", ["funds", 0]]; - _orgName = EGVAR(org,OrgClass) call ["get", ["name", ""]]; - }; - - createHashMapFromArray [ - ["session", createHashMapFromArray [ - ["mode", ["bank", "atm"] select (toLowerANSI _mode isEqualTo "atm")], - ["orgFunds", _orgFunds], - ["orgName", _orgName], - ["playerName", name player], - ["transferTargets", _self call ["buildTransferTargets", []]], - ["uid", getPlayerUID player] - ]], - ["account", createHashMapFromArray [ - ["bank", _account getOrDefault ["bank", 0]], - ["cash", _account getOrDefault ["cash", 0]], - ["earnings", _account getOrDefault ["earnings", 0]], - ["pin", str (_account getOrDefault ["pin", 1234])], - ["transactions", _account getOrDefault ["transactions", []]] - ]] - ] - }] -]; - -GVAR(BankSessionService) = createHashMapObject [GVAR(BankSessionServiceBaseClass)]; -GVAR(BankSessionService) diff --git a/arma/client/addons/bank/functions/fnc_initUIBridge.sqf b/arma/client/addons/bank/functions/fnc_initUIBridge.sqf index 32e1b0b..59d57da 100644 --- a/arma/client/addons/bank/functions/fnc_initUIBridge.sqf +++ b/arma/client/addons/bank/functions/fnc_initUIBridge.sqf @@ -3,10 +3,20 @@ /* * File: fnc_initUIBridge.sqf * Author: IDSolutions + * Date: 2026-03-27 * Public: No * * Description: - * Initializes the bank web UI bridge. + * Initializes the bank UI bridge for browser control state and bank UI events. + * + * Arguments: + * None + * + * Return Value: + * Bank UI bridge object [HASHMAP OBJECT] + * + * Example: + * call forge_client_bank_fnc_initUIBridge; */ #pragma hemtt ignore_variables ["_self"] @@ -19,9 +29,6 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [ ["#create", compileFinal { _self set ["mode", "bank"]; }], - ["buildPayload", compileFinal { - GVAR(BankSessionService) call ["buildPayload", [_self call ["getMode", []]]] - }], ["getActiveBrowserControl", compileFinal { private _display = uiNamespace getVariable ["RscBank", displayNull]; if (isNull _display) exitWith { @@ -36,14 +43,16 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [ ["getMode", compileFinal { _self getOrDefault ["mode", "bank"] }], + ["hasOpenScreen", compileFinal { + private _screen = _self call ["getScreen", []]; + private _control = _self call ["getActiveBrowserControl", []]; + + !(isNull _control) && { _screen call ["isReady", []] } + }], ["handleDepositEarningsRequest", compileFinal { params [["_data", createHashMap, [createHashMap]]]; private _amount = floor (_data getOrDefault ["amount", 0]); - if (_amount <= 0) exitWith { - _self call ["sendNotice", ["error", "No earnings are available to deposit."]]; - }; - [SRPC(bank,requestDepositEarnings), [getPlayerUID player, _amount]] call CFUNC(serverEvent); true }], @@ -51,22 +60,53 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [ params [["_data", createHashMap, [createHashMap]]]; private _amount = floor (_data getOrDefault ["amount", 0]); - if (_amount <= 0) exitWith { - _self call ["sendNotice", ["error", "Enter a valid deposit amount."]]; - }; - [SRPC(bank,requestDeposit), [getPlayerUID player, _amount]] call CFUNC(serverEvent); true }], + ["handleRepayCreditLineRequest", compileFinal { + params [["_data", createHashMap, [createHashMap]]]; + + private _amount = floor (_data getOrDefault ["amount", 0]); + [SRPC(bank,requestRepayCreditLine), [getPlayerUID player, _amount]] call CFUNC(serverEvent); + true + }], + ["handleHydrateResponse", compileFinal { + params [["_data", createHashMap, [createHashMap]], ["_event", "bank::hydrate", [""]]]; + + if !(_self call ["hasOpenScreen", []]) exitWith { false }; + + _self call ["sendEvent", [_event, _data, _self call ["getActiveBrowserControl", []]]] + }], + ["handleAccountSyncResponse", compileFinal { + params [["_data", createHashMap, [createHashMap]]]; + + if !(_self call ["hasOpenScreen", []]) exitWith { false }; + + _self call ["sendEvent", ["bank::sync", _data, _self call ["getActiveBrowserControl", []]]] + }], + ["handleNoticeResponse", compileFinal { + params [["_type", "error", [""]], ["_message", "", [""]]]; + + _self call ["sendNotice", [_type, _message]] + }], ["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 ["sendEvent", ["bank::hydrate", _self call ["buildPayload", []], _control]]; + + _self call ["requestHydrate", [true]] + }], + ["handleSubmitPinRequest", compileFinal { + params [["_data", createHashMap, [createHashMap]]]; + + private _pin = _data getOrDefault ["pin", ""]; + if !(_pin isEqualType "") then { _pin = str _pin; }; + + [SRPC(bank,requestSubmitPin), [getPlayerUID player, _pin]] call CFUNC(serverEvent); + true }], ["handleTransferRequest", compileFinal { params [["_data", createHashMap, [createHashMap]]]; @@ -75,18 +115,6 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [ private _target = _data getOrDefault ["target", ""]; private _from = toLowerANSI (_data getOrDefault ["from", "bank"]); - if (_target isEqualTo "") exitWith { - _self call ["sendNotice", ["error", "Select a transfer recipient."]]; - }; - - if (_target isEqualTo getPlayerUID player) exitWith { - _self call ["sendNotice", ["error", "You cannot transfer funds to yourself."]]; - }; - - if (_amount <= 0) exitWith { - _self call ["sendNotice", ["error", "Enter a valid transfer amount."]]; - }; - [SRPC(bank,requestTransfer), [getPlayerUID player, _target, _from, _amount]] call CFUNC(serverEvent); true }], @@ -94,23 +122,24 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [ params [["_data", createHashMap, [createHashMap]]]; private _amount = floor (_data getOrDefault ["amount", 0]); - if (_amount <= 0) exitWith { - _self call ["sendNotice", ["error", "Enter a valid withdrawal amount."]]; - }; - [SRPC(bank,requestWithdraw), [getPlayerUID player, _amount]] call CFUNC(serverEvent); true }], ["refreshSession", compileFinal { - private _control = _self call ["getActiveBrowserControl", []]; - if (isNull _control) exitWith { false }; + _self call ["requestHydrate", [false]] + }], + ["requestHydrate", compileFinal { + params [["_resetAuthorization", false, [false]]]; - _self call ["sendEvent", ["bank::sync", _self call ["buildPayload", []], _control]] + if !(_self call ["hasOpenScreen", []]) exitWith { false }; + + [SRPC(bank,requestHydrateBank), [getPlayerUID player, _self call ["getMode", []], _resetAuthorization]] call CFUNC(serverEvent); + true }], ["sendNotice", compileFinal { params [["_type", "error", [""]], ["_message", "", [""]], ["_control", controlNull, [controlNull]]]; - if (_message isEqualTo "") exitWith { false }; + if (_message isEqualTo "" || { !(_self call ["hasOpenScreen", []]) }) exitWith { false }; _self call ["sendEvent", ["bank::notice", createHashMapFromArray [ ["message", _message], @@ -121,9 +150,7 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [ params [["_mode", "bank", [""]]]; private _finalMode = toLowerANSI _mode; - if !(_finalMode in ["bank", "atm"]) then { - _finalMode = "bank"; - }; + if !(_finalMode in ["bank", "atm"]) then { _finalMode = "bank"; }; _self set ["mode", _finalMode]; _finalMode diff --git a/arma/client/addons/bank/ui/_site/bank-ui.js b/arma/client/addons/bank/ui/_site/bank-ui.js index cf05616..b733bec 100644 --- a/arma/client/addons/bank/ui/_site/bank-ui.js +++ b/arma/client/addons/bank/ui/_site/bank-ui.js @@ -1 +1 @@ -!function(){const n=window.ForgeWebUI;(window.BankApp=window.BankApp||{}).runtime=n,window.AppRuntime=n}(),function(){const n=window.BankApp=window.BankApp||{},e={mode:"bank",orgFunds:0,orgName:"",playerName:"",transferTargets:[],uid:""},t={bank:0,cash:0,earnings:0,pin:"1234",transactions:[]};function a(n,e){var t;Object.keys(n).forEach(e=>delete n[e]),Object.assign(n,(t=e,JSON.parse(JSON.stringify(t))))}n.data={account:Object.assign({},t),session:Object.assign({},e),applyHydratePayload(n){a(this.session,Object.assign({},e,n?.session||{})),a(this.account,Object.assign({},t,n?.account||{}))}}}(),function(){const n=window.BankApp=window.BankApp||{},{createSignal:e}=n.runtime;n.store=new class{constructor(){[this.getMode,this.setMode]=e("bank"),[this.getNotice,this.setNotice]=e({text:"",type:""}),[this.getPendingAction,this.setPendingAction]=e(""),[this.getAtmView,this.setAtmView]=e("pin"),[this.getEnteredPin,this.setEnteredPin]=e(""),[this.getCustomAmount,this.setCustomAmount]=e(""),[this.getAccountVersion,this.setAccountVersion]=e(0),[this.getSessionVersion,this.setSessionVersion]=e(0)}finishAction(){this.setPendingAction("")}hydrateFromPayload(n){const e=String(n?.session?.mode||"bank").trim().toLowerCase(),t=this.getMode(),a=this.getAtmView();this.setMode("atm"===e?"atm":"bank"),this.setPendingAction(""),this.setNotice({text:"",type:""}),this.setEnteredPin(""),this.setCustomAmount(""),this.setAccountVersion(this.getAccountVersion()+1),this.setSessionVersion(this.getSessionVersion()+1),"atm"!==e?this.setAtmView("dashboard"):this.setAtmView("atm"===t?a:"pin")}resetAtm(){this.setEnteredPin(""),this.setCustomAmount(""),this.setAtmView("pin")}startAction(n){this.setPendingAction(String(n||"").trim().toLowerCase())}}}(),function(){const n=window.BankApp=window.BankApp||{},e=n.store,t=window.ForgeWebUI.createBridge({closeEvent:"bank::close",globalName:"ForgeBridge",readyEvent:"bank::ready"});function a(t){n.data.applyHydratePayload(t),e.hydrateFromPayload(t)}t.on("bank::hydrate",a),t.on("bank::sync",a),t.on("bank::notice",e=>{n.actions&&n.actions.showNotice(e.type||"error",e.message||"Bank notice received.")}),n.bridge={notifyReady:()=>t.ready({loaded:!0}),receive:t.receive,requestClose:()=>t.close({}),requestDeposit:n=>t.send("bank::deposit::request",n),requestDepositEarnings:n=>t.send("bank::depositEarnings::request",n),requestRefresh:()=>t.send("bank::refresh",{}),requestTransfer:n=>t.send("bank::transfer::request",n),requestWithdraw:n=>t.send("bank::withdraw::request",n),sendEvent:t.send}}(),function(){const n=window.BankApp=window.BankApp||{},e=n.store;let t=null;function a(){return n.data?.account||{}}function s(n){const e=Math.floor(Number(n||0));return Number.isFinite(e)?e:0}function i(n,a){e.setNotice({type:n,text:a}),t&&clearTimeout(t),t=setTimeout(()=>{e.setNotice({text:"",type:""}),t=null},3200)}function o(t){const o=s(t),r=a();if(o<=0)return i("error","Enter a valid deposit amount."),!1;if(o>Number(r.cash||0))return i("error","Cash on hand cannot cover that deposit."),!1;const c=n.bridge;if(!c||"function"!=typeof c.requestDeposit)return i("error","Deposit bridge is unavailable."),!1;e.startAction("deposit");return!!c.requestDeposit({amount:o})||(e.finishAction(),i("error","Deposit bridge is unavailable."),!1)}function r(t){const o=s(t),r=a();if(o<=0)return i("error","Enter a valid withdrawal amount."),!1;if(o>Number(r.bank||0))return i("error","Bank balance cannot cover that withdrawal."),!1;const c=n.bridge;if(!c||"function"!=typeof c.requestWithdraw)return i("error","Withdraw bridge is unavailable."),!1;e.startAction("withdraw");return!!c.requestWithdraw({amount:o})||(e.finishAction(),i("error","Withdraw bridge is unavailable."),!1)}function c(){e.setEnteredPin("")}n.actions={appendCustomAmountDigit:function(n){const t=String(n||"").trim();if(!t)return;const a=String(e.getCustomAmount()||"");a.length>=7||e.setCustomAmount(a+t)},appendPinDigit:function(n){const t=String(n||"").trim();if(!t)return;const a=String(e.getEnteredPin()||"");a.length>=4||e.setEnteredPin(a+t)},backspaceCustomAmount:function(){const n=String(e.getCustomAmount()||"");e.setCustomAmount(n.slice(0,-1))},backspacePin:function(){const n=String(e.getEnteredPin()||"");e.setEnteredPin(n.slice(0,-1))},clearCustomAmount:function(){e.setCustomAmount("")},clearPin:c,closeBank:function(){const e=n.bridge;if(e&&"function"==typeof e.requestClose){if(e.requestClose())return!0}return i("error","Bank bridge is unavailable."),!1},refreshBank:function(){const e=n.bridge;if(e&&"function"==typeof e.requestRefresh){if(e.requestRefresh())return!0}return i("error","Bank refresh bridge is unavailable."),!1},requestAtmAmount:function(n,t){const a="deposit"===String(n||"").trim().toLowerCase()?o(t):r(t);return a&&e.setAtmView("menu"),a},requestDeposit:o,requestDepositEarnings:function(t){const o=s(t),r=a();if(o<=0)return i("error","No earnings are available to deposit."),!1;if(o>Number(r.earnings||0))return i("error","Pending earnings cannot cover that deposit request."),!1;const c=n.bridge;return c&&"function"==typeof c.requestDepositEarnings?(e.startAction("depositearnings"),!!c.requestDepositEarnings({amount:o})||(e.finishAction(),i("error","Earnings bridge is unavailable."),!1)):(i("error","Earnings bridge is unavailable."),!1)},requestTransfer:function(t,o){const r=s(o),c=n.data?.session||{},u=a(),l=String(t||"").trim();if(!l)return i("error","Select a transfer recipient."),!1;if(l===String(c.uid||""))return i("error","You cannot transfer funds to yourself."),!1;if(r<=0)return i("error","Enter a valid transfer amount."),!1;if(r>Number(u.bank||0))return i("error","Bank balance cannot cover that transfer."),!1;const m=n.bridge;return m&&"function"==typeof m.requestTransfer?(e.startAction("transfer"),!!m.requestTransfer({amount:r,from:"bank",target:l})||(e.finishAction(),i("error","Transfer bridge is unavailable."),!1)):(i("error","Transfer bridge is unavailable."),!1)},requestWithdraw:r,selectAtmView:function(n){const t=String(n||"").trim();return!!t&&("pin"===t?(e.resetAtm(),!0):(e.setCustomAmount(""),e.setAtmView(t),!0))},showNotice:i,submitCustomAmount:function(n){const t=s(e.getCustomAmount()),a=String(n||"").trim().toLowerCase();if(t<=0)return i("error","Enter a valid transaction amount."),!1;const c="deposit"===a?o(t):r(t);return c&&(e.setCustomAmount(""),e.setAtmView("menu")),c},submitPin:function(){const n=String(e.getEnteredPin()||""),t=String(a().pin||"1234");return 4!==n.length?(i("error","Enter your four-digit access PIN."),!1):n!==t?(c(),i("error","Incorrect PIN."),!1):(c(),e.setAtmView("menu"),!0)}}}(),function(){const n=window.BankApp=window.BankApp||{},{h:e}=n.runtime,t=n.store,{account:a}=n.data;function s(n){return`$${Math.round(Number(n||0)).toLocaleString()}`}n.componentFns=n.componentFns||{},Object.assign(n.componentFns,{clearInputValue:function(n){const e=document.getElementById(n);e&&(e.value="")},formatCurrency:s,keypad:function(n,t,a,s){return e("div",{className:"bank-keypad"},["1","2","3","4","5","6","7","8","9"].map(t=>e("button",{type:"button",className:"bank-key",onClick:()=>n(t)},t)),e("button",{type:"button",className:"bank-key is-muted",onClick:a},"C"),e("button",{type:"button",className:"bank-key",onClick:()=>n("0")},"0"),e("button",{type:"button",className:"bank-key is-accent",onClick:s},"Enter"),e("button",{type:"button",className:"bank-key is-wide",onClick:t},"Backspace"))},metricCard:function(n,t,a,s=""){return e("div",{className:s?`bank-metric-card is-${s}`:"bank-metric-card"},e("span",{className:"bank-eyebrow"},n),e("span",{className:"bank-metric-value"},t),e("span",{className:"bank-metric-copy"},a))},pending:function(n){return t.getPendingAction()===n},pinIndicators:function(n){const t=String(n||"");return e("div",{className:"bank-pin-indicators"},[0,1,2,3].map(n=>e("span",{className:ne("div",{className:"bank-history-row"},e("div",{className:"bank-history-copy"},e("span",{className:"bank-history-title"},n.type||"Transaction"),e("span",{className:"bank-history-meta"},n.date||"Pending timestamp")),e("span",{className:"bank-history-value"},s(n.amount||0)))))}})}(),function(){const n=window.BankApp=window.BankApp||{},{h:e}=n.runtime,t=n.store,a=n.actions,{account:s,session:i}=n.data,{formatCurrency:o,statCard:r}=n.componentFns;n.componentFns=n.componentFns||{},n.componentFns.BankSidebar=function(){return t.getAccountVersion(),t.getSessionVersion(),e("aside",{className:"bank-sidebar"},e("section",{className:"bank-module"},e("div",{className:"bank-module-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Account"),e("h2",{className:"bank-section-title"},"Balances")),e("span",{className:"bank-pill"},"Live")),e("div",{className:"bank-summary-grid"},r("Bank",o(s.bank),"accent"),r("Cash",o(s.cash)),r("Earnings",o(s.earnings),s.earnings>0?"warning":""),r("Org Funds",o(i.orgFunds),i.orgFunds>0?"success":""))),e("section",{className:"bank-module"},e("div",{className:"bank-module-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Profile"),e("h2",{className:"bank-section-title"},"Account Holder")),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.refreshBank()},"Refresh")),e("div",{className:"bank-profile-stack"},r("Name",i.playerName||"Unknown"),r("UID",i.uid||"-"),r("Organization",i.orgName||"No active organization"))))}}(),function(){const n=window.BankApp=window.BankApp||{},{h:e}=n.runtime,t=n.store,{account:a,session:s}=n.data,{formatCurrency:i}=n.componentFns;n.componentFns=n.componentFns||{},n.componentFns.BankFooter=function(){t.getAccountVersion(),t.getSessionVersion();const n=[{title:"Banking Resources",items:["Account Access Policy","Transfer & Wire Guidelines","Cash Handling Schedule","Terminal Security Notice"]},{title:"Bank Support",items:s.orgName?[`Organization: ${s.orgName}`,`Treasury Reference: ${i(s.orgFunds)}`,`${s.transferTargets.length} transfer recipient(s) currently visible.`,`Primary Ledger: ${i(a.bank)}`]:["Organization: No active treasury link",`${s.transferTargets.length} transfer recipient(s) currently visible.`,`Primary Ledger: ${i(a.bank)}`,`Cash On Hand: ${i(a.cash)}`]}];return e("footer",{className:"bank-footer-bar"},e("div",{className:"bank-footer"},...n.map(n=>e("div",{className:"bank-footer-block"},e("h3",{className:"bank-footer-title"},n.title),e("ul",{className:"bank-footer-list"},...(n.items||[]).map(n=>e("li",{className:"bank-footer-copy"},n)))))))}}(),function(){const n=window.BankApp=window.BankApp||{},{h:e}=n.runtime,t=n.store,a=n.actions,{account:s,session:i}=n.data,{clearInputValue:o,formatCurrency:r,metricCard:c,pending:u,readInputValue:l,transactionRows:m}=n.componentFns;function d(){t.getAccountVersion()}function b(){t.getSessionVersion()}n.componentFns=n.componentFns||{},n.componentFns.BankPageHeader=function(){return b(),e("div",{className:"bank-page-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Treasury Desk"),e("h1",{className:"bank-title"},"Personal Banking")),e("span",{className:"bank-pill"},i.playerName||"Account Holder"))},n.componentFns.BankSummarySection=function(){return d(),b(),e("section",{className:"bank-page-section bank-summary-section"},e("div",{className:"bank-section-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Overview"),e("h2",{className:"bank-section-title"},"Financial Position")),e("span",{className:"bank-pill"},"Banking Desk")),e("div",{className:"bank-summary-band"},c("Primary Balance",r(s.bank),"Available for transfers and withdrawals.","accent"),c("Cash On Hand",r(s.cash),"Funds currently carried by the player."),c("Pending Earnings",r(s.earnings),"Ready to sweep into the main account ledger.",s.earnings>0?"warning":""),c("Org Snapshot",r(i.orgFunds),"Reference value pulled from the organization treasury.",i.orgFunds>0?"success":"")))},n.componentFns.BankActionSections=function(){return b(),e("div",{className:"bank-action-sections"},e("section",{className:"bank-page-section"},e("div",{className:"bank-section-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Movement"),e("h2",{className:"bank-section-title"},"Deposit / Withdraw"))),e("div",{className:"bank-form-stack"},e("input",{id:"bank-amount-input",className:"bank-input",type:"number",min:"1",placeholder:"Enter amount"}),e("div",{className:"bank-action-row"},e("button",{type:"button",className:"bank-btn bank-btn-primary",disabled:u("deposit"),onClick:()=>{a.requestDeposit(l("bank-amount-input"))&&o("bank-amount-input")}},u("deposit")?"Depositing...":"Deposit"),e("button",{type:"button",className:"bank-btn bank-btn-secondary",disabled:u("withdraw"),onClick:()=>{a.requestWithdraw(l("bank-amount-input"))&&o("bank-amount-input")}},u("withdraw")?"Withdrawing...":"Withdraw")))),e("section",{className:"bank-page-section"},e("div",{className:"bank-section-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Transfer"),e("h2",{className:"bank-section-title"},"Wire Funds"))),e("div",{className:"bank-form-stack"},e("select",{id:"bank-transfer-target",className:"bank-select"},e("option",{value:""},i.transferTargets.length>0?"Select recipient":"No available recipients"),i.transferTargets.map(n=>e("option",{value:n.uid},n.name||n.uid))),e("input",{id:"bank-transfer-amount",className:"bank-input",type:"number",min:"1",placeholder:"Enter transfer amount"}),e("button",{type:"button",className:"bank-btn bank-btn-primary",disabled:u("transfer")||0===i.transferTargets.length,onClick:()=>{a.requestTransfer(l("bank-transfer-target"),l("bank-transfer-amount"))&&o("bank-transfer-amount")}},u("transfer")?"Transferring...":"Transfer Funds"))))},n.componentFns.BankSupportSection=function(){return d(),e("div",{className:"bank-support-sections"},e("section",{className:"bank-page-section"},e("div",{className:"bank-section-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Sweep"),e("h2",{className:"bank-section-title"},"Deposit Earnings"))),e("p",{className:"bank-card-copy"},"Sweep pending earnings into the primary account when you want them reflected in the main balance."),e("button",{type:"button",className:"bank-btn bank-btn-primary",disabled:u("depositearnings")||Number(s.earnings||0)<=0,onClick:()=>a.requestDepositEarnings(s.earnings)},u("depositearnings")?"Depositing...":"Deposit Earnings")))},n.componentFns.BankHistorySection=function(){return d(),e("section",{className:"bank-page-section bank-history-section"},e("div",{className:"bank-section-header"},e("div",null,e("span",{className:"bank-eyebrow"},"History"),e("h2",{className:"bank-section-title"},"Recent Transactions"))),m())}}(),function(){const n=window.BankApp=window.BankApp||{},{h:e}=n.runtime,t=n.store,a=n.actions,{account:s}=n.data,{formatCurrency:i,keypad:o,pinIndicators:r}=n.componentFns;function c(n){const t="deposit"===n?"Deposit":"Withdraw";return e("div",{className:"bank-atm-action-grid"},[20,50,100,500].map(s=>e("button",{type:"button",className:"bank-btn bank-btn-primary",onClick:()=>a.requestAtmAmount(n,s)},`${t} ${i(s)}`)),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.selectAtmView("deposit"===n?"customDeposit":"customWithdraw")},"Custom Amount"),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.selectAtmView("menu")},"Back"))}function u(n){const s="deposit"===n?"Deposit":"Withdraw";return e("div",{className:"bank-atm-stack"},e("div",{className:"bank-pin-display"},t.getCustomAmount()?i(t.getCustomAmount()):"$0"),o(a.appendCustomAmountDigit,a.backspaceCustomAmount,a.clearCustomAmount,()=>a.submitCustomAmount(n)),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.selectAtmView("menu")},`Cancel ${s}`))}n.componentFns=n.componentFns||{},n.componentFns.ATMView=function(){t.getAccountVersion();const n=t.getAtmView(),l=String(t.getEnteredPin()||"");let m="Terminal Access",d="Authenticate with the four-digit account PIN before using the terminal.",b=null;switch(n){case"menu":m="ATM Menu",d="Select a banking action. The ATM can deposit, withdraw, and show the live account balance.",b=e("div",{className:"bank-atm-action-grid"},e("button",{type:"button",className:"bank-btn bank-btn-primary",onClick:()=>a.selectAtmView("withdraw")},"Withdraw Cash"),e("button",{type:"button",className:"bank-btn bank-btn-primary",onClick:()=>a.selectAtmView("deposit")},"Deposit Cash"),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.selectAtmView("balance")},"Check Balance"),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.closeBank()},"Exit Terminal"));break;case"withdraw":m="Withdraw Cash",d="Choose a preset amount or enter a custom amount for withdrawal.",b=c("withdraw");break;case"deposit":m="Deposit Cash",d="Move cash on hand back into the main bank balance from the terminal.",b=c("deposit");break;case"customWithdraw":m="Custom Withdraw",d="Enter the exact withdrawal amount.",b=u("withdraw");break;case"customDeposit":m="Custom Deposit",d="Enter the exact deposit amount.",b=u("deposit");break;case"balance":m="Available Balance",d="Current bank balance available at this terminal.",b=e("div",{className:"bank-atm-stack"},e("div",{className:"bank-balance-display"},i(s.bank)),e("button",{type:"button",className:"bank-btn bank-btn-primary",onClick:()=>a.selectAtmView("menu")},"Return to Menu"));break;default:b=e("div",{className:"bank-atm-stack"},e("div",{className:"bank-pin-display"},r(l)),o(a.appendPinDigit,a.backspacePin,a.clearPin,a.submitPin),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.closeBank()},"Exit Terminal"))}return e("div",{className:"bank-atm-shell"},e("section",{className:"bank-atm-panel"},e("div",{className:"bank-panel-header"},e("div",null,e("span",{className:"bank-eyebrow"},"ATM"),e("h1",{className:"bank-title"},m)),e("span",{className:"bank-pill"},"Secure Terminal")),e("p",{className:"bank-panel-copy"},d),b))}}(),function(){const n=window.BankApp=window.BankApp||{},{h:e}=n.runtime,t=window.SharedUI.componentFns.WindowTitleBar,a=n.store,s=n.actions;n.componentFns=n.componentFns||{},n.componentFns.NoticeLayer=function(){const n=a.getNotice();return n.text?e("div",{className:"bank-notice-stack"},e("div",{className:"error"===n.type?"bank-notice is-error":"bank-notice is-success"},n.text)):null},n.components=n.components||{},n.components.App=function(){const n=a.getMode();return e("div",{className:"atm"===n?"bank-shell is-atm":"bank-shell"},"atm"===n?null:t({kicker:"FORGE Finance",title:"Global Banking Network",onClose:()=>s.closeBank(),closeLabel:"Close banking interface"}),e("div",{id:"bank-notice-root"}),"atm"===n?e("div",{id:"bank-atm-root"}):[e("div",{className:"bank-scroll-shell","data-preserve-scroll-id":"bank-page-scroll"},[e("div",{className:"bank-layout"},e("div",{id:"bank-sidebar-root"}),e("main",{className:"bank-main"},e("div",{className:"bank-page"},e("div",{id:"bank-page-header-root"}),e("p",{className:"bank-page-copy"},"Manage deposits, withdrawals, transfers, and earnings sweeps from the same shared financial console."),e("div",{className:"bank-page-divider"}),e("div",{className:"bank-page-body"},e("div",{id:"bank-summary-section-root"}),e("div",{id:"bank-action-sections-root"}),e("div",{id:"bank-support-section-root"}),e("div",{id:"bank-history-section-root"}))))),e("div",{id:"bank-footer-root"})])])}}(),function(){const n=window.ForgeWebUI,e=window.BankApp,t=[{id:"bank-notice-root",preserveScroll:!1,render:()=>e.componentFns.NoticeLayer()},{id:"bank-sidebar-root",preserveScroll:!1,render:()=>e.componentFns.BankSidebar()},{id:"bank-page-header-root",preserveScroll:!1,render:()=>e.componentFns.BankPageHeader()},{id:"bank-summary-section-root",preserveScroll:!1,render:()=>e.componentFns.BankSummarySection()},{id:"bank-action-sections-root",preserveScroll:!1,render:()=>e.componentFns.BankActionSections()},{id:"bank-support-section-root",preserveScroll:!1,render:()=>e.componentFns.BankSupportSection()},{id:"bank-history-section-root",preserveScroll:!1,render:()=>e.componentFns.BankHistorySection()},{id:"bank-atm-root",preserveScroll:!1,render:()=>e.componentFns.ATMView()},{id:"bank-footer-root",preserveScroll:!1,render:()=>e.componentFns.BankFooter()}];n.createApp({name:"bank",root:"#app",setup({root:a}){const s=function(){const e=new Map;return{sync:function(){t.forEach(t=>{const a=document.getElementById(t.id),s=e.get(t.id);if(!a)return void(s&&(s.handle.dispose(),e.delete(t.id)));if(s&&s.container===a)return;s&&s.handle.dispose();const i=n.mount(a,t.render,{preserveScroll:t.preserveScroll});e.set(t.id,{container:a,handle:i})})}}}();n.mount(a,()=>e.components.App(),{preserveScroll:!1}),e.bridge&&e.bridge.notifyReady(),n.effect(()=>{e.store.getMode(),requestAnimationFrame(()=>{s.sync()})})}}).start()}(); \ No newline at end of file +!function(){const n=window.ForgeWebUI;(window.BankApp=window.BankApp||{}).runtime=n,window.AppRuntime=n}(),function(){const n=window.BankApp=window.BankApp||{},e={atmAuthorized:!1,creditLine:{amountDue:0,approvedAmount:0,availableAmount:0,interestRate:.1,outstandingPrincipal:0},mode:"bank",orgFunds:0,orgName:"",playerName:"",transferTargets:[],uid:""},t={bank:0,cash:0,earnings:0,transactions:[]};function a(n,e){var t;Object.keys(n).forEach(e=>delete n[e]),Object.assign(n,(t=e,JSON.parse(JSON.stringify(t))))}n.data={account:Object.assign({},t),session:Object.assign({},e),applyAccountPatch(n){const e=Object.assign({},this.account,n||{});a(this.account,Object.assign({},t,e))},applyHydratePayload(n){a(this.session,Object.assign({},e,n?.session||{})),a(this.account,Object.assign({},t,n?.account||{}))}}}(),function(){const n=window.BankApp=window.BankApp||{},{createSignal:e}=n.runtime;n.store=new class{constructor(){[this.getMode,this.setMode]=e("bank"),[this.getNotice,this.setNotice]=e({text:"",type:""}),[this.getPendingAction,this.setPendingAction]=e(""),[this.getAtmView,this.setAtmView]=e("pin"),[this.getEnteredPin,this.setEnteredPin]=e(""),[this.getCustomAmount,this.setCustomAmount]=e(""),[this.getAccountVersion,this.setAccountVersion]=e(0),[this.getSessionVersion,this.setSessionVersion]=e(0)}finishAction(){this.setPendingAction("")}hydrateFromPayload(n){const e=String(n?.session?.mode||"bank").trim().toLowerCase(),t=Boolean(n?.session?.atmAuthorized),a=this.getMode(),s=this.getAtmView(),i=this.getPendingAction();if(this.setMode("atm"===e?"atm":"bank"),this.setPendingAction(""),this.setEnteredPin(""),this.setCustomAmount(""),this.setAccountVersion(this.getAccountVersion()+1),this.setSessionVersion(this.getSessionVersion()+1),"atm"===e)return t?"deposit"===i||"withdraw"===i||"pin"===s||"atm"!==a?void this.setAtmView("menu"):void this.setAtmView(s):void this.setAtmView("pin");this.setAtmView("dashboard")}syncAccountPatch(){this.setPendingAction(""),this.setAccountVersion(this.getAccountVersion()+1)}resetAtm(){this.setEnteredPin(""),this.setCustomAmount(""),this.setAtmView("pin")}startAction(n){this.setPendingAction(String(n||"").trim().toLowerCase())}}}(),function(){const n=window.BankApp=window.BankApp||{},e=n.store,t=window.ForgeWebUI.createBridge({closeEvent:"bank::close",globalName:"ForgeBridge",readyEvent:"bank::ready"});t.on("bank::hydrate",function(t){n.data.applyHydratePayload(t),e.hydrateFromPayload(t)}),t.on("bank::sync",function(t){n.data.applyAccountPatch(t),e.syncAccountPatch()}),t.on("bank::notice",t=>{e.finishAction(),n.actions&&n.actions.showNotice(t.type||"error",t.message||"Bank notice received.")}),n.bridge={notifyReady:()=>t.ready({loaded:!0}),receive:t.receive,requestClose:()=>t.close({}),requestDeposit:n=>t.send("bank::deposit::request",n),requestDepositEarnings:n=>t.send("bank::depositEarnings::request",n),requestRepayCreditLine:n=>t.send("bank::repayCreditLine::request",n),requestRefresh:()=>t.send("bank::refresh",{}),requestSubmitPin:n=>t.send("bank::pin::request",n),requestTransfer:n=>t.send("bank::transfer::request",n),requestWithdraw:n=>t.send("bank::withdraw::request",n),sendEvent:t.send}}(),function(){const n=window.BankApp=window.BankApp||{},e=n.store;let t=null;function a(n){const e=Math.floor(Number(n||0));return Number.isFinite(e)?e:0}function s(n,a){e.setNotice({type:n,text:a}),t&&clearTimeout(t),t=setTimeout(()=>{e.setNotice({text:"",type:""}),t=null},3200)}function i(t){const i=a(t),o=n.bridge;if(!o||"function"!=typeof o.requestDeposit)return s("error","Deposit bridge is unavailable."),!1;e.startAction("deposit");return!!o.requestDeposit({amount:i})||(e.finishAction(),s("error","Deposit bridge is unavailable."),!1)}function o(t){const i=a(t),o=n.bridge;if(!o||"function"!=typeof o.requestWithdraw)return s("error","Withdraw bridge is unavailable."),!1;e.startAction("withdraw");return!!o.requestWithdraw({amount:i})||(e.finishAction(),s("error","Withdraw bridge is unavailable."),!1)}function r(){e.setEnteredPin("")}n.actions={appendCustomAmountDigit:function(n){const t=String(n||"").trim();if(!t)return;const a=String(e.getCustomAmount()||"");a.length>=7||e.setCustomAmount(a+t)},appendPinDigit:function(n){const t=String(n||"").trim();if(!t)return;const a=String(e.getEnteredPin()||"");a.length>=4||e.setEnteredPin(a+t)},backspaceCustomAmount:function(){const n=String(e.getCustomAmount()||"");e.setCustomAmount(n.slice(0,-1))},backspacePin:function(){const n=String(e.getEnteredPin()||"");e.setEnteredPin(n.slice(0,-1))},clearCustomAmount:function(){e.setCustomAmount("")},clearPin:r,closeBank:function(){const e=n.bridge;if(e&&"function"==typeof e.requestClose){if(e.requestClose())return!0}return s("error","Bank bridge is unavailable."),!1},refreshBank:function(){const e=n.bridge;if(e&&"function"==typeof e.requestRefresh){if(e.requestRefresh())return!0}return s("error","Bank refresh bridge is unavailable."),!1},requestAtmAmount:function(n,e){return"deposit"===String(n||"").trim().toLowerCase()?i(e):o(e)},requestDeposit:i,requestDepositEarnings:function(t){const i=a(t),o=n.bridge;return o&&"function"==typeof o.requestDepositEarnings?(e.startAction("depositearnings"),!!o.requestDepositEarnings({amount:i})||(e.finishAction(),s("error","Earnings bridge is unavailable."),!1)):(s("error","Earnings bridge is unavailable."),!1)},requestRepayCreditLine:function(t){const i=a(t),o=n.bridge;return o&&"function"==typeof o.requestRepayCreditLine?(e.startAction("repaycreditline"),!!o.requestRepayCreditLine({amount:i})||(e.finishAction(),s("error","Credit repayment bridge is unavailable."),!1)):(s("error","Credit repayment bridge is unavailable."),!1)},requestTransfer:function(t,i){const o=a(i),r=String(t||"").trim(),c=n.bridge;return c&&"function"==typeof c.requestTransfer?(e.startAction("transfer"),!!c.requestTransfer({amount:o,from:"bank",target:r})||(e.finishAction(),s("error","Transfer bridge is unavailable."),!1)):(s("error","Transfer bridge is unavailable."),!1)},requestWithdraw:o,selectAtmView:function(n){const t=String(n||"").trim();return!!t&&("pin"===t?(e.resetAtm(),!0):(e.setCustomAmount(""),e.setAtmView(t),!0))},showNotice:s,submitCustomAmount:function(n){const t=a(e.getCustomAmount()),r=String(n||"").trim().toLowerCase();if(t<=0)return s("error","Enter a valid transaction amount."),!1;const c="deposit"===r?i(t):o(t);return c&&e.setCustomAmount(""),c},submitPin:function(){const t=String(e.getEnteredPin()||""),a=n.bridge;return a&&"function"==typeof a.requestSubmitPin?(e.startAction("pin"),a.requestSubmitPin({pin:t})?(r(),!0):(e.finishAction(),s("error","PIN bridge is unavailable."),!1)):(s("error","PIN bridge is unavailable."),!1)}}}(),function(){const n=window.BankApp=window.BankApp||{},{h:e}=n.runtime,t=n.store,{account:a}=n.data;function s(n){return`$${Math.round(Number(n||0)).toLocaleString()}`}n.componentFns=n.componentFns||{},Object.assign(n.componentFns,{clearInputValue:function(n){const e=document.getElementById(n);e&&(e.value="")},formatCurrency:s,keypad:function(n,t,a,s){return e("div",{className:"bank-keypad"},["1","2","3","4","5","6","7","8","9"].map(t=>e("button",{type:"button",className:"bank-key",onClick:()=>n(t)},t)),e("button",{type:"button",className:"bank-key is-muted",onClick:a},"C"),e("button",{type:"button",className:"bank-key",onClick:()=>n("0")},"0"),e("button",{type:"button",className:"bank-key is-accent",onClick:s},"Enter"),e("button",{type:"button",className:"bank-key is-wide",onClick:t},"Backspace"))},metricCard:function(n,t,a,s=""){return e("div",{className:s?`bank-metric-card is-${s}`:"bank-metric-card"},e("span",{className:"bank-eyebrow"},n),e("span",{className:"bank-metric-value"},t),e("span",{className:"bank-metric-copy"},a))},pending:function(n){return t.getPendingAction()===n},pinIndicators:function(n){const t=String(n||"");return e("div",{className:"bank-pin-indicators"},[0,1,2,3].map(n=>e("span",{className:ne("div",{className:"bank-history-row"},e("div",{className:"bank-history-copy"},e("span",{className:"bank-history-title"},n.type||"Transaction"),e("span",{className:"bank-history-meta"},n.date||"Pending timestamp")),e("span",{className:"bank-history-value"},s(n.amount||0)))))}})}(),function(){const n=window.BankApp=window.BankApp||{},{h:e}=n.runtime,t=n.store,a=n.actions,{account:s,session:i}=n.data,{formatCurrency:o,statCard:r}=n.componentFns;n.componentFns=n.componentFns||{},n.componentFns.BankSidebar=function(){return t.getAccountVersion(),t.getSessionVersion(),e("aside",{className:"bank-sidebar"},e("section",{className:"bank-module"},e("div",{className:"bank-module-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Account"),e("h2",{className:"bank-section-title"},"Balances")),e("span",{className:"bank-pill"},"Live")),e("div",{className:"bank-summary-grid"},r("Bank",o(s.bank),"accent"),r("Cash",o(s.cash)),r("Earnings",o(s.earnings),s.earnings>0?"warning":""),r("Org Funds",o(i.orgFunds),i.orgFunds>0?"success":""))),e("section",{className:"bank-module"},e("div",{className:"bank-module-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Profile"),e("h2",{className:"bank-section-title"},"Account Holder")),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.refreshBank()},"Refresh")),e("div",{className:"bank-profile-stack"},r("Name",i.playerName||"Unknown"),r("UID",i.uid||"-"),r("Organization",i.orgName||"No active organization"))))}}(),function(){const n=window.BankApp=window.BankApp||{},{h:e}=n.runtime,t=n.store,{account:a,session:s}=n.data,{formatCurrency:i}=n.componentFns;n.componentFns=n.componentFns||{},n.componentFns.BankFooter=function(){t.getAccountVersion(),t.getSessionVersion();const n=[{title:"Banking Resources",items:["Account Access Policy","Transfer & Wire Guidelines","Cash Handling Schedule","Terminal Security Notice"]},{title:"Bank Support",items:s.orgName?[`Organization: ${s.orgName}`,`Treasury Reference: ${i(s.orgFunds)}`,`${s.transferTargets.length} transfer recipient(s) currently visible.`,`Primary Ledger: ${i(a.bank)}`]:["Organization: No active treasury link",`${s.transferTargets.length} transfer recipient(s) currently visible.`,`Primary Ledger: ${i(a.bank)}`,`Cash On Hand: ${i(a.cash)}`]}];return e("footer",{className:"bank-footer-bar"},e("div",{className:"bank-footer"},...n.map(n=>e("div",{className:"bank-footer-block"},e("h3",{className:"bank-footer-title"},n.title),e("ul",{className:"bank-footer-list"},...(n.items||[]).map(n=>e("li",{className:"bank-footer-copy"},n)))))))}}(),function(){const n=window.BankApp=window.BankApp||{},{h:e}=n.runtime,t=n.store,a=n.actions,{account:s,session:i}=n.data,{clearInputValue:o,formatCurrency:r,metricCard:c,pending:u,readInputValue:l,transactionRows:d}=n.componentFns;function m(){t.getAccountVersion()}function b(){t.getSessionVersion()}n.componentFns=n.componentFns||{},n.componentFns.BankPageHeader=function(){return b(),e("div",{className:"bank-page-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Treasury Desk"),e("h1",{className:"bank-title"},"Personal Banking")),e("span",{className:"bank-pill"},i.playerName||"Account Holder"))},n.componentFns.BankSummarySection=function(){return m(),b(),e("section",{className:"bank-page-section bank-summary-section"},e("div",{className:"bank-section-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Overview"),e("h2",{className:"bank-section-title"},"Financial Position")),e("span",{className:"bank-pill"},"Banking Desk")),e("div",{className:"bank-summary-band"},c("Primary Balance",r(s.bank),"Available for transfers and withdrawals.","accent"),c("Cash On Hand",r(s.cash),"Funds currently carried by the player."),c("Pending Earnings",r(s.earnings),"Ready to sweep into the main account ledger.",s.earnings>0?"warning":""),c("Org Snapshot",r(i.orgFunds),"Reference value pulled from the organization treasury.",i.orgFunds>0?"success":""),c("Credit Due",r(i.creditLine?.amountDue||0),Number(i.creditLine?.amountDue||0)>0?`Outstanding principal ${r(i.creditLine?.outstandingPrincipal||0)} at ${Math.round(100*Number(i.creditLine?.interestRate||0))}% interest.`:"No active credit repayment is currently due.",Number(i.creditLine?.amountDue||0)>0?"warning":"")))},n.componentFns.BankActionSections=function(){return b(),e("div",{className:"bank-action-sections"},e("section",{className:"bank-page-section"},e("div",{className:"bank-section-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Movement"),e("h2",{className:"bank-section-title"},"Deposit / Withdraw"))),e("div",{className:"bank-form-stack"},e("input",{id:"bank-amount-input",className:"bank-input",type:"number",min:"1",placeholder:"Enter amount"}),e("div",{className:"bank-action-row"},e("button",{type:"button",className:"bank-btn bank-btn-primary",disabled:u("deposit"),onClick:()=>{a.requestDeposit(l("bank-amount-input"))&&o("bank-amount-input")}},u("deposit")?"Depositing...":"Deposit"),e("button",{type:"button",className:"bank-btn bank-btn-secondary",disabled:u("withdraw"),onClick:()=>{a.requestWithdraw(l("bank-amount-input"))&&o("bank-amount-input")}},u("withdraw")?"Withdrawing...":"Withdraw")))),e("section",{className:"bank-page-section"},e("div",{className:"bank-section-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Transfer"),e("h2",{className:"bank-section-title"},"Wire Funds"))),e("div",{className:"bank-form-stack"},e("select",{id:"bank-transfer-target",className:"bank-select"},e("option",{value:""},i.transferTargets.length>0?"Select recipient":"No available recipients"),i.transferTargets.map(n=>e("option",{value:n.uid},n.name||n.uid))),e("input",{id:"bank-transfer-amount",className:"bank-input",type:"number",min:"1",placeholder:"Enter transfer amount"}),e("button",{type:"button",className:"bank-btn bank-btn-primary",disabled:u("transfer")||0===i.transferTargets.length,onClick:()=>{a.requestTransfer(l("bank-transfer-target"),l("bank-transfer-amount"))&&o("bank-transfer-amount")}},u("transfer")?"Transferring...":"Transfer Funds"))),e("section",{className:"bank-page-section"},e("div",{className:"bank-section-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Credit"),e("h2",{className:"bank-section-title"},"Repay Org Credit"))),e("div",{className:"bank-form-stack"},e("p",{className:"bank-card-copy"},Number(i.creditLine?.amountDue||0)>0?`Outstanding due ${r(i.creditLine.amountDue||0)}. Available reserved credit ${r(i.creditLine.availableAmount||0)}.`:"No repayment is currently due on the assigned organization credit line."),e("input",{id:"bank-credit-line-amount",className:"bank-input",type:"number",min:"1",placeholder:"Enter repayment amount"}),e("button",{type:"button",className:"bank-btn bank-btn-primary",disabled:u("repaycreditline")||Number(i.creditLine?.amountDue||0)<=0,onClick:()=>{a.requestRepayCreditLine(l("bank-credit-line-amount"))&&o("bank-credit-line-amount")}},u("repaycreditline")?"Posting Repayment...":"Repay Credit Line"))))},n.componentFns.BankSupportSection=function(){return m(),e("div",{className:"bank-support-sections"},e("section",{className:"bank-page-section"},e("div",{className:"bank-section-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Sweep"),e("h2",{className:"bank-section-title"},"Deposit Earnings"))),e("p",{className:"bank-card-copy"},"Sweep pending earnings into the primary account when you want them reflected in the main balance."),e("button",{type:"button",className:"bank-btn bank-btn-primary",disabled:u("depositearnings")||Number(s.earnings||0)<=0,onClick:()=>a.requestDepositEarnings(s.earnings)},u("depositearnings")?"Depositing...":"Deposit Earnings")))},n.componentFns.BankHistorySection=function(){return m(),e("section",{className:"bank-page-section bank-history-section"},e("div",{className:"bank-section-header"},e("div",null,e("span",{className:"bank-eyebrow"},"History"),e("h2",{className:"bank-section-title"},"Recent Transactions"))),d())}}(),function(){const n=window.BankApp=window.BankApp||{},{h:e}=n.runtime,t=n.store,a=n.actions,{account:s}=n.data,{formatCurrency:i,keypad:o,pinIndicators:r}=n.componentFns;function c(n){const t="deposit"===n?"Deposit":"Withdraw";return e("div",{className:"bank-atm-action-grid"},[20,50,100,500].map(s=>e("button",{type:"button",className:"bank-btn bank-btn-primary",onClick:()=>a.requestAtmAmount(n,s)},`${t} ${i(s)}`)),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.selectAtmView("deposit"===n?"customDeposit":"customWithdraw")},"Custom Amount"),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.selectAtmView("menu")},"Back"))}function u(n){const s="deposit"===n?"Deposit":"Withdraw";return e("div",{className:"bank-atm-stack"},e("div",{className:"bank-pin-display"},t.getCustomAmount()?i(t.getCustomAmount()):"$0"),o(a.appendCustomAmountDigit,a.backspaceCustomAmount,a.clearCustomAmount,()=>a.submitCustomAmount(n)),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.selectAtmView("menu")},`Cancel ${s}`))}n.componentFns=n.componentFns||{},n.componentFns.ATMView=function(){t.getAccountVersion();const n=t.getAtmView(),l=String(t.getEnteredPin()||"");let d="Terminal Access",m="Authenticate with the four-digit account PIN before using the terminal.",b=null;switch(n){case"menu":d="ATM Menu",m="Select a banking action. The ATM can deposit, withdraw, and show the live account balance.",b=e("div",{className:"bank-atm-action-grid"},e("button",{type:"button",className:"bank-btn bank-btn-primary",onClick:()=>a.selectAtmView("withdraw")},"Withdraw Cash"),e("button",{type:"button",className:"bank-btn bank-btn-primary",onClick:()=>a.selectAtmView("deposit")},"Deposit Cash"),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.selectAtmView("balance")},"Check Balance"),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.closeBank()},"Exit Terminal"));break;case"withdraw":d="Withdraw Cash",m="Choose a preset amount or enter a custom amount for withdrawal.",b=c("withdraw");break;case"deposit":d="Deposit Cash",m="Move cash on hand back into the main bank balance from the terminal.",b=c("deposit");break;case"customWithdraw":d="Custom Withdraw",m="Enter the exact withdrawal amount.",b=u("withdraw");break;case"customDeposit":d="Custom Deposit",m="Enter the exact deposit amount.",b=u("deposit");break;case"balance":d="Available Balance",m="Current bank balance available at this terminal.",b=e("div",{className:"bank-atm-stack"},e("div",{className:"bank-balance-display"},i(s.bank)),e("button",{type:"button",className:"bank-btn bank-btn-primary",onClick:()=>a.selectAtmView("menu")},"Return to Menu"));break;default:b=e("div",{className:"bank-atm-stack"},e("div",{className:"bank-pin-display"},r(l)),o(a.appendPinDigit,a.backspacePin,a.clearPin,a.submitPin),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.closeBank()},"Exit Terminal"))}return e("div",{className:"bank-atm-shell"},e("section",{className:"bank-atm-panel"},e("div",{className:"bank-panel-header"},e("div",null,e("span",{className:"bank-eyebrow"},"ATM"),e("h1",{className:"bank-title"},d)),e("span",{className:"bank-pill"},"Secure Terminal")),e("p",{className:"bank-panel-copy"},m),b))}}(),function(){const n=window.BankApp=window.BankApp||{},{h:e}=n.runtime,t=window.SharedUI.componentFns.WindowTitleBar,a=n.store,s=n.actions;n.componentFns=n.componentFns||{},n.componentFns.NoticeLayer=function(){const n=a.getNotice();return n.text?e("div",{className:"bank-notice-stack"},e("div",{className:"error"===n.type?"bank-notice is-error":"bank-notice is-success"},n.text)):null},n.components=n.components||{},n.components.App=function(){const n=a.getMode();return e("div",{className:"atm"===n?"bank-shell is-atm":"bank-shell"},"atm"===n?null:t({kicker:"FORGE Finance",title:"Global Banking Network",onClose:()=>s.closeBank(),closeLabel:"Close banking interface"}),e("div",{id:"bank-notice-root"}),"atm"===n?e("div",{id:"bank-atm-root"}):[e("div",{className:"bank-scroll-shell","data-preserve-scroll-id":"bank-page-scroll"},[e("div",{className:"bank-layout"},e("div",{id:"bank-sidebar-root"}),e("main",{className:"bank-main"},e("div",{className:"bank-page"},e("div",{id:"bank-page-header-root"}),e("p",{className:"bank-page-copy"},"Manage deposits, withdrawals, transfers, and earnings sweeps from the same shared financial console."),e("div",{className:"bank-page-divider"}),e("div",{className:"bank-page-body"},e("div",{id:"bank-summary-section-root"}),e("div",{id:"bank-action-sections-root"}),e("div",{id:"bank-support-section-root"}),e("div",{id:"bank-history-section-root"}))))),e("div",{id:"bank-footer-root"})])])}}(),function(){const n=window.ForgeWebUI,e=window.BankApp,t=[{id:"bank-notice-root",preserveScroll:!1,render:()=>e.componentFns.NoticeLayer()},{id:"bank-sidebar-root",preserveScroll:!1,render:()=>e.componentFns.BankSidebar()},{id:"bank-page-header-root",preserveScroll:!1,render:()=>e.componentFns.BankPageHeader()},{id:"bank-summary-section-root",preserveScroll:!1,render:()=>e.componentFns.BankSummarySection()},{id:"bank-action-sections-root",preserveScroll:!1,render:()=>e.componentFns.BankActionSections()},{id:"bank-support-section-root",preserveScroll:!1,render:()=>e.componentFns.BankSupportSection()},{id:"bank-history-section-root",preserveScroll:!1,render:()=>e.componentFns.BankHistorySection()},{id:"bank-atm-root",preserveScroll:!1,render:()=>e.componentFns.ATMView()},{id:"bank-footer-root",preserveScroll:!1,render:()=>e.componentFns.BankFooter()}];n.createApp({name:"bank",root:"#app",setup({root:a}){const s=function(){const e=new Map;return{sync:function(){t.forEach(t=>{const a=document.getElementById(t.id),s=e.get(t.id);if(!a)return void(s&&(s.handle.dispose(),e.delete(t.id)));if(s&&s.container===a)return;s&&s.handle.dispose();const i=n.mount(a,t.render,{preserveScroll:t.preserveScroll});e.set(t.id,{container:a,handle:i})})}}}();n.mount(a,()=>e.components.App(),{preserveScroll:!1}),e.bridge&&e.bridge.notifyReady(),n.effect(()=>{e.store.getMode(),requestAnimationFrame(()=>{s.sync()})})}}).start()}(); \ No newline at end of file diff --git a/arma/client/addons/bank/ui/src/bridge.js b/arma/client/addons/bank/ui/src/bridge.js index 1ceed4e..fab579f 100644 --- a/arma/client/addons/bank/ui/src/bridge.js +++ b/arma/client/addons/bank/ui/src/bridge.js @@ -12,9 +12,15 @@ store.hydrateFromPayload(payloadData); } + function syncAccount(payloadData) { + BankApp.data.applyAccountPatch(payloadData); + store.syncAccountPatch(); + } + bridge.on("bank::hydrate", hydrate); - bridge.on("bank::sync", hydrate); + bridge.on("bank::sync", syncAccount); bridge.on("bank::notice", (payloadData) => { + store.finishAction(); if (BankApp.actions) { BankApp.actions.showNotice( payloadData.type || "error", @@ -37,9 +43,15 @@ requestDepositEarnings(payload) { return bridge.send("bank::depositEarnings::request", payload); }, + requestRepayCreditLine(payload) { + return bridge.send("bank::repayCreditLine::request", payload); + }, requestRefresh() { return bridge.send("bank::refresh", {}); }, + requestSubmitPin(payload) { + return bridge.send("bank::pin::request", payload); + }, requestTransfer(payload) { return bridge.send("bank::transfer::request", payload); }, diff --git a/arma/client/addons/bank/ui/src/data.js b/arma/client/addons/bank/ui/src/data.js index 856ca90..5398487 100644 --- a/arma/client/addons/bank/ui/src/data.js +++ b/arma/client/addons/bank/ui/src/data.js @@ -2,6 +2,14 @@ const BankApp = (window.BankApp = window.BankApp || {}); const defaultSession = { + atmAuthorized: false, + creditLine: { + amountDue: 0, + approvedAmount: 0, + availableAmount: 0, + interestRate: 0.1, + outstandingPrincipal: 0, + }, mode: "bank", orgFunds: 0, orgName: "", @@ -14,7 +22,6 @@ bank: 0, cash: 0, earnings: 0, - pin: "1234", transactions: [], }; @@ -30,6 +37,13 @@ BankApp.data = { account: Object.assign({}, defaultAccount), session: Object.assign({}, defaultSession), + applyAccountPatch(patch) { + const nextAccount = Object.assign({}, this.account, patch || {}); + replaceObject( + this.account, + Object.assign({}, defaultAccount, nextAccount), + ); + }, applyHydratePayload(payload) { replaceObject( this.session, diff --git a/arma/client/addons/bank/ui/src/pages/BankView.js b/arma/client/addons/bank/ui/src/pages/BankView.js index e3f8f0a..bdbdc87 100644 --- a/arma/client/addons/bank/ui/src/pages/BankView.js +++ b/arma/client/addons/bank/ui/src/pages/BankView.js @@ -89,6 +89,16 @@ "Reference value pulled from the organization treasury.", session.orgFunds > 0 ? "success" : "", ), + metricCard( + "Credit Due", + formatCurrency(session.creditLine?.amountDue || 0), + Number(session.creditLine?.amountDue || 0) > 0 + ? `Outstanding principal ${formatCurrency(session.creditLine?.outstandingPrincipal || 0)} at ${Math.round(Number(session.creditLine?.interestRate || 0) * 100)}% interest.` + : "No active credit repayment is currently due.", + Number(session.creditLine?.amountDue || 0) > 0 + ? "warning" + : "", + ), ), ); } @@ -238,6 +248,63 @@ ), ), ), + h( + "section", + { className: "bank-page-section" }, + h( + "div", + { className: "bank-section-header" }, + h( + "div", + null, + h("span", { className: "bank-eyebrow" }, "Credit"), + h( + "h2", + { className: "bank-section-title" }, + "Repay Org Credit", + ), + ), + ), + h( + "div", + { className: "bank-form-stack" }, + h( + "p", + { className: "bank-card-copy" }, + Number(session.creditLine?.amountDue || 0) > 0 + ? `Outstanding due ${formatCurrency(session.creditLine.amountDue || 0)}. Available reserved credit ${formatCurrency(session.creditLine.availableAmount || 0)}.` + : "No repayment is currently due on the assigned organization credit line.", + ), + h("input", { + id: "bank-credit-line-amount", + className: "bank-input", + type: "number", + min: "1", + placeholder: "Enter repayment amount", + }), + h( + "button", + { + type: "button", + className: "bank-btn bank-btn-primary", + disabled: + pending("repaycreditline") || + Number(session.creditLine?.amountDue || 0) <= 0, + onClick: () => { + const sent = actions.requestRepayCreditLine( + readInputValue("bank-credit-line-amount"), + ); + if (sent) { + clearInputValue("bank-credit-line-amount"); + } + }, + }, + pending("repaycreditline") + ? "Posting Repayment..." + : "Repay Credit Line", + ), + ), + ), ); } diff --git a/arma/client/addons/bank/ui/src/registry/events.js b/arma/client/addons/bank/ui/src/registry/events.js index 01facaa..7ccde92 100644 --- a/arma/client/addons/bank/ui/src/registry/events.js +++ b/arma/client/addons/bank/ui/src/registry/events.js @@ -4,14 +4,6 @@ let noticeTimer = null; - function getAccount() { - return BankApp.data?.account || {}; - } - - function getSession() { - return BankApp.data?.session || {}; - } - function normalizeAmount(value) { const amount = Math.floor(Number(value || 0)); return Number.isFinite(amount) ? amount : 0; @@ -58,18 +50,6 @@ function requestDeposit(amountValue) { const amount = normalizeAmount(amountValue); - const account = getAccount(); - - if (amount <= 0) { - showNotice("error", "Enter a valid deposit amount."); - return false; - } - - if (amount > Number(account.cash || 0)) { - showNotice("error", "Cash on hand cannot cover that deposit."); - return false; - } - const bridge = BankApp.bridge; if (!bridge || typeof bridge.requestDeposit !== "function") { showNotice("error", "Deposit bridge is unavailable."); @@ -89,18 +69,6 @@ function requestWithdraw(amountValue) { const amount = normalizeAmount(amountValue); - const account = getAccount(); - - if (amount <= 0) { - showNotice("error", "Enter a valid withdrawal amount."); - return false; - } - - if (amount > Number(account.bank || 0)) { - showNotice("error", "Bank balance cannot cover that withdrawal."); - return false; - } - const bridge = BankApp.bridge; if (!bridge || typeof bridge.requestWithdraw !== "function") { showNotice("error", "Withdraw bridge is unavailable."); @@ -120,30 +88,8 @@ function requestTransfer(targetUid, amountValue) { const amount = normalizeAmount(amountValue); - const session = getSession(); - const account = getAccount(); const targetId = String(targetUid || "").trim(); - if (!targetId) { - showNotice("error", "Select a transfer recipient."); - return false; - } - - if (targetId === String(session.uid || "")) { - showNotice("error", "You cannot transfer funds to yourself."); - return false; - } - - if (amount <= 0) { - showNotice("error", "Enter a valid transfer amount."); - return false; - } - - if (amount > Number(account.bank || 0)) { - showNotice("error", "Bank balance cannot cover that transfer."); - return false; - } - const bridge = BankApp.bridge; if (!bridge || typeof bridge.requestTransfer !== "function") { showNotice("error", "Transfer bridge is unavailable."); @@ -167,21 +113,6 @@ function requestDepositEarnings(amountValue) { const amount = normalizeAmount(amountValue); - const account = getAccount(); - - if (amount <= 0) { - showNotice("error", "No earnings are available to deposit."); - return false; - } - - if (amount > Number(account.earnings || 0)) { - showNotice( - "error", - "Pending earnings cannot cover that deposit request.", - ); - return false; - } - const bridge = BankApp.bridge; if (!bridge || typeof bridge.requestDepositEarnings !== "function") { showNotice("error", "Earnings bridge is unavailable."); @@ -199,6 +130,25 @@ return true; } + function requestRepayCreditLine(amountValue) { + const amount = normalizeAmount(amountValue); + const bridge = BankApp.bridge; + if (!bridge || typeof bridge.requestRepayCreditLine !== "function") { + showNotice("error", "Credit repayment bridge is unavailable."); + return false; + } + + store.startAction("repaycreditline"); + const sent = bridge.requestRepayCreditLine({ amount }); + if (!sent) { + store.finishAction(); + showNotice("error", "Credit repayment bridge is unavailable."); + return false; + } + + return true; + } + function appendPinDigit(digit) { const nextDigit = String(digit || "").trim(); if (!nextDigit) { @@ -224,21 +174,21 @@ function submitPin() { const enteredPin = String(store.getEnteredPin() || ""); - const actualPin = String(getAccount().pin || "1234"); - - if (enteredPin.length !== 4) { - showNotice("error", "Enter your four-digit access PIN."); + const bridge = BankApp.bridge; + if (!bridge || typeof bridge.requestSubmitPin !== "function") { + showNotice("error", "PIN bridge is unavailable."); return false; } - if (enteredPin !== actualPin) { - clearPin(); - showNotice("error", "Incorrect PIN."); + store.startAction("pin"); + const sent = bridge.requestSubmitPin({ pin: enteredPin }); + if (!sent) { + store.finishAction(); + showNotice("error", "PIN bridge is unavailable."); return false; } clearPin(); - store.setAtmView("menu"); return true; } @@ -299,7 +249,6 @@ if (success) { store.setCustomAmount(""); - store.setAtmView("menu"); } return success; @@ -314,10 +263,6 @@ ? requestDeposit(amount) : requestWithdraw(amount); - if (success) { - store.setAtmView("menu"); - } - return success; } @@ -333,6 +278,7 @@ requestAtmAmount, requestDeposit, requestDepositEarnings, + requestRepayCreditLine, requestTransfer, requestWithdraw, selectAtmView, diff --git a/arma/client/addons/bank/ui/src/registry/store.js b/arma/client/addons/bank/ui/src/registry/store.js index 56b7233..d3ff565 100644 --- a/arma/client/addons/bank/ui/src/registry/store.js +++ b/arma/client/addons/bank/ui/src/registry/store.js @@ -25,25 +25,46 @@ const mode = String(payload?.session?.mode || "bank") .trim() .toLowerCase(); + const atmAuthorized = Boolean(payload?.session?.atmAuthorized); const currentMode = this.getMode(); const currentAtmView = this.getAtmView(); + const currentPendingAction = this.getPendingAction(); this.setMode(mode === "atm" ? "atm" : "bank"); this.setPendingAction(""); - this.setNotice({ text: "", type: "" }); this.setEnteredPin(""); this.setCustomAmount(""); this.setAccountVersion(this.getAccountVersion() + 1); this.setSessionVersion(this.getSessionVersion() + 1); if (mode === "atm") { - this.setAtmView(currentMode === "atm" ? currentAtmView : "pin"); + if (!atmAuthorized) { + this.setAtmView("pin"); + return; + } + + if ( + currentPendingAction === "deposit" || + currentPendingAction === "withdraw" || + currentAtmView === "pin" || + currentMode !== "atm" + ) { + this.setAtmView("menu"); + return; + } + + this.setAtmView(currentAtmView); return; } this.setAtmView("dashboard"); } + syncAccountPatch() { + this.setPendingAction(""); + this.setAccountVersion(this.getAccountVersion() + 1); + } + resetAtm() { this.setEnteredPin(""); this.setCustomAmount(""); 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..289a18f --- /dev/null +++ b/arma/client/addons/cad/CfgEventHandlers.hpp @@ -0,0 +1,11 @@ +class Extended_PreInit_EventHandlers { + class ADDON { + init = QUOTE(call COMPILE_SCRIPT(XEH_preInit)); + }; +}; + +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..a64fb15 --- /dev/null +++ b/arma/client/addons/cad/XEH_postInitClient.sqf @@ -0,0 +1,40 @@ +#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(responseHydrateCad), { + params [["_payload", createHashMap, [createHashMap]]]; + + GVAR(CADUIBridge) call ["handleHydrateResponse", [_payload]]; +}] call CFUNC(addEventHandler); + +[QGVAR(responseCadAssignment), { + params [["_result", createHashMap, [createHashMap]]]; + + GVAR(CADUIBridge) call ["handleAssignmentResponse", [_result]]; +}] call CFUNC(addEventHandler); + +[QGVAR(responseCadGroupUpdate), { + params [["_result", createHashMap, [createHashMap]]]; + + GVAR(CADUIBridge) call ["handleGroupUpdateResponse", [_result]]; +}] call CFUNC(addEventHandler); + +[QGVAR(responseCadRequest), { + params [["_result", createHashMap, [createHashMap]]]; + + GVAR(CADUIBridge) call ["handleRequestResponse", [_result]]; +}] call CFUNC(addEventHandler); + +[QGVAR(invalidateCadState), { + if (isNil QGVAR(CADRepository)) exitWith {}; + if !(GVAR(CADRepository) getOrDefault ["isOpen", false]) exitWith {}; + if (isNil QGVAR(CADUIBridge)) exitWith {}; + + GVAR(CADUIBridge) call ["requestHydrate", []]; +}] 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..6357e9d --- /dev/null +++ b/arma/client/addons/cad/functions/fnc_handleUIEvents.sqf @@ -0,0 +1,229 @@ +#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::topbar::ready": { + GVAR(CADUIBridge) call ["handleTopBarReady", []]; + }; + case "cad::ready": { + GVAR(CADUIBridge) call ["handleReady", [_control, _data]]; + }; + case "cad::dispatcher::ready": { + GVAR(CADUIBridge) call ["handleDispatcherReady", []]; + }; + case "cad::mode::set": { + private _mode = ""; + if (_data isEqualType createHashMap) then { + _mode = _data getOrDefault ["mode", ""]; + }; + + GVAR(CADUIBridge) call ["setMode", [_mode]]; + }; + case "cad::dispatchView::set": { + private _dispatchView = ""; + if (_data isEqualType createHashMap) then { + _dispatchView = _data getOrDefault ["dispatchView", ""]; + }; + + GVAR(CADUIBridge) call ["setDispatchView", [_dispatchView]]; + }; + case "cad::refresh": { + GVAR(CADUIBridge) call ["requestHydrate", []]; + }; + case "cad::tasks::assign": { + private _taskID = ""; + private _groupID = ""; + private _note = ""; + if (_data isEqualType createHashMap) then { + _taskID = _data getOrDefault ["taskID", ""]; + _groupID = _data getOrDefault ["groupID", ""]; + _note = _data getOrDefault ["note", ""]; + }; + + GVAR(CADUIBridge) call ["requestAssignTask", [_taskID, _groupID, _note]]; + }; + case "cad::dispatchOrder::create": { + private _assigneeGroupID = ""; + private _targetGroupID = ""; + private _note = ""; + private _priority = "priority"; + private _request = createHashMap; + if (_data isEqualType createHashMap) then { + _assigneeGroupID = _data getOrDefault ["assigneeGroupID", ""]; + _targetGroupID = _data getOrDefault ["targetGroupID", ""]; + _note = _data getOrDefault ["note", ""]; + _priority = _data getOrDefault ["priority", "priority"]; + _request = _data getOrDefault ["request", createHashMap]; + }; + + GVAR(CADUIBridge) call ["requestCreateDispatchOrder", [_assigneeGroupID, _targetGroupID, _note, _priority, _request]]; + }; + case "cad::supportRequest::submit": { + private _type = ""; + private _fields = createHashMap; + private _priority = "priority"; + if (_data isEqualType createHashMap) then { + _type = _data getOrDefault ["type", ""]; + _fields = _data getOrDefault ["fields", createHashMap]; + _priority = _data getOrDefault ["priority", "priority"]; + }; + + GVAR(CADUIBridge) call ["requestSubmitSupportRequest", [_type, _fields, _priority]]; + }; + case "cad::dispatchOrder::close": { + private _taskID = ""; + if (_data isEqualType createHashMap) then { + _taskID = _data getOrDefault ["taskID", ""]; + }; + + GVAR(CADUIBridge) call ["requestCloseDispatchOrder", [_taskID]]; + }; + case "cad::supportRequest::close": { + private _requestID = ""; + if (_data isEqualType createHashMap) then { + _requestID = _data getOrDefault ["requestID", ""]; + }; + + GVAR(CADUIBridge) call ["requestCloseSupportRequest", [_requestID]]; + }; + case "cad::tasks::acknowledge": { + private _taskID = ""; + if (_data isEqualType createHashMap) then { + _taskID = _data getOrDefault ["taskID", ""]; + }; + + GVAR(CADUIBridge) call ["requestAcknowledgeTask", [_taskID]]; + }; + case "cad::tasks::decline": { + private _taskID = ""; + if (_data isEqualType createHashMap) then { + _taskID = _data getOrDefault ["taskID", ""]; + }; + + GVAR(CADUIBridge) call ["requestDeclineTask", [_taskID]]; + }; + case "cad::groups::status": { + private _groupID = ""; + private _status = ""; + if (_data isEqualType createHashMap) then { + _groupID = _data getOrDefault ["groupID", ""]; + _status = _data getOrDefault ["status", ""]; + }; + + GVAR(CADUIBridge) call ["requestGroupStatus", [_groupID, _status]]; + }; + case "cad::groups::role": { + private _groupID = ""; + private _role = ""; + if (_data isEqualType createHashMap) then { + _groupID = _data getOrDefault ["groupID", ""]; + _role = _data getOrDefault ["role", ""]; + }; + + GVAR(CADUIBridge) call ["requestGroupRole", [_groupID, _role]]; + }; + case "cad::groups::profile": { + private _groupID = ""; + private _status = ""; + private _role = ""; + if (_data isEqualType createHashMap) then { + _groupID = _data getOrDefault ["groupID", ""]; + _status = _data getOrDefault ["status", ""]; + _role = _data getOrDefault ["role", ""]; + }; + + GVAR(CADUIBridge) call ["requestGroupProfile", [_groupID, _status, _role]]; + }; + case "cad::groups::focus": { + private _groupID = ""; + if (_data isEqualType createHashMap) then { + _groupID = _data getOrDefault ["groupID", ""]; + }; + + GVAR(CADUIBridge) call ["focusGroup", [_groupID]]; + }; + case "cad::tasks::focus": { + private _taskID = ""; + if (_data isEqualType createHashMap) then { + _taskID = _data getOrDefault ["taskID", ""]; + }; + + GVAR(CADUIBridge) call ["focusTask", [_taskID]]; + }; + case "cad::requests::focus": { + private _requestID = ""; + if (_data isEqualType createHashMap) then { + _requestID = _data getOrDefault ["requestID", ""]; + }; + + GVAR(CADUIBridge) call ["focusRequest", [_requestID]]; + }; + 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..07e535d --- /dev/null +++ b/arma/client/addons/cad/functions/fnc_initRepository.sqf @@ -0,0 +1,105 @@ +#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 ["groups", []]; + _self set ["contracts", []]; + _self set ["requests", []]; + _self set ["assignments", []]; + _self set ["activity", []]; + _self set ["session", createHashMap]; + _self set ["mode", "operations"]; + _self set ["dispatchView", "board"]; + }], + ["getHydratePayload", compileFinal { + createHashMapFromArray [ + ["groups", +(_self getOrDefault ["groups", []])], + ["contracts", +(_self getOrDefault ["contracts", []])], + ["requests", +(_self getOrDefault ["requests", []])], + ["assignments", +(_self getOrDefault ["assignments", []])], + ["activity", +(_self getOrDefault ["activity", []])], + ["session", +(_self getOrDefault ["session", createHashMap])], + ["mode", _self getOrDefault ["mode", "operations"]], + ["dispatchView", _self getOrDefault ["dispatchView", "board"]] + ] + }], + ["getCurrentGroup", compileFinal { + private _session = _self getOrDefault ["session", createHashMap]; + private _groupID = _session getOrDefault ["groupId", ""]; + if (_groupID isEqualTo "") exitWith { createHashMap }; + + private _groups = _self getOrDefault ["groups", []]; + private _group = _groups findIf { (_x getOrDefault ["groupId", ""]) isEqualTo _groupID }; + if (_group < 0) exitWith { createHashMap }; + + +(_groups # _group) + }], + ["pushHydratePayload", compileFinal { + params [["_bridge", createHashMap, [createHashMap]]]; + + if (_bridge isEqualTo createHashMap) exitWith { false }; + + _bridge call ["sendEvent", ["cad::hydrate", _self call ["getHydratePayload", []]]] + }], + ["setHydratePayload", compileFinal { + params [["_payload", createHashMap, [createHashMap]]]; + + _self set ["groups", +(_payload getOrDefault ["groups", []])]; + _self set ["contracts", +(_payload getOrDefault ["contracts", []])]; + _self set ["requests", +(_payload getOrDefault ["requests", []])]; + _self set ["assignments", +(_payload getOrDefault ["assignments", []])]; + _self set ["activity", +(_payload getOrDefault ["activity", []])]; + _self set ["session", +(_payload getOrDefault ["session", createHashMap])]; + true + }], + ["setMode", compileFinal { + params [["_mode", "operations", [""]]]; + + if !(_mode in ["operations", "dispatch"]) then { + _mode = "operations"; + }; + + _self set ["mode", _mode]; + _mode + }], + ["setDispatchView", compileFinal { + params [["_dispatchView", "board", [""]]]; + + if !(_dispatchView in ["board", "map"]) then { + _dispatchView = "board"; + }; + + _self set ["dispatchView", _dispatchView]; + _dispatchView + }], + ["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..981d317 --- /dev/null +++ b/arma/client/addons/cad/functions/fnc_initUI.sqf @@ -0,0 +1,51 @@ +#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; +private _dispatcherCtrl = _display displayCtrl 1006; + +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]; +uiNamespace setVariable [QGVAR(DispatcherCtrl), _dispatcherCtrl]; + +_dispatcherCtrl ctrlShow false; + +private _center = if (isNull player) then { + [worldSize / 2, worldSize / 2, 0] +} else { + getPosATL player +}; + +_mapCtrl ctrlMapAnimAdd [0, 0.2, _center]; +ctrlMapAnimCommit _mapCtrl; + +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..8e27f70 --- /dev/null +++ b/arma/client/addons/cad/functions/fnc_initUIBridge.sqf @@ -0,0 +1,450 @@ +#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 CAD 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"], + ["#create", compileFinal { + _self set ["dispatcherReady", false]; + _self set ["topBarReady", false]; + }], + ["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 + }], + ["getTopBarControl", compileFinal { + private _display = uiNamespace getVariable [QGVAR(Display), displayNull]; + if (isNull _display) exitWith { controlNull }; + + _display displayCtrl 1002 + }], + ["getBottomBarControl", compileFinal { + private _display = uiNamespace getVariable [QGVAR(Display), displayNull]; + if (isNull _display) exitWith { controlNull }; + + _display displayCtrl 1003 + }], + ["getMapControl", compileFinal { + private _display = uiNamespace getVariable [QGVAR(Display), displayNull]; + if (isNull _display) exitWith { controlNull }; + + _display displayCtrl 1001 + }], + ["getDispatcherControl", compileFinal { + private _display = uiNamespace getVariable [QGVAR(Display), displayNull]; + if (isNull _display) exitWith { controlNull }; + + _display displayCtrl 1006 + }], + ["hasOpenScreen", compileFinal { + private _screen = _self call ["getScreen", []]; + private _control = _self call ["getActiveBrowserControl", []]; + !(isNull _control) && { _screen call ["isReady", []] } + }], + ["isDispatcher", compileFinal { + if (isNil QGVAR(CADRepository)) exitWith { false }; + + private _session = GVAR(CADRepository) getOrDefault ["session", createHashMap]; + _session getOrDefault ["isDispatcher", false] + }], + ["applyLayout", compileFinal { + private _mode = if (isNil QGVAR(CADRepository)) then { + "operations" + } else { + GVAR(CADRepository) getOrDefault ["mode", "operations"] + }; + private _dispatchView = if (isNil QGVAR(CADRepository)) then { + "board" + } else { + GVAR(CADRepository) getOrDefault ["dispatchView", "board"] + }; + + private _mapCtrl = _self call ["getMapControl", []]; + private _bottomBarCtrl = _self call ["getBottomBarControl", []]; + private _sidePanelCtrl = _self call ["getActiveBrowserControl", []]; + private _dispatcherCtrl = _self call ["getDispatcherControl", []]; + + private _showMapLayout = (_mode isEqualTo "operations") || { _mode isEqualTo "dispatch" && { _dispatchView isEqualTo "map" } }; + + if !(isNull _mapCtrl) then { _mapCtrl ctrlShow _showMapLayout; }; + if !(isNull _bottomBarCtrl) then { _bottomBarCtrl ctrlShow true; }; + if !(isNull _sidePanelCtrl) then { _sidePanelCtrl ctrlShow _showMapLayout; }; + if !(isNull _dispatcherCtrl) then { _dispatcherCtrl ctrlShow (_mode isEqualTo "dispatch" && { _dispatchView isEqualTo "board" }); }; + + _self call ["refreshHydrate", []]; + _self call ["refreshTopBarState", []]; + _self call ["refreshDispatcher", []]; + true + }], + ["setMode", compileFinal { + params [["_mode", "operations", [""]]]; + + if (isNil QGVAR(CADRepository)) exitWith { false }; + + private _targetMode = _mode; + if !(_targetMode in ["operations", "dispatch"]) then { + _targetMode = "operations"; + }; + + if (_targetMode isEqualTo "dispatch" && !(_self call ["isDispatcher", []])) then { + _targetMode = "operations"; + }; + + GVAR(CADRepository) call ["setMode", [_targetMode]]; + if (_targetMode isEqualTo "dispatch") then { + GVAR(CADRepository) call ["setDispatchView", ["board"]]; + }; + _self call ["applyLayout", []] + }], + ["setDispatchView", compileFinal { + params [["_dispatchView", "board", [""]]]; + + if (isNil QGVAR(CADRepository)) exitWith { false }; + if ((GVAR(CADRepository) getOrDefault ["mode", "operations"]) isNotEqualTo "dispatch") exitWith { false }; + if !(_self call ["isDispatcher", []]) exitWith { false }; + + GVAR(CADRepository) call ["setDispatchView", [_dispatchView]]; + _self call ["applyLayout", []] + }], + ["refreshTopBarState", compileFinal { + if !(_self getOrDefault ["topBarReady", false]) exitWith { false }; + + if (isNil QGVAR(CADRepository)) exitWith { false }; + + private _topBarCtrl = _self call ["getTopBarControl", []]; + if (isNull _topBarCtrl) exitWith { false }; + + private _session = +(GVAR(CADRepository) getOrDefault ["session", createHashMap]); + private _currentGroup = GVAR(CADRepository) call ["getCurrentGroup", []]; + private _payload = createHashMapFromArray [ + ["mode", GVAR(CADRepository) getOrDefault ["mode", "operations"]], + ["dispatchView", GVAR(CADRepository) getOrDefault ["dispatchView", "board"]], + ["session", _session], + ["currentGroup", _currentGroup] + ]; + + _topBarCtrl ctrlWebBrowserAction ["ExecJS", format [ + "window.cadTopbar && window.cadTopbar.receiveState(%1);", + toJSON _payload + ]]; + true + }], + ["refreshDispatcher", compileFinal { + if !(_self getOrDefault ["dispatcherReady", false]) exitWith { false }; + if (isNil QGVAR(CADRepository)) exitWith { false }; + + private _dispatcherCtrl = _self call ["getDispatcherControl", []]; + if (isNull _dispatcherCtrl) exitWith { false }; + + private _payload = GVAR(CADRepository) call ["getHydratePayload", []]; + _dispatcherCtrl ctrlWebBrowserAction ["ExecJS", format [ + "window.cadDispatcher && window.cadDispatcher.receiveHydrate(%1);", + toJSON _payload + ]]; + true + }], + ["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 ["requestHydrate", []]; + _self call ["refreshHydrate", []]; + _self call ["refreshTopBarState", []]; + true + }], + ["handleClose", compileFinal { + _self set ["dispatcherReady", false]; + _self set ["topBarReady", false]; + + private _screen = _self call ["getScreen", []]; + _screen call ["dispose", []]; + true + }], + ["handleTopBarReady", compileFinal { + _self set ["topBarReady", true]; + _self call ["refreshTopBarState", []] + }], + ["handleDispatcherReady", compileFinal { + _self set ["dispatcherReady", true]; + _self call ["refreshDispatcher", []] + }], + ["requestHydrate", compileFinal { + [SRPC(cad,requestHydrateCad), [getPlayerUID player]] call CFUNC(serverEvent); + true + }], + ["requestAssignTask", compileFinal { + params [["_taskID", "", [""]], ["_groupID", "", [""]], ["_note", "", [""]]]; + + if (_taskID isEqualTo "" || { _groupID isEqualTo "" }) exitWith { false }; + + [SRPC(cad,requestAssignCadTask), [getPlayerUID player, _taskID, _groupID, _note]] call CFUNC(serverEvent); + true + }], + ["requestCreateDispatchOrder", compileFinal { + params [ + ["_assigneeGroupID", "", [""]], + ["_targetGroupID", "", [""]], + ["_note", "", [""]], + ["_priority", "priority", [""]], + ["_request", createHashMap, [createHashMap]] + ]; + + if (_assigneeGroupID isEqualTo "" || { _targetGroupID isEqualTo "" }) exitWith { false }; + + [SRPC(cad,requestCreateCadDispatchOrder), [getPlayerUID player, _assigneeGroupID, _targetGroupID, _note, _priority, _request]] call CFUNC(serverEvent); + true + }], + ["requestSubmitSupportRequest", compileFinal { + params [ + ["_type", "", [""]], + ["_fields", createHashMap, [createHashMap]], + ["_priority", "priority", [""]] + ]; + + if (_type isEqualTo "") exitWith { false }; + + [SRPC(cad,requestSubmitCadSupportRequest), [getPlayerUID player, _type, _fields, _priority]] call CFUNC(serverEvent); + true + }], + ["requestCloseDispatchOrder", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + [SRPC(cad,requestCloseCadDispatchOrder), [getPlayerUID player, _taskID]] call CFUNC(serverEvent); + true + }], + ["requestCloseSupportRequest", compileFinal { + params [["_requestID", "", [""]]]; + + if (_requestID isEqualTo "") exitWith { false }; + + [SRPC(cad,requestCloseCadSupportRequest), [getPlayerUID player, _requestID]] call CFUNC(serverEvent); + true + }], + ["requestAcknowledgeTask", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + [SRPC(cad,requestAcknowledgeCadTask), [getPlayerUID player, _taskID]] call CFUNC(serverEvent); + true + }], + ["requestDeclineTask", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + [SRPC(cad,requestDeclineCadTask), [getPlayerUID player, _taskID]] call CFUNC(serverEvent); + true + }], + ["requestGroupStatus", compileFinal { + params [["_groupID", "", [""]], ["_status", "", [""]]]; + + if (_groupID isEqualTo "" || { _status isEqualTo "" }) exitWith { false }; + + [SRPC(cad,requestUpdateCadGroupStatus), [getPlayerUID player, _groupID, _status]] call CFUNC(serverEvent); + true + }], + ["requestGroupRole", compileFinal { + params [["_groupID", "", [""]], ["_role", "", [""]]]; + + if (_groupID isEqualTo "" || { _role isEqualTo "" }) exitWith { false }; + + [SRPC(cad,requestUpdateCadGroupRole), [getPlayerUID player, _groupID, _role]] call CFUNC(serverEvent); + true + }], + ["requestGroupProfile", compileFinal { + params [["_groupID", "", [""]], ["_status", "", [""]], ["_role", "", [""]]]; + + if (_groupID isEqualTo "") exitWith { false }; + if (_status isEqualTo "" && { _role isEqualTo "" }) exitWith { false }; + + [SRPC(cad,requestUpdateCadGroupProfile), [getPlayerUID player, _groupID, _status, _role]] call CFUNC(serverEvent); + true + }], + ["focusGroup", compileFinal { + params [["_groupID", "", [""]]]; + + if (_groupID isEqualTo "") exitWith { false }; + if (isNil QGVAR(CADRepository)) exitWith { false }; + + private _groups = GVAR(CADRepository) getOrDefault ["groups", []]; + private _groupIndex = _groups findIf { (_x getOrDefault ["groupId", ""]) isEqualTo _groupID }; + if (_groupIndex < 0) exitWith { false }; + + private _group = _groups # _groupIndex; + private _position = _group getOrDefault ["position", []]; + if !(_position isEqualType []) exitWith { false }; + if ((count _position) < 2) exitWith { false }; + + private _mapCtrl = _self call ["getMapControl", []]; + if (isNull _mapCtrl) exitWith { false }; + + private _targetPosition = [_position # 0, _position # 1, 0]; + _mapCtrl ctrlMapAnimAdd [0.35, ctrlMapScale _mapCtrl, _targetPosition]; + ctrlMapAnimCommit _mapCtrl; + true + }], + ["focusTask", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + if (isNil QGVAR(CADRepository)) exitWith { false }; + + private _contracts = GVAR(CADRepository) getOrDefault ["contracts", []]; + private _taskIndex = _contracts findIf { + private _entryTaskID = _x getOrDefault ["taskId", _x getOrDefault ["taskID", ""]]; + _entryTaskID isEqualTo _taskID + }; + if (_taskIndex < 0) exitWith { false }; + + private _task = _contracts # _taskIndex; + private _position = _task getOrDefault ["position", []]; + if !(_position isEqualType []) exitWith { false }; + if ((count _position) < 2) exitWith { false }; + + private _mapCtrl = _self call ["getMapControl", []]; + if (isNull _mapCtrl) exitWith { false }; + + private _targetPosition = [_position # 0, _position # 1, 0]; + _mapCtrl ctrlMapAnimAdd [0.35, ctrlMapScale _mapCtrl, _targetPosition]; + ctrlMapAnimCommit _mapCtrl; + true + }], + ["focusRequest", compileFinal { + params [["_requestID", "", [""]]]; + + if (_requestID isEqualTo "") exitWith { false }; + if (isNil QGVAR(CADRepository)) exitWith { false }; + + private _requests = GVAR(CADRepository) getOrDefault ["requests", []]; + private _requestIndex = _requests findIf { (_x getOrDefault ["requestId", ""]) isEqualTo _requestID }; + if (_requestIndex < 0) exitWith { false }; + + private _request = _requests # _requestIndex; + private _position = _request getOrDefault ["position", []]; + if !(_position isEqualType []) exitWith { false }; + if ((count _position) < 2) exitWith { false }; + + private _mapCtrl = _self call ["getMapControl", []]; + if (isNull _mapCtrl) exitWith { false }; + + private _targetPosition = [_position # 0, _position # 1, 0]; + _mapCtrl ctrlMapAnimAdd [0.35, ctrlMapScale _mapCtrl, _targetPosition]; + ctrlMapAnimCommit _mapCtrl; + true + }], + ["refreshHydrate", compileFinal { + if (isNil QGVAR(CADRepository)) exitWith { false }; + GVAR(CADRepository) call ["pushHydratePayload", [_self]] + }], + ["handleHydrateResponse", compileFinal { + params [["_payload", createHashMap, [createHashMap]]]; + + if (isNil QGVAR(CADRepository)) exitWith { false }; + + GVAR(CADRepository) call ["setHydratePayload", [_payload]]; + if !(_self call ["isDispatcher", []]) then { + GVAR(CADRepository) call ["setMode", ["operations"]]; + }; + + _self call ["refreshHydrate", []]; + _self call ["refreshTopBarState", []]; + _self call ["refreshDispatcher", []]; + _self call ["applyLayout", []] + }], + ["handleAssignmentResponse", compileFinal { + params [["_result", createHashMap, [createHashMap]]]; + + if (_self getOrDefault ["dispatcherReady", false]) then { + private _dispatcherCtrl = _self call ["getDispatcherControl", []]; + if !(isNull _dispatcherCtrl) then { + _dispatcherCtrl ctrlWebBrowserAction ["ExecJS", format [ + "window.cadDispatcher && window.cadDispatcher.setStatus(%1, %2);", + str (_result getOrDefault ["message", "Task request processed."]), + str ([ "error", "success" ] select (_result getOrDefault ["success", false])) + ]]; + }; + }; + + _self call ["sendEvent", ["cad::assignment::response", createHashMapFromArray [ + ["message", _result getOrDefault ["message", "Task request processed."]], + ["success", _result getOrDefault ["success", false]] + ]]] + }], + ["handleGroupUpdateResponse", compileFinal { + params [["_result", createHashMap, [createHashMap]]]; + + if (_self getOrDefault ["dispatcherReady", false]) then { + private _dispatcherCtrl = _self call ["getDispatcherControl", []]; + if !(isNull _dispatcherCtrl) then { + _dispatcherCtrl ctrlWebBrowserAction ["ExecJS", format [ + "window.cadDispatcher && window.cadDispatcher.setStatus(%1, %2);", + str (_result getOrDefault ["message", "Group update processed."]), + str ([ "error", "success" ] select (_result getOrDefault ["success", false])) + ]]; + }; + }; + + _self call ["sendEvent", ["cad::group::response", createHashMapFromArray [ + ["message", _result getOrDefault ["message", "Group update processed."]], + ["success", _result getOrDefault ["success", false]] + ]]] + }], + ["handleRequestResponse", compileFinal { + params [["_result", createHashMap, [createHashMap]]]; + + if (_self getOrDefault ["dispatcherReady", false]) then { + private _dispatcherCtrl = _self call ["getDispatcherControl", []]; + if !(isNull _dispatcherCtrl) then { + _dispatcherCtrl ctrlWebBrowserAction ["ExecJS", format [ + "window.cadDispatcher && window.cadDispatcher.setStatus(%1, %2);", + str (_result getOrDefault ["message", "Request processed."]), + str (["error", "success"] select (_result getOrDefault ["success", false])) + ]]; + }; + }; + + _self call ["sendEvent", ["cad::request::response", createHashMapFromArray [ + ["message", _result getOrDefault ["message", "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..d648613 --- /dev/null +++ b/arma/client/addons/cad/functions/fnc_openUI.sqf @@ -0,0 +1,49 @@ +#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; +private _dispatcherCtrl = _display displayCtrl 1006; + +{ + _x ctrlAddEventHandler ["JSDialog", { + params ["_control", "_isConfirmDialog", "_message"]; + [_control, _isConfirmDialog, _message] call FUNC(handleUIEvents); + }]; +} forEach [_topBarCtrl, _bottomBarCtrl, _sidePanelCtrl, _dispatcherCtrl]; + +_topBarCtrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\topbar.html)]; +_bottomBarCtrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\bottombar.html)]; +_sidePanelCtrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\sidepanel.html)]; +_dispatcherCtrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\dispatcher.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..f4bffd3 --- /dev/null +++ b/arma/client/addons/cad/ui/RscMapUI.hpp @@ -0,0 +1,109 @@ +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]; uiNamespace setVariable ['forge_client_cad_DispatcherCtrl', nil]; if !(isNil 'forge_client_cad_CADRepository') then { forge_client_cad_CADRepository set ['isOpen', false]; };"; + + class controlsBackground { + class SurfaceBackground: RscText { + idc = -1; + x = "safeZoneX + (safeZoneW * 0.1)"; + y = "safeZoneY + (safeZoneH * 0.1)"; + w = "safeZoneW * 0.8"; + h = "safeZoneH * 0.8"; + colorBackground[] = {0.04, 0.06, 0.09, 0.96}; + }; + + class MapControl: RscMapControl { + idc = 1001; + x = "safeZoneX + (safeZoneW * 0.1)"; // 10% margin (80% width centered) + y = "safeZoneY + (safeZoneH * 0.1) + 0.10372"; // 10% margin + 56px visible top bar + w = "safeZoneW * 0.8"; // 80% width + h = "(safeZoneH * 0.8) - 0.10372 - 0.0556"; // 80% height minus visible 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.24076"; // 130px, allows dropdowns to open over the map + 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.5550"; // Right edge of 80% box minus panel width + y = "safeZoneY + (safeZoneH * 0.1) + 0.10372"; // Below visible top bar + w = "0.5550"; // Wider panel for four-tab operations layout + h = "(safeZoneH * 0.8) - 0.10372 - 0.0556"; // Full height minus visible bars + colorBackground[] = {0, 0, 0, 0}; + }; + + class DispatcherBrowser: RscText { + type = 106; + idc = 1006; + x = "safeZoneX + (safeZoneW * 0.1)"; + y = "safeZoneY + (safeZoneH * 0.1) + 0.10372"; + w = "safeZoneW * 0.8"; + h = "(safeZoneH * 0.8) - 0.10372 - 0.0556"; + 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..57d4b5e --- /dev/null +++ b/arma/client/addons/cad/ui/_site/bottombar.html @@ -0,0 +1 @@ +CAD Systems by IDS v1.0.0 \ 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..d6213e6 --- /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}.footer-brand,.footer-version{color:#f5f8ffcc;text-shadow:0 1px 10px #00000047;font-size:12px}.footer-brand{color:var(--accent);letter-spacing:.08em;text-transform:uppercase;font-weight:600}.footer-version{color:#f5f8ff9e} \ 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..7710154 --- /dev/null +++ b/arma/client/addons/cad/ui/_site/cad-bottombar.js @@ -0,0 +1 @@ +window.CADBottombar=window.CADBottombar||{init:()=>!0},window.CADBottombar.init(); \ 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-dispatcher.css b/arma/client/addons/cad/ui/_site/cad-dispatcher.css new file mode 100644 index 0000000..c65da95 --- /dev/null +++ b/arma/client/addons/cad/ui/_site/cad-dispatcher.css @@ -0,0 +1 @@ +html,body{background:radial-gradient(circle at 0 0,#29455d2e,#0000 30%),linear-gradient(#090e14f5,#0f161ffa);width:100%;height:100%;margin:0;padding:0;overflow:hidden}body{color:var(--text);font-family:var(--font)}.dispatch-shell{flex-direction:column;gap:14px;height:100%;padding:18px;display:flex}.dispatch-header{justify-content:space-between;align-items:center;gap:16px;display:flex}.dispatch-kicker{color:var(--accent);text-transform:uppercase;letter-spacing:.1em;margin:0 0 4px;font-size:11px;font-weight:700}.dispatch-header h2{margin:0;font-size:24px;font-weight:650}.dispatch-header button,.dispatch-btn,.dispatch-select{color:var(--text);background:#181f28e6;border:1px solid #ffffff1f}.dispatch-header button,.dispatch-btn{cursor:pointer;padding:10px 14px}.dispatch-btn-secondary{background:#352827eb}.dispatch-status{color:#e9f1f8c7;min-height:20px;font-size:13px}.dispatch-status[data-type=success]{color:#79d28a}.dispatch-status[data-type=error]{color:#ff8a80}.dispatch-danger-alert{color:#ffd4cf;letter-spacing:.06em;text-transform:uppercase;background:linear-gradient(90deg,#5c1212f0,#801d1dd1);border:1px solid #ff6b6b61;padding:10px 12px;font-size:12px;font-weight:700;animation:1.35s ease-in-out infinite cad-danger-pulse}.dispatch-danger-alert.is-hidden{display:none}.dispatch-warning-alert{color:#ffe9b2;letter-spacing:.06em;text-transform:uppercase;background:linear-gradient(90deg,#59400cf0,#7d5c12d6);border:1px solid #f6c6546b;padding:10px 12px;font-size:12px;font-weight:700;animation:1.35s ease-in-out infinite cad-warning-pulse}.dispatch-warning-alert.is-hidden{display:none}.dispatch-metrics{grid-template-columns:repeat(5,minmax(0,1fr));gap:12px;display:grid}.metric-card{background:#0d131ab8;border:1px solid #ffffff14;padding:14px}.metric-label{color:#e9f1f899;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;font-size:11px;display:block}.metric-card strong{font-size:28px;font-weight:700}.metric-card.is-danger{background:linear-gradient(#4a1111db,#160d10eb);border-color:#ff6b6b57;animation:1.35s ease-in-out infinite cad-danger-pulse;box-shadow:inset 0 0 0 1px #ff6b6b1f}.metric-card.is-warning{background:linear-gradient(#5c410edb,#1d160beb);border-color:#f6c65457;animation:1.35s ease-in-out infinite cad-warning-pulse;box-shadow:inset 0 0 0 1px #f6c6541f}.dispatch-grid{flex:1;grid-template-columns:repeat(12,minmax(0,1fr));grid-auto-rows:minmax(0,1fr);gap:14px;min-height:0;display:grid}.dispatch-panel{background:#0b1118c7;border:1px solid #ffffff14;flex-direction:column;min-width:0;min-height:0;display:flex}.dispatch-panel-open{grid-column:span 5}.dispatch-panel-assigned{grid-column:span 7}.dispatch-panel-groups{grid-column:span 8}.dispatch-panel-activity{grid-column:span 4}.dispatch-panel-header{border-bottom:1px solid #ffffff14;justify-content:space-between;align-items:center;padding:12px 14px;display:flex}.dispatch-panel-header h3{text-transform:uppercase;letter-spacing:.08em;color:var(--accent);margin:0;font-size:13px}.dispatch-list{flex-direction:column;flex:1;gap:10px;padding:12px;display:flex;overflow:auto}.dispatch-inline-section{flex-direction:column;gap:10px;display:flex}.dispatch-inline-header{color:var(--accent);text-transform:uppercase;letter-spacing:.08em;font-size:11px;font-weight:700}.dispatch-card{background:#131a22b8;border:1px solid #ffffff0f;padding:12px}.dispatch-card-interactive{cursor:pointer}.dispatch-card-interactive:hover{background:#171f28d1;border-color:#5bbbff33}.dispatch-card-header,.dispatch-meta{justify-content:space-between;gap:10px;display:flex}.dispatch-card-header-actions{align-items:center;gap:8px;display:flex}.dispatch-card-header-main{align-items:center;gap:8px;min-width:0;display:flex}.dispatch-card-header{margin-bottom:8px}.dispatch-description{color:#f1f6fbd1;margin:0 0 10px;font-size:13px;line-height:1.45}.dispatch-meta{color:#e5edf4b3;margin-bottom:10px;font-size:12px}.dispatch-badge{color:var(--accent);text-transform:uppercase;background:#102b3db3;border:1px solid #5bbbff2e;padding:3px 7px;font-size:11px}.dispatch-alert-badge{color:#ffd8d1;text-transform:uppercase;letter-spacing:.08em;background:#5f1717e0;border:1px solid #ff6b6b70;padding:3px 7px;font-size:11px;font-weight:700}.dispatch-icon-btn{width:32px;height:32px;color:var(--text);cursor:pointer;background:#181f28eb;border:1px solid #ffffff24;padding:0}.dispatch-icon-btn:hover{background:#202a34f5}.dispatch-actions{flex-direction:column;gap:8px;display:flex}.dispatch-card.is-danger{background:linear-gradient(#451416c7,#1c1115eb);border-color:#ff6b6b57;animation:1.35s ease-in-out infinite cad-danger-pulse;box-shadow:inset 0 0 0 1px #ff6b6b1a}.dispatch-card.is-danger .dispatch-meta,.dispatch-card.is-danger .dispatch-description{color:#ffe8e4d1}.dispatch-card.is-warning{background:linear-gradient(#564011c7,#221b10eb);border-color:#f6c65457;animation:1.35s ease-in-out infinite cad-warning-pulse;box-shadow:inset 0 0 0 1px #f6c6541a}.dispatch-card.is-warning .dispatch-meta,.dispatch-card.is-warning .dispatch-description{color:#fff3d6d6}.dispatch-actions-split{margin-top:10px}.dispatch-select{width:100%;padding:9px 10px}.dispatch-textarea{width:100%;min-height:92px;color:var(--text);font:inherit;resize:vertical;box-sizing:border-box;background:#181f28eb;border:1px solid #ffffff1f;padding:10px 12px}.placeholder-message{text-align:center;color:#e9f1f899;padding:18px}.dispatch-modal{z-index:30;box-sizing:border-box;justify-content:center;align-items:center;padding:32px 24px;display:flex;position:fixed;inset:0}.dispatch-modal.is-hidden{display:none}.dispatch-modal-backdrop{background:#04080cb8;position:absolute;inset:0}.dispatch-modal-dialog{background:#0b1118fa;border:1px solid #ffffff1f;flex-direction:column;width:min(560px,100% - 48px);max-height:calc(100vh - 64px);margin:0;display:flex;position:relative;box-shadow:0 24px 64px #0000006b}.dispatch-modal-header,.dispatch-modal-actions{justify-content:space-between;align-items:center;gap:12px;padding:14px 16px;display:flex}.dispatch-modal-header{border-bottom:1px solid #ffffff14}.dispatch-modal-header h3{margin:0;font-size:22px;font-weight:650}.dispatch-modal-body{flex:1;min-height:0;padding:16px;overflow:auto}.dispatch-meta-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin-bottom:18px;display:grid}.dispatch-meta-grid strong{margin-top:4px;font-size:14px;font-weight:600;display:block}.dispatch-modal-fields{gap:12px;display:grid}.dispatch-field{gap:6px;display:grid}.dispatch-field span{text-transform:uppercase;letter-spacing:.08em;color:#e9f1f8b3;font-size:12px;font-weight:650}.dispatch-modal-actions{border-top:1px solid #ffffff14;justify-content:flex-end}.dispatch-detail-block,.dispatch-detail-list{background:#131a22b8;border:1px solid #ffffff14}.dispatch-detail-block{color:#f1f6fbd1;white-space:pre-wrap;padding:12px;line-height:1.45}.dispatch-detail-list{gap:1px;display:grid;overflow:hidden}.dispatch-detail-row{background:#0e141ceb;grid-template-columns:minmax(0,180px) minmax(0,1fr);gap:12px;padding:10px 12px;display:grid}.dispatch-detail-label{color:#e9f1f8a3;text-transform:uppercase;letter-spacing:.06em;font-size:12px;font-weight:650}.dispatch-detail-value{color:#f1f6fbd6;word-break:break-word;white-space:pre-wrap;line-height:1.4}@keyframes cad-danger-pulse{0%,to{box-shadow:inset 0 0 0 1px #ff6b6b14,0 0 #ff6b6b00}50%{box-shadow:inset 0 0 0 1px #ff8d8d38,0 0 18px #ff6b6b29}}@keyframes cad-warning-pulse{0%,to{box-shadow:inset 0 0 0 1px #f6c65414,0 0 #f6c65400}50%{box-shadow:inset 0 0 0 1px #fbd47638,0 0 18px #f6c65429}} \ No newline at end of file diff --git a/arma/client/addons/cad/ui/_site/cad-dispatcher.js b/arma/client/addons/cad/ui/_site/cad-dispatcher.js new file mode 100644 index 0000000..d9c1de4 --- /dev/null +++ b/arma/client/addons/cad/ui/_site/cad-dispatcher.js @@ -0,0 +1 @@ +window.cadDispatcherFormatters={getDangerGroups(){return this.groups.filter(e=>"danger"===(e.status||""))},getSupportAlertRequests(){return this.requests.filter(e=>["medevac_9line","fire_support","air_support"].includes(e.type||""))},buildSupportAlertMessage(){const e=this.getSupportAlertRequests();if(!e.length)return"";return`Support request alert: ${e.map(e=>`${e.groupCallsign||e.groupId||"Unknown Group"} ${this.getRequestTypeLabel(e.type||"request")}`).join(", ")}`},getSortedGroups(){return this.groups.slice().sort((e,t)=>{const s="danger"===(e.status||"")?0:1,n="danger"===(t.status||"")?0:1;if(s!==n)return s-n;const r=e.callsign||e.groupId||"",i=t.callsign||t.groupId||"";return r.localeCompare(i)})},isDispatchOrder:e=>!!e.isDispatchOrder||"dispatch_order"===(e.type||""),formatTypeLabel(e){const t=(e.type||"task").replaceAll("_"," ");return this.isDispatchOrder(e)?"dispatch order":t},getRequestTypeLabel(e){switch(e){case"medevac_9line":return"9-Line MEDEVAC";case"ace_lace":return"ACE/LACE";case"fire_support":return"Fire Support";case"air_support":return"Air Support";case"logreq":return"LOGREQ";default:return(e||"request").replaceAll("_"," ")}},buildGroupOptions(e){return this.getSortedGroups().map(t=>{const s=t.groupId||"";return``}).join("")},formatRequestFieldLabel:e=>(e||"field").replaceAll("_"," ").replace(/\b\w/g,e=>e.toUpperCase()),formatRequestFieldValue(e){if(Array.isArray(e))return e.join(", ");if(e&&"object"==typeof e)return JSON.stringify(e);return String(e??"").trim()||"Not provided"},buildRequestOrderNote(e){const t=this.getRequestTypeLabel(e.type||"request"),s=e.groupCallsign||e.groupId||"Unknown Group",n=(e.summary||"").trim(),r=e.fields&&"object"==typeof e.fields?Object.entries(e.fields).map(([e,t])=>{const s=this.formatRequestFieldValue(t);return"Not provided"===s?"":`${this.formatRequestFieldLabel(e)} ${s}`}).filter(Boolean):[],i=r.length?r:[n].filter(Boolean);return i.length?`${t} requested by ${s}. ${i.join(" | ")}`:`${t} requested by ${s}.`}},window.cadDispatcherModals={openOrderModal(){this.convertingRequestId="",this.populateOrderModal(),document.getElementById("dispatcherOrderModalTitle").textContent="Create Support Order",document.getElementById("dispatcherOrderModal").classList.remove("is-hidden")},closeOrderModal(){this.convertingRequestId="",document.getElementById("dispatcherOrderNoteInput").value="",document.getElementById("dispatcherOrderPrioritySelect").value="priority",document.getElementById("dispatcherOrderModalTitle").textContent="Create Support Order",document.getElementById("dispatcherOrderModal").classList.add("is-hidden")},openRequestModal(e){const t=this.requests.find(t=>t.requestId===e);t&&(this.viewingRequestId=e,this.populateRequestModal(t),document.getElementById("dispatcherRequestModal").classList.remove("is-hidden"))},closeRequestModal(){this.viewingRequestId="",document.getElementById("dispatcherRequestModal").classList.add("is-hidden")},syncRequestModal(){if(!this.viewingRequestId)return;const e=this.requests.find(e=>e.requestId===this.viewingRequestId);e?this.populateRequestModal(e):this.closeRequestModal()},populateRequestModal(e){const t=e.fields&&"object"==typeof e.fields?Object.entries(e.fields):[],s=t.length?t.map(([e,t])=>`\n
\n ${this.formatRequestFieldLabel(e)}\n ${this.formatRequestFieldValue(t)}\n
\n `).join(""):'

No submitted fields.

';document.getElementById("dispatcherRequestTitle").textContent=e.title||e.requestId||"Support Request",document.getElementById("dispatcherRequestPriority").textContent=(e.priority||"priority").replaceAll("_"," "),document.getElementById("dispatcherRequestGroup").textContent=e.groupCallsign||e.groupId||"Unknown",document.getElementById("dispatcherRequestType").textContent=this.getRequestTypeLabel(e.type||"request"),document.getElementById("dispatcherRequestSummary").textContent=e.summary||"No summary provided.",document.getElementById("dispatcherRequestFields").innerHTML=s},convertRequestToOrder(e){const t=this.requests.find(t=>(t.requestId||"")===e);if(!t)return void this.setStatus("Selected request is no longer available.","error");const s=t.groupId||"";if(!s)return void this.setStatus("Selected request has no owning group to target.","error");this.groups.find(e=>(e.groupId||"")===s)?(this.convertingRequestId=e,this.populateOrderModal({selectedAssigneeID:this.getSortedGroups().find(e=>(e.groupId||"")!==s)?.groupId||"",selectedTargetID:s,note:this.buildRequestOrderNote(t),priority:t.priority||"priority"}),document.getElementById("dispatcherOrderModalTitle").textContent="Create Order From Request",document.getElementById("dispatcherOrderModal").classList.remove("is-hidden"),this.setStatus("Preparing dispatch order from request...","info")):this.setStatus("Selected request group is no longer available.","error")},convertViewedRequestToOrder(){if(!this.viewingRequestId)return;const e=this.viewingRequestId;this.closeRequestModal(),this.convertRequestToOrder(e)},populateOrderModal(e={}){const t=this.getSortedGroups(),s=document.getElementById("dispatcherOrderAssigneeSelect"),n=document.getElementById("dispatcherOrderTargetSelect"),r=document.getElementById("dispatcherOrderNoteInput"),i=document.getElementById("dispatcherOrderPrioritySelect");if(!s||!n)return;const d=e.selectedAssigneeID||"",a=e.selectedTargetID||"",o=d||t.find(e=>(e.groupId||"")!==a)?.groupId||t[0]?.groupId||"",c=a||t.find(e=>(e.groupId||"")!==o)?.groupId||t[0]?.groupId||"";s.innerHTML=this.buildGroupOptions(o),n.innerHTML=this.buildGroupOptions(c),r&&(r.value=e.note||""),i&&(i.value=e.priority||"priority")},syncOrderModal(){const e=document.getElementById("dispatcherOrderModal");e&&!e.classList.contains("is-hidden")&&this.populateOrderModal({selectedAssigneeID:document.getElementById("dispatcherOrderAssigneeSelect")?.value||"",selectedTargetID:document.getElementById("dispatcherOrderTargetSelect")?.value||"",note:document.getElementById("dispatcherOrderNoteInput")?.value||"",priority:document.getElementById("dispatcherOrderPrioritySelect")?.value||"priority"})},openGroupModal(e){const t=this.groups.find(t=>t.groupId===e);t&&(this.editingGroupId=e,document.getElementById("dispatcherModalGroupCallsign").textContent=t.callsign||t.groupId||"Unknown",document.getElementById("dispatcherModalGroupLeader").textContent=t.leaderName||"Unknown",document.getElementById("dispatcherModalGroupTask").textContent=t.currentTaskId||"None",document.getElementById("dispatcherModalGroupOrg").textContent=t.orgId||"default",document.getElementById("dispatcherModalRoleSelect").innerHTML=this.roles.map(e=>``).join(""),document.getElementById("dispatcherModalStatusSelect").innerHTML=this.statuses.map(e=>``).join(""),document.getElementById("dispatcherGroupModal").classList.remove("is-hidden"))},closeGroupModal(){this.editingGroupId="",document.getElementById("dispatcherGroupModal").classList.add("is-hidden")},syncOpenModal(){if(!this.editingGroupId)return;const e=this.groups.find(e=>e.groupId===this.editingGroupId);e?(document.getElementById("dispatcherModalGroupCallsign").textContent=e.callsign||e.groupId||"Unknown",document.getElementById("dispatcherModalGroupLeader").textContent=e.leaderName||"Unknown",document.getElementById("dispatcherModalGroupTask").textContent=e.currentTaskId||"None",document.getElementById("dispatcherModalGroupOrg").textContent=e.orgId||"default"):this.closeGroupModal()}},window.cadDispatcherRender={updateDangerAlert(){const e=document.getElementById("dispatcherDangerAlert");if(!e)return;const t=this.getDangerGroups();if(!t.length)return e.textContent="",void e.classList.add("is-hidden");const s=t.map(e=>e.callsign||e.groupId||"Unknown Group");e.textContent=`Danger alert active: ${s.join(", ")}`,e.classList.remove("is-hidden")},updateRequestAlert(){const e=document.getElementById("dispatcherRequestAlert");if(!e)return;const t=this.buildSupportAlertMessage();if(!t)return e.textContent="",void e.classList.add("is-hidden");e.textContent=t,e.classList.remove("is-hidden")},buildGroupEditorButton:e=>`\n \n ⚙\n \n `,buildCloseOrderButton:e=>`\n \n Close\n \n `,buildCloseRequestButton:e=>`\n \n Close\n \n `,buildConvertRequestButton:e=>`\n \n Convert to Order\n \n `,renderMetrics(){const e=this.contracts.filter(e=>"unassigned"!==(e.assignmentState||"unassigned")),t=this.contracts.filter(e=>"unassigned"===(e.assignmentState||"unassigned")),s=this.requests.length,n=this.getSupportAlertRequests(),r=this.groups.filter(e=>"danger"===(e.status||""));document.getElementById("metricOpenContracts").textContent=t.length,document.getElementById("metricAssignedContracts").textContent=e.length,document.getElementById("metricActiveGroups").textContent=this.groups.length,document.getElementById("metricOpenRequests").textContent=s,document.getElementById("metricDangerGroups").textContent=r.length;const i=document.getElementById("metricDangerGroupsCard");i&&i.classList.toggle("is-danger",r.length>0);const d=document.getElementById("metricOpenRequestsCard");d&&d.classList.toggle("is-warning",n.length>0)},renderOpenContracts(){const e=document.getElementById("dispatcherOpenContracts"),t=this.contracts.filter(e=>"unassigned"===(e.assignmentState||"unassigned"));if(!t.length)return void(e.innerHTML='

No open contracts.

');const s=this.buildGroupOptions("");e.innerHTML=t.map(e=>{const t=e.taskId||e.taskID||"",n=Array.isArray(e.position)?e.position:[0,0,0],r=this.groups.find(t=>t.groupId===(e.targetGroupId||""));return`\n
\n
\n ${e.title||t}\n ${this.formatTypeLabel(e)}\n
\n

${e.description||""}

\n
\n Unassigned\n ${window.mapUI.formatPosition(n)}\n
\n
\n Target: ${r?r.callsign:e.targetGroupCallsign||"None"}\n Priority: ${(e.priority||"priority").replaceAll("_"," ")}\n
\n
\n \n \n
\n
\n `}).join("")},renderAssignedContracts(){const e=document.getElementById("dispatcherAssignedContracts"),t=this.contracts.filter(e=>"unassigned"!==(e.assignmentState||"unassigned"));t.length?e.innerHTML=t.map(e=>{const t=e.taskId||e.taskID||"",s=this.groups.find(t=>t.groupId===(e.assignedGroupId||"")),n=this.groups.find(t=>t.groupId===(e.targetGroupId||"")),r=this.isDispatchOrder(e);return`\n
\n
\n ${e.title||t}\n ${e.assignmentState||"assigned"}\n
\n

${e.description||""}

\n
\n Group: ${s?s.callsign:e.assignedGroupId||"Unknown"}\n Type: ${this.formatTypeLabel(e)}\n
\n
\n Target: ${n?n.callsign:e.targetGroupCallsign||"None"}\n Priority: ${(e.priority||"priority").replaceAll("_"," ")}\n
\n ${r?`
${this.buildCloseOrderButton(t)}
`:""}\n
\n `}).join(""):e.innerHTML='

No assigned contracts.

'},renderGroups(){const e=document.getElementById("dispatcherGroups");this.groups.length?e.innerHTML=this.getSortedGroups().map(e=>{const t="danger"===(e.status||"");return`\n
\n
\n
\n ${e.callsign||e.groupId}\n ${e.role||"group"}\n ${t?'Danger':""}\n
\n
\n ${this.buildGroupEditorButton(e.groupId)}\n
\n
\n
\n Leader: ${e.leaderName||"Unknown"}\n Status: ${e.status||"unknown"}\n
\n
\n Org: ${e.orgId||"default"}\n Task: ${e.currentTaskId||"None"}\n
\n
\n `}).join(""):e.innerHTML='

No active groups available.

'},renderActivity(){const e=document.getElementById("dispatcherActivity"),t=this.requests.length?this.requests.map(e=>`\n
\n
\n ${e.title||e.requestId||"Support Request"}\n ${(e.priority||"priority").replaceAll("_"," ")}\n
\n

${e.summary||""}

\n
\n Group: ${e.groupCallsign||e.groupId||"Unknown"}\n ${this.getRequestTypeLabel(e.type||"request")}\n
\n
\n ${this.buildConvertRequestButton(e.requestId||"")}\n ${this.buildCloseRequestButton(e.requestId||"")}\n
\n
\n `).join(""):'

No active support requests.

',s=this.activity.length?this.activity.slice().reverse().slice(0,8).map(e=>`\n
\n
\n ${e.type||"activity"}\n ${Math.round(e.timestamp||0)}s\n
\n

${e.message||""}

\n
\n `).join(""):'

No recent activity.

';e.innerHTML=`\n
\n
Support Requests
\n ${t}\n
\n
\n
Recent Activity
\n ${s}\n
\n `},render(){this.updateDangerAlert(),this.updateRequestAlert(),this.renderMetrics(),this.renderOpenContracts(),this.renderAssignedContracts(),this.renderGroups(),this.renderActivity()}};const dispatcherFormatters=window.cadDispatcherFormatters||{},dispatcherModals=window.cadDispatcherModals||{},dispatcherRender=window.cadDispatcherRender||{};window.cadDispatcher={contracts:[],requests:[],groups:[],activity:[],session:{},editingGroupId:"",viewingRequestId:"",convertingRequestId:"",statuses:["available","en_route","on_task","holding","danger","unavailable"],roles:["infantry","recon","armor","air","logistics","support"],...dispatcherFormatters,...dispatcherModals,...dispatcherRender,init(){document.getElementById("dispatcherCreateOrderBtn").addEventListener("click",()=>{this.openOrderModal()}),document.getElementById("dispatcherGroupModalCloseBtn").addEventListener("click",()=>{this.closeGroupModal()}),document.getElementById("dispatcherGroupModalSaveBtn").addEventListener("click",()=>{this.applyGroupUpdates()}),document.querySelector("#dispatcherGroupModal .dispatch-modal-backdrop").addEventListener("click",()=>{this.closeGroupModal()}),document.getElementById("dispatcherOrderModalCloseBtn").addEventListener("click",()=>{this.closeOrderModal()}),document.getElementById("dispatcherOrderModalSaveBtn").addEventListener("click",()=>{this.createDispatchOrder()}),document.querySelector("#dispatcherOrderModal .dispatch-modal-backdrop").addEventListener("click",()=>{this.closeOrderModal()}),document.getElementById("dispatcherRequestModalCloseBtn").addEventListener("click",()=>{this.closeRequestModal()}),document.getElementById("dispatcherRequestModalDoneBtn").addEventListener("click",()=>{this.closeRequestModal()}),document.getElementById("dispatcherRequestConvertBtn").addEventListener("click",()=>{this.convertViewedRequestToOrder()}),document.querySelector("#dispatcherRequestModal .dispatch-modal-backdrop").addEventListener("click",()=>{this.closeRequestModal()}),window.mapUI.sendEvent("cad::dispatcher::ready",{})},receiveHydrate(e){this.contracts=Array.isArray(e.contracts)?e.contracts:[],this.requests=Array.isArray(e.requests)?e.requests:[],this.groups=Array.isArray(e.groups)?e.groups:[],this.activity=Array.isArray(e.activity)?e.activity:[],this.session=e.session&&"object"==typeof e.session?e.session:{};const t=document.getElementById("dispatcherStatusMessage");!t||t.dataset.type&&"info"!==t.dataset.type||this.setStatus("",""),this.syncOpenModal(),this.syncOrderModal(),this.syncRequestModal(),this.render()},setStatus(e,t){const s=document.getElementById("dispatcherStatusMessage");s&&(s.textContent=e||"",s.dataset.type=t||"")},createDispatchOrder(){const e=document.getElementById("dispatcherOrderAssigneeSelect").value,t=document.getElementById("dispatcherOrderTargetSelect").value,s=document.getElementById("dispatcherOrderPrioritySelect").value,n=document.getElementById("dispatcherOrderNoteInput").value,r=this.convertingRequestId&&this.requests.find(e=>(e.requestId||"")===this.convertingRequestId)||null;e&&t?e!==t?(this.setStatus(this.convertingRequestId?"Creating dispatch order from request...":"Creating dispatch order...","info"),window.mapUI.sendEvent("cad::dispatchOrder::create",{assigneeGroupID:e,targetGroupID:t,note:n.trim(),priority:s,request:r?{requestId:r.requestId||"",type:r.type||"",title:r.title||"",summary:r.summary||"",fields:r.fields&&"object"==typeof r.fields?r.fields:{}}:{}}),this.closeOrderModal()):this.setStatus("Assignee and target groups must be different.","error"):this.setStatus("Select both an assignee and a target group.","error")},assignTask(e){const t=document.getElementById(`dispatcher-assign-group-${e}`);t&&t.value?(this.setStatus("Submitting assignment...","info"),window.mapUI.sendEvent("cad::tasks::assign",{taskID:e,groupID:t.value,note:""})):this.setStatus("Select a group before assigning a contract.","error")},applyGroupUpdates(){if(!this.editingGroupId)return;const e=this.groups.find(e=>e.groupId===this.editingGroupId);if(!e)return void this.closeGroupModal();const t=document.getElementById("dispatcherModalRoleSelect").value,s=document.getElementById("dispatcherModalStatusSelect").value,n=t&&t!==(e.role||"")?t:"",r=s&&s!==(e.status||"")?s:"";if(!(n||r))return this.setStatus("No group changes to save.","info"),void this.closeGroupModal();this.setStatus("Updating group profile...","info"),window.mapUI.sendEvent("cad::groups::profile",{groupID:this.editingGroupId,role:n,status:r}),this.closeGroupModal()},closeDispatchOrder(e){e&&(this.setStatus("Closing dispatch order...","info"),window.mapUI.sendEvent("cad::dispatchOrder::close",{taskID:e}))},closeSupportRequest(e){e&&(this.setStatus("Closing support request...","info"),window.mapUI.sendEvent("cad::supportRequest::close",{requestID:e}))}},window.cadDispatcher.init(); \ 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..7032729 --- /dev/null +++ b/arma/client/addons/cad/ui/_site/cad-shared.js @@ -0,0 +1 @@ +window.mapUIState={layersPanelVisible:!0,sidePanelElement:null},window.mapUI={formatGridCoordinate:t=>Math.round(Number(t)||0).toString().padStart(4,"0"),formatPosition(t){const e=Array.isArray(t)?t:[0,0,0];return`X: ${this.formatGridCoordinate(e[0])} Y: ${this.formatGridCoordinate(e[1])}`},sendEvent(t,e){A3API.SendAlert(JSON.stringify({event:t,data:e}))},updateCoordinates(t,e){const n=document.getElementById("coordsDisplay");n&&(n.textContent=this.formatPosition([t,e,0]))},updateScale(t){const e=document.getElementById("scaleDisplay");e&&(e.textContent=`Scale: 1:${Math.round(t)}`)},updateStatus(t){const e=document.getElementById("statusText");e&&(e.textContent=t)}},window.updateCoordinates=window.mapUI.updateCoordinates,window.updateScale=window.mapUI.updateScale,window.updateStatus=window.mapUI.updateStatus,window.ForgeBridge=window.ForgeBridge||{_handlers:{},on(t,e){this._handlers[t]=this._handlers[t]||[],this._handlers[t].push(e)},ready:t=>(window.mapUI.sendEvent("cad::ready",t||{}),!0),receive(t){if(!t||"object"!=typeof t)return;(this._handlers[t.event]||[]).forEach(e=>e(t.data||{}))},send:(t,e)=>(window.mapUI.sendEvent(t,e||{}),!0),close:t=>(window.mapUI.sendEvent("map::close",t||{}),!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..7f3b6d0 --- /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}.cad-tabs{grid-template-columns:repeat(4,1fr);gap:5px;margin-bottom:12px;display:grid}.cad-tabs.is-two-col{grid-template-columns:repeat(2,1fr)}.cad-tabs.is-three-col{grid-template-columns:repeat(3,1fr)}.cad-tab{color:#f3f6f9c7;text-transform:uppercase;letter-spacing:.08em;white-space:nowrap;cursor:pointer;background:#141b21e0;border:1px solid #ffffff24;min-width:0;padding:8px 7px;font-size:10px}.cad-tab:hover{color:#f3f6f9;background:#1f282ff0}.cad-tab.is-active{color:var(--accent);background:#0f283af5;border-color:#5bbbff6b}.cad-tab-panels{min-height:0}.cad-section{display:none}.cad-section.is-active{display:block}.cad-section-header{color:var(--accent);text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;font-size:12px;font-weight:700}.task-accept-btn,.task-secondary-btn,.cad-select{color:#f3f6f9;background:#1e252be6;border:1px solid #fff3;width:100%;padding:8px 10px}.task-accept-btn,.task-secondary-btn{cursor:pointer}.task-accept-btn:hover,.task-secondary-btn:hover{background:#2e3942f2}.task-accept-btn:disabled,.task-secondary-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}.cad-modal{z-index:40;position:fixed;inset:0}.cad-modal.is-hidden{display:none}.cad-modal-backdrop{background:#04080cc2;position:absolute;inset:0}.cad-modal-dialog{background:#0b1118fa;border:1px solid #ffffff1f;width:min(480px,100% - 28px);margin:32px auto 0;position:relative;box-shadow:0 24px 64px #0000006b}.cad-modal-header,.cad-modal-actions{justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;display:flex}.cad-modal-header{border-bottom:1px solid #ffffff14}.cad-modal-header h3{margin:4px 0 0;font-size:18px;font-weight:650}.cad-modal-body{max-height:62vh;padding:14px;overflow:auto}.cad-modal-fields{gap:10px;display:grid}.cad-field{gap:6px;display:grid}.cad-field span{text-transform:uppercase;letter-spacing:.08em;color:#e9f1f8b3;font-size:11px;font-weight:700}.cad-input,.cad-textarea{color:#f3f6f9;box-sizing:border-box;width:100%;font:inherit;background:#1e252be6;border:1px solid #fff3;padding:8px 10px}.cad-textarea{resize:vertical;min-height:74px}.cad-icon-btn{width:30px;height:30px;color:var(--text);cursor:pointer;background:#181f28eb;border:1px solid #ffffff24;padding:0}.cad-modal-actions{border-top:1px solid #ffffff14;justify-content:flex-end}.cad-danger-alert{color:#ffd4cf;letter-spacing:.06em;text-transform:uppercase;background:linear-gradient(90deg,#5c1212f0,#801d1dd1);border:1px solid #ff6b6b5c;margin-bottom:10px;padding:8px 10px;font-size:11px;font-weight:700;animation:1.35s ease-in-out infinite cad-danger-pulse}.cad-danger-alert.is-hidden{display:none}.cad-warning-alert{color:#ffe9b2;letter-spacing:.06em;text-transform:uppercase;background:linear-gradient(90deg,#59400cf0,#7d5c12d6);border:1px solid #f6c65466;margin-bottom:10px;padding:8px 10px;font-size:11px;font-weight:700;animation:1.35s ease-in-out infinite cad-warning-pulse}.cad-warning-alert.is-hidden{display:none}.task-list{flex-direction:column;gap:10px;display:flex}.cad-request-actions{gap:8px;display:grid}.cad-request-btn{text-align:left}.task-action-stack,.task-action-row{flex-direction:column;gap:8px;display:flex}.task-action-row{flex-direction:row}.task-card{background:#0c10149e;border:1px solid #ffffff14;padding:10px}.task-card.is-danger,.roster-summary-card.is-danger{background:linear-gradient(#451416c7,#1c1115eb);border-color:#ff6b6b57;animation:1.35s ease-in-out infinite cad-danger-pulse;box-shadow:inset 0 0 0 1px #ff6b6b1a}.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}.task-secondary-btn{background:#3c302deb}.roster-summary-card{background:#10171dd1;border:1px solid #ffffff14;padding:10px}.task-alert-badge{color:#ffd8d1;letter-spacing:.08em;text-transform:uppercase;background:#5f1717e0;border:1px solid #ff6b6b70;align-items:center;padding:2px 8px;font-size:10px;font-weight:700;display:inline-flex}.roster-member-card{background:#0c1014bd}.dispatch-map-group-card{text-align:left;-webkit-appearance:none;appearance:none;width:100%;color:var(--text);font:inherit;cursor:pointer;border-radius:0;transition:border-color .12s,background .12s,transform .12s}.dispatch-map-group-card strong{color:var(--text)}.dispatch-map-group-card .task-type{color:var(--accent);opacity:.9}.dispatch-map-group-card .task-meta{color:var(--muted);opacity:1}.dispatch-map-group-card:hover{background:#121d26e6;border-color:#5bbbff42;transform:translate(-2px)}.dispatch-map-group-card.is-selected{background:#0f283aeb;border-color:#5bbbff85;box-shadow:inset 0 0 0 1px #5bbbff2e}.dispatch-map-group-card.is-danger:not(.is-selected){background:linear-gradient(#451416c7,#1c1115eb);border-color:#ff6b6b57}.dispatch-map-group-card.is-danger .task-meta,.roster-summary-card.is-danger .task-meta{color:#ffe8e4d1}.dispatch-map-card{text-align:left;-webkit-appearance:none;appearance:none;width:100%;color:var(--text);font:inherit;cursor:pointer;border-radius:0;transition:border-color .12s,background .12s,transform .12s}.dispatch-map-card strong{color:var(--text)}.dispatch-map-card .task-type{color:var(--accent);opacity:.9}.dispatch-map-card .task-description{color:var(--muted)}.dispatch-map-card .task-meta{color:var(--muted);opacity:1}.dispatch-map-card:hover{background:#121d26e6;border-color:#5bbbff42;transform:translate(-2px)}.dispatch-map-card.is-selected{background:#0f283aeb;border-color:#5bbbff85;box-shadow:inset 0 0 0 1px #5bbbff2e}.dispatch-map-card.is-warning:not(.is-selected){background:linear-gradient(#564011c7,#221b10eb);border-color:#f6c65457}.dispatch-map-card.is-warning .task-meta,.dispatch-map-card.is-warning .task-description{color:#fff3d6d6}.roster-leader-badge{color:var(--accent);letter-spacing:.06em;text-transform:uppercase;background:#0f283ad1;border:1px solid #5bbbff47;align-items:center;padding:2px 8px;font-size:10px;font-weight:700;display:inline-flex}@keyframes cad-danger-pulse{0%,to{box-shadow:inset 0 0 0 1px #ff6b6b14,0 0 #ff6b6b00}50%{box-shadow:inset 0 0 0 1px #ff8d8d38,0 0 14px #ff6b6b24}}@keyframes cad-warning-pulse{0%,to{box-shadow:inset 0 0 0 1px #f6c65414,0 0 #f6c65400}50%{box-shadow:inset 0 0 0 1px #fbd47638,0 0 18px #f6c65429}} \ 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..abba8a2 --- /dev/null +++ b/arma/client/addons/cad/ui/_site/cad-sidepanel.js @@ -0,0 +1 @@ +window.cadTasks={contracts:[],requests:[],groups:[],activity:[],session:{},mode:"operations",dispatchView:"board",activeTab:"contracts",selectedDispatchGroupId:"",selectedDispatchTaskId:"",selectedDispatchRequestId:"",focusStatusTimer:null,requestModalType:"",statuses:["available","en_route","on_task","holding","danger","unavailable"],roles:["infantry","recon","armor","air","logistics","support"],requestTypes:[{id:"medevac_9line",label:"9-Line MEDEVAC",defaultPriority:"emergency",fields:[{id:"pickup_location",label:"Line 1 Pickup Location",type:"text",defaultFromGroupPosition:!0},{id:"radio_freq",label:"Line 2 Radio / Call Sign",type:"text"},{id:"precedence",label:"Line 3 Precedence",type:"select",options:["urgent","urgent_surgical","priority","routine","convenience"]},{id:"special_equipment",label:"Line 4 Special Equipment",type:"select",options:["none","hoist","extraction","ventilator"]},{id:"patient_type",label:"Line 5 Patient Type",type:"select",options:["litter","ambulatory","mixed"]},{id:"security",label:"Line 6 Security",type:"select",options:["secure","possible_enemy","enemy_in_area","hot"]},{id:"marking",label:"Line 7 Marking",type:"select",options:["panels","smoke","ir","none","other"]},{id:"patient_nationality",label:"Line 8 Patient Nationality",type:"select",options:["coalition","civilian","enemy","epw","mixed"]},{id:"terrain",label:"Line 9 Terrain",type:"select",options:["flat","restricted","slope","rooftop","wooded"]}]},{id:"ace_lace",label:"ACE/LACE",defaultPriority:"routine",fields:[{id:"ammo",label:"Ammo",type:"textarea"},{id:"casualties",label:"Casualties",type:"textarea"},{id:"equipment",label:"Equipment",type:"textarea"},{id:"notes",label:"Notes",type:"textarea"}]},{id:"fire_support",label:"Fire Support",defaultPriority:"priority",fields:[{id:"target_location",label:"Target Location",type:"text",defaultFromGroupPosition:!0},{id:"target_description",label:"Target Description",type:"textarea"},{id:"requested_effect",label:"Requested Effect",type:"select",options:["suppress","destroy","illum","smoke","screen"]},{id:"ordnance",label:"Requested Ordnance",type:"text"},{id:"danger_close",label:"Danger Close",type:"select",options:["no","yes"]},{id:"remarks",label:"Remarks",type:"textarea"}]},{id:"air_support",label:"Air Support",defaultPriority:"priority",fields:[{id:"target_location",label:"Target Location",type:"text",defaultFromGroupPosition:!0},{id:"target_description",label:"Target Description",type:"textarea"},{id:"target_marking",label:"Target Marking",type:"select",options:["smoke","ir","laser","grid","visual"]},{id:"requested_effect",label:"Requested Effect",type:"select",options:["show_of_force","escort","suppress","destroy","recon"]},{id:"remarks",label:"Remarks",type:"textarea"}]},{id:"logreq",label:"LOGREQ",defaultPriority:"priority",fields:[{id:"category",label:"Category",type:"select",options:["ammo","medical","fuel","repair","vehicle","equipment","weapons","mixed"]},{id:"delivery_method",label:"Delivery Method",type:"select",options:["ground","airdrop","pickup","dispatch_discretion"]},{id:"delivery_location",label:"Delivery Location",type:"text",defaultFromGroupPosition:!0},{id:"requested_items",label:"Requested Items",type:"textarea"},{id:"quantity",label:"Quantity / Package",type:"text"},{id:"remarks",label:"Remarks",type:"textarea"}]}],init(){document.querySelectorAll(".cad-tab").forEach(e=>{e.addEventListener("click",()=>{this.setActiveTab(e.dataset.tab||"contracts")})}),document.getElementById("cadRequestModalCloseBtn").addEventListener("click",()=>{this.closeRequestModal()}),document.getElementById("cadRequestModalSaveBtn").addEventListener("click",()=>{this.submitSupportRequest()}),document.querySelector("#cadRequestModal .cad-modal-backdrop").addEventListener("click",()=>{this.closeRequestModal()}),window.ForgeBridge.on("cad::hydrate",e=>{this.setHydratePayload(e||{})}),window.ForgeBridge.on("cad::assignment::response",e=>{this.handleServerResponse(!!e.success,e.message||"")}),window.ForgeBridge.on("cad::group::response",e=>{this.handleServerResponse(!!e.success,e.message||"")}),window.ForgeBridge.on("cad::request::response",e=>{this.handleServerResponse(!!e.success,e.message||"")}),window.ForgeBridge.ready({loaded:!0})},setActiveTab(e){this.activeTab=e||"contracts",document.querySelectorAll(".cad-tab").forEach(e=>{e.classList.toggle("is-active",e.dataset.tab===this.activeTab)}),document.querySelectorAll("[data-panel]").forEach(e=>{e.classList.toggle("is-active",e.dataset.panel===this.activeTab)})},syncLayoutState(){const e=document.querySelector(".cad-tabs"),t=document.getElementById("tabContractsBtn"),s=document.getElementById("tabRosterBtn"),a=document.getElementById("tabRequestsBtn"),n=document.getElementById("tabActivityBtn"),i=document.getElementById("contractsPanel"),r=document.getElementById("rosterPanel"),o=document.getElementById("requestsPanel"),d=document.getElementById("activityPanel"),c=i?.querySelector(".cad-section-header"),l=r?.querySelector(".cad-section-header");if(this.isDispatchMapMode())return e&&(e.style.display="",e.classList.remove("is-two-col"),e.classList.add("is-three-col")),t&&(t.style.display=""),s&&(s.textContent="Groups"),n&&(n.style.display="none"),a&&(a.style.display=""),d&&(d.classList.remove("is-active"),d.style.display="none"),o&&(o.style.display=""),r&&(r.style.display=""),l&&(l.textContent="Active Groups"),i&&(i.style.display=""),c&&(c.textContent="Contracts"),void(["contracts","roster","requests"].includes(this.activeTab)||(this.activeTab="contracts"));e&&(e.style.display="",e.classList.remove("is-three-col"),e.classList.remove("is-two-col")),t&&(t.style.display=""),s&&(s.textContent="Roster"),n&&(n.style.display=""),a&&(a.style.display=""),i&&(i.style.display=""),d&&(d.style.display=""),o&&(o.style.display=""),r&&(r.style.display=""),l&&(l.textContent="Roster"),c&&(c.textContent="Contracts")},setHydratePayload(e){this.contracts=Array.isArray(e.contracts)?e.contracts:[],this.requests=Array.isArray(e.requests)?e.requests:[],this.groups=Array.isArray(e.groups)?e.groups:[],this.activity=Array.isArray(e.activity)?e.activity:[],this.session=e.session&&"object"==typeof e.session?e.session:{},this.mode=e&&"string"==typeof e.mode?e.mode:"operations",this.dispatchView=e&&"string"==typeof e.dispatchView?e.dispatchView:"board";const t=document.getElementById("cadStatusMessage");!t||t.dataset.type&&"info"!==t.dataset.type||this.setStatus("",""),this.selectedDispatchGroupId&&!this.groups.some(e=>e.groupId===this.selectedDispatchGroupId)&&(this.selectedDispatchGroupId=""),this.selectedDispatchTaskId&&!this.contracts.some(e=>(e.taskId||e.taskID||"")===this.selectedDispatchTaskId)&&(this.selectedDispatchTaskId=""),this.selectedDispatchRequestId&&!this.requests.some(e=>(e.requestId||"")===this.selectedDispatchRequestId)&&(this.selectedDispatchRequestId=""),"dispatch"!==this.mode||"map"!==this.dispatchView||["contracts","roster","requests"].includes(this.activeTab)||(this.activeTab="contracts"),this.render()},setStatus(e,t){const s=document.getElementById("cadStatusMessage");s&&(s.textContent=e||"",s.dataset.type=t||"")},getDangerGroups(){return this.groups.filter(e=>"danger"===(e.status||""))},getSupportAlertRequests(){return this.requests.filter(e=>["medevac_9line","fire_support","air_support"].includes(e.type||""))},buildSupportAlertMessage(){const e=this.getSupportAlertRequests();if(!e.length)return"";return`Support request alert: ${e.map(e=>`${e.groupCallsign||e.groupId||"Unknown Group"} ${this.getRequestTypeLabel(e.type||"request")}`).join(", ")}`},getCurrentGroupCoordinates(){const e=this.getCurrentGroup(),t=Array.isArray(e?.position)?e.position:[0,0,0];return window.mapUI.formatPosition(t)},getSortedGroups(){return this.groups.slice().sort((e,t)=>{const s="danger"===(e.status||"")?0:1,a="danger"===(t.status||"")?0:1;if(s!==a)return s-a;const n=e.callsign||e.groupId||"",i=t.callsign||t.groupId||"";return n.localeCompare(i)})},isDispatchOrder:e=>!!e.isDispatchOrder||"dispatch_order"===(e.type||""),formatTypeLabel(e){const t=(e.type||"task").replaceAll("_"," ");return this.isDispatchOrder(e)?"dispatch order":t},getRequestDefinition(e){return this.requestTypes.find(t=>t.id===e)||null},getRequestTypeLabel(e){return this.getRequestDefinition(e)?.label||e},canSubmitSupportRequest(){return"operations"===this.mode&&this.isLeader()},openRequestModal(e){const t=this.getRequestDefinition(e);t&&(this.requestModalType=e,document.getElementById("cadRequestModalTitle").textContent=t.label,document.getElementById("cadRequestPrioritySelect").value=t.defaultPriority||"priority",this.renderRequestFields(t),document.getElementById("cadRequestModal").classList.remove("is-hidden"))},closeRequestModal(){this.requestModalType="",document.getElementById("cadRequestFields").innerHTML="",document.getElementById("cadRequestModal").classList.add("is-hidden")},renderRequestFields(e){const t=document.getElementById("cadRequestFields");if(!t||!e)return;const s=this.getCurrentGroupCoordinates();t.innerHTML=e.fields.map(e=>{const t=e.defaultFromGroupPosition?s:"";return"select"===e.type?`\n \n `:"textarea"===e.type?`\n \n `:`\n \n `}).join("")},submitSupportRequest(){const e=this.getRequestDefinition(this.requestModalType);if(!e)return;const t={};e.fields.forEach(e=>{const s=document.getElementById(`cadRequestField_${e.id}`);t[e.id]=s?String(s.value||"").trim():""});const s=document.getElementById("cadRequestPrioritySelect").value;this.setStatus("Submitting support request...","info"),window.mapUI.sendEvent("cad::supportRequest::submit",{type:e.id,fields:t,priority:s}),this.closeRequestModal()},closeSupportRequest(e){e&&(this.setStatus(this.isDispatchMode()?"Closing support request...":"Cancelling support request...","info"),window.mapUI.sendEvent("cad::supportRequest::close",{requestID:e}))},renderRequests(){const e=document.getElementById("requestList");if(!e)return;if(this.isDispatchMapMode()){const t=this.requests.slice().sort((e,t)=>{const s=e.title||e.requestId||"",a=t.title||t.requestId||"";return s.localeCompare(a)});return t.length?void(e.innerHTML=t.map(e=>{const t=e.requestId||"",s=Array.isArray(e.position)?e.position:[0,0,0];return`\n \n
\n ${e.title||t||"Support Request"}\n ${this.getRequestTypeLabel(e.type||"request")}\n
\n

${e.summary||""}

\n
\n Group: ${e.groupCallsign||e.groupId||"Unknown"}\n ${(e.priority||"priority").replaceAll("_"," ")}\n
\n
\n ${window.mapUI.formatPosition(s)}\n ${t||"request"}\n
\n \n `}).join("")):void(e.innerHTML='

No support requests are currently active.

')}const t=this.canSubmitSupportRequest()?`\n
\n ${this.requestTypes.map(e=>`\n \n ${e.label}\n \n `).join("")}\n
\n `:"";this.requests.length?e.innerHTML=`\n ${t}\n ${this.requests.map(e=>{const t=this.isLeader()&&(e.groupId||"")===this.getPlayerGroupId(),s=this.canDispatch()||t,a=this.isDispatchMode()?"Close":"Cancel";return`\n
\n
\n ${e.title||this.getRequestTypeLabel(e.type||"")}\n ${(e.priority||"priority").replaceAll("_"," ")}\n
\n

${e.summary||""}

\n
\n Group: ${e.groupCallsign||e.groupId||"Unknown"}\n ${this.getRequestTypeLabel(e.type||"")}\n
\n ${s?`
\n \n
`:""}\n
\n `}).join("")}\n `:e.innerHTML=`\n ${t}\n

No support requests are currently active.

\n `},updateDangerAlert(){const e=document.getElementById("cadDangerAlert");if(!e)return;if(!this.isDispatchMapMode())return e.textContent="",void e.classList.add("is-hidden");const t=this.getDangerGroups();if(!t.length)return e.textContent="",void e.classList.add("is-hidden");const s=t.map(e=>e.callsign||e.groupId||"Unknown Group");e.textContent=`Danger alert active: ${s.join(", ")}`,e.classList.remove("is-hidden")},updateRequestAlert(){const e=document.getElementById("cadRequestAlert");if(!e)return;if(!this.isDispatchMapMode())return e.textContent="",void e.classList.add("is-hidden");const t=this.buildSupportAlertMessage();if(!t)return e.textContent="",void e.classList.add("is-hidden");e.textContent=t,e.classList.remove("is-hidden")},clearFocusStatusSoon(e){this.focusStatusTimer&&window.clearTimeout(this.focusStatusTimer),this.focusStatusTimer=window.setTimeout(()=>{const t=document.getElementById("cadStatusMessage");t&&"info"===t.dataset.type&&t.textContent===e&&this.setStatus("","")},1800)},handleServerResponse(e,t){this.setStatus(t||(e?"CAD update succeeded.":"CAD update failed."),e?"success":"error")},acknowledgeTask(e){this.setStatus("Acknowledging contract...","info"),window.mapUI.sendEvent("cad::tasks::acknowledge",{taskID:e})},declineTask(e){this.setStatus("Declining contract...","info"),window.mapUI.sendEvent("cad::tasks::decline",{taskID:e})},updateGroupStatus(e,t){this.setStatus("Updating group status...","info"),window.mapUI.sendEvent("cad::groups::status",{groupID:e,status:t})},updateGroupRole(e,t){this.setStatus("Updating group role...","info"),window.mapUI.sendEvent("cad::groups::role",{groupID:e,role:t})},focusGroup(e){const t=this.groups.find(t=>t.groupId===e);if(!t)return void this.setStatus("Selected group is no longer available.","error");this.selectedDispatchGroupId=e,this.selectedDispatchTaskId="",this.selectedDispatchRequestId="";const s=`Centering map on ${t.callsign||t.groupId||"group"}...`;this.setStatus(s,"info"),this.clearFocusStatusSoon(s),window.mapUI.sendEvent("cad::groups::focus",{groupID:e}),this.render()},focusTask(e){const t=this.contracts.find(t=>(t.taskId||t.taskID||"")===e);if(!t)return void this.setStatus("Selected contract is no longer available.","error");this.selectedDispatchTaskId=e,this.selectedDispatchGroupId="",this.selectedDispatchRequestId="";const s=`Centering map on ${t.title||e}...`;this.setStatus(s,"info"),this.clearFocusStatusSoon(s),window.mapUI.sendEvent("cad::tasks::focus",{taskID:e}),this.render()},focusRequest(e){const t=this.requests.find(t=>(t.requestId||"")===e);if(!t)return void this.setStatus("Selected request is no longer available.","error");if((Array.isArray(t.position)?t.position:[]).length<2)return void this.setStatus("Selected request has no map position.","error");this.selectedDispatchRequestId=e,this.selectedDispatchGroupId="",this.selectedDispatchTaskId="";const s=`Centering map on ${t.title||e}...`;this.setStatus(s,"info"),this.clearFocusStatusSoon(s),window.mapUI.sendEvent("cad::requests::focus",{requestID:e}),this.render()},getPlayerGroupId(){return this.session.groupId||""},getCurrentGroup(){const e=this.getPlayerGroupId();return this.groups.find(t=>t.groupId===e)||null},normalizeCollection:e=>Array.isArray(e)?e:e&&"object"==typeof e?Object.values(e):[],canDispatch(){return!!this.session.isDispatcher},isDispatchMode(){return"dispatch"===this.mode},isDispatchMapMode(){return"dispatch"===this.mode&&"map"===this.dispatchView},isLeader(){return!!this.session.isLeader},renderContracts(){const e=document.getElementById("taskList");if(!e)return;if(this.isDispatchMapMode()){if(!this.contracts.length)return void(e.innerHTML='

No contracts are currently available.

');const t=this.contracts.slice().sort((e,t)=>{const s="unassigned"===(e.assignmentState||"unassigned")?0:1,a="unassigned"===(t.assignmentState||"unassigned")?0:1;if(s!==a)return s-a;const n=e.taskId||e.taskID||"",i=t.taskId||t.taskID||"";return n.localeCompare(i)});return void(e.innerHTML=t.map(e=>{const t=e.taskId||e.taskID||"",s=Array.isArray(e.position)?e.position:[0,0,0],a=e.assignedGroupId||"",n=e.assignmentState||"unassigned",i=this.groups.find(e=>e.groupId===a),r=t===this.selectedDispatchTaskId,o="unassigned"===n?"Unassigned":`${n}: ${i?i.callsign:a||"Unknown"}`;return`\n \n
\n ${e.title||t}\n ${this.formatTypeLabel(e)}\n
\n

${e.description||""}

\n
\n ${o}\n ${window.mapUI.formatPosition(s)}\n
\n \n `}).join(""))}const t=this.getPlayerGroupId(),s=this.contracts.filter(e=>(e.assignedGroupId||"")===t);s.length?e.innerHTML=s.map(e=>{const s=e.taskId||e.taskID||"",a=Array.isArray(e.position)?e.position:[0,0,0],n=e.assignedGroupId||"",i=e.assignmentState||"unassigned",r=this.groups.find(e=>e.groupId===n),o=this.isLeader()&&n===t;return`\n
\n
\n ${e.title||s}\n ${this.formatTypeLabel(e)}\n
\n

${e.description||""}

\n
\n ${"unassigned"===i?"Available":`${i}: ${r?r.callsign:n}`}\n ${window.mapUI.formatPosition(a)}\n
\n ${o&&"assigned"===i?`
\n \n \n
`:""}\n
\n `}).join(""):e.innerHTML='

No contract is currently assigned to your group.

'},renderRoster(){const e=document.getElementById("rosterList");if(!e)return;if(this.isDispatchMapMode())return this.groups.length?void(e.innerHTML=this.getSortedGroups().map(e=>{const t=(e.groupId||"")===this.selectedDispatchGroupId,s="danger"===(e.status||"");return`\n \n
\n ${e.callsign||e.groupId||"Unknown Group"}\n ${e.role||"group"}\n ${s?'Danger':""}\n
\n
\n Leader: ${e.leaderName||"Unknown"}\n Status: ${e.status||"unknown"}\n
\n
\n Members: ${this.normalizeCollection(e.members).length}\n Task: ${e.currentTaskId||"None"}\n
\n \n `}).join("")):void(e.innerHTML='

No active groups are currently available.

');const t=this.getCurrentGroup();if(!t)return void(e.innerHTML='

Your group is not currently available.

');const s=this.normalizeCollection(t.members),a="danger"===(t.status||"");s.length?e.innerHTML=`\n
\n
\n ${t.callsign||t.groupId||"Current Group"}\n ${s.length} member${1===s.length?"":"s"}\n ${a?'Danger':""}\n
\n
\n Leader: ${t.leaderName||"Unknown"}\n Status: ${t.status||"unknown"}\n
\n
\n Role: ${t.role||"unassigned"}\n Task: ${t.currentTaskId||"None"}\n
\n
\n ${s.map(e=>{const t=(e.lifeState||"unknown").replaceAll("_"," "),s=e.isLeader?'Leader':"";return`\n
\n
\n ${e.name||"Unknown Operator"}\n ${t}\n
\n
\n ${e.uid||"No UID"}\n ${s}\n
\n
\n `}).join("")}\n `:e.innerHTML='

No roster members are currently available.

'},renderActivity(){const e=document.getElementById("activityList");e&&(this.activity.length?e.innerHTML=this.activity.slice().reverse().slice(0,8).map(e=>`\n
\n
\n ${e.type||"activity"}\n ${Math.round(e.timestamp||0)}s\n
\n

${e.message||""}

\n
\n `).join(""):e.innerHTML='

No recent activity.

')},render(){this.updateDangerAlert(),this.updateRequestAlert(),this.syncLayoutState(),this.renderContracts(),this.renderRoster(),this.renderRequests(),this.renderActivity(),this.setActiveTab(this.activeTab)}},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..90ccdde --- /dev/null +++ b/arma/client/addons/cad/ui/_site/cad-topbar.css @@ -0,0 +1 @@ +body{background:0 0;grid-template-columns:auto minmax(0,1fr) auto auto auto;align-items:center;column-gap:16px;height:60px;padding:0 16px;display:grid;position:absolute;top:0;left:0;right:0;overflow:visible}body[data-mode=operations]{grid-template-columns:auto minmax(0,1fr) auto auto}body[data-mode=dispatch]{grid-template-columns:auto minmax(0,1fr) auto auto auto}body:before{content:"";height:60px;box-shadow:none;-webkit-backdrop-filter:blur(18px);z-index:0;pointer-events:none;background:linear-gradient(90deg,#10161ff5,#131a24f0 55%,#0f141cf5);border-bottom:none;position:absolute;inset:0 0 auto}body>*{z-index:1;position:relative}.logo{color:var(--accent);text-transform:uppercase;letter-spacing:.08em;text-shadow:0 1px 12px #00000059;font-size:15px;font-weight:650}.header-main{align-items:center;gap:12px;min-width:0;display:flex}.title-block{flex-direction:column;flex:none;gap:1px;min-width:0;display:flex}.title-kicker{color:#dae3ec8f;text-transform:uppercase;letter-spacing:.12em;font-size:10px}.title-main{color:#f5f8ffeb;font-size:15px;font-weight:600}.operator-strip{flex:auto;align-items:center;gap:8px;min-width:0;display:flex}.operator-strip.is-hidden,.operator-controls.is-hidden{display:none}.operator-info{flex-direction:column;gap:0;min-width:88px;display:flex}.operator-label{color:#dae3ec80;text-transform:uppercase;letter-spacing:.12em;font-size:9px}.operator-info strong{color:#f5f8ffe6;font-size:12px;font-weight:550}.operator-controls{align-items:center;gap:6px;min-width:0;display:flex}.operator-select{min-width:92px;max-width:112px;color:var(--text);background:#0e141cf5;border:1px solid #ffffff24;padding:5px 8px;font-size:11px}.btn-operator{text-transform:uppercase;letter-spacing:.08em;min-width:84px;font-size:10px}.mode-controls{justify-self:end;align-items:center;gap:8px;display:flex}.mode-controls.is-hidden{display:none}.dispatch-view-controls{justify-self:end;align-items:center;gap:6px;display:flex}.dispatch-view-controls.is-hidden{display:none}.controls{justify-self:end;align-items:center;gap:8px;display:flex}.mode-text{color:#e9f1f8b8;text-transform:uppercase;letter-spacing:.1em;font-size:10px}.mode-switch{align-items:center;width:54px;height:28px;display:inline-flex;position:relative}.mode-switch input{opacity:0;pointer-events:none;position:absolute}.mode-slider{background:#161d27eb;border:1px solid #ffffff24;border-radius:999px;width:54px;height:28px;transition:border-color .16s,background .16s;position:relative;box-shadow:inset 0 1px 10px #00000038}.mode-slider:after{content:"";background:linear-gradient(#edf4fbfa,#bdcdddeb);border-radius:50%;width:20px;height:20px;transition:transform .16s,background .16s;position:absolute;top:3px;left:3px;box-shadow:0 4px 12px #00000042}.mode-switch input:checked+.mode-slider{background:#0e2538f2;border-color:#5bbbff6b}.mode-switch input:checked+.mode-slider:after{background:linear-gradient(#83d4fffa,#48aae7f0);transform:translate(26px)}.btn-close{min-width:42px}.btn-dispatch-view{text-transform:uppercase;letter-spacing:.08em;min-width:66px;padding:6px 10px;font-size:10px}.btn-icon{justify-content:center;align-items:center;width:34px;min-width:34px;height:30px;padding:0;font-size:16px;line-height:1;display:inline-flex}.btn-refresh{width:40px;min-width:40px;font-size:17px;font-weight:600}.btn-dispatch-view.is-active{color:var(--accent);background:#0f283af5;border-color:#5bbbff6b}.btn-close{font-size:14px}body{pointer-events:none}body .logo,body .title-block,body .operator-strip,body .operator-controls,body .mode-controls,body .dispatch-view-controls,body .controls,body .mode-switch,body .mode-switch *,body button,body select,body label{pointer-events:auto} \ 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..6d3986f --- /dev/null +++ b/arma/client/addons/cad/ui/_site/cad-topbar.js @@ -0,0 +1 @@ +window.cadTopbar={mode:"operations",dispatchView:"board",currentGroup:null,session:{},init(){document.getElementById("btnClose").addEventListener("click",()=>{window.mapUI.sendEvent("map::close",null)}),document.getElementById("modeToggle").addEventListener("change",e=>{window.mapUI.sendEvent("cad::mode::set",{mode:e.target.checked?"dispatch":"operations"})}),document.getElementById("dispatchRefreshBtn").addEventListener("click",()=>{window.mapUI.sendEvent("cad::refresh",{})}),document.getElementById("dispatchBoardBtn").addEventListener("click",()=>{window.mapUI.sendEvent("cad::dispatchView::set",{dispatchView:"board"})}),document.getElementById("dispatchMapBtn").addEventListener("click",()=>{window.mapUI.sendEvent("cad::dispatchView::set",{dispatchView:"map"})}),document.getElementById("operatorRoleBtn").addEventListener("click",()=>{this.currentGroup&&window.mapUI.sendEvent("cad::groups::role",{groupID:this.currentGroup.groupId||"",role:document.getElementById("operatorRoleSelect").value})}),document.getElementById("operatorStatusBtn").addEventListener("click",()=>{this.currentGroup&&window.mapUI.sendEvent("cad::groups::status",{groupID:this.currentGroup.groupId||"",status:document.getElementById("operatorStatusSelect").value})}),window.mapUI.sendEvent("cad::topbar::ready",{})},formatLocation(e){const t=Array.isArray(e?.position)?e.position:[0,0,0];return window.mapUI.formatPosition(t)},receiveState(e){this.session=e&&e.session&&"object"==typeof e.session?e.session:{},this.mode=e&&"string"==typeof e.mode?e.mode:"operations",this.dispatchView=e&&"string"==typeof e.dispatchView?e.dispatchView:"board",this.currentGroup=e&&e.currentGroup&&"object"==typeof e.currentGroup?e.currentGroup:null;const t=document.getElementById("modeControls"),o=!!this.session.isDispatcher,s=!(!this.currentGroup||!this.session.isLeader&&!this.session.isDispatcher),n=document.getElementById("operatorStrip"),d=document.getElementById("operatorControls"),i=document.getElementById("dispatchViewControls"),r=document.getElementById("dispatchRefreshBtn"),a=document.getElementById("dispatchBoardBtn"),c=document.getElementById("dispatchMapBtn");t.classList.toggle("is-hidden",!o),i.classList.toggle("is-hidden",!o||"dispatch"!==this.mode),n.classList.toggle("is-hidden","operations"!==this.mode||!this.currentGroup),d.classList.toggle("is-hidden",!s),document.body.dataset.mode=this.mode,document.body.dataset.dispatcher=o?"true":"false",document.getElementById("modeToggle").checked="dispatch"===this.mode,a.classList.toggle("is-active","board"===this.dispatchView),c.classList.toggle("is-active","map"===this.dispatchView),r.title="dispatch"===this.mode?"Refresh dispatch board":"Refresh CAD",r.setAttribute("aria-label","dispatch"===this.mode?"Refresh dispatch board":"Refresh CAD"),document.getElementById("operatorGroupName").textContent=this.currentGroup?this.currentGroup.callsign||this.currentGroup.groupId||"Current Group":"No Group",document.getElementById("operatorLocation").textContent=this.currentGroup?this.formatLocation(this.currentGroup):"Unavailable",this.currentGroup&&(document.getElementById("operatorRoleSelect").value=this.currentGroup.role||"infantry",document.getElementById("operatorStatusSelect").value=this.currentGroup.status||"available")}},window.cadTopbar.init(); \ No newline at end of file diff --git a/arma/client/addons/cad/ui/_site/dispatcher.html b/arma/client/addons/cad/ui/_site/dispatcher.html new file mode 100644 index 0000000..a3b7124 --- /dev/null +++ b/arma/client/addons/cad/ui/_site/dispatcher.html @@ -0,0 +1 @@ +

Dispatch Dashboard

Operational Board

Open Contracts 0
Assigned Contracts 0
Active Groups 0
Open Requests 0
Groups In Danger 0

Available Contracts

Assigned Contracts

Group Board

Requests & Activity

\ 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..5afd6e0 --- /dev/null +++ b/arma/client/addons/cad/ui/_site/sidepanel.html @@ -0,0 +1 @@ +

CAD System

Contracts

Loading contracts...

Roster

Loading roster...

Support Requests

No support requests.

Activity

No recent activity.

\ 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..47eb58f --- /dev/null +++ b/arma/client/addons/cad/ui/_site/topbar.html @@ -0,0 +1 @@ +
Cad Systems FORGE Command & Dispatch
\ 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..061c255 --- /dev/null +++ b/arma/client/addons/cad/ui/src/bottombar.html @@ -0,0 +1,49 @@ + + + + + + + CAD Systems by IDS + v1.0.0 + + + + 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..1afc0b7 --- /dev/null +++ b/arma/client/addons/cad/ui/src/bottombar.js @@ -0,0 +1,7 @@ +window.CADBottombar = window.CADBottombar || { + init() { + return true; + }, +}; + +window.CADBottombar.init(); diff --git a/arma/client/addons/cad/ui/src/dispatcher.html b/arma/client/addons/cad/ui/src/dispatcher.html new file mode 100644 index 0000000..bfce3dd --- /dev/null +++ b/arma/client/addons/cad/ui/src/dispatcher.html @@ -0,0 +1,372 @@ + + + + + + +
+
+
+

Dispatch Dashboard

+

Operational Board

+
+
+ +
+ + + +
+
+ Open Contracts + 0 +
+
+ Assigned Contracts + 0 +
+
+ Active Groups + 0 +
+
+ Open Requests + 0 +
+
+ Groups In Danger + 0 +
+
+ +
+
+
+

Available Contracts

+ +
+
+
+ +
+
+

Assigned Contracts

+
+
+
+ +
+
+

Group Board

+
+
+
+ +
+
+

Requests & Activity

+
+
+
+
+ + + + + + +
+ + + + diff --git a/arma/client/addons/cad/ui/src/dispatcher/formatters.js b/arma/client/addons/cad/ui/src/dispatcher/formatters.js new file mode 100644 index 0000000..c78a92b --- /dev/null +++ b/arma/client/addons/cad/ui/src/dispatcher/formatters.js @@ -0,0 +1,120 @@ +window.cadDispatcherFormatters = { + getDangerGroups() { + return this.groups.filter((group) => (group.status || "") === "danger"); + }, + getSupportAlertRequests() { + return this.requests.filter((request) => + ["medevac_9line", "fire_support", "air_support"].includes( + request.type || "", + ), + ); + }, + buildSupportAlertMessage() { + const alertRequests = this.getSupportAlertRequests(); + if (!alertRequests.length) { + return ""; + } + + const labels = alertRequests.map((request) => { + const groupLabel = + request.groupCallsign || request.groupId || "Unknown Group"; + const typeLabel = this.getRequestTypeLabel( + request.type || "request", + ); + return `${groupLabel} ${typeLabel}`; + }); + + return `Support request alert: ${labels.join(", ")}`; + }, + getSortedGroups() { + return this.groups.slice().sort((left, right) => { + const leftDanger = (left.status || "") === "danger" ? 0 : 1; + const rightDanger = (right.status || "") === "danger" ? 0 : 1; + + if (leftDanger !== rightDanger) { + return leftDanger - rightDanger; + } + + const leftCallsign = left.callsign || left.groupId || ""; + const rightCallsign = right.callsign || right.groupId || ""; + return leftCallsign.localeCompare(rightCallsign); + }); + }, + isDispatchOrder(entry) { + return ( + !!entry.isDispatchOrder || (entry.type || "") === "dispatch_order" + ); + }, + formatTypeLabel(entry) { + const typeLabel = (entry.type || "task").replaceAll("_", " "); + return this.isDispatchOrder(entry) ? "dispatch order" : typeLabel; + }, + getRequestTypeLabel(typeID) { + switch (typeID) { + case "medevac_9line": + return "9-Line MEDEVAC"; + case "ace_lace": + return "ACE/LACE"; + case "fire_support": + return "Fire Support"; + case "air_support": + return "Air Support"; + case "logreq": + return "LOGREQ"; + default: + return (typeID || "request").replaceAll("_", " "); + } + }, + buildGroupOptions(selectedGroupID) { + return this.getSortedGroups() + .map((group) => { + const groupID = group.groupId || ""; + return ``; + }) + .join(""); + }, + formatRequestFieldLabel(fieldID) { + return (fieldID || "field") + .replaceAll("_", " ") + .replace(/\b\w/g, (character) => character.toUpperCase()); + }, + formatRequestFieldValue(value) { + if (Array.isArray(value)) { + return value.join(", "); + } + + if (value && typeof value === "object") { + return JSON.stringify(value); + } + + const text = String(value ?? "").trim(); + return text || "Not provided"; + }, + buildRequestOrderNote(request) { + const typeLabel = this.getRequestTypeLabel(request.type || "request"); + const groupLabel = + request.groupCallsign || request.groupId || "Unknown Group"; + const summary = (request.summary || "").trim(); + const fieldDetails = + request.fields && typeof request.fields === "object" + ? Object.entries(request.fields) + .map(([fieldID, value]) => { + const fieldValue = + this.formatRequestFieldValue(value); + if (fieldValue === "Not provided") { + return ""; + } + + return `${this.formatRequestFieldLabel(fieldID)} ${fieldValue}`; + }) + .filter(Boolean) + : []; + const details = fieldDetails.length + ? fieldDetails + : [summary].filter(Boolean); + + return details.length + ? `${typeLabel} requested by ${groupLabel}. ${details.join(" | ")}` + : `${typeLabel} requested by ${groupLabel}.`; + }, +}; diff --git a/arma/client/addons/cad/ui/src/dispatcher/index.js b/arma/client/addons/cad/ui/src/dispatcher/index.js new file mode 100644 index 0000000..a41293c --- /dev/null +++ b/arma/client/addons/cad/ui/src/dispatcher/index.js @@ -0,0 +1,274 @@ +const dispatcherFormatters = window.cadDispatcherFormatters || {}; +const dispatcherModals = window.cadDispatcherModals || {}; +const dispatcherRender = window.cadDispatcherRender || {}; + +window.cadDispatcher = { + contracts: [], + requests: [], + groups: [], + activity: [], + session: {}, + editingGroupId: "", + viewingRequestId: "", + convertingRequestId: "", + statuses: [ + "available", + "en_route", + "on_task", + "holding", + "danger", + "unavailable", + ], + roles: ["infantry", "recon", "armor", "air", "logistics", "support"], + ...dispatcherFormatters, + ...dispatcherModals, + ...dispatcherRender, + init() { + document + .getElementById("dispatcherCreateOrderBtn") + .addEventListener("click", () => { + this.openOrderModal(); + }); + + document + .getElementById("dispatcherGroupModalCloseBtn") + .addEventListener("click", () => { + this.closeGroupModal(); + }); + + document + .getElementById("dispatcherGroupModalSaveBtn") + .addEventListener("click", () => { + this.applyGroupUpdates(); + }); + + document + .querySelector("#dispatcherGroupModal .dispatch-modal-backdrop") + .addEventListener("click", () => { + this.closeGroupModal(); + }); + + document + .getElementById("dispatcherOrderModalCloseBtn") + .addEventListener("click", () => { + this.closeOrderModal(); + }); + + document + .getElementById("dispatcherOrderModalSaveBtn") + .addEventListener("click", () => { + this.createDispatchOrder(); + }); + + document + .querySelector("#dispatcherOrderModal .dispatch-modal-backdrop") + .addEventListener("click", () => { + this.closeOrderModal(); + }); + + document + .getElementById("dispatcherRequestModalCloseBtn") + .addEventListener("click", () => { + this.closeRequestModal(); + }); + + document + .getElementById("dispatcherRequestModalDoneBtn") + .addEventListener("click", () => { + this.closeRequestModal(); + }); + + document + .getElementById("dispatcherRequestConvertBtn") + .addEventListener("click", () => { + this.convertViewedRequestToOrder(); + }); + + document + .querySelector("#dispatcherRequestModal .dispatch-modal-backdrop") + .addEventListener("click", () => { + this.closeRequestModal(); + }); + + window.mapUI.sendEvent("cad::dispatcher::ready", {}); + }, + receiveHydrate(payload) { + this.contracts = Array.isArray(payload.contracts) + ? payload.contracts + : []; + this.requests = Array.isArray(payload.requests) ? payload.requests : []; + this.groups = Array.isArray(payload.groups) ? payload.groups : []; + this.activity = Array.isArray(payload.activity) ? payload.activity : []; + this.session = + payload.session && typeof payload.session === "object" + ? payload.session + : {}; + + const statusEl = document.getElementById("dispatcherStatusMessage"); + if ( + statusEl && + (!statusEl.dataset.type || statusEl.dataset.type === "info") + ) { + this.setStatus("", ""); + } + + this.syncOpenModal(); + this.syncOrderModal(); + this.syncRequestModal(); + this.render(); + }, + setStatus(message, type) { + const statusEl = document.getElementById("dispatcherStatusMessage"); + if (!statusEl) { + return; + } + + statusEl.textContent = message || ""; + statusEl.dataset.type = type || ""; + }, + createDispatchOrder() { + const assigneeGroupID = document.getElementById( + "dispatcherOrderAssigneeSelect", + ).value; + const targetGroupID = document.getElementById( + "dispatcherOrderTargetSelect", + ).value; + const priority = document.getElementById( + "dispatcherOrderPrioritySelect", + ).value; + const note = document.getElementById("dispatcherOrderNoteInput").value; + const sourceRequest = this.convertingRequestId + ? this.requests.find( + (entry) => + (entry.requestId || "") === this.convertingRequestId, + ) || null + : null; + + if (!assigneeGroupID || !targetGroupID) { + this.setStatus( + "Select both an assignee and a target group.", + "error", + ); + return; + } + + if (assigneeGroupID === targetGroupID) { + this.setStatus( + "Assignee and target groups must be different.", + "error", + ); + return; + } + + this.setStatus( + this.convertingRequestId + ? "Creating dispatch order from request..." + : "Creating dispatch order...", + "info", + ); + window.mapUI.sendEvent("cad::dispatchOrder::create", { + assigneeGroupID: assigneeGroupID, + targetGroupID: targetGroupID, + note: note.trim(), + priority: priority, + request: sourceRequest + ? { + requestId: sourceRequest.requestId || "", + type: sourceRequest.type || "", + title: sourceRequest.title || "", + summary: sourceRequest.summary || "", + fields: + sourceRequest.fields && + typeof sourceRequest.fields === "object" + ? sourceRequest.fields + : {}, + } + : {}, + }); + + this.closeOrderModal(); + }, + assignTask(taskID) { + const selector = document.getElementById( + `dispatcher-assign-group-${taskID}`, + ); + if (!selector || !selector.value) { + this.setStatus( + "Select a group before assigning a contract.", + "error", + ); + return; + } + + this.setStatus("Submitting assignment...", "info"); + window.mapUI.sendEvent("cad::tasks::assign", { + taskID: taskID, + groupID: selector.value, + note: "", + }); + }, + applyGroupUpdates() { + if (!this.editingGroupId) { + return; + } + + const group = this.groups.find( + (entry) => entry.groupId === this.editingGroupId, + ); + if (!group) { + this.closeGroupModal(); + return; + } + + const roleValue = document.getElementById( + "dispatcherModalRoleSelect", + ).value; + const statusValue = document.getElementById( + "dispatcherModalStatusSelect", + ).value; + const nextRole = + roleValue && roleValue !== (group.role || "") ? roleValue : ""; + const nextStatus = + statusValue && statusValue !== (group.status || "") + ? statusValue + : ""; + const hasChanges = nextRole || nextStatus; + + if (!hasChanges) { + this.setStatus("No group changes to save.", "info"); + this.closeGroupModal(); + return; + } + + this.setStatus("Updating group profile...", "info"); + window.mapUI.sendEvent("cad::groups::profile", { + groupID: this.editingGroupId, + role: nextRole, + status: nextStatus, + }); + + this.closeGroupModal(); + }, + closeDispatchOrder(taskID) { + if (!taskID) { + return; + } + + this.setStatus("Closing dispatch order...", "info"); + window.mapUI.sendEvent("cad::dispatchOrder::close", { + taskID: taskID, + }); + }, + closeSupportRequest(requestID) { + if (!requestID) { + return; + } + + this.setStatus("Closing support request...", "info"); + window.mapUI.sendEvent("cad::supportRequest::close", { + requestID: requestID, + }); + }, +}; + +window.cadDispatcher.init(); diff --git a/arma/client/addons/cad/ui/src/dispatcher/modals.js b/arma/client/addons/cad/ui/src/dispatcher/modals.js new file mode 100644 index 0000000..e053169 --- /dev/null +++ b/arma/client/addons/cad/ui/src/dispatcher/modals.js @@ -0,0 +1,269 @@ +window.cadDispatcherModals = { + openOrderModal() { + this.convertingRequestId = ""; + this.populateOrderModal(); + document.getElementById("dispatcherOrderModalTitle").textContent = + "Create Support Order"; + document + .getElementById("dispatcherOrderModal") + .classList.remove("is-hidden"); + }, + closeOrderModal() { + this.convertingRequestId = ""; + document.getElementById("dispatcherOrderNoteInput").value = ""; + document.getElementById("dispatcherOrderPrioritySelect").value = + "priority"; + document.getElementById("dispatcherOrderModalTitle").textContent = + "Create Support Order"; + document + .getElementById("dispatcherOrderModal") + .classList.add("is-hidden"); + }, + openRequestModal(requestID) { + const request = this.requests.find( + (entry) => entry.requestId === requestID, + ); + if (!request) { + return; + } + + this.viewingRequestId = requestID; + this.populateRequestModal(request); + document + .getElementById("dispatcherRequestModal") + .classList.remove("is-hidden"); + }, + closeRequestModal() { + this.viewingRequestId = ""; + document + .getElementById("dispatcherRequestModal") + .classList.add("is-hidden"); + }, + syncRequestModal() { + if (!this.viewingRequestId) { + return; + } + + const request = this.requests.find( + (entry) => entry.requestId === this.viewingRequestId, + ); + if (!request) { + this.closeRequestModal(); + return; + } + + this.populateRequestModal(request); + }, + populateRequestModal(request) { + const fields = + request.fields && typeof request.fields === "object" + ? Object.entries(request.fields) + : []; + const fieldsHTML = fields.length + ? fields + .map( + ([fieldID, value]) => ` +
+ ${this.formatRequestFieldLabel(fieldID)} + ${this.formatRequestFieldValue(value)} +
+ `, + ) + .join("") + : '

No submitted fields.

'; + + document.getElementById("dispatcherRequestTitle").textContent = + request.title || request.requestId || "Support Request"; + document.getElementById("dispatcherRequestPriority").textContent = ( + request.priority || "priority" + ).replaceAll("_", " "); + document.getElementById("dispatcherRequestGroup").textContent = + request.groupCallsign || request.groupId || "Unknown"; + document.getElementById("dispatcherRequestType").textContent = + this.getRequestTypeLabel(request.type || "request"); + document.getElementById("dispatcherRequestSummary").textContent = + request.summary || "No summary provided."; + document.getElementById("dispatcherRequestFields").innerHTML = + fieldsHTML; + }, + convertRequestToOrder(requestID) { + const request = this.requests.find( + (entry) => (entry.requestId || "") === requestID, + ); + if (!request) { + this.setStatus("Selected request is no longer available.", "error"); + return; + } + + const targetGroupID = request.groupId || ""; + if (!targetGroupID) { + this.setStatus( + "Selected request has no owning group to target.", + "error", + ); + return; + } + + const targetGroup = this.groups.find( + (group) => (group.groupId || "") === targetGroupID, + ); + if (!targetGroup) { + this.setStatus( + "Selected request group is no longer available.", + "error", + ); + return; + } + + this.convertingRequestId = requestID; + this.populateOrderModal({ + selectedAssigneeID: + this.getSortedGroups().find( + (group) => (group.groupId || "") !== targetGroupID, + )?.groupId || "", + selectedTargetID: targetGroupID, + note: this.buildRequestOrderNote(request), + priority: request.priority || "priority", + }); + document.getElementById("dispatcherOrderModalTitle").textContent = + "Create Order From Request"; + document + .getElementById("dispatcherOrderModal") + .classList.remove("is-hidden"); + this.setStatus("Preparing dispatch order from request...", "info"); + }, + convertViewedRequestToOrder() { + if (!this.viewingRequestId) { + return; + } + + const requestID = this.viewingRequestId; + this.closeRequestModal(); + this.convertRequestToOrder(requestID); + }, + populateOrderModal(options = {}) { + const sortedGroups = this.getSortedGroups(); + const assigneeSelect = document.getElementById( + "dispatcherOrderAssigneeSelect", + ); + const targetSelect = document.getElementById( + "dispatcherOrderTargetSelect", + ); + const noteInput = document.getElementById("dispatcherOrderNoteInput"); + const prioritySelect = document.getElementById( + "dispatcherOrderPrioritySelect", + ); + if (!assigneeSelect || !targetSelect) { + return; + } + + const selectedAssigneeID = options.selectedAssigneeID || ""; + const selectedTargetID = options.selectedTargetID || ""; + const fallbackAssignee = + selectedAssigneeID || + sortedGroups.find( + (group) => (group.groupId || "") !== selectedTargetID, + )?.groupId || + sortedGroups[0]?.groupId || + ""; + const fallbackTarget = + selectedTargetID || + sortedGroups.find( + (group) => (group.groupId || "") !== fallbackAssignee, + )?.groupId || + sortedGroups[0]?.groupId || + ""; + + assigneeSelect.innerHTML = this.buildGroupOptions(fallbackAssignee); + targetSelect.innerHTML = this.buildGroupOptions(fallbackTarget); + if (noteInput) { + noteInput.value = options.note || ""; + } + if (prioritySelect) { + prioritySelect.value = options.priority || "priority"; + } + }, + syncOrderModal() { + const modalEl = document.getElementById("dispatcherOrderModal"); + if (!modalEl || modalEl.classList.contains("is-hidden")) { + return; + } + + this.populateOrderModal({ + selectedAssigneeID: + document.getElementById("dispatcherOrderAssigneeSelect") + ?.value || "", + selectedTargetID: + document.getElementById("dispatcherOrderTargetSelect")?.value || + "", + note: + document.getElementById("dispatcherOrderNoteInput")?.value || + "", + priority: + document.getElementById("dispatcherOrderPrioritySelect") + ?.value || "priority", + }); + }, + openGroupModal(groupID) { + const group = this.groups.find((entry) => entry.groupId === groupID); + if (!group) { + return; + } + + this.editingGroupId = groupID; + document.getElementById("dispatcherModalGroupCallsign").textContent = + group.callsign || group.groupId || "Unknown"; + document.getElementById("dispatcherModalGroupLeader").textContent = + group.leaderName || "Unknown"; + document.getElementById("dispatcherModalGroupTask").textContent = + group.currentTaskId || "None"; + document.getElementById("dispatcherModalGroupOrg").textContent = + group.orgId || "default"; + document.getElementById("dispatcherModalRoleSelect").innerHTML = + this.roles + .map( + (role) => + ``, + ) + .join(""); + document.getElementById("dispatcherModalStatusSelect").innerHTML = + this.statuses + .map( + (status) => + ``, + ) + .join(""); + + document + .getElementById("dispatcherGroupModal") + .classList.remove("is-hidden"); + }, + closeGroupModal() { + this.editingGroupId = ""; + document + .getElementById("dispatcherGroupModal") + .classList.add("is-hidden"); + }, + syncOpenModal() { + if (!this.editingGroupId) { + return; + } + + const group = this.groups.find( + (entry) => entry.groupId === this.editingGroupId, + ); + if (!group) { + this.closeGroupModal(); + return; + } + + document.getElementById("dispatcherModalGroupCallsign").textContent = + group.callsign || group.groupId || "Unknown"; + document.getElementById("dispatcherModalGroupLeader").textContent = + group.leaderName || "Unknown"; + document.getElementById("dispatcherModalGroupTask").textContent = + group.currentTaskId || "None"; + document.getElementById("dispatcherModalGroupOrg").textContent = + group.orgId || "default"; + }, +}; diff --git a/arma/client/addons/cad/ui/src/dispatcher/render.js b/arma/client/addons/cad/ui/src/dispatcher/render.js new file mode 100644 index 0000000..022745e --- /dev/null +++ b/arma/client/addons/cad/ui/src/dispatcher/render.js @@ -0,0 +1,325 @@ +window.cadDispatcherRender = { + updateDangerAlert() { + const alertEl = document.getElementById("dispatcherDangerAlert"); + if (!alertEl) { + return; + } + + const dangerGroups = this.getDangerGroups(); + if (!dangerGroups.length) { + alertEl.textContent = ""; + alertEl.classList.add("is-hidden"); + return; + } + + const callsigns = dangerGroups.map( + (group) => group.callsign || group.groupId || "Unknown Group", + ); + alertEl.textContent = `Danger alert active: ${callsigns.join(", ")}`; + alertEl.classList.remove("is-hidden"); + }, + updateRequestAlert() { + const alertEl = document.getElementById("dispatcherRequestAlert"); + if (!alertEl) { + return; + } + + const alertMessage = this.buildSupportAlertMessage(); + if (!alertMessage) { + alertEl.textContent = ""; + alertEl.classList.add("is-hidden"); + return; + } + + alertEl.textContent = alertMessage; + alertEl.classList.remove("is-hidden"); + }, + buildGroupEditorButton(groupID) { + return ` + + `; + }, + buildCloseOrderButton(taskID) { + return ` + + `; + }, + buildCloseRequestButton(requestID) { + return ` + + `; + }, + buildConvertRequestButton(requestID) { + return ` + + `; + }, + renderMetrics() { + const assignedContracts = this.contracts.filter( + (entry) => (entry.assignmentState || "unassigned") !== "unassigned", + ); + const openContracts = this.contracts.filter( + (entry) => (entry.assignmentState || "unassigned") === "unassigned", + ); + const openRequests = this.requests.length; + const supportAlertRequests = this.getSupportAlertRequests(); + const dangerGroups = this.groups.filter( + (group) => (group.status || "") === "danger", + ); + + document.getElementById("metricOpenContracts").textContent = + openContracts.length; + document.getElementById("metricAssignedContracts").textContent = + assignedContracts.length; + document.getElementById("metricActiveGroups").textContent = + this.groups.length; + document.getElementById("metricOpenRequests").textContent = + openRequests; + document.getElementById("metricDangerGroups").textContent = + dangerGroups.length; + + const dangerMetricCard = document.getElementById( + "metricDangerGroupsCard", + ); + if (dangerMetricCard) { + dangerMetricCard.classList.toggle( + "is-danger", + dangerGroups.length > 0, + ); + } + + const requestMetricCard = document.getElementById( + "metricOpenRequestsCard", + ); + if (requestMetricCard) { + requestMetricCard.classList.toggle( + "is-warning", + supportAlertRequests.length > 0, + ); + } + }, + renderOpenContracts() { + const container = document.getElementById("dispatcherOpenContracts"); + const openContracts = this.contracts.filter( + (entry) => (entry.assignmentState || "unassigned") === "unassigned", + ); + + if (!openContracts.length) { + container.innerHTML = + '

No open contracts.

'; + return; + } + + const groupOptions = this.buildGroupOptions(""); + + container.innerHTML = openContracts + .map((task) => { + const taskId = task.taskId || task.taskID || ""; + const position = Array.isArray(task.position) + ? task.position + : [0, 0, 0]; + const targetGroup = this.groups.find( + (group) => group.groupId === (task.targetGroupId || ""), + ); + + return ` +
+
+ ${task.title || taskId} + ${this.formatTypeLabel(task)} +
+

${task.description || ""}

+
+ Unassigned + ${window.mapUI.formatPosition(position)} +
+
+ Target: ${targetGroup ? targetGroup.callsign : task.targetGroupCallsign || "None"} + Priority: ${(task.priority || "priority").replaceAll("_", " ")} +
+
+ + +
+
+ `; + }) + .join(""); + }, + renderAssignedContracts() { + const container = document.getElementById( + "dispatcherAssignedContracts", + ); + const assignedContracts = this.contracts.filter( + (entry) => (entry.assignmentState || "unassigned") !== "unassigned", + ); + + if (!assignedContracts.length) { + container.innerHTML = + '

No assigned contracts.

'; + return; + } + + container.innerHTML = assignedContracts + .map((task) => { + const taskId = task.taskId || task.taskID || ""; + const assignedGroup = this.groups.find( + (group) => group.groupId === (task.assignedGroupId || ""), + ); + const targetGroup = this.groups.find( + (group) => group.groupId === (task.targetGroupId || ""), + ); + const isDispatchOrder = this.isDispatchOrder(task); + + return ` +
+
+ ${task.title || taskId} + ${task.assignmentState || "assigned"} +
+

${task.description || ""}

+
+ Group: ${assignedGroup ? assignedGroup.callsign : task.assignedGroupId || "Unknown"} + Type: ${this.formatTypeLabel(task)} +
+
+ Target: ${targetGroup ? targetGroup.callsign : task.targetGroupCallsign || "None"} + Priority: ${(task.priority || "priority").replaceAll("_", " ")} +
+ ${isDispatchOrder ? `
${this.buildCloseOrderButton(taskId)}
` : ""} +
+ `; + }) + .join(""); + }, + renderGroups() { + const container = document.getElementById("dispatcherGroups"); + if (!this.groups.length) { + container.innerHTML = + '

No active groups available.

'; + return; + } + + container.innerHTML = this.getSortedGroups() + .map((group) => { + const isDanger = (group.status || "") === "danger"; + return ` +
+
+
+ ${group.callsign || group.groupId} + ${group.role || "group"} + ${isDanger ? 'Danger' : ""} +
+
+ ${this.buildGroupEditorButton(group.groupId)} +
+
+
+ Leader: ${group.leaderName || "Unknown"} + Status: ${group.status || "unknown"} +
+
+ Org: ${group.orgId || "default"} + Task: ${group.currentTaskId || "None"} +
+
+ `; + }) + .join(""); + }, + renderActivity() { + const container = document.getElementById("dispatcherActivity"); + const requestsHTML = this.requests.length + ? this.requests + .map( + (request) => ` +
+
+ ${request.title || request.requestId || "Support Request"} + ${(request.priority || "priority").replaceAll("_", " ")} +
+

${request.summary || ""}

+
+ Group: ${request.groupCallsign || request.groupId || "Unknown"} + ${this.getRequestTypeLabel(request.type || "request")} +
+
+ ${this.buildConvertRequestButton(request.requestId || "")} + ${this.buildCloseRequestButton(request.requestId || "")} +
+
+ `, + ) + .join("") + : '

No active support requests.

'; + + const activityHTML = this.activity.length + ? this.activity + .slice() + .reverse() + .slice(0, 8) + .map( + (entry) => ` +
+
+ ${entry.type || "activity"} + ${Math.round(entry.timestamp || 0)}s +
+

${entry.message || ""}

+
+ `, + ) + .join("") + : '

No recent activity.

'; + + container.innerHTML = ` +
+
Support Requests
+ ${requestsHTML} +
+
+
Recent Activity
+ ${activityHTML} +
+ `; + }, + render() { + this.updateDangerAlert(); + this.updateRequestAlert(); + this.renderMetrics(); + this.renderOpenContracts(); + this.renderAssignedContracts(); + this.renderGroups(); + this.renderActivity(); + }, +}; 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..d1b3813 --- /dev/null +++ b/arma/client/addons/cad/ui/src/shared.js @@ -0,0 +1,74 @@ +/** + * Shared JavaScript for Map UI + * Provides common utilities and state management across all UI components + */ + +window.mapUIState = { + layersPanelVisible: true, + sidePanelElement: null, +}; + +window.mapUI = { + formatGridCoordinate(value) { + return Math.round(Number(value) || 0) + .toString() + .padStart(4, "0"); + }, + formatPosition(position) { + const safePosition = Array.isArray(position) ? position : [0, 0, 0]; + return `X: ${this.formatGridCoordinate(safePosition[0])} Y: ${this.formatGridCoordinate(safePosition[1])}`; + }, + sendEvent(event, data) { + A3API.SendAlert(JSON.stringify({ event: event, data: data })); + }, + updateCoordinates(x, y) { + const coordDisplay = document.getElementById("coordsDisplay"); + if (coordDisplay) { + coordDisplay.textContent = this.formatPosition([x, y, 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..1838951 --- /dev/null +++ b/arma/client/addons/cad/ui/src/sidepanel.html @@ -0,0 +1,190 @@ + + + + + + +
+

CAD System

+
+
+
+ + +
+ + + + +
+
+
+
Contracts
+
+
+

Loading contracts...

+
+
+
+
+
Roster
+
+
+

Loading roster...

+
+
+
+
+
Support Requests
+
+
+

No support requests.

+
+
+
+
+
Activity
+
+
+

No recent activity.

+
+
+
+
+
+ + + + + + 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..d9febcf --- /dev/null +++ b/arma/client/addons/cad/ui/src/sidepanel.js @@ -0,0 +1,1238 @@ +window.cadTasks = { + contracts: [], + requests: [], + groups: [], + activity: [], + session: {}, + mode: "operations", + dispatchView: "board", + activeTab: "contracts", + selectedDispatchGroupId: "", + selectedDispatchTaskId: "", + selectedDispatchRequestId: "", + focusStatusTimer: null, + requestModalType: "", + statuses: [ + "available", + "en_route", + "on_task", + "holding", + "danger", + "unavailable", + ], + roles: ["infantry", "recon", "armor", "air", "logistics", "support"], + requestTypes: [ + { + id: "medevac_9line", + label: "9-Line MEDEVAC", + defaultPriority: "emergency", + fields: [ + { + id: "pickup_location", + label: "Line 1 Pickup Location", + type: "text", + defaultFromGroupPosition: true, + }, + { + id: "radio_freq", + label: "Line 2 Radio / Call Sign", + type: "text", + }, + { + id: "precedence", + label: "Line 3 Precedence", + type: "select", + options: [ + "urgent", + "urgent_surgical", + "priority", + "routine", + "convenience", + ], + }, + { + id: "special_equipment", + label: "Line 4 Special Equipment", + type: "select", + options: ["none", "hoist", "extraction", "ventilator"], + }, + { + id: "patient_type", + label: "Line 5 Patient Type", + type: "select", + options: ["litter", "ambulatory", "mixed"], + }, + { + id: "security", + label: "Line 6 Security", + type: "select", + options: [ + "secure", + "possible_enemy", + "enemy_in_area", + "hot", + ], + }, + { + id: "marking", + label: "Line 7 Marking", + type: "select", + options: ["panels", "smoke", "ir", "none", "other"], + }, + { + id: "patient_nationality", + label: "Line 8 Patient Nationality", + type: "select", + options: ["coalition", "civilian", "enemy", "epw", "mixed"], + }, + { + id: "terrain", + label: "Line 9 Terrain", + type: "select", + options: [ + "flat", + "restricted", + "slope", + "rooftop", + "wooded", + ], + }, + ], + }, + { + id: "ace_lace", + label: "ACE/LACE", + defaultPriority: "routine", + fields: [ + { id: "ammo", label: "Ammo", type: "textarea" }, + { id: "casualties", label: "Casualties", type: "textarea" }, + { id: "equipment", label: "Equipment", type: "textarea" }, + { id: "notes", label: "Notes", type: "textarea" }, + ], + }, + { + id: "fire_support", + label: "Fire Support", + defaultPriority: "priority", + fields: [ + { + id: "target_location", + label: "Target Location", + type: "text", + defaultFromGroupPosition: true, + }, + { + id: "target_description", + label: "Target Description", + type: "textarea", + }, + { + id: "requested_effect", + label: "Requested Effect", + type: "select", + options: [ + "suppress", + "destroy", + "illum", + "smoke", + "screen", + ], + }, + { id: "ordnance", label: "Requested Ordnance", type: "text" }, + { + id: "danger_close", + label: "Danger Close", + type: "select", + options: ["no", "yes"], + }, + { id: "remarks", label: "Remarks", type: "textarea" }, + ], + }, + { + id: "air_support", + label: "Air Support", + defaultPriority: "priority", + fields: [ + { + id: "target_location", + label: "Target Location", + type: "text", + defaultFromGroupPosition: true, + }, + { + id: "target_description", + label: "Target Description", + type: "textarea", + }, + { + id: "target_marking", + label: "Target Marking", + type: "select", + options: ["smoke", "ir", "laser", "grid", "visual"], + }, + { + id: "requested_effect", + label: "Requested Effect", + type: "select", + options: [ + "show_of_force", + "escort", + "suppress", + "destroy", + "recon", + ], + }, + { id: "remarks", label: "Remarks", type: "textarea" }, + ], + }, + { + id: "logreq", + label: "LOGREQ", + defaultPriority: "priority", + fields: [ + { + id: "category", + label: "Category", + type: "select", + options: [ + "ammo", + "medical", + "fuel", + "repair", + "vehicle", + "equipment", + "weapons", + "mixed", + ], + }, + { + id: "delivery_method", + label: "Delivery Method", + type: "select", + options: [ + "ground", + "airdrop", + "pickup", + "dispatch_discretion", + ], + }, + { + id: "delivery_location", + label: "Delivery Location", + type: "text", + defaultFromGroupPosition: true, + }, + { + id: "requested_items", + label: "Requested Items", + type: "textarea", + }, + { + id: "quantity", + label: "Quantity / Package", + type: "text", + }, + { + id: "remarks", + label: "Remarks", + type: "textarea", + }, + ], + }, + ], + init() { + document.querySelectorAll(".cad-tab").forEach((tab) => { + tab.addEventListener("click", () => { + this.setActiveTab(tab.dataset.tab || "contracts"); + }); + }); + + document + .getElementById("cadRequestModalCloseBtn") + .addEventListener("click", () => { + this.closeRequestModal(); + }); + + document + .getElementById("cadRequestModalSaveBtn") + .addEventListener("click", () => { + this.submitSupportRequest(); + }); + + document + .querySelector("#cadRequestModal .cad-modal-backdrop") + .addEventListener("click", () => { + this.closeRequestModal(); + }); + + window.ForgeBridge.on("cad::hydrate", (payload) => { + this.setHydratePayload(payload || {}); + }); + + window.ForgeBridge.on("cad::assignment::response", (payload) => { + this.handleServerResponse(!!payload.success, payload.message || ""); + }); + + window.ForgeBridge.on("cad::group::response", (payload) => { + this.handleServerResponse(!!payload.success, payload.message || ""); + }); + + window.ForgeBridge.on("cad::request::response", (payload) => { + this.handleServerResponse(!!payload.success, payload.message || ""); + }); + + window.ForgeBridge.ready({ loaded: true }); + }, + setActiveTab(tabName) { + this.activeTab = tabName || "contracts"; + + document.querySelectorAll(".cad-tab").forEach((tab) => { + tab.classList.toggle( + "is-active", + tab.dataset.tab === this.activeTab, + ); + }); + + document.querySelectorAll("[data-panel]").forEach((panel) => { + panel.classList.toggle( + "is-active", + panel.dataset.panel === this.activeTab, + ); + }); + }, + syncLayoutState() { + const tabsEl = document.querySelector(".cad-tabs"); + const contractsTab = document.getElementById("tabContractsBtn"); + const rosterTab = document.getElementById("tabRosterBtn"); + const requestsTab = document.getElementById("tabRequestsBtn"); + const activityTab = document.getElementById("tabActivityBtn"); + const contractsPanel = document.getElementById("contractsPanel"); + const rosterPanel = document.getElementById("rosterPanel"); + const requestsPanel = document.getElementById("requestsPanel"); + const activityPanel = document.getElementById("activityPanel"); + const contractsHeader = contractsPanel?.querySelector( + ".cad-section-header", + ); + const rosterHeader = rosterPanel?.querySelector(".cad-section-header"); + + if (this.isDispatchMapMode()) { + if (tabsEl) { + tabsEl.style.display = ""; + tabsEl.classList.remove("is-two-col"); + tabsEl.classList.add("is-three-col"); + } + if (contractsTab) { + contractsTab.style.display = ""; + } + if (rosterTab) { + rosterTab.textContent = "Groups"; + } + if (activityTab) { + activityTab.style.display = "none"; + } + if (requestsTab) { + requestsTab.style.display = ""; + } + if (activityPanel) { + activityPanel.classList.remove("is-active"); + activityPanel.style.display = "none"; + } + if (requestsPanel) { + requestsPanel.style.display = ""; + } + if (rosterPanel) { + rosterPanel.style.display = ""; + } + if (rosterHeader) { + rosterHeader.textContent = "Active Groups"; + } + if (contractsPanel) { + contractsPanel.style.display = ""; + } + if (contractsHeader) { + contractsHeader.textContent = "Contracts"; + } + if (!["contracts", "roster", "requests"].includes(this.activeTab)) { + this.activeTab = "contracts"; + } + return; + } + + if (tabsEl) { + tabsEl.style.display = ""; + tabsEl.classList.remove("is-three-col"); + tabsEl.classList.remove("is-two-col"); + } + if (contractsTab) { + contractsTab.style.display = ""; + } + if (rosterTab) { + rosterTab.textContent = "Roster"; + } + if (activityTab) { + activityTab.style.display = ""; + } + if (requestsTab) { + requestsTab.style.display = ""; + } + if (contractsPanel) { + contractsPanel.style.display = ""; + } + if (activityPanel) { + activityPanel.style.display = ""; + } + if (requestsPanel) { + requestsPanel.style.display = ""; + } + if (rosterPanel) { + rosterPanel.style.display = ""; + } + if (rosterHeader) { + rosterHeader.textContent = "Roster"; + } + if (contractsHeader) { + contractsHeader.textContent = "Contracts"; + } + }, + setHydratePayload(payload) { + this.contracts = Array.isArray(payload.contracts) + ? payload.contracts + : []; + this.requests = Array.isArray(payload.requests) ? payload.requests : []; + this.groups = Array.isArray(payload.groups) ? payload.groups : []; + this.activity = Array.isArray(payload.activity) ? payload.activity : []; + this.session = + payload.session && typeof payload.session === "object" + ? payload.session + : {}; + this.mode = + payload && typeof payload.mode === "string" + ? payload.mode + : "operations"; + this.dispatchView = + payload && typeof payload.dispatchView === "string" + ? payload.dispatchView + : "board"; + + const statusEl = document.getElementById("cadStatusMessage"); + if ( + statusEl && + (!statusEl.dataset.type || statusEl.dataset.type === "info") + ) { + this.setStatus("", ""); + } + + if ( + this.selectedDispatchGroupId && + !this.groups.some( + (group) => group.groupId === this.selectedDispatchGroupId, + ) + ) { + this.selectedDispatchGroupId = ""; + } + + if ( + this.selectedDispatchTaskId && + !this.contracts.some((task) => { + const taskId = task.taskId || task.taskID || ""; + return taskId === this.selectedDispatchTaskId; + }) + ) { + this.selectedDispatchTaskId = ""; + } + + if ( + this.selectedDispatchRequestId && + !this.requests.some( + (request) => + (request.requestId || "") === + this.selectedDispatchRequestId, + ) + ) { + this.selectedDispatchRequestId = ""; + } + + if ( + this.mode === "dispatch" && + this.dispatchView === "map" && + !["contracts", "roster", "requests"].includes(this.activeTab) + ) { + this.activeTab = "contracts"; + } + + this.render(); + }, + setStatus(message, type) { + const statusEl = document.getElementById("cadStatusMessage"); + if (!statusEl) { + return; + } + + statusEl.textContent = message || ""; + statusEl.dataset.type = type || ""; + }, + getDangerGroups() { + return this.groups.filter((group) => (group.status || "") === "danger"); + }, + getSupportAlertRequests() { + return this.requests.filter((request) => + ["medevac_9line", "fire_support", "air_support"].includes( + request.type || "", + ), + ); + }, + buildSupportAlertMessage() { + const alertRequests = this.getSupportAlertRequests(); + if (!alertRequests.length) { + return ""; + } + + const labels = alertRequests.map((request) => { + const groupLabel = + request.groupCallsign || request.groupId || "Unknown Group"; + const typeLabel = this.getRequestTypeLabel( + request.type || "request", + ); + return `${groupLabel} ${typeLabel}`; + }); + + return `Support request alert: ${labels.join(", ")}`; + }, + getCurrentGroupCoordinates() { + const currentGroup = this.getCurrentGroup(); + const position = Array.isArray(currentGroup?.position) + ? currentGroup.position + : [0, 0, 0]; + return window.mapUI.formatPosition(position); + }, + getSortedGroups() { + return this.groups.slice().sort((left, right) => { + const leftDanger = (left.status || "") === "danger" ? 0 : 1; + const rightDanger = (right.status || "") === "danger" ? 0 : 1; + + if (leftDanger !== rightDanger) { + return leftDanger - rightDanger; + } + + const leftCallsign = left.callsign || left.groupId || ""; + const rightCallsign = right.callsign || right.groupId || ""; + return leftCallsign.localeCompare(rightCallsign); + }); + }, + isDispatchOrder(entry) { + return ( + !!entry.isDispatchOrder || (entry.type || "") === "dispatch_order" + ); + }, + formatTypeLabel(entry) { + const typeLabel = (entry.type || "task").replaceAll("_", " "); + return this.isDispatchOrder(entry) ? "dispatch order" : typeLabel; + }, + getRequestDefinition(typeID) { + return this.requestTypes.find((entry) => entry.id === typeID) || null; + }, + getRequestTypeLabel(typeID) { + return this.getRequestDefinition(typeID)?.label || typeID; + }, + canSubmitSupportRequest() { + return this.mode === "operations" && this.isLeader(); + }, + openRequestModal(typeID) { + const definition = this.getRequestDefinition(typeID); + if (!definition) { + return; + } + + this.requestModalType = typeID; + document.getElementById("cadRequestModalTitle").textContent = + definition.label; + document.getElementById("cadRequestPrioritySelect").value = + definition.defaultPriority || "priority"; + this.renderRequestFields(definition); + document + .getElementById("cadRequestModal") + .classList.remove("is-hidden"); + }, + closeRequestModal() { + this.requestModalType = ""; + document.getElementById("cadRequestFields").innerHTML = ""; + document.getElementById("cadRequestModal").classList.add("is-hidden"); + }, + renderRequestFields(definition) { + const container = document.getElementById("cadRequestFields"); + if (!container || !definition) { + return; + } + + const coords = this.getCurrentGroupCoordinates(); + container.innerHTML = definition.fields + .map((field) => { + const defaultValue = field.defaultFromGroupPosition + ? coords + : ""; + + if (field.type === "select") { + return ` + + `; + } + + if (field.type === "textarea") { + return ` + + `; + } + + return ` + + `; + }) + .join(""); + }, + submitSupportRequest() { + const definition = this.getRequestDefinition(this.requestModalType); + if (!definition) { + return; + } + + const fields = {}; + definition.fields.forEach((field) => { + const input = document.getElementById( + `cadRequestField_${field.id}`, + ); + fields[field.id] = input ? String(input.value || "").trim() : ""; + }); + + const priority = document.getElementById( + "cadRequestPrioritySelect", + ).value; + this.setStatus("Submitting support request...", "info"); + window.mapUI.sendEvent("cad::supportRequest::submit", { + type: definition.id, + fields: fields, + priority: priority, + }); + this.closeRequestModal(); + }, + closeSupportRequest(requestID) { + if (!requestID) { + return; + } + + this.setStatus( + this.isDispatchMode() + ? "Closing support request..." + : "Cancelling support request...", + "info", + ); + window.mapUI.sendEvent("cad::supportRequest::close", { + requestID: requestID, + }); + }, + renderRequests() { + const listEl = document.getElementById("requestList"); + if (!listEl) { + return; + } + + if (this.isDispatchMapMode()) { + const dispatchRequests = this.requests + .slice() + .sort((left, right) => { + const leftTitle = left.title || left.requestId || ""; + const rightTitle = right.title || right.requestId || ""; + return leftTitle.localeCompare(rightTitle); + }); + + if (!dispatchRequests.length) { + listEl.innerHTML = + '

No support requests are currently active.

'; + return; + } + + listEl.innerHTML = dispatchRequests + .map((request) => { + const requestID = request.requestId || ""; + const position = Array.isArray(request.position) + ? request.position + : [0, 0, 0]; + const isSelected = + requestID === this.selectedDispatchRequestId; + const isWarning = [ + "medevac_9line", + "fire_support", + "air_support", + ].includes(request.type || ""); + + return ` + + `; + }) + .join(""); + return; + } + + const requestButtons = this.canSubmitSupportRequest() + ? ` +
+ ${this.requestTypes + .map( + (requestType) => ` + + `, + ) + .join("")} +
+ ` + : ""; + + if (!this.requests.length) { + listEl.innerHTML = ` + ${requestButtons} +

No support requests are currently active.

+ `; + return; + } + + listEl.innerHTML = ` + ${requestButtons} + ${this.requests + .map((request) => { + const isOwnGroupLeader = + this.isLeader() && + (request.groupId || "") === this.getPlayerGroupId(); + const canClose = this.canDispatch() || isOwnGroupLeader; + const requestActionLabel = this.isDispatchMode() + ? "Close" + : "Cancel"; + return ` +
+
+ ${request.title || this.getRequestTypeLabel(request.type || "")} + ${(request.priority || "priority").replaceAll("_", " ")} +
+

${request.summary || ""}

+
+ Group: ${request.groupCallsign || request.groupId || "Unknown"} + ${this.getRequestTypeLabel(request.type || "")} +
+ ${ + canClose + ? `
+ +
` + : "" + } +
+ `; + }) + .join("")} + `; + }, + updateDangerAlert() { + const alertEl = document.getElementById("cadDangerAlert"); + if (!alertEl) { + return; + } + + if (!this.isDispatchMapMode()) { + alertEl.textContent = ""; + alertEl.classList.add("is-hidden"); + return; + } + + const dangerGroups = this.getDangerGroups(); + if (!dangerGroups.length) { + alertEl.textContent = ""; + alertEl.classList.add("is-hidden"); + return; + } + + const callsigns = dangerGroups.map( + (group) => group.callsign || group.groupId || "Unknown Group", + ); + alertEl.textContent = `Danger alert active: ${callsigns.join(", ")}`; + alertEl.classList.remove("is-hidden"); + }, + updateRequestAlert() { + const alertEl = document.getElementById("cadRequestAlert"); + if (!alertEl) { + return; + } + + if (!this.isDispatchMapMode()) { + alertEl.textContent = ""; + alertEl.classList.add("is-hidden"); + return; + } + + const alertMessage = this.buildSupportAlertMessage(); + if (!alertMessage) { + alertEl.textContent = ""; + alertEl.classList.add("is-hidden"); + return; + } + + alertEl.textContent = alertMessage; + alertEl.classList.remove("is-hidden"); + }, + clearFocusStatusSoon(message) { + if (this.focusStatusTimer) { + window.clearTimeout(this.focusStatusTimer); + } + + this.focusStatusTimer = window.setTimeout(() => { + const statusEl = document.getElementById("cadStatusMessage"); + if (!statusEl) { + return; + } + + if ( + statusEl.dataset.type === "info" && + statusEl.textContent === message + ) { + this.setStatus("", ""); + } + }, 1800); + }, + handleServerResponse(success, message) { + this.setStatus( + message || + (success ? "CAD update succeeded." : "CAD update failed."), + success ? "success" : "error", + ); + }, + acknowledgeTask(taskID) { + this.setStatus("Acknowledging contract...", "info"); + window.mapUI.sendEvent("cad::tasks::acknowledge", { taskID: taskID }); + }, + declineTask(taskID) { + this.setStatus("Declining contract...", "info"); + window.mapUI.sendEvent("cad::tasks::decline", { taskID: taskID }); + }, + updateGroupStatus(groupID, status) { + this.setStatus("Updating group status...", "info"); + window.mapUI.sendEvent("cad::groups::status", { + groupID: groupID, + status: status, + }); + }, + updateGroupRole(groupID, role) { + this.setStatus("Updating group role...", "info"); + window.mapUI.sendEvent("cad::groups::role", { + groupID: groupID, + role: role, + }); + }, + focusGroup(groupID) { + const group = this.groups.find((entry) => entry.groupId === groupID); + if (!group) { + this.setStatus("Selected group is no longer available.", "error"); + return; + } + + this.selectedDispatchGroupId = groupID; + this.selectedDispatchTaskId = ""; + this.selectedDispatchRequestId = ""; + const statusMessage = `Centering map on ${group.callsign || group.groupId || "group"}...`; + this.setStatus(statusMessage, "info"); + this.clearFocusStatusSoon(statusMessage); + window.mapUI.sendEvent("cad::groups::focus", { + groupID: groupID, + }); + this.render(); + }, + focusTask(taskID) { + const task = this.contracts.find((entry) => { + const entryTaskID = entry.taskId || entry.taskID || ""; + return entryTaskID === taskID; + }); + if (!task) { + this.setStatus( + "Selected contract is no longer available.", + "error", + ); + return; + } + + this.selectedDispatchTaskId = taskID; + this.selectedDispatchGroupId = ""; + this.selectedDispatchRequestId = ""; + const statusMessage = `Centering map on ${task.title || taskID}...`; + this.setStatus(statusMessage, "info"); + this.clearFocusStatusSoon(statusMessage); + window.mapUI.sendEvent("cad::tasks::focus", { + taskID: taskID, + }); + this.render(); + }, + focusRequest(requestID) { + const request = this.requests.find( + (entry) => (entry.requestId || "") === requestID, + ); + if (!request) { + this.setStatus("Selected request is no longer available.", "error"); + return; + } + + const position = Array.isArray(request.position) + ? request.position + : []; + if (position.length < 2) { + this.setStatus("Selected request has no map position.", "error"); + return; + } + + this.selectedDispatchRequestId = requestID; + this.selectedDispatchGroupId = ""; + this.selectedDispatchTaskId = ""; + const statusMessage = `Centering map on ${request.title || requestID}...`; + this.setStatus(statusMessage, "info"); + this.clearFocusStatusSoon(statusMessage); + window.mapUI.sendEvent("cad::requests::focus", { + requestID: requestID, + }); + this.render(); + }, + getPlayerGroupId() { + return this.session.groupId || ""; + }, + getCurrentGroup() { + const currentGroupId = this.getPlayerGroupId(); + return ( + this.groups.find((group) => group.groupId === currentGroupId) || + null + ); + }, + normalizeCollection(value) { + if (Array.isArray(value)) { + return value; + } + + if (value && typeof value === "object") { + return Object.values(value); + } + + return []; + }, + canDispatch() { + return !!this.session.isDispatcher; + }, + isDispatchMode() { + return this.mode === "dispatch"; + }, + isDispatchMapMode() { + return this.mode === "dispatch" && this.dispatchView === "map"; + }, + isLeader() { + return !!this.session.isLeader; + }, + renderContracts() { + const listEl = document.getElementById("taskList"); + if (!listEl) { + return; + } + + if (this.isDispatchMapMode()) { + if (!this.contracts.length) { + listEl.innerHTML = + '

No contracts are currently available.

'; + return; + } + + const dispatchContracts = this.contracts + .slice() + .sort((left, right) => { + const leftAssigned = + (left.assignmentState || "unassigned") === "unassigned" + ? 0 + : 1; + const rightAssigned = + (right.assignmentState || "unassigned") === "unassigned" + ? 0 + : 1; + + if (leftAssigned !== rightAssigned) { + return leftAssigned - rightAssigned; + } + + const leftId = left.taskId || left.taskID || ""; + const rightId = right.taskId || right.taskID || ""; + return leftId.localeCompare(rightId); + }); + + listEl.innerHTML = dispatchContracts + .map((task) => { + const taskId = task.taskId || task.taskID || ""; + const position = Array.isArray(task.position) + ? task.position + : [0, 0, 0]; + const assignedGroupId = task.assignedGroupId || ""; + const assignmentState = + task.assignmentState || "unassigned"; + const assignedGroup = this.groups.find( + (group) => group.groupId === assignedGroupId, + ); + const isSelected = taskId === this.selectedDispatchTaskId; + const stateLabel = + assignmentState === "unassigned" + ? "Unassigned" + : `${assignmentState}: ${assignedGroup ? assignedGroup.callsign : assignedGroupId || "Unknown"}`; + + return ` + + `; + }) + .join(""); + return; + } + + const currentGroupId = this.getPlayerGroupId(); + const visibleContracts = this.contracts.filter( + (task) => (task.assignedGroupId || "") === currentGroupId, + ); + + if (!visibleContracts.length) { + listEl.innerHTML = + '

No contract is currently assigned to your group.

'; + return; + } + + listEl.innerHTML = visibleContracts + .map((task) => { + const taskId = task.taskId || task.taskID || ""; + const position = Array.isArray(task.position) + ? task.position + : [0, 0, 0]; + const assignedGroupId = task.assignedGroupId || ""; + const assignmentState = task.assignmentState || "unassigned"; + const assignedGroup = this.groups.find( + (group) => group.groupId === assignedGroupId, + ); + const isAssignedToLeader = + this.isLeader() && assignedGroupId === currentGroupId; + + return ` +
+
+ ${task.title || taskId} + ${this.formatTypeLabel(task)} +
+

${task.description || ""}

+
+ ${assignmentState === "unassigned" ? "Available" : `${assignmentState}: ${assignedGroup ? assignedGroup.callsign : assignedGroupId}`} + ${window.mapUI.formatPosition(position)} +
+ ${ + isAssignedToLeader && assignmentState === "assigned" + ? `
+ + +
` + : "" + } +
+ `; + }) + .join(""); + }, + renderRoster() { + const listEl = document.getElementById("rosterList"); + if (!listEl) { + return; + } + + if (this.isDispatchMapMode()) { + if (!this.groups.length) { + listEl.innerHTML = + '

No active groups are currently available.

'; + return; + } + + listEl.innerHTML = this.getSortedGroups() + .map((group) => { + const isSelected = + (group.groupId || "") === this.selectedDispatchGroupId; + const isDanger = (group.status || "") === "danger"; + return ` + + `; + }) + .join(""); + return; + } + + const currentGroup = this.getCurrentGroup(); + if (!currentGroup) { + listEl.innerHTML = + '

Your group is not currently available.

'; + return; + } + + const roster = this.normalizeCollection(currentGroup.members); + const isDanger = (currentGroup.status || "") === "danger"; + + if (!roster.length) { + listEl.innerHTML = + '

No roster members are currently available.

'; + return; + } + + listEl.innerHTML = ` +
+
+ ${currentGroup.callsign || currentGroup.groupId || "Current Group"} + ${roster.length} member${roster.length === 1 ? "" : "s"} + ${isDanger ? 'Danger' : ""} +
+
+ Leader: ${currentGroup.leaderName || "Unknown"} + Status: ${currentGroup.status || "unknown"} +
+
+ Role: ${currentGroup.role || "unassigned"} + Task: ${currentGroup.currentTaskId || "None"} +
+
+ ${roster + .map((member) => { + const lifeState = ( + member.lifeState || "unknown" + ).replaceAll("_", " "); + const leaderBadge = member.isLeader + ? 'Leader' + : ""; + + return ` +
+
+ ${member.name || "Unknown Operator"} + ${lifeState} +
+
+ ${member.uid || "No UID"} + ${leaderBadge} +
+
+ `; + }) + .join("")} + `; + }, + renderActivity() { + const listEl = document.getElementById("activityList"); + if (!listEl) { + return; + } + + if (!this.activity.length) { + listEl.innerHTML = + '

No recent activity.

'; + return; + } + + listEl.innerHTML = this.activity + .slice() + .reverse() + .slice(0, 8) + .map( + (entry) => ` +
+
+ ${entry.type || "activity"} + ${Math.round(entry.timestamp || 0)}s +
+

${entry.message || ""}

+
+ `, + ) + .join(""); + }, + render() { + this.updateDangerAlert(); + this.updateRequestAlert(); + this.syncLayoutState(); + this.renderContracts(); + this.renderRoster(); + this.renderRequests(); + this.renderActivity(); + this.setActiveTab(this.activeTab); + }, +}; + +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..b9468e2 --- /dev/null +++ b/arma/client/addons/cad/ui/src/styles/bottombar.css @@ -0,0 +1,40 @@ +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; +} + +.footer-brand, +.footer-version { + color: rgba(245, 248, 255, 0.8); + font-size: 12px; + text-shadow: 0 1px 10px rgba(0, 0, 0, 0.28); +} + +.footer-brand { + color: var(--accent); + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.footer-version { + color: rgba(245, 248, 255, 0.62); +} 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/dispatcher.css b/arma/client/addons/cad/ui/src/styles/dispatcher.css new file mode 100644 index 0000000..6b291d7 --- /dev/null +++ b/arma/client/addons/cad/ui/src/styles/dispatcher.css @@ -0,0 +1,562 @@ +html, +body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + overflow: hidden; + background: + radial-gradient( + circle at top left, + rgba(41, 69, 93, 0.18), + transparent 30% + ), + linear-gradient(180deg, rgba(9, 14, 20, 0.96), rgba(15, 22, 31, 0.98)); +} + +body { + color: var(--text); + font-family: var(--font); +} + +.dispatch-shell { + height: 100%; + display: flex; + flex-direction: column; + padding: 18px; + gap: 14px; +} + +.dispatch-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.dispatch-kicker { + margin: 0 0 4px; + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.1em; + font-size: 11px; + font-weight: 700; +} + +.dispatch-header h2 { + margin: 0; + font-size: 24px; + font-weight: 650; +} + +.dispatch-header button, +.dispatch-btn, +.dispatch-select { + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(24, 31, 40, 0.9); + color: var(--text); +} + +.dispatch-header button, +.dispatch-btn { + padding: 10px 14px; + cursor: pointer; +} + +.dispatch-btn-secondary { + background: rgba(53, 40, 39, 0.92); +} + +.dispatch-status { + min-height: 20px; + font-size: 13px; + color: rgba(233, 241, 248, 0.78); +} + +.dispatch-status[data-type="success"] { + color: #79d28a; +} + +.dispatch-status[data-type="error"] { + color: #ff8a80; +} + +.dispatch-danger-alert { + padding: 10px 12px; + border: 1px solid rgba(255, 107, 107, 0.38); + background: linear-gradient( + 90deg, + rgba(92, 18, 18, 0.94), + rgba(128, 29, 29, 0.82) + ); + color: #ffd4cf; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + animation: cad-danger-pulse 1.35s ease-in-out infinite; +} + +.dispatch-danger-alert.is-hidden { + display: none; +} + +.dispatch-warning-alert { + padding: 10px 12px; + border: 1px solid rgba(246, 198, 84, 0.42); + background: linear-gradient( + 90deg, + rgba(89, 64, 12, 0.94), + rgba(125, 92, 18, 0.84) + ); + color: #ffe9b2; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + animation: cad-warning-pulse 1.35s ease-in-out infinite; +} + +.dispatch-warning-alert.is-hidden { + display: none; +} + +.dispatch-metrics { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 12px; +} + +.metric-card { + padding: 14px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(13, 19, 26, 0.72); +} + +.metric-label { + display: block; + margin-bottom: 8px; + color: rgba(233, 241, 248, 0.6); + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 11px; +} + +.metric-card strong { + font-size: 28px; + font-weight: 700; +} + +.metric-card.is-danger { + border-color: rgba(255, 107, 107, 0.34); + background: linear-gradient( + 180deg, + rgba(74, 17, 17, 0.86), + rgba(22, 13, 16, 0.92) + ); + box-shadow: inset 0 0 0 1px rgba(255, 107, 107, 0.12); + animation: cad-danger-pulse 1.35s ease-in-out infinite; +} + +.metric-card.is-warning { + border-color: rgba(246, 198, 84, 0.34); + background: linear-gradient( + 180deg, + rgba(92, 65, 14, 0.86), + rgba(29, 22, 11, 0.92) + ); + box-shadow: inset 0 0 0 1px rgba(246, 198, 84, 0.12); + animation: cad-warning-pulse 1.35s ease-in-out infinite; +} + +.dispatch-grid { + flex: 1; + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + grid-auto-rows: minmax(0, 1fr); + gap: 14px; + min-height: 0; +} + +.dispatch-panel { + display: flex; + flex-direction: column; + min-height: 0; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(11, 17, 24, 0.78); + min-width: 0; +} + +.dispatch-panel-open { + grid-column: span 5; +} + +.dispatch-panel-assigned { + grid-column: span 7; +} + +.dispatch-panel-groups { + grid-column: span 8; +} + +.dispatch-panel-activity { + grid-column: span 4; +} + +.dispatch-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 14px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.dispatch-panel-header h3 { + margin: 0; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--accent); +} + +.dispatch-list { + flex: 1; + overflow: auto; + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px; +} + +.dispatch-inline-section { + display: flex; + flex-direction: column; + gap: 10px; +} + +.dispatch-inline-header { + color: var(--accent); + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.dispatch-card { + padding: 12px; + border: 1px solid rgba(255, 255, 255, 0.06); + background: rgba(19, 26, 34, 0.72); +} + +.dispatch-card-interactive { + cursor: pointer; +} + +.dispatch-card-interactive:hover { + border-color: rgba(91, 187, 255, 0.2); + background: rgba(23, 31, 40, 0.82); +} + +.dispatch-card-header, +.dispatch-meta { + display: flex; + justify-content: space-between; + gap: 10px; +} + +.dispatch-card-header-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.dispatch-card-header-main { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.dispatch-card-header { + margin-bottom: 8px; +} + +.dispatch-description { + margin: 0 0 10px; + line-height: 1.45; + color: rgba(241, 246, 251, 0.82); + font-size: 13px; +} + +.dispatch-meta { + margin-bottom: 10px; + font-size: 12px; + color: rgba(229, 237, 244, 0.7); +} + +.dispatch-badge { + padding: 3px 7px; + border: 1px solid rgba(91, 187, 255, 0.18); + background: rgba(16, 43, 61, 0.7); + color: var(--accent); + font-size: 11px; + text-transform: uppercase; +} + +.dispatch-alert-badge { + padding: 3px 7px; + border: 1px solid rgba(255, 107, 107, 0.44); + background: rgba(95, 23, 23, 0.88); + color: #ffd8d1; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.dispatch-icon-btn { + width: 32px; + height: 32px; + padding: 0; + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(24, 31, 40, 0.92); + color: var(--text); + cursor: pointer; +} + +.dispatch-icon-btn:hover { + background: rgba(32, 42, 52, 0.96); +} + +.dispatch-actions { + display: flex; + flex-direction: column; + gap: 8px; +} + +.dispatch-card.is-danger { + border-color: rgba(255, 107, 107, 0.34); + background: linear-gradient( + 180deg, + rgba(69, 20, 22, 0.78), + rgba(28, 17, 21, 0.92) + ); + box-shadow: inset 0 0 0 1px rgba(255, 107, 107, 0.1); + animation: cad-danger-pulse 1.35s ease-in-out infinite; +} + +.dispatch-card.is-danger .dispatch-meta, +.dispatch-card.is-danger .dispatch-description { + color: rgba(255, 232, 228, 0.82); +} + +.dispatch-card.is-warning { + border-color: rgba(246, 198, 84, 0.34); + background: linear-gradient( + 180deg, + rgba(86, 64, 17, 0.78), + rgba(34, 27, 16, 0.92) + ); + box-shadow: inset 0 0 0 1px rgba(246, 198, 84, 0.1); + animation: cad-warning-pulse 1.35s ease-in-out infinite; +} + +.dispatch-card.is-warning .dispatch-meta, +.dispatch-card.is-warning .dispatch-description { + color: rgba(255, 243, 214, 0.84); +} + +.dispatch-actions-split { + margin-top: 10px; +} + +.dispatch-select { + width: 100%; + padding: 9px 10px; +} + +.dispatch-textarea { + width: 100%; + min-height: 92px; + padding: 10px 12px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(24, 31, 40, 0.92); + color: var(--text); + font: inherit; + resize: vertical; + box-sizing: border-box; +} + +.placeholder-message { + padding: 18px; + text-align: center; + color: rgba(233, 241, 248, 0.6); +} + +.dispatch-modal { + position: fixed; + inset: 0; + z-index: 30; + display: flex; + align-items: center; + justify-content: center; + padding: 32px 24px; + box-sizing: border-box; +} + +.dispatch-modal.is-hidden { + display: none; +} + +.dispatch-modal-backdrop { + position: absolute; + inset: 0; + background: rgba(4, 8, 12, 0.72); +} + +.dispatch-modal-dialog { + position: relative; + display: flex; + flex-direction: column; + width: min(560px, calc(100% - 48px)); + max-height: calc(100vh - 64px); + margin: 0; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(11, 17, 24, 0.98); + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.42); +} + +.dispatch-modal-header, +.dispatch-modal-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 14px 16px; +} + +.dispatch-modal-header { + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.dispatch-modal-header h3 { + margin: 0; + font-size: 22px; + font-weight: 650; +} + +.dispatch-modal-body { + flex: 1; + min-height: 0; + padding: 16px; + overflow: auto; +} + +.dispatch-meta-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + margin-bottom: 18px; +} + +.dispatch-meta-grid strong { + display: block; + margin-top: 4px; + font-size: 14px; + font-weight: 600; +} + +.dispatch-modal-fields { + display: grid; + gap: 12px; +} + +.dispatch-field { + display: grid; + gap: 6px; +} + +.dispatch-field span { + font-size: 12px; + font-weight: 650; + text-transform: uppercase; + letter-spacing: 0.08em; + color: rgba(233, 241, 248, 0.7); +} + +.dispatch-modal-actions { + justify-content: flex-end; + border-top: 1px solid rgba(255, 255, 255, 0.08); +} + +.dispatch-detail-block, +.dispatch-detail-list { + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(19, 26, 34, 0.72); +} + +.dispatch-detail-block { + padding: 12px; + color: rgba(241, 246, 251, 0.82); + line-height: 1.45; + white-space: pre-wrap; +} + +.dispatch-detail-list { + display: grid; + gap: 1px; + overflow: hidden; +} + +.dispatch-detail-row { + display: grid; + grid-template-columns: minmax(0, 180px) minmax(0, 1fr); + gap: 12px; + padding: 10px 12px; + background: rgba(14, 20, 28, 0.92); +} + +.dispatch-detail-label { + color: rgba(233, 241, 248, 0.64); + font-size: 12px; + font-weight: 650; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.dispatch-detail-value { + color: rgba(241, 246, 251, 0.84); + line-height: 1.4; + word-break: break-word; + white-space: pre-wrap; +} + +@keyframes cad-danger-pulse { + 0%, + 100% { + box-shadow: + inset 0 0 0 1px rgba(255, 107, 107, 0.08), + 0 0 0 rgba(255, 107, 107, 0); + } + + 50% { + box-shadow: + inset 0 0 0 1px rgba(255, 141, 141, 0.22), + 0 0 18px rgba(255, 107, 107, 0.16); + } +} + +@keyframes cad-warning-pulse { + 0%, + 100% { + box-shadow: + inset 0 0 0 1px rgba(246, 198, 84, 0.08), + 0 0 0 rgba(246, 198, 84, 0); + } + + 50% { + box-shadow: + inset 0 0 0 1px rgba(251, 212, 118, 0.22), + 0 0 18px rgba(246, 198, 84, 0.16); + } +} 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..34f6d12 --- /dev/null +++ b/arma/client/addons/cad/ui/src/styles/sidepanel.css @@ -0,0 +1,554 @@ +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; +} + +.cad-tabs { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 5px; + margin-bottom: 12px; +} + +.cad-tabs.is-two-col { + grid-template-columns: repeat(2, 1fr); +} + +.cad-tabs.is-three-col { + grid-template-columns: repeat(3, 1fr); +} + +.cad-tab { + min-width: 0; + padding: 8px 7px; + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(20, 27, 33, 0.88); + color: rgba(243, 246, 249, 0.78); + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 10px; + white-space: nowrap; + cursor: pointer; +} + +.cad-tab:hover { + background: rgba(31, 40, 47, 0.94); + color: #f3f6f9; +} + +.cad-tab.is-active { + border-color: rgba(91, 187, 255, 0.42); + background: rgba(15, 40, 58, 0.96); + color: var(--accent); +} + +.cad-tab-panels { + min-height: 0; +} + +.cad-section { + display: none; +} + +.cad-section.is-active { + display: block; +} + +.cad-section-header { + margin-bottom: 8px; + color: var(--accent); + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.task-accept-btn, +.task-secondary-btn, +.cad-select { + width: 100%; + padding: 8px 10px; + border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(30, 37, 43, 0.9); + color: #f3f6f9; +} + +.task-accept-btn, +.task-secondary-btn { + cursor: pointer; +} + +.task-accept-btn:hover, +.task-secondary-btn:hover { + background: rgba(46, 57, 66, 0.95); +} + +.task-accept-btn:disabled, +.task-secondary-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; +} + +.cad-modal { + position: fixed; + inset: 0; + z-index: 40; +} + +.cad-modal.is-hidden { + display: none; +} + +.cad-modal-backdrop { + position: absolute; + inset: 0; + background: rgba(4, 8, 12, 0.76); +} + +.cad-modal-dialog { + position: relative; + width: min(480px, calc(100% - 28px)); + margin: 32px auto 0; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(11, 17, 24, 0.98); + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.42); +} + +.cad-modal-header, +.cad-modal-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 14px; +} + +.cad-modal-header { + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.cad-modal-header h3 { + margin: 4px 0 0; + font-size: 18px; + font-weight: 650; +} + +.cad-modal-body { + padding: 14px; + max-height: 62vh; + overflow: auto; +} + +.cad-modal-fields { + display: grid; + gap: 10px; +} + +.cad-field { + display: grid; + gap: 6px; +} + +.cad-field span { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: rgba(233, 241, 248, 0.7); +} + +.cad-input, +.cad-textarea { + width: 100%; + padding: 8px 10px; + border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(30, 37, 43, 0.9); + color: #f3f6f9; + box-sizing: border-box; + font: inherit; +} + +.cad-textarea { + min-height: 74px; + resize: vertical; +} + +.cad-icon-btn { + width: 30px; + height: 30px; + padding: 0; + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(24, 31, 40, 0.92); + color: var(--text); + cursor: pointer; +} + +.cad-modal-actions { + justify-content: flex-end; + border-top: 1px solid rgba(255, 255, 255, 0.08); +} + +.cad-danger-alert { + margin-bottom: 10px; + padding: 8px 10px; + border: 1px solid rgba(255, 107, 107, 0.36); + background: linear-gradient( + 90deg, + rgba(92, 18, 18, 0.94), + rgba(128, 29, 29, 0.82) + ); + color: #ffd4cf; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + animation: cad-danger-pulse 1.35s ease-in-out infinite; +} + +.cad-danger-alert.is-hidden { + display: none; +} + +.cad-warning-alert { + margin-bottom: 10px; + padding: 8px 10px; + border: 1px solid rgba(246, 198, 84, 0.4); + background: linear-gradient( + 90deg, + rgba(89, 64, 12, 0.94), + rgba(125, 92, 18, 0.84) + ); + color: #ffe9b2; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + animation: cad-warning-pulse 1.35s ease-in-out infinite; +} + +.cad-warning-alert.is-hidden { + display: none; +} + +.task-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.cad-request-actions { + display: grid; + gap: 8px; +} + +.cad-request-btn { + text-align: left; +} + +.task-action-stack, +.task-action-row { + display: flex; + flex-direction: column; + gap: 8px; +} + +.task-action-row { + flex-direction: row; +} + +.task-card { + padding: 10px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(12, 16, 20, 0.62); +} + +.task-card.is-danger, +.roster-summary-card.is-danger { + border-color: rgba(255, 107, 107, 0.34); + background: linear-gradient( + 180deg, + rgba(69, 20, 22, 0.78), + rgba(28, 17, 21, 0.92) + ); + box-shadow: inset 0 0 0 1px rgba(255, 107, 107, 0.1); + animation: cad-danger-pulse 1.35s ease-in-out infinite; +} + +.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; +} + +.task-secondary-btn { + background: rgba(60, 48, 45, 0.92); +} + +.roster-summary-card { + padding: 10px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(16, 23, 29, 0.82); +} + +.task-alert-badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border: 1px solid rgba(255, 107, 107, 0.44); + background: rgba(95, 23, 23, 0.88); + color: #ffd8d1; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.roster-member-card { + background: rgba(12, 16, 20, 0.74); +} + +.dispatch-map-group-card { + width: 100%; + text-align: left; + appearance: none; + -webkit-appearance: none; + border-radius: 0; + color: var(--text); + font: inherit; + cursor: pointer; + transition: + border-color 120ms ease, + background 120ms ease, + transform 120ms ease; +} + +.dispatch-map-group-card strong { + color: var(--text); +} + +.dispatch-map-group-card .task-type { + color: var(--accent); + opacity: 0.9; +} + +.dispatch-map-group-card .task-meta { + color: var(--muted); + opacity: 1; +} + +.dispatch-map-group-card:hover { + border-color: rgba(91, 187, 255, 0.26); + background: rgba(18, 29, 38, 0.9); + transform: translateX(-2px); +} + +.dispatch-map-group-card.is-selected { + border-color: rgba(91, 187, 255, 0.52); + background: rgba(15, 40, 58, 0.92); + box-shadow: inset 0 0 0 1px rgba(91, 187, 255, 0.18); +} + +.dispatch-map-group-card.is-danger:not(.is-selected) { + border-color: rgba(255, 107, 107, 0.34); + background: linear-gradient( + 180deg, + rgba(69, 20, 22, 0.78), + rgba(28, 17, 21, 0.92) + ); +} + +.dispatch-map-group-card.is-danger .task-meta, +.roster-summary-card.is-danger .task-meta { + color: rgba(255, 232, 228, 0.82); +} + +.dispatch-map-card { + width: 100%; + text-align: left; + appearance: none; + -webkit-appearance: none; + border-radius: 0; + color: var(--text); + font: inherit; + cursor: pointer; + transition: + border-color 120ms ease, + background 120ms ease, + transform 120ms ease; +} + +.dispatch-map-card strong { + color: var(--text); +} + +.dispatch-map-card .task-type { + color: var(--accent); + opacity: 0.9; +} + +.dispatch-map-card .task-description { + color: var(--muted); +} + +.dispatch-map-card .task-meta { + color: var(--muted); + opacity: 1; +} + +.dispatch-map-card:hover { + border-color: rgba(91, 187, 255, 0.26); + background: rgba(18, 29, 38, 0.9); + transform: translateX(-2px); +} + +.dispatch-map-card.is-selected { + border-color: rgba(91, 187, 255, 0.52); + background: rgba(15, 40, 58, 0.92); + box-shadow: inset 0 0 0 1px rgba(91, 187, 255, 0.18); +} + +.dispatch-map-card.is-warning:not(.is-selected) { + border-color: rgba(246, 198, 84, 0.34); + background: linear-gradient( + 180deg, + rgba(86, 64, 17, 0.78), + rgba(34, 27, 16, 0.92) + ); +} + +.dispatch-map-card.is-warning .task-meta, +.dispatch-map-card.is-warning .task-description { + color: rgba(255, 243, 214, 0.84); +} + +.roster-leader-badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border: 1px solid rgba(91, 187, 255, 0.28); + background: rgba(15, 40, 58, 0.82); + color: var(--accent); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +@keyframes cad-danger-pulse { + 0%, + 100% { + box-shadow: + inset 0 0 0 1px rgba(255, 107, 107, 0.08), + 0 0 0 rgba(255, 107, 107, 0); + } + + 50% { + box-shadow: + inset 0 0 0 1px rgba(255, 141, 141, 0.22), + 0 0 14px rgba(255, 107, 107, 0.14); + } +} + +@keyframes cad-warning-pulse { + 0%, + 100% { + box-shadow: + inset 0 0 0 1px rgba(246, 198, 84, 0.08), + 0 0 0 rgba(246, 198, 84, 0); + } + + 50% { + box-shadow: + inset 0 0 0 1px rgba(251, 212, 118, 0.22), + 0 0 18px rgba(246, 198, 84, 0.16); + } +} 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..0f24d57 --- /dev/null +++ b/arma/client/addons/cad/ui/src/styles/topbar.css @@ -0,0 +1,296 @@ +body { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 60px; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto auto auto; + align-items: center; + column-gap: 16px; + padding: 0 16px; + background: transparent; + overflow: visible; +} + +body[data-mode="operations"] { + grid-template-columns: auto minmax(0, 1fr) auto auto; +} + +body[data-mode="dispatch"] { + grid-template-columns: auto minmax(0, 1fr) auto auto auto; +} + +body::before { + content: ""; + position: absolute; + inset: 0 0 auto 0; + height: 60px; + 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: none; + box-shadow: none; + backdrop-filter: blur(18px); + -webkit-backdrop-filter: blur(18px); + z-index: 0; + pointer-events: none; +} + +body > * { + position: relative; + z-index: 1; +} + +.logo { + color: var(--accent); + font-size: 15px; + font-weight: 650; + text-transform: uppercase; + letter-spacing: 0.08em; + text-shadow: 0 1px 12px rgba(0, 0, 0, 0.35); +} + +.header-main { + min-width: 0; + display: flex; + align-items: center; + gap: 12px; +} + +.title-block { + display: flex; + flex-direction: column; + gap: 1px; + min-width: 0; + flex: 0 0 auto; +} + +.title-kicker { + color: rgba(218, 227, 236, 0.56); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.12em; +} + +.title-main { + color: rgba(245, 248, 255, 0.92); + font-size: 15px; + font-weight: 600; +} + +.operator-strip { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + flex: 1 1 auto; +} + +.operator-strip.is-hidden, +.operator-controls.is-hidden { + display: none; +} + +.operator-info { + display: flex; + flex-direction: column; + min-width: 88px; + gap: 0; +} + +.operator-label { + color: rgba(218, 227, 236, 0.5); + font-size: 9px; + text-transform: uppercase; + letter-spacing: 0.12em; +} + +.operator-info strong { + color: rgba(245, 248, 255, 0.9); + font-size: 12px; + font-weight: 550; +} + +.operator-controls { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +.operator-select { + min-width: 92px; + max-width: 112px; + padding: 5px 8px; + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(14, 20, 28, 0.96); + color: var(--text); + font-size: 11px; +} + +.btn-operator { + min-width: 84px; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.mode-controls { + display: flex; + gap: 8px; + align-items: center; + justify-self: end; +} + +.mode-controls.is-hidden { + display: none; +} + +.dispatch-view-controls { + display: flex; + align-items: center; + gap: 6px; + justify-self: end; +} + +.dispatch-view-controls.is-hidden { + display: none; +} + +.controls { + display: flex; + gap: 8px; + align-items: center; + justify-self: end; +} + +.mode-text { + color: rgba(233, 241, 248, 0.72); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.1em; +} + +.mode-switch { + position: relative; + width: 54px; + height: 28px; + display: inline-flex; + align-items: center; +} + +.mode-switch input { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.mode-slider { + position: relative; + width: 54px; + height: 28px; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 999px; + background: rgba(22, 29, 39, 0.92); + box-shadow: inset 0 1px 10px rgba(0, 0, 0, 0.22); + transition: + border-color 0.16s ease, + background 0.16s ease; +} + +.mode-slider::after { + content: ""; + position: absolute; + top: 3px; + left: 3px; + width: 20px; + height: 20px; + border-radius: 50%; + background: linear-gradient( + 180deg, + rgba(237, 244, 251, 0.98), + rgba(189, 205, 221, 0.92) + ); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.26); + transition: + transform 0.16s ease, + background 0.16s ease; +} + +.mode-switch input:checked + .mode-slider { + border-color: rgba(91, 187, 255, 0.42); + background: rgba(14, 37, 56, 0.95); +} + +.mode-switch input:checked + .mode-slider::after { + transform: translateX(26px); + background: linear-gradient( + 180deg, + rgba(131, 212, 255, 0.98), + rgba(72, 170, 231, 0.94) + ); +} + +.btn-close { + min-width: 42px; +} + +.btn-dispatch-view { + min-width: 66px; + padding: 6px 10px; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.btn-icon { + min-width: 34px; + width: 34px; + height: 30px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + font-size: 16px; + line-height: 1; +} + +.btn-refresh { + min-width: 40px; + width: 40px; + font-size: 17px; + font-weight: 600; +} + +.btn-dispatch-view.is-active { + border-color: rgba(91, 187, 255, 0.42); + background: rgba(15, 40, 58, 0.96); + color: var(--accent); +} + +.btn-close { + font-size: 14px; +} + +body { + pointer-events: none; +} + +body .logo, +body .title-block, +body .operator-strip, +body .operator-controls, +body .mode-controls, +body .dispatch-view-controls, +body .controls, +body .mode-switch, +body .mode-switch *, +body button, +body select, +body label { + pointer-events: auto; +} 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..8736261 --- /dev/null +++ b/arma/client/addons/cad/ui/src/topbar.html @@ -0,0 +1,132 @@ + + + + + + + +
+
+ Cad Systems + FORGE Command & Dispatch +
+ +
+ + +
+ + +
+ + + + 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..42a22e3 --- /dev/null +++ b/arma/client/addons/cad/ui/src/topbar.js @@ -0,0 +1,162 @@ +window.cadTopbar = { + mode: "operations", + dispatchView: "board", + currentGroup: null, + session: {}, + init() { + document.getElementById("btnClose").addEventListener("click", () => { + window.mapUI.sendEvent("map::close", null); + }); + + document + .getElementById("modeToggle") + .addEventListener("change", (event) => { + window.mapUI.sendEvent("cad::mode::set", { + mode: event.target.checked ? "dispatch" : "operations", + }); + }); + + document + .getElementById("dispatchRefreshBtn") + .addEventListener("click", () => { + window.mapUI.sendEvent("cad::refresh", {}); + }); + + document + .getElementById("dispatchBoardBtn") + .addEventListener("click", () => { + window.mapUI.sendEvent("cad::dispatchView::set", { + dispatchView: "board", + }); + }); + + document + .getElementById("dispatchMapBtn") + .addEventListener("click", () => { + window.mapUI.sendEvent("cad::dispatchView::set", { + dispatchView: "map", + }); + }); + + document + .getElementById("operatorRoleBtn") + .addEventListener("click", () => { + if (!this.currentGroup) { + return; + } + + window.mapUI.sendEvent("cad::groups::role", { + groupID: this.currentGroup.groupId || "", + role: document.getElementById("operatorRoleSelect").value, + }); + }); + + document + .getElementById("operatorStatusBtn") + .addEventListener("click", () => { + if (!this.currentGroup) { + return; + } + + window.mapUI.sendEvent("cad::groups::status", { + groupID: this.currentGroup.groupId || "", + status: document.getElementById("operatorStatusSelect") + .value, + }); + }); + + window.mapUI.sendEvent("cad::topbar::ready", {}); + }, + formatLocation(group) { + const position = Array.isArray(group?.position) + ? group.position + : [0, 0, 0]; + return window.mapUI.formatPosition(position); + }, + receiveState(payload) { + this.session = + payload && payload.session && typeof payload.session === "object" + ? payload.session + : {}; + this.mode = + payload && typeof payload.mode === "string" + ? payload.mode + : "operations"; + this.dispatchView = + payload && typeof payload.dispatchView === "string" + ? payload.dispatchView + : "board"; + this.currentGroup = + payload && + payload.currentGroup && + typeof payload.currentGroup === "object" + ? payload.currentGroup + : null; + + const modeControls = document.getElementById("modeControls"); + const canDispatch = !!this.session.isDispatcher; + const canOperateGroup = + !!this.currentGroup && + (!!this.session.isLeader || !!this.session.isDispatcher); + const operatorStrip = document.getElementById("operatorStrip"); + const operatorControls = document.getElementById("operatorControls"); + const dispatchViewControls = document.getElementById( + "dispatchViewControls", + ); + const dispatchRefreshBtn = + document.getElementById("dispatchRefreshBtn"); + const dispatchBoardBtn = document.getElementById("dispatchBoardBtn"); + const dispatchMapBtn = document.getElementById("dispatchMapBtn"); + + modeControls.classList.toggle("is-hidden", !canDispatch); + dispatchViewControls.classList.toggle( + "is-hidden", + !canDispatch || this.mode !== "dispatch", + ); + operatorStrip.classList.toggle( + "is-hidden", + this.mode !== "operations" || !this.currentGroup, + ); + operatorControls.classList.toggle("is-hidden", !canOperateGroup); + + document.body.dataset.mode = this.mode; + document.body.dataset.dispatcher = canDispatch ? "true" : "false"; + + document.getElementById("modeToggle").checked = + this.mode === "dispatch"; + dispatchBoardBtn.classList.toggle( + "is-active", + this.dispatchView === "board", + ); + dispatchMapBtn.classList.toggle( + "is-active", + this.dispatchView === "map", + ); + dispatchRefreshBtn.title = + this.mode === "dispatch" ? "Refresh dispatch board" : "Refresh CAD"; + dispatchRefreshBtn.setAttribute( + "aria-label", + this.mode === "dispatch" ? "Refresh dispatch board" : "Refresh CAD", + ); + + document.getElementById("operatorGroupName").textContent = this + .currentGroup + ? this.currentGroup.callsign || + this.currentGroup.groupId || + "Current Group" + : "No Group"; + document.getElementById("operatorLocation").textContent = this + .currentGroup + ? this.formatLocation(this.currentGroup) + : "Unavailable"; + + if (this.currentGroup) { + document.getElementById("operatorRoleSelect").value = + this.currentGroup.role || "infantry"; + document.getElementById("operatorStatusSelect").value = + this.currentGroup.status || "available"; + } + }, +}; + +window.cadTopbar.init(); 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..366f58e --- /dev/null +++ b/arma/client/addons/cad/ui/ui.config.mjs @@ -0,0 +1,89 @@ +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 dispatcher app", + output: "cad-dispatcher.js", + sources: [ + "src/dispatcher/formatters.js", + "src/dispatcher/modals.js", + "src/dispatcher/render.js", + "src/dispatcher/index.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 dispatcher styles", + output: "cad-dispatcher.css", + sources: ["src/styles/dispatcher.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 dispatcher page", + output: "dispatcher.html", + source: "src/dispatcher.html", + }, + { + name: "CAD bottombar page", + output: "bottombar.html", + source: "src/bottombar.html", + }, + ], + site: {}, +}; diff --git a/arma/client/addons/common/XEH_preStart.sqf b/arma/client/addons/common/XEH_preStart.sqf index 0228885..a51262a 100644 --- a/arma/client/addons/common/XEH_preStart.sqf +++ b/arma/client/addons/common/XEH_preStart.sqf @@ -1,3 +1,2 @@ #include "script_component.hpp" - #include "XEH_PREP.hpp" diff --git a/arma/client/addons/garage/XEH_PREP.hpp b/arma/client/addons/garage/XEH_PREP.hpp index 9db23f1..f39faee 100644 --- a/arma/client/addons/garage/XEH_PREP.hpp +++ b/arma/client/addons/garage/XEH_PREP.hpp @@ -1,8 +1,10 @@ PREP(handleUIEvents); -PREP(initCatalogService); -PREP(initClass); -PREP(initSessionService); +PREP(initActionService); +PREP(initContextService); +PREP(initHelperService); +PREP(initPayloadService); +PREP(initRepository); PREP(initUIBridge); -PREP(initVGClass); +PREP(initVGRepository); PREP(openUI); PREP(openVG); diff --git a/arma/client/addons/garage/XEH_postInitClient.sqf b/arma/client/addons/garage/XEH_postInitClient.sqf index 7d53dc5..76cd623 100644 --- a/arma/client/addons/garage/XEH_postInitClient.sqf +++ b/arma/client/addons/garage/XEH_postInitClient.sqf @@ -1,19 +1,21 @@ #include "script_component.hpp" -if (isNil QGVAR(GarageCatalogService)) then { call FUNC(initCatalogService); }; -if (isNil QGVAR(GarageClass)) then { call FUNC(initClass); }; -if (isNil QGVAR(GarageSessionService)) then { call FUNC(initSessionService); }; +if (isNil QGVAR(GarageHelperService)) then { call FUNC(initHelperService); }; +if (isNil QGVAR(GarageRepository)) then { call FUNC(initRepository); }; +if (isNil QGVAR(GarageContextService)) then { call FUNC(initContextService); }; +if (isNil QGVAR(GaragePayloadService)) then { call FUNC(initPayloadService); }; +if (isNil QGVAR(GarageActionService)) then { call FUNC(initActionService); }; if (isNil QGVAR(GarageUIBridge)) then { call FUNC(initUIBridge); }; -if (isNil QGVAR(VGClass)) then { call FUNC(initVGClass); }; +if (isNil QGVAR(VGRepository)) then { call FUNC(initVGRepository); }; [QGVAR(initGarage), { - GVAR(GarageClass) call ["init", []]; + GVAR(GarageRepository) call ["init", []]; }] call CFUNC(addEventHandler); [QGVAR(responseInitGarage), { params [["_data", createHashMap, [createHashMap]]]; - GVAR(GarageClass) call ["sync", [_data]]; + GVAR(GarageRepository) call ["sync", [_data]]; if !(isNil QGVAR(GarageUIBridge)) then { GVAR(GarageUIBridge) call ["refreshGarage", []]; }; @@ -22,7 +24,7 @@ if (isNil QGVAR(VGClass)) then { call FUNC(initVGClass); }; [QGVAR(responseSyncGarage), { params [["_data", createHashMap, [createHashMap, []]]]; - GVAR(GarageClass) call ["sync", [_data]]; + GVAR(GarageRepository) call ["sync", [_data]]; if !(isNil QGVAR(GarageUIBridge)) then { GVAR(GarageUIBridge) call ["refreshGarage", []]; }; @@ -31,35 +33,35 @@ if (isNil QGVAR(VGClass)) then { call FUNC(initVGClass); }; [QGVAR(responseGarageAction), { params [["_payload", createHashMap, [createHashMap]]]; - if !(isNil QGVAR(GarageUIBridge)) then { - GVAR(GarageUIBridge) call ["handleActionResponse", [_payload]]; + if !(isNil QGVAR(GarageActionService)) then { + GVAR(GarageActionService) call ["handleActionResponse", [_payload]]; }; }] call CFUNC(addEventHandler); [QGVAR(initVG), { - GVAR(VGClass) call ["init", []]; + GVAR(VGRepository) call ["init", []]; }] call CFUNC(addEventHandler); [QGVAR(responseInitVG), { params [["_data", createHashMap, [createHashMap]]]; - GVAR(VGClass) call ["sync", [_data]]; + GVAR(VGRepository) call ["sync", [_data]]; }] call CFUNC(addEventHandler); [QGVAR(responseSyncVG), { params [["_data", createHashMap, [createHashMap, []]]]; - GVAR(VGClass) call ["sync", [_data]]; + GVAR(VGRepository) call ["sync", [_data]]; }] call CFUNC(addEventHandler); [{ - EGVAR(bank,BankClass) get "isLoaded"; + EGVAR(bank,BankRepository) get "isLoaded"; }, { [QGVAR(initGarage), []] call CFUNC(localEvent); }] call CFUNC(waitUntilAndExecute); [{ - GVAR(GarageClass) get "isLoaded"; + GVAR(GarageRepository) get "isLoaded"; }, { [QGVAR(initVG), []] call CFUNC(localEvent); }] call CFUNC(waitUntilAndExecute); diff --git a/arma/client/addons/garage/functions/fnc_handleUIEvents.sqf b/arma/client/addons/garage/functions/fnc_handleUIEvents.sqf index ae6ee6e..94c1ad1 100644 --- a/arma/client/addons/garage/functions/fnc_handleUIEvents.sqf +++ b/arma/client/addons/garage/functions/fnc_handleUIEvents.sqf @@ -44,13 +44,13 @@ switch (_event) do { }; }; case "garage::vehicle::retrieve::request": { - if !(isNil QGVAR(GarageUIBridge)) then { - GVAR(GarageUIBridge) call ["handleRetrieveRequest", [_data]]; + if !(isNil QGVAR(GarageActionService)) then { + GVAR(GarageActionService) call ["handleRetrieveRequest", [_data]]; }; }; case "garage::vehicle::store::request": { - if !(isNil QGVAR(GarageUIBridge)) then { - GVAR(GarageUIBridge) call ["handleStoreRequest", [_data]]; + if !(isNil QGVAR(GarageActionService)) then { + GVAR(GarageActionService) call ["handleStoreRequest", [_data]]; }; }; case "garage::refresh": { diff --git a/arma/client/addons/garage/functions/fnc_initActionService.sqf b/arma/client/addons/garage/functions/fnc_initActionService.sqf new file mode 100644 index 0000000..408d5a8 --- /dev/null +++ b/arma/client/addons/garage/functions/fnc_initActionService.sqf @@ -0,0 +1,133 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initActionService.sqf + * Author: IDSolutions + * Date: 2026-03-27 + * Public: No + * + * Description: + * Initializes the garage action service for retrieve and store world actions. + * + * Arguments: + * None + * + * Return Value: + * Garage action service object [HASHMAP OBJECT] + * + * Example: + * call forge_client_garage_fnc_initActionService; + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(GarageActionServiceBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "GarageActionServiceBaseClass"], + ["#create", compileFinal { + _self set ["pendingStoreVehicle", objNull]; + _self set ["pendingRetrieve", createHashMap]; + }], + ["handleRetrieveRequest", compileFinal { + params [["_data", createHashMap, [createHashMap]]]; + + private _plate = _data getOrDefault ["plate", ""]; + if (_plate isEqualTo "") exitWith { + GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", "Select a stored vehicle to retrieve."]]]]; + }; + + private _garageMap = if (isNil QGVAR(GarageRepository)) then { createHashMap } else { GVAR(GarageRepository) call ["getState", []] }; + private _vehicleData = _garageMap getOrDefault [_plate, createHashMap]; + if (_vehicleData isEqualTo createHashMap) exitWith { + GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", "Stored vehicle record could not be found."]]]]; + }; + + private _context = GVAR(GarageContextService) call ["getContext", []]; + private _spawnPosition = _context getOrDefault ["spawnPosition", getPosATL player]; + private _spawnHeading = _context getOrDefault ["spawnHeading", getDir player]; + private _spawnRadius = _context getOrDefault ["spawnRadius", 6]; + private _blockingVehicles = []; + { _blockingVehicles pushBackUnique _x; } forEach (_spawnPosition nearEntities [["Car", "Tank", "Air", "Ship"], _spawnRadius]); + { _blockingVehicles pushBackUnique _x; } forEach (nearestObjects [_spawnPosition, ["Car", "Tank", "Air", "Ship"], _spawnRadius]); + if (_blockingVehicles isNotEqualTo []) exitWith { + GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", "The garage spawn area is blocked."]]]]; + }; + + private _className = _vehicleData getOrDefault ["classname", ""]; + if (_className isEqualTo "") exitWith { + GVAR(GarageUIBridge) call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [["message", "Stored vehicle record is missing a classname."]]]]; + }; + + private _vehicle = createVehicle [_className, _spawnPosition, [], 0, "CAN_COLLIDE"]; + _vehicle setDir _spawnHeading; + _vehicle setFuel (_vehicleData getOrDefault ["fuel", 0]); + _vehicle setDamage (_vehicleData getOrDefault ["damage", 0]); + + private _hitPoints = _vehicleData getOrDefault ["hit_points", createHashMap]; + private _hitPointNames = _hitPoints getOrDefault ["names", []]; + private _hitPointValues = _hitPoints getOrDefault ["values", []]; + for "_index" from 0 to ((count _hitPointNames) - 1) do { + _vehicle setHitPointDamage [_hitPointNames param [_index, ""], _hitPointValues param [_index, 0]]; + }; + + _vehicle setVariable ["forge_garage_plate", _plate, true]; + _vehicle setVariable ["forge_garage_owner_uid", getPlayerUID player, true]; + + _self set ["pendingRetrieve", createHashMapFromArray [["plate", _plate], ["vehicle", _vehicle]]]; + [SRPC(garage,requestRetrieveVehicle), [getPlayerUID player, _plate]] call CFUNC(serverEvent); + }], + ["handleStoreRequest", compileFinal { + params [["_data", createHashMap, [createHashMap]]]; + + private _netId = _data getOrDefault ["netId", ""]; + if (_netId isEqualTo "") exitWith { + GVAR(GarageUIBridge) call ["sendEvent", ["garage::store::failure", createHashMapFromArray [["message", "Select a nearby vehicle to store."]]]]; + }; + + private _vehicle = objectFromNetId _netId; + if (isNull _vehicle) exitWith { + GVAR(GarageUIBridge) call ["sendEvent", ["garage::store::failure", createHashMapFromArray [["message", "The selected vehicle is no longer available."]]]]; + }; + + if (crew _vehicle isNotEqualTo []) exitWith { + GVAR(GarageUIBridge) call ["sendEvent", ["garage::store::failure", createHashMapFromArray [["message", "All crew must exit the vehicle before storing it."]]]]; + }; + + private _rawHitPoints = getAllHitPointsDamage _vehicle; + private _hitPointsJson = toJSON (createHashMapFromArray [["names", _rawHitPoints param [0, []]], ["selections", _rawHitPoints param [1, []]], ["values", _rawHitPoints param [2, []]]]); + + _self set ["pendingStoreVehicle", _vehicle]; + [SRPC(garage,requestStoreVehicle), [getPlayerUID player, typeOf _vehicle, fuel _vehicle, damage _vehicle, _hitPointsJson]] call CFUNC(serverEvent); + }], + ["handleActionResponse", compileFinal { + params [["_payload", createHashMap, [createHashMap]]]; + + private _action = _payload getOrDefault ["action", ""]; + private _success = _payload getOrDefault ["success", false]; + private _message = _payload getOrDefault ["message", "Garage action failed."]; + + switch (_action) do { + case "retrieve": { + private _pendingRetrieve = _self getOrDefault ["pendingRetrieve", createHashMap]; + private _vehicle = _pendingRetrieve getOrDefault ["vehicle", objNull]; + if (!_success && { !isNull _vehicle }) then { deleteVehicle _vehicle; }; + _self set ["pendingRetrieve", createHashMap]; + GVAR(GarageUIBridge) call ["sendEvent", [[ "garage::retrieve::failure", "garage::retrieve::success" ] select _success, createHashMapFromArray [["message", _message]]]]; + }; + case "store": { + private _vehicle = _self getOrDefault ["pendingStoreVehicle", objNull]; + if (_success && { !isNull _vehicle }) then { deleteVehicle _vehicle; }; + _self set ["pendingStoreVehicle", objNull]; + GVAR(GarageUIBridge) call ["sendEvent", [[ "garage::store::failure", "garage::store::success" ] select _success, createHashMapFromArray [["message", _message]]]]; + }; + }; + + [] spawn { + sleep 0.05; + if !(isNil QGVAR(GarageUIBridge)) then { + GVAR(GarageUIBridge) call ["refreshGarage", []]; + }; + }; + }] +]; + +GVAR(GarageActionService) = createHashMapObject [GVAR(GarageActionServiceBaseClass)]; +GVAR(GarageActionService) diff --git a/arma/client/addons/garage/functions/fnc_initContextService.sqf b/arma/client/addons/garage/functions/fnc_initContextService.sqf new file mode 100644 index 0000000..45da63e --- /dev/null +++ b/arma/client/addons/garage/functions/fnc_initContextService.sqf @@ -0,0 +1,146 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initContextService.sqf + * Author: IDSolutions + * Date: 2026-03-27 + * Public: No + * + * Description: + * Initializes the garage context service for local garage context and nearby state. + * + * Arguments: + * None + * + * Return Value: + * Garage context service object [HASHMAP OBJECT] + * + * Example: + * call forge_client_garage_fnc_initContextService; + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(GarageContextServiceBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "GarageContextServiceBaseClass"], + ["#create", compileFinal { _self set ["lastContext", createHashMap]; }], + ["#delete", compileFinal { _self set ["lastContext", createHashMap]; }], + ["createDefaultContext", compileFinal { + createHashMapFromArray [ + ["name", "Vehicle Garage"], + ["anchorPosition", getPosATL player], + ["sourceObject", objNull], + ["spawnHeading", getDir player], + ["spawnPosition", player getPos [8, getDir player]], + ["spawnRadius", 6], + ["nearbyRadius", 30] + ] + }], + ["scanEntryValues", compileFinal { + params [["_values", [], [[]]], ["_state", createHashMap, [createHashMap]]]; + { + if (_x isEqualType "" && { (_state getOrDefault ["name", "Vehicle Garage"]) isEqualTo "Vehicle Garage" }) then { _state set ["name", _x]; }; + if (_x isEqualType "") then { + private _resolvedObject = _state getOrDefault ["sourceObject", objNull]; + if (isNull _resolvedObject) then { + private _namedObject = missionNamespace getVariable [_x, objNull]; + if (!isNull _namedObject) then { _state set ["sourceObject", _namedObject]; }; + }; + if ((_state getOrDefault ["anchorPosition", []]) isEqualTo [] && { _x in allMapMarkers }) then { _state set ["anchorPosition", markerPos _x]; }; + continue; + }; + if (_x isEqualType objNull && { isNull (_state getOrDefault ["sourceObject", objNull]) }) then { + _state set ["sourceObject", _x]; + if ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) then { _state set ["anchorPosition", getPosATL _x]; }; + continue; + }; + if (_x isEqualType 0 && { (_state getOrDefault ["spawnHeading", -1]) < 0 }) then { _state set ["spawnHeading", _x]; continue; }; + if (_x isEqualType [] && { count _x > 0 }) then { + if ({ _x isEqualType 0 } count _x >= 2 && { ((_state getOrDefault ["offset", []]) isEqualTo []) || ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) }) then { + if ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) then { _state set ["anchorPosition", _x]; } else { _state set ["offset", _x]; }; + continue; + }; + _self call ["scanEntryValues", [_x, _state]]; + }; + } forEach _values; + _state + }], + ["resolveEntry", compileFinal { + params [["_entry", [], [[]]]]; + private _state = createHashMapFromArray [["name", "Vehicle Garage"], ["anchorPosition", []], ["sourceObject", objNull], ["offset", []], ["spawnHeading", -1]]; + _self call ["scanEntryValues", [_entry, _state]]; + private _anchorPosition = _state getOrDefault ["anchorPosition", []]; + private _offset = _state getOrDefault ["offset", []]; + private _spawnPosition = if (_anchorPosition isEqualTo []) then { [] } else { if (_offset isEqualTo []) then { _anchorPosition } else { _anchorPosition vectorAdd _offset } }; + createHashMapFromArray [["name", _state getOrDefault ["name", "Vehicle Garage"]], ["anchorPosition", _anchorPosition], ["sourceObject", _state getOrDefault ["sourceObject", objNull]], ["spawnHeading", _state getOrDefault ["spawnHeading", -1]], ["spawnPosition", _spawnPosition]] + }], + ["resolveContext", compileFinal { + private _context = _self call ["createDefaultContext", []]; + private _locations = (missionConfigFile >> "FORGE_CfgGarages" >> "locations") call BFUNC(getCfgData); + if !(_locations isEqualType []) exitWith { _self set ["lastContext", _context]; _context }; + + private _nearestEntry = []; + private _nearestDistance = 1e10; + { + private _entry = _self call ["resolveEntry", [_x]]; + private _anchorPosition = _entry getOrDefault ["anchorPosition", []]; + if (_anchorPosition isEqualTo []) then { continue; }; + private _distance = player distance2D _anchorPosition; + if (_distance < _nearestDistance) then { _nearestDistance = _distance; _nearestEntry = _entry; }; + } forEach _locations; + + if (_nearestEntry isEqualTo []) exitWith { _self set ["lastContext", _context]; _context }; + + private _anchorPosition = _nearestEntry getOrDefault ["anchorPosition", []]; + private _garageObject = _nearestEntry getOrDefault ["sourceObject", objNull]; + private _garageName = _nearestEntry getOrDefault ["name", "Vehicle Garage"]; + private _spawnHeading = _nearestEntry getOrDefault ["spawnHeading", getDir player]; + if (_spawnHeading < 0) then { _spawnHeading = if (!isNull _garageObject) then { getDir _garageObject } else { getDir player }; }; + + private _spawnPosition = _nearestEntry getOrDefault ["spawnPosition", []]; + if (_spawnPosition isEqualTo []) then { _spawnPosition = if (_anchorPosition isEqualTo []) then { player getPos [8, _spawnHeading] } else { _anchorPosition }; }; + + _context set ["name", _garageName]; + _context set ["anchorPosition", _anchorPosition]; + _context set ["sourceObject", _garageObject]; + _context set ["spawnHeading", _spawnHeading]; + _context set ["spawnPosition", _spawnPosition]; + _self set ["lastContext", _context]; + _context + }], + ["getContext", compileFinal { _self call ["resolveContext", []] }], + ["buildNearbyState", compileFinal { + private _context = _self call ["getContext", []]; + private _anchorPosition = _context getOrDefault ["anchorPosition", []]; + private _spawnPosition = _context getOrDefault ["spawnPosition", getPosATL player]; + private _spawnRadius = _context getOrDefault ["spawnRadius", 6]; + private _nearbyRadius = _context getOrDefault ["nearbyRadius", 30]; + private _nearbyOrigin = [_anchorPosition, _spawnPosition] select (_anchorPosition isEqualTo []); + private _nearbyVehicles = []; + private _nearbyEntities = []; + private _candidateVehicles = []; + { _candidateVehicles pushBackUnique _x; } forEach (_nearbyOrigin nearEntities [["Car", "Tank", "Air", "Ship"], _nearbyRadius]); + { _candidateVehicles pushBackUnique _x; } forEach ((getPosATL player) nearEntities [["Car", "Tank", "Air", "Ship"], _nearbyRadius]); + { _candidateVehicles pushBackUnique _x; } forEach (nearestObjects [_nearbyOrigin, ["AllVehicles"], _nearbyRadius]); + { _candidateVehicles pushBackUnique _x; } forEach (nearestObjects [getPosATL player, ["AllVehicles"], _nearbyRadius]); + { + if (isNull _x) then { continue; }; + if (_x isKindOf "CAManBase") then { continue; }; + if !(_x isKindOf "Car" || _x isKindOf "Tank" || _x isKindOf "Air" || _x isKindOf "Ship") then { continue; }; + _nearbyEntities pushBackUnique _x; + } forEach _candidateVehicles; + { + if (isNull _x) then { continue; }; + private _builtVehicle = GVAR(GarageHelperService) call ["buildNearbyVehicle", [_x, _nearbyOrigin]]; + if (_builtVehicle isEqualTo createHashMap) then { continue; }; + _nearbyVehicles pushBack _builtVehicle; + } forEach _nearbyEntities; + private _nearbyVehiclePairs = _nearbyVehicles apply { [_x getOrDefault ["distance", 0], _x] }; + _nearbyVehiclePairs sort true; + _nearbyVehicles = _nearbyVehiclePairs apply { _x param [1, createHashMap] }; + private _spawnBlocked = ((_spawnPosition nearEntities [["Car", "Tank", "Air", "Ship"], _spawnRadius]) + (nearestObjects [_spawnPosition, ["Car", "Tank", "Air", "Ship"], _spawnRadius])) isNotEqualTo []; + createHashMapFromArray [["session", createHashMapFromArray [["garageName", _context getOrDefault ["name", "Vehicle Garage"]], ["nearbyCount", count _nearbyVehicles], ["spawnBlocked", _spawnBlocked], ["spawnStatus", ["Ready", "Blocked"] select _spawnBlocked]]], ["nearby", createHashMapFromArray [["vehicles", _nearbyVehicles]]]] + }] +]; + +GVAR(GarageContextService) = createHashMapObject [GVAR(GarageContextServiceBaseClass)]; +GVAR(GarageContextService) diff --git a/arma/client/addons/garage/functions/fnc_initCatalogService.sqf b/arma/client/addons/garage/functions/fnc_initHelperService.sqf similarity index 91% rename from arma/client/addons/garage/functions/fnc_initCatalogService.sqf rename to arma/client/addons/garage/functions/fnc_initHelperService.sqf index 748b5e4..714d2a2 100644 --- a/arma/client/addons/garage/functions/fnc_initCatalogService.sqf +++ b/arma/client/addons/garage/functions/fnc_initHelperService.sqf @@ -1,18 +1,27 @@ #include "..\script_component.hpp" /* - * File: fnc_initCatalogService.sqf + * File: fnc_initHelperService.sqf * Author: IDSolutions - * Date: 2026-03-14 + * Date: 2026-03-27 * Public: No * * Description: - * Initializes the garage catalog service for vehicle metadata and UI-friendly shaping. + * Initializes the garage helper service for vehicle metadata and UI-friendly shaping. + * + * Arguments: + * None + * + * Return Value: + * Garage helper service object [HASHMAP OBJECT] + * + * Example: + * call forge_client_garage_fnc_initHelperService; */ #pragma hemtt ignore_variables ["_self"] -GVAR(GarageCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ - ["#type", "GarageCatalogServiceBaseClass"], +GVAR(GarageHelperServiceBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "GarageHelperServiceBaseClass"], ["resolveCategory", compileFinal { params [["_className", "", [""]]]; @@ -156,5 +165,5 @@ GVAR(GarageCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ }] ]; -GVAR(GarageCatalogService) = createHashMapObject [GVAR(GarageCatalogServiceBaseClass)]; -GVAR(GarageCatalogService) +GVAR(GarageHelperService) = createHashMapObject [GVAR(GarageHelperServiceBaseClass)]; +GVAR(GarageHelperService) diff --git a/arma/client/addons/garage/functions/fnc_initPayloadService.sqf b/arma/client/addons/garage/functions/fnc_initPayloadService.sqf new file mode 100644 index 0000000..184065e --- /dev/null +++ b/arma/client/addons/garage/functions/fnc_initPayloadService.sqf @@ -0,0 +1,44 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initPayloadService.sqf + * Author: IDSolutions + * Date: 2026-03-27 + * Public: No + * + * Description: + * Initializes the garage payload service for browser hydrate payload composition. + * + * Arguments: + * None + * + * Return Value: + * Garage payload service object [HASHMAP OBJECT] + * + * Example: + * call forge_client_garage_fnc_initPayloadService; + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(GaragePayloadServiceBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "GaragePayloadServiceBaseClass"], + ["buildStoredVehicles", compileFinal { + private _garageMap = if (isNil QGVAR(GarageRepository)) then { createHashMap } else { GVAR(GarageRepository) call ["getState", []] }; + private _storedVehicles = []; + { _storedVehicles pushBack (GVAR(GarageHelperService) call ["buildStoredVehicle", [_x, _y]]); } forEach _garageMap; + private _storedVehiclePairs = _storedVehicles apply { [toLowerANSI (_x getOrDefault ["displayName", ""]), _x] }; + _storedVehiclePairs sort true; + _storedVehiclePairs apply { _x param [1, createHashMap] } + }], + ["buildPayload", compileFinal { + private _localState = GVAR(GarageContextService) call ["buildNearbyState", []]; + private _storedVehicles = _self call ["buildStoredVehicles", []]; + private _session = +(_localState getOrDefault ["session", createHashMap]); + _session set ["capacityUsed", count _storedVehicles]; + _session set ["capacityMax", 5]; + createHashMapFromArray [["session", _session], ["garage", createHashMapFromArray [["vehicles", _storedVehicles]]], ["nearby", +(_localState getOrDefault ["nearby", createHashMap])]] + }] +]; + +GVAR(GaragePayloadService) = createHashMapObject [GVAR(GaragePayloadServiceBaseClass)]; +GVAR(GaragePayloadService) diff --git a/arma/client/addons/garage/functions/fnc_initClass.sqf b/arma/client/addons/garage/functions/fnc_initRepository.sqf similarity index 58% rename from arma/client/addons/garage/functions/fnc_initClass.sqf rename to arma/client/addons/garage/functions/fnc_initRepository.sqf index 841550b..5129610 100644 --- a/arma/client/addons/garage/functions/fnc_initClass.sqf +++ b/arma/client/addons/garage/functions/fnc_initRepository.sqf @@ -1,48 +1,44 @@ #include "..\script_component.hpp" /* - * File: fnc_initClass.sqf + * File: fnc_initRepository.sqf * Author: IDSolutions - * Date: 2025-12-17 - * Last Update: 2026-02-13 + * Date: 2026-03-27 * Public: No * * Description: - * Initializes the Garage class for managing player vehicles. - * Provides methods for syncing, saving, and applying vehicles to the player's garage. + * Initializes the garage repository for persisted stored vehicle records. * * Arguments: * None * * Return Value: - * Garage class object [HASHMAP OBJECT] + * Garage repository object [HASHMAP OBJECT] * * Example: - * call forge_client_garage_fnc_initClass + * call forge_client_garage_fnc_initRepository; */ #pragma hemtt ignore_variables ["_self"] -GVAR(GarageBaseClass) = compileFinal createHashMapFromArray [ - ["#type", "GarageBaseClass"], +GVAR(GarageRepositoryBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "GarageRepositoryBaseClass"], ["#create", compileFinal { - _self set ["uid", (getPlayerUID player)]; + _self set ["uid", getPlayerUID player]; _self set ["garage", createHashMap]; _self set ["isLoaded", false]; _self set ["lastSave", time]; }], ["init", compileFinal { private _uid = _self get "uid"; - private _garage = _self get "garage"; + [SRPC(garage,requestInitGarage), [_uid]] call CFUNC(serverEvent); + _self set ["lastSave", time]; - [SRPC(garage,requestInitGarage), [_uid, _garage]] call CFUNC(serverEvent); - - systemChat format ["Garage loaded for %1", (name player)]; - diag_log "[FORGE:Client:Garage] Garage Class Initialized!"; + systemChat format ["Garage loaded for %1", name player]; + diag_log "[FORGE:Client:Garage] Garage Repository Initialized!"; }], ["save", compileFinal { private _uid = _self get "uid"; [SRPC(garage,requestSaveGarage), [_uid]] call CFUNC(serverEvent); - _self set ["lastSave", time]; }], ["sync", compileFinal { @@ -50,14 +46,13 @@ GVAR(GarageBaseClass) = compileFinal createHashMapFromArray [ private _isLoaded = _self get "isLoaded"; private _garage = createHashMap; - { _garage set [_x, _y]; } forEach _data; _self set ["garage", _garage]; if !(_isLoaded) then { _self set ["isLoaded", true]; }; - diag_log "[FORGE:Client:Garage] Sync completed"; + diag_log "[FORGE:Client:Garage] Repository sync completed"; }], - ["getGarageState", compileFinal { + ["getState", compileFinal { _self getOrDefault ["garage", createHashMap] }], ["get", compileFinal { @@ -68,5 +63,5 @@ GVAR(GarageBaseClass) = compileFinal createHashMapFromArray [ }] ]; -GVAR(GarageClass) = createHashMapObject [GVAR(GarageBaseClass)]; -GVAR(GarageClass) +GVAR(GarageRepository) = createHashMapObject [GVAR(GarageRepositoryBaseClass)]; +GVAR(GarageRepository) diff --git a/arma/client/addons/garage/functions/fnc_initSessionService.sqf b/arma/client/addons/garage/functions/fnc_initSessionService.sqf deleted file mode 100644 index 7fe871b..0000000 --- a/arma/client/addons/garage/functions/fnc_initSessionService.sqf +++ /dev/null @@ -1,298 +0,0 @@ -#include "..\script_component.hpp" - -/* - * File: fnc_initSessionService.sqf - * Author: IDSolutions - * Date: 2026-03-14 - * Public: No - * - * Description: - * Initializes the typed garage session service responsible for resolving the - * active garage context and building the browser hydrate payload. - */ - -#pragma hemtt ignore_variables ["_self"] - -GVAR(GarageSessionServiceBaseClass) = compileFinal createHashMapFromArray [ - ["#type", "GarageSessionServiceBaseClass"], - ["#create", compileFinal { - _self set ["lastContext", createHashMap]; - }], - ["#delete", compileFinal { - _self set ["lastContext", createHashMap]; - }], - ["createDefaultContext", compileFinal { - createHashMapFromArray [ - ["name", "Vehicle Garage"], - ["anchorPosition", getPosATL player], - ["sourceObject", objNull], - ["spawnHeading", getDir player], - ["spawnPosition", player getPos [8, getDir player]], - ["spawnRadius", 6], - ["nearbyRadius", 30] - ] - }], - ["scanEntryValues", compileFinal { - params [ - ["_values", [], [[]]], - ["_state", createHashMap, [createHashMap]] - ]; - - { - if (_x isEqualType "" && { (_state getOrDefault ["name", "Vehicle Garage"]) isEqualTo "Vehicle Garage" }) then { - _state set ["name", _x]; - }; - - if (_x isEqualType "") then { - private _resolvedObject = _state getOrDefault ["sourceObject", objNull]; - if (isNull _resolvedObject) then { - private _namedObject = missionNamespace getVariable [_x, objNull]; - if (!isNull _namedObject) then { - _state set ["sourceObject", _namedObject]; - }; - }; - - if ((_state getOrDefault ["anchorPosition", []]) isEqualTo [] && { _x in allMapMarkers }) then { - _state set ["anchorPosition", markerPos _x]; - }; - - continue; - }; - - if (_x isEqualType objNull && { isNull (_state getOrDefault ["sourceObject", objNull]) }) then { - _state set ["sourceObject", _x]; - if ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) then { - _state set ["anchorPosition", getPosATL _x]; - }; - continue; - }; - - if (_x isEqualType 0 && { (_state getOrDefault ["spawnHeading", -1]) < 0 }) then { - _state set ["spawnHeading", _x]; - continue; - }; - - if (_x isEqualType [] && { count _x > 0 }) then { - if ( - { _x isEqualType 0 } count _x >= 2 && - { - ((_state getOrDefault ["offset", []]) isEqualTo []) || - ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) - } - ) then { - if ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) then { - _state set ["anchorPosition", _x]; - } else { - _state set ["offset", _x]; - }; - continue; - }; - - _self call ["scanEntryValues", [_x, _state]]; - }; - } forEach _values; - - _state - }], - ["resolveEntry", compileFinal { - params [["_entry", [], [[]]]]; - - private _state = createHashMapFromArray [ - ["name", "Vehicle Garage"], - ["anchorPosition", []], - ["sourceObject", objNull], - ["offset", []], - ["spawnHeading", -1] - ]; - - _self call ["scanEntryValues", [_entry, _state]]; - - private _anchorPosition = _state getOrDefault ["anchorPosition", []]; - private _offset = _state getOrDefault ["offset", []]; - private _spawnPosition = if (_anchorPosition isEqualTo []) then { - [] - } else { - if (_offset isEqualTo []) then { - _anchorPosition - } else { - _anchorPosition vectorAdd _offset - } - }; - - createHashMapFromArray [ - ["name", _state getOrDefault ["name", "Vehicle Garage"]], - ["anchorPosition", _anchorPosition], - ["sourceObject", _state getOrDefault ["sourceObject", objNull]], - ["spawnHeading", _state getOrDefault ["spawnHeading", -1]], - ["spawnPosition", _spawnPosition] - ] - }], - ["resolveContext", compileFinal { - private _context = _self call ["createDefaultContext", []]; - private _locations = (missionConfigFile >> "FORGE_CfgGarages" >> "locations") call BFUNC(getCfgData); - if !(_locations isEqualType []) exitWith { - _self set ["lastContext", _context]; - _context - }; - - private _nearestEntry = []; - private _nearestDistance = 1e10; - - { - private _entry = _self call ["resolveEntry", [_x]]; - private _anchorPosition = _entry getOrDefault ["anchorPosition", []]; - if (_anchorPosition isEqualTo []) then { - continue; - }; - - private _distance = player distance2D _anchorPosition; - if (_distance < _nearestDistance) then { - _nearestDistance = _distance; - _nearestEntry = _entry; - }; - } forEach _locations; - - if (_nearestEntry isEqualTo []) exitWith { - _self set ["lastContext", _context]; - _context - }; - - private _anchorPosition = _nearestEntry getOrDefault ["anchorPosition", []]; - private _garageObject = _nearestEntry getOrDefault ["sourceObject", objNull]; - private _garageName = _nearestEntry getOrDefault ["name", "Vehicle Garage"]; - private _spawnHeading = _nearestEntry getOrDefault ["spawnHeading", getDir player]; - if (_spawnHeading < 0) then { - _spawnHeading = if (!isNull _garageObject) then { getDir _garageObject } else { getDir player }; - }; - - private _spawnPosition = _nearestEntry getOrDefault ["spawnPosition", []]; - if (_spawnPosition isEqualTo []) then { - _spawnPosition = if (_anchorPosition isEqualTo []) then { - player getPos [8, _spawnHeading] - } else { - _anchorPosition - }; - }; - - _context set ["name", _garageName]; - _context set ["anchorPosition", _anchorPosition]; - _context set ["sourceObject", _garageObject]; - _context set ["spawnHeading", _spawnHeading]; - _context set ["spawnPosition", _spawnPosition]; - - _self set ["lastContext", _context]; - _context - }], - ["getContext", compileFinal { - _self call ["resolveContext", []] - }], - ["buildPayload", compileFinal { - private _context = _self call ["getContext", []]; - private _garageMap = if (isNil QGVAR(GarageClass)) then { - createHashMap - } else { - GVAR(GarageClass) call ["getGarageState", []] - }; - - private _anchorPosition = _context getOrDefault ["anchorPosition", []]; - private _spawnPosition = _context getOrDefault ["spawnPosition", getPosATL player]; - private _spawnRadius = _context getOrDefault ["spawnRadius", 6]; - private _nearbyRadius = _context getOrDefault ["nearbyRadius", 30]; - private _nearbyOrigin = [_anchorPosition, _spawnPosition] select (_anchorPosition isEqualTo []); - - private _storedVehicles = []; - private _nearbyVehicles = []; - private _nearbyEntities = []; - private _candidateVehicles = []; - - { - _candidateVehicles pushBackUnique _x; - } forEach (_nearbyOrigin nearEntities [["Car", "Tank", "Air", "Ship"], _nearbyRadius]); - { - _candidateVehicles pushBackUnique _x; - } forEach ((getPosATL player) nearEntities [["Car", "Tank", "Air", "Ship"], _nearbyRadius]); - { - _candidateVehicles pushBackUnique _x; - } forEach (nearestObjects [_nearbyOrigin, ["AllVehicles"], _nearbyRadius]); - { - _candidateVehicles pushBackUnique _x; - } forEach (nearestObjects [getPosATL player, ["AllVehicles"], _nearbyRadius]); - - { - if (isNull _x) then { - continue; - }; - - if (_x isKindOf "CAManBase") then { - continue; - }; - - if !( - _x isKindOf "Car" || - _x isKindOf "Tank" || - _x isKindOf "Air" || - _x isKindOf "Ship" - ) then { - continue; - }; - - _nearbyEntities pushBackUnique _x; - } forEach _candidateVehicles; - - { - _storedVehicles pushBack ( - GVAR(GarageCatalogService) call ["buildStoredVehicle", [_x, _y]] - ); - } forEach _garageMap; - - private _storedVehiclePairs = _storedVehicles apply { - [toLowerANSI (_x getOrDefault ["displayName", ""]), _x] - }; - _storedVehiclePairs sort true; - _storedVehicles = _storedVehiclePairs apply { _x param [1, createHashMap] }; - - { - if (isNull _x) then { - continue; - }; - - private _builtVehicle = GVAR(GarageCatalogService) call ["buildNearbyVehicle", [_x, _nearbyOrigin]]; - if (_builtVehicle isEqualTo createHashMap) then { - continue; - }; - - _nearbyVehicles pushBack _builtVehicle; - } forEach _nearbyEntities; - - private _nearbyVehiclePairs = _nearbyVehicles apply { - [_x getOrDefault ["distance", 0], _x] - }; - _nearbyVehiclePairs sort true; - _nearbyVehicles = _nearbyVehiclePairs apply { _x param [1, createHashMap] }; - - private _spawnBlocked = ( - (_spawnPosition nearEntities [["Car", "Tank", "Air", "Ship"], _spawnRadius]) + - (nearestObjects [_spawnPosition, ["Car", "Tank", "Air", "Ship"], _spawnRadius]) - ) isNotEqualTo []; - - createHashMapFromArray [ - ["session", createHashMapFromArray [ - ["garageName", _context getOrDefault ["name", "Vehicle Garage"]], - ["capacityUsed", count _storedVehicles], - ["capacityMax", 5], - ["nearbyCount", count _nearbyVehicles], - ["spawnBlocked", _spawnBlocked], - ["spawnStatus", ["Ready", "Blocked"] select _spawnBlocked] - ]], - ["garage", createHashMapFromArray [ - ["vehicles", _storedVehicles] - ]], - ["nearby", createHashMapFromArray [ - ["vehicles", _nearbyVehicles] - ]] - ] - }] -]; - -GVAR(GarageSessionService) = createHashMapObject [GVAR(GarageSessionServiceBaseClass)]; -GVAR(GarageSessionService) diff --git a/arma/client/addons/garage/functions/fnc_initUIBridge.sqf b/arma/client/addons/garage/functions/fnc_initUIBridge.sqf index ec15d50..ee20a28 100644 --- a/arma/client/addons/garage/functions/fnc_initUIBridge.sqf +++ b/arma/client/addons/garage/functions/fnc_initUIBridge.sqf @@ -3,11 +3,20 @@ /* * File: fnc_initUIBridge.sqf * Author: IDSolutions - * Date: 2026-03-14 + * Date: 2026-03-27 * Public: No * * Description: - * Initializes the garage UI bridge for browser control state and retrieve/store actions. + * Initializes the garage UI bridge for browser control state and UI events. + * + * Arguments: + * None + * + * Return Value: + * Garage UI bridge object [HASHMAP OBJECT] + * + * Example: + * call forge_client_garage_fnc_initUIBridge; */ #pragma hemtt ignore_variables ["_self"] @@ -17,10 +26,6 @@ private _webUIBridgeDeclaration = _webUIDeclarations get "bridgeDeclaration"; GVAR(GarageUIBridgeBaseClass) = compileFinal createHashMapFromArray [ ["#base", _webUIBridgeDeclaration], ["#type", "GarageUIBridgeBaseClass"], - ["#create", compileFinal { - _self set ["pendingStoreVehicle", objNull]; - _self set ["pendingRetrieve", createHashMap]; - }], ["getActiveBrowserControl", compileFinal { private _display = uiNamespace getVariable ["RscGarage", displayNull]; if (isNull _display) exitWith { @@ -40,164 +45,13 @@ GVAR(GarageUIBridgeBaseClass) = compileFinal createHashMapFromArray [ _screen call ["markReady", [true]]; _self call ["flushPendingEvents", []]; - _self call ["sendEvent", ["garage::hydrate", GVAR(GarageSessionService) call ["buildPayload", []], _control]]; + _self call ["sendEvent", ["garage::hydrate", GVAR(GaragePayloadService) call ["buildPayload", []], _control]]; }], ["refreshGarage", compileFinal { private _control = _self call ["getActiveBrowserControl", []]; if (isNull _control) exitWith { false }; - _self call ["sendEvent", ["garage::sync", GVAR(GarageSessionService) call ["buildPayload", []], _control]] - }], - ["handleRetrieveRequest", compileFinal { - params [["_data", createHashMap, [createHashMap]]]; - - private _plate = _data getOrDefault ["plate", ""]; - if (_plate isEqualTo "") exitWith { - _self call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [ - ["message", "Select a stored vehicle to retrieve."] - ]]]; - }; - - private _garageMap = if (isNil QGVAR(GarageClass)) then { - createHashMap - } else { - GVAR(GarageClass) call ["getGarageState", []] - }; - private _vehicleData = _garageMap getOrDefault [_plate, createHashMap]; - if (_vehicleData isEqualTo createHashMap) exitWith { - _self call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [ - ["message", "Stored vehicle record could not be found."] - ]]]; - }; - - private _context = GVAR(GarageSessionService) call ["getContext", []]; - private _spawnPosition = _context getOrDefault ["spawnPosition", getPosATL player]; - private _spawnHeading = _context getOrDefault ["spawnHeading", getDir player]; - private _spawnRadius = _context getOrDefault ["spawnRadius", 6]; - private _blockingVehicles = []; - { - _blockingVehicles pushBackUnique _x; - } forEach (_spawnPosition nearEntities [["Car", "Tank", "Air", "Ship"], _spawnRadius]); - { - _blockingVehicles pushBackUnique _x; - } forEach (nearestObjects [_spawnPosition, ["Car", "Tank", "Air", "Ship"], _spawnRadius]); - if (_blockingVehicles isNotEqualTo []) exitWith { - _self call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [ - ["message", "The garage spawn area is blocked."] - ]]]; - }; - - private _className = _vehicleData getOrDefault ["classname", ""]; - if (_className isEqualTo "") exitWith { - _self call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [ - ["message", "Stored vehicle record is missing a classname."] - ]]]; - }; - - private _vehicle = createVehicle [_className, _spawnPosition, [], 0, "CAN_COLLIDE"]; - _vehicle setDir _spawnHeading; - _vehicle setFuel (_vehicleData getOrDefault ["fuel", 0]); - _vehicle setDamage (_vehicleData getOrDefault ["damage", 0]); - - private _hitPoints = _vehicleData getOrDefault ["hit_points", createHashMap]; - private _hitPointNames = _hitPoints getOrDefault ["names", []]; - private _hitPointValues = _hitPoints getOrDefault ["values", []]; - for "_index" from 0 to ((count _hitPointNames) - 1) do { - _vehicle setHitPointDamage [_hitPointNames param [_index, ""], _hitPointValues param [_index, 0]]; - }; - - _vehicle setVariable ["forge_garage_plate", _plate, true]; - _vehicle setVariable ["forge_garage_owner_uid", getPlayerUID player, true]; - - _self set ["pendingRetrieve", createHashMapFromArray [ - ["plate", _plate], - ["vehicle", _vehicle] - ]]; - - [SRPC(garage,requestRetrieveVehicle), [getPlayerUID player, _plate]] call CFUNC(serverEvent); - }], - ["handleStoreRequest", compileFinal { - params [["_data", createHashMap, [createHashMap]]]; - - private _netId = _data getOrDefault ["netId", ""]; - if (_netId isEqualTo "") exitWith { - _self call ["sendEvent", ["garage::store::failure", createHashMapFromArray [ - ["message", "Select a nearby vehicle to store."] - ]]]; - }; - - private _vehicle = objectFromNetId _netId; - if (isNull _vehicle) exitWith { - _self call ["sendEvent", ["garage::store::failure", createHashMapFromArray [ - ["message", "The selected vehicle is no longer available."] - ]]]; - }; - - if (crew _vehicle isNotEqualTo []) exitWith { - _self call ["sendEvent", ["garage::store::failure", createHashMapFromArray [ - ["message", "All crew must exit the vehicle before storing it."] - ]]]; - }; - - private _rawHitPoints = getAllHitPointsDamage _vehicle; - private _hitPointsJson = toJSON (createHashMapFromArray [ - ["names", _rawHitPoints param [0, []]], - ["selections", _rawHitPoints param [1, []]], - ["values", _rawHitPoints param [2, []]] - ]); - - _self set ["pendingStoreVehicle", _vehicle]; - [SRPC(garage,requestStoreVehicle), [ - getPlayerUID player, - typeOf _vehicle, - fuel _vehicle, - damage _vehicle, - _hitPointsJson - ]] call CFUNC(serverEvent); - }], - ["handleActionResponse", compileFinal { - params [["_payload", createHashMap, [createHashMap]]]; - - private _action = _payload getOrDefault ["action", ""]; - private _success = _payload getOrDefault ["success", false]; - private _message = _payload getOrDefault ["message", "Garage action failed."]; - - switch (_action) do { - case "retrieve": { - private _pendingRetrieve = _self getOrDefault ["pendingRetrieve", createHashMap]; - private _vehicle = _pendingRetrieve getOrDefault ["vehicle", objNull]; - - if (!_success && { !isNull _vehicle }) then { - deleteVehicle _vehicle; - }; - - _self set ["pendingRetrieve", createHashMap]; - _self call ["sendEvent", [[ - "garage::retrieve::failure", - "garage::retrieve::success" - ] select _success, createHashMapFromArray [["message", _message]]]]; - }; - case "store": { - private _vehicle = _self getOrDefault ["pendingStoreVehicle", objNull]; - - if (_success && { !isNull _vehicle }) then { - deleteVehicle _vehicle; - }; - - _self set ["pendingStoreVehicle", objNull]; - _self call ["sendEvent", [[ - "garage::store::failure", - "garage::store::success" - ] select _success, createHashMapFromArray [["message", _message]]]]; - }; - }; - - [] spawn { - sleep 0.05; - if !(isNil QGVAR(GarageUIBridge)) then { - GVAR(GarageUIBridge) call ["refreshGarage", []]; - }; - }; + _self call ["sendEvent", ["garage::sync", GVAR(GaragePayloadService) call ["buildPayload", []], _control]] }] ]; diff --git a/arma/client/addons/garage/functions/fnc_initVGClass.sqf b/arma/client/addons/garage/functions/fnc_initVGRepository.sqf similarity index 74% rename from arma/client/addons/garage/functions/fnc_initVGClass.sqf rename to arma/client/addons/garage/functions/fnc_initVGRepository.sqf index f3901f9..4e99a8a 100644 --- a/arma/client/addons/garage/functions/fnc_initVGClass.sqf +++ b/arma/client/addons/garage/functions/fnc_initVGRepository.sqf @@ -1,50 +1,45 @@ #include "..\script_component.hpp" /* - * File: fnc_initVGClass.sqf + * File: fnc_initVGRepository.sqf * Author: IDSolutions - * Date: 2025-12-16 - * Last Update: 2026-02-13 + * Date: 2026-03-27 * Public: No * * Description: - * Initializes the Virtual Garage class for managing player garage unlocks. - * Provides methods for syncing, saving, and applying virtual items to BIS Garage. + * Initializes the virtual garage repository for BIS virtual garage state. * * Arguments: * None * * Return Value: - * vGarage class object [HASHMAP OBJECT] + * Virtual garage repository object [HASHMAP OBJECT] * * Example: - * call forge_client_garage_fnc_initVGClass; + * call forge_client_garage_fnc_initVGRepository; */ #pragma hemtt ignore_variables ["_self"] -GVAR(VGBaseClass) = compileFinal createHashMapFromArray [ - ["#type", "VGBaseClass"], +GVAR(VGRepositoryBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "VGRepositoryBaseClass"], ["#create", compileFinal { GVAR(isPreLoaded) = false; - - _self set ["uid", (getPlayerUID player)]; + _self set ["uid", getPlayerUID player]; _self set ["vGarage", createHashMap]; _self set ["isLoaded", false]; _self set ["lastSave", time]; }], ["init", compileFinal { private _uid = _self get "uid"; - private _vGarage = _self get "vGarage"; + [SRPC(garage,requestInitVG), [_uid]] call CFUNC(serverEvent); + _self set ["lastSave", time]; - [SRPC(garage,requestInitVG), [_uid, _vGarage]] call CFUNC(serverEvent); - - systemChat format ["VGarage loaded for %1", (name player)]; - diag_log "[FORGE:Client:VGarage] VGarage Class Initialized!"; + systemChat format ["VGarage loaded for %1", name player]; + diag_log "[FORGE:Client:VGarage] Repository Initialized!"; }], ["save", compileFinal { private _uid = _self get "uid"; [SRPC(garage,requestSaveVG), [_uid]] call CFUNC(serverEvent); - _self set ["lastSave", time]; }], ["sync", compileFinal { @@ -55,7 +50,6 @@ GVAR(VGBaseClass) = compileFinal createHashMapFromArray [ { _vGarage set [_x, _y]; - switch (_x) do { case "cars": { _self call ["apply", ["cars"]]; }; case "armor": { _self call ["apply", ["armor"]]; }; @@ -68,9 +62,8 @@ GVAR(VGBaseClass) = compileFinal createHashMapFromArray [ } forEach _data; _self set ["vGarage", _vGarage]; - if !(_isLoaded) then { _self set ["isLoaded", true]; }; - diag_log "[FORGE:Client:VGarage] Sync completed"; + diag_log "[FORGE:Client:VGarage] Repository sync completed"; }], ["get", compileFinal { params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]]; @@ -83,7 +76,6 @@ GVAR(VGBaseClass) = compileFinal createHashMapFromArray [ private _vehicles = _self call ["get", [_key, []]]; private _appliedVehicles = []; - { _appliedVehicles append [getText (configFile >> "CfgVehicles" >> _x >> "model"), [configFile >> "CfgVehicles" >> _x]]; } forEach _vehicles; @@ -100,5 +92,5 @@ GVAR(VGBaseClass) = compileFinal createHashMapFromArray [ }] ]; -GVAR(VGClass) = createHashMapObject [GVAR(VGBaseClass)]; -GVAR(VGClass) +GVAR(VGRepository) = createHashMapObject [GVAR(VGRepositoryBaseClass)]; +GVAR(VGRepository) diff --git a/arma/client/addons/garage/functions/fnc_openVG.sqf b/arma/client/addons/garage/functions/fnc_openVG.sqf index ddc3d6b..70c3518 100644 --- a/arma/client/addons/garage/functions/fnc_openVG.sqf +++ b/arma/client/addons/garage/functions/fnc_openVG.sqf @@ -89,7 +89,7 @@ if !(GVAR(isPreLoaded)) then { private _nearVehicles = FORGE_VehSpawnPos nearEntities [["Car", "Tank", "Air", "Ship"], 5]; if (_nearVehicles isNotEqualTo []) exitWith { private _params = ["warning", "Virtual Garage", "Vehicle spawn position is blocked. Please move the vehicle before accessing the garage.", 3000]; - EGVAR(notifications,NotificationClass) call ["create", _params]; + EGVAR(notifications,NotificationService) call ["create", _params]; }; ["Open", true] call BFUNC(garage); diff --git a/arma/client/addons/locker/XEH_PREP.hpp b/arma/client/addons/locker/XEH_PREP.hpp index f3e1d08..b979dfe 100644 --- a/arma/client/addons/locker/XEH_PREP.hpp +++ b/arma/client/addons/locker/XEH_PREP.hpp @@ -1,2 +1,2 @@ -PREP(initLockerClass); -PREP(initVAClass); +PREP(initRepository); +PREP(initVARepository); diff --git a/arma/client/addons/locker/XEH_postInitClient.sqf b/arma/client/addons/locker/XEH_postInitClient.sqf index d4baf95..20123fa 100644 --- a/arma/client/addons/locker/XEH_postInitClient.sqf +++ b/arma/client/addons/locker/XEH_postInitClient.sqf @@ -1,48 +1,48 @@ #include "script_component.hpp" -if (isNil QGVAR(LockerClass)) then { call FUNC(initLockerClass); }; -if (isNil QGVAR(VAClass)) then { call FUNC(initVAClass); }; +if (isNil QGVAR(LockerRepository)) then { call FUNC(initRepository); }; +if (isNil QGVAR(VARepository)) then { call FUNC(initVARepository); }; [QGVAR(initLocker), { - GVAR(LockerClass) call ["init", []]; + GVAR(LockerRepository) call ["init", []]; }] call CFUNC(addEventHandler); [QGVAR(responseInitLocker), { params [["_data", createHashMap, [createHashMap]]]; - GVAR(LockerClass) call ["sync", [_data]]; + GVAR(LockerRepository) call ["sync", [_data]]; }] call CFUNC(addEventHandler); [QGVAR(responseSyncLocker), { params [["_data", createHashMap, [createHashMap, []]], ["_jip", false, [false]]]; - GVAR(LockerClass) call ["sync", [_data, _jip]]; + GVAR(LockerRepository) call ["sync", [_data, _jip]]; }] call CFUNC(addEventHandler); [QGVAR(initVA), { - GVAR(VAClass) call ["init", []]; + GVAR(VARepository) call ["init", []]; }] call CFUNC(addEventHandler); [QGVAR(responseInitVA), { params [["_data", createHashMap, [createHashMap]]]; - GVAR(VAClass) call ["sync", [_data]]; + GVAR(VARepository) call ["sync", [_data]]; }] call CFUNC(addEventHandler); [QGVAR(responseSyncVA), { params [["_data", createHashMap, [createHashMap, []]], ["_jip", false, [false]]]; - GVAR(VAClass) call ["sync", [_data, _jip]]; + GVAR(VARepository) call ["sync", [_data, _jip]]; }] call CFUNC(addEventHandler); [{ - EGVAR(garage,GarageClass) get "isLoaded"; + EGVAR(garage,GarageRepository) get "isLoaded"; }, { [QGVAR(initLocker), []] call CFUNC(localEvent); }] call CFUNC(waitUntilAndExecute); [{ - GVAR(LockerClass) get "isLoaded"; + GVAR(LockerRepository) get "isLoaded"; }, { [QGVAR(initVA), []] call CFUNC(localEvent); }] call CFUNC(waitUntilAndExecute); diff --git a/arma/client/addons/locker/functions/fnc_initLockerClass.sqf b/arma/client/addons/locker/functions/fnc_initRepository.sqf similarity index 86% rename from arma/client/addons/locker/functions/fnc_initLockerClass.sqf rename to arma/client/addons/locker/functions/fnc_initRepository.sqf index 68c5a94..e67157e 100644 --- a/arma/client/addons/locker/functions/fnc_initLockerClass.sqf +++ b/arma/client/addons/locker/functions/fnc_initRepository.sqf @@ -1,31 +1,29 @@ #include "..\script_component.hpp" /* - * File: fnc_initLockerClass.sqf + * File: fnc_initRepository.sqf * Author: IDSolutions - * Date: 2025-12-17 - * Last Update: 2026-02-13 + * Date: 2026-03-27 * Public: No * * Description: - * Initializes the Locker class for managing player locker items. - * Provides methods for syncing, saving, and applying locker items to the player's locker. + * Initializes the locker repository for managing player locker items. * * Arguments: * None * * Return Value: - * Locker class object [HASHMAP OBJECT] + * Locker repository object [HASHMAP OBJECT] * * Example: - * call forge_client_locker_fnc_initLockerClass + * call forge_client_locker_fnc_initRepository; */ #pragma hemtt ignore_variables ["_self"] -GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [ - ["#type", "LockerBaseClass"], +GVAR(LockerRepositoryBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "LockerRepositoryBaseClass"], ["#create", compileFinal { - _self set ["uid", (getPlayerUID player)]; + _self set ["uid", getPlayerUID player]; _self set ["isLoaded", false]; _self set ["lastSave", time]; _self set ["locker", createHashMap]; @@ -34,9 +32,10 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [ private _uid = _self get "uid"; [SRPC(locker,requestInitLocker), [_uid]] call CFUNC(serverEvent); + _self set ["lastSave", time]; - systemChat format ["Locker loaded for %1", (name player)]; - diag_log "[FORGE:Client:Locker] Locker Class Initialized!"; + systemChat format ["Locker loaded for %1", name player]; + diag_log "[FORGE:Client:Locker] Locker Repository Initialized!"; }], ["get", compileFinal { params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]]; @@ -83,8 +82,8 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [ private _cfgWeapons = configFile >> "CfgWeapons" >> _containerClass; private _itemInfoType = getNumber (_cfgWeapons >> "ItemInfo" >> "type"); private _isBackpack = isClass _cfgVehicles; - private _isUniform = isClass _cfgWeapons && {_itemInfoType == TYPE_UNIFORM}; - private _isVest = isClass _cfgWeapons && {_itemInfoType == TYPE_VEST}; + private _isUniform = isClass _cfgWeapons && { _itemInfoType == TYPE_UNIFORM }; + private _isVest = isClass _cfgWeapons && { _itemInfoType == TYPE_VEST }; if (!_isBackpack && !_isVest && !_isUniform) then { continue; }; @@ -141,7 +140,6 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [ private _weaponItems = weaponsItemsCargo _container; { - // private _weapon = _x param [0, ""]; private _muzzle = _x param [1, ""]; private _pointer = _x param [2, ""]; private _optic = _x param [3, ""]; @@ -149,7 +147,7 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [ private _underbarrel = _x param [5, ""]; private _bipod = _x param [6, ""]; private _secondaryMag = _x param [7, ["", 0]]; - private _attachments = [_muzzle, _pointer, _optic, _underbarrel, _bipod] select {(_x isEqualType "") && {_x != ""}}; + private _attachments = [_muzzle, _pointer, _optic, _underbarrel, _bipod] select { (_x isEqualType "") && { _x != "" } }; { private _existing = _locker getOrDefault [_x, createHashMap]; private _existingCount = _existing getOrDefault ["amount", 0]; @@ -162,7 +160,7 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [ } forEach _attachments; if (_primaryMag isNotEqualTo ["", 0]) then { - _primaryMag params ["_magClass", "_ammoCount"]; // TODO: Add ammo count to locker + _primaryMag params ["_magClass", "_ammoCount"]; if (_magClass != "") then { private _existing = _locker getOrDefault [_magClass, createHashMap]; private _existingCount = _existing getOrDefault ["amount", 0]; @@ -176,7 +174,7 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [ }; if (_secondaryMag isNotEqualTo ["", 0]) then { - _secondaryMag params ["_magClass", "_ammoCount"]; // TODO: Add ammo count to locker + _secondaryMag params ["_magClass", "_ammoCount"]; if (_magClass != "") then { private _existing = _locker getOrDefault [_magClass, createHashMap]; private _existingCount = _existing getOrDefault ["amount", 0]; @@ -204,7 +202,7 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [ _locker addEventHandler ["ContainerOpened", { params ["_container", "_unit"]; - private _index = GVAR(LockerClass) get "locker"; + private _index = GVAR(LockerRepository) get "locker"; clearBackpackCargo _container; clearItemCargo _container; @@ -227,7 +225,7 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [ if (count _index > 25) then { private _params = ["warning", "Over Capacity", "Locker has more then 25 items, please remove some items", 3000]; - GVAR(NotificationClass) call ["create", _params]; + GVAR(NotificationService) call ["create", _params]; }; }]; @@ -235,17 +233,17 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [ params ["_container", "_unit"]; private _newLocker = createHashMap; - _newLocker = GVAR(LockerClass) call ["getCargo", [_container, _newLocker]]; - _newLocker = GVAR(LockerClass) call ["getContainerItems", [_container, _newLocker]]; - _newLocker = GVAR(LockerClass) call ["getAttachments", [_container, _newLocker]]; + _newLocker = GVAR(LockerRepository) call ["getCargo", [_container, _newLocker]]; + _newLocker = GVAR(LockerRepository) call ["getContainerItems", [_container, _newLocker]]; + _newLocker = GVAR(LockerRepository) call ["getAttachments", [_container, _newLocker]]; private _uid = getPlayerUID _unit; [SRPC(locker,requestOverrideLocker), [_uid, _newLocker]] call CFUNC(serverEvent); - GVAR(LockerClass) set ["locker", _newLocker]; + GVAR(LockerRepository) set ["locker", _newLocker]; if (count _newLocker > 25) then { private _params = ["warning", "Over Capacity", "Locker has more then 25 items, please remove some items", 3000]; - GVAR(NotificationClass) call ["create", _params]; + GVAR(NotificationService) call ["create", _params]; }; }]; }], @@ -295,5 +293,5 @@ GVAR(LockerBaseClass) = compileFinal createHashMapFromArray [ }] ]; -GVAR(LockerClass) = createHashMapObject [GVAR(LockerBaseClass)]; -GVAR(LockerClass) +GVAR(LockerRepository) = createHashMapObject [GVAR(LockerRepositoryBaseClass)]; +GVAR(LockerRepository) diff --git a/arma/client/addons/locker/functions/fnc_initVAClass.sqf b/arma/client/addons/locker/functions/fnc_initVARepository.sqf similarity index 79% rename from arma/client/addons/locker/functions/fnc_initVAClass.sqf rename to arma/client/addons/locker/functions/fnc_initVARepository.sqf index 5711d63..d4aa4b9 100644 --- a/arma/client/addons/locker/functions/fnc_initVAClass.sqf +++ b/arma/client/addons/locker/functions/fnc_initVARepository.sqf @@ -1,31 +1,29 @@ #include "..\script_component.hpp" /* - * File: fnc_init.sqf + * File: fnc_initVARepository.sqf * Author: IDSolutions - * Date: 2025-12-16 - * Last Update: 2026-02-13 + * Date: 2026-03-27 * Public: No * * Description: - * Initializes the Virtual Arsenal class for managing player arsenal unlocks. - * Provides methods for syncing, saving, and applying virtual items to BIS Arsenal. + * Initializes the virtual arsenal repository for managing player arsenal unlocks. * * Arguments: * None * * Return Value: - * vArsenal class object [HASHMAP OBJECT] + * Virtual arsenal repository object [HASHMAP OBJECT] * * Example: - * call forge_client_locker_fnc_init; + * call forge_client_locker_fnc_initVARepository; */ #pragma hemtt ignore_variables ["_self"] -GVAR(VABaseClass) = compileFinal createHashMapFromArray [ - ["#type", "VABaseClass"], +GVAR(VARepositoryBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "VARepositoryBaseClass"], ["#create", compileFinal { - _self set ["uid", (getPlayerUID player)]; + _self set ["uid", getPlayerUID player]; _self set ["vArsenal", createHashMap]; _self set ["isLoaded", false]; _self set ["lastSave", time]; @@ -34,9 +32,10 @@ GVAR(VABaseClass) = compileFinal createHashMapFromArray [ private _uid = _self get "uid"; FORGE_Locker_Box = "ReammoBox_F" createVehicleLocal [0, 0, -999]; [SRPC(locker,requestInitVA), [_uid]] call CFUNC(serverEvent); + _self set ["lastSave", time]; - systemChat format ["VArsenal loaded for %1", (name player)]; - diag_log "[FORGE:Client:VArsenal] VArsenal Class Initialized!"; + systemChat format ["VArsenal loaded for %1", name player]; + diag_log "[FORGE:Client:VArsenal] Repository Initialized!"; }], ["save", compileFinal { private _uid = _self get "uid"; @@ -91,5 +90,5 @@ GVAR(VABaseClass) = compileFinal createHashMapFromArray [ }] ]; -GVAR(VAClass) = createHashMapObject [GVAR(VABaseClass)]; -GVAR(VAClass) +GVAR(VARepository) = createHashMapObject [GVAR(VARepositoryBaseClass)]; +GVAR(VARepository) diff --git a/arma/client/addons/notifications/XEH_PREP.hpp b/arma/client/addons/notifications/XEH_PREP.hpp index c0e2dad..e3b9ad1 100644 --- a/arma/client/addons/notifications/XEH_PREP.hpp +++ b/arma/client/addons/notifications/XEH_PREP.hpp @@ -1,3 +1,3 @@ PREP(handleUIEvents); -PREP(initNotificationClass); +PREP(initService); PREP(openUI); diff --git a/arma/client/addons/notifications/XEH_postInitClient.sqf b/arma/client/addons/notifications/XEH_postInitClient.sqf index 6eada6a..6504295 100644 --- a/arma/client/addons/notifications/XEH_postInitClient.sqf +++ b/arma/client/addons/notifications/XEH_postInitClient.sqf @@ -1,16 +1,16 @@ #include "script_component.hpp" [{ - EGVAR(actor,ActorClass) get "isLoaded"; + EGVAR(locker,VARepository) get "isLoaded"; }, { ("NotificationHudLayer" call BFUNC(rscLayer)) cutRsc ["RscNotifications", "PLAIN"]; call FUNC(openUI); - if (isNil QGVAR(NotificationClass)) then { call FUNC(initNotificationClass); }; + if (isNil QGVAR(NotificationService)) then { call FUNC(initService); }; }] call CFUNC(waitUntilAndExecute); [QGVAR(recieveNotification), { params [["_type", "", [""]], ["_title", "", [""]], ["_content", "", [""]], ["_duration", 4000, [4000]]]; playSound QGVAR(notify); - GVAR(NotificationClass) call ["create", [_type, _title, _content, _duration]]; + GVAR(NotificationService) call ["create", [_type, _title, _content, _duration]]; }] call CFUNC(addEventHandler); diff --git a/arma/client/addons/notifications/XEH_preStart.sqf b/arma/client/addons/notifications/XEH_preStart.sqf index 0228885..a51262a 100644 --- a/arma/client/addons/notifications/XEH_preStart.sqf +++ b/arma/client/addons/notifications/XEH_preStart.sqf @@ -1,3 +1,2 @@ #include "script_component.hpp" - #include "XEH_PREP.hpp" diff --git a/arma/client/addons/notifications/functions/fnc_handleUIEvents.sqf b/arma/client/addons/notifications/functions/fnc_handleUIEvents.sqf index 7c84d95..287842c 100644 --- a/arma/client/addons/notifications/functions/fnc_handleUIEvents.sqf +++ b/arma/client/addons/notifications/functions/fnc_handleUIEvents.sqf @@ -32,7 +32,7 @@ diag_log format ["[FORGE:Client:Notifications] Handling UI event: %1 with data: switch (_event) do { case "notifications::ready": { - GVAR(NotificationClass) call ["init", []]; + GVAR(NotificationService) call ["init", []]; }; default { hint format ["[FORGE:Client:Notifications] Unhandled event: %1", _event]; }; }; diff --git a/arma/client/addons/notifications/functions/fnc_initNotificationClass.sqf b/arma/client/addons/notifications/functions/fnc_initService.sqf similarity index 66% rename from arma/client/addons/notifications/functions/fnc_initNotificationClass.sqf rename to arma/client/addons/notifications/functions/fnc_initService.sqf index 06ed9cc..cfdb3ce 100644 --- a/arma/client/addons/notifications/functions/fnc_initNotificationClass.sqf +++ b/arma/client/addons/notifications/functions/fnc_initService.sqf @@ -1,29 +1,27 @@ #include "..\script_component.hpp" /* - * File: fnc_initNotificationClass.sqf + * File: fnc_initService.sqf * Author: IDSolutions - * Date: 2026-01-28 - * Last Update: 2026-01-30 + * Date: 2026-03-27 * Public: No * * Description: - * Initializes the notification class for managing player notifications. - * Provides methods for creating and displaying notifications. + * Initializes the notification service for client notification display. * * Arguments: * None * * Return Value: - * Notification class object [HASHMAP OBJECT] + * Notification service object [HASHMAP OBJECT] * * Example: - * call forge_client_notifications_fnc_initNotificationClass + * call forge_client_notifications_fnc_initService; */ #pragma hemtt ignore_variables ["_self"] -GVAR(NotificationClass) = createHashMapObject [[ - ["#type", "INotificationClass"], +GVAR(NotificationService) = createHashMapObject [[ + ["#type", "INotificationService"], ["#create", { private _display = uiNamespace getVariable ["RscNotifications", nil]; private _control = _display displayCtrl 1004; @@ -37,8 +35,8 @@ GVAR(NotificationClass) = createHashMapObject [[ _self call ["create", _params]; _self set ["isLoaded", true]; - systemChat format ["Notifications loaded for %1", (name player)]; - diag_log "[FORGE:Client:Notifications] Notification Class Initialized!"; + systemChat format ["Notifications loaded for %1", name player]; + diag_log "[FORGE:Client:Notifications] Notification Service Initialized!"; }], ["create", { params [["_type", "", ["info"]], ["_title", "", [""]], ["_content", "", [""]], ["_duration", 4000]]; @@ -55,4 +53,4 @@ GVAR(NotificationClass) = createHashMapObject [[ }] ]]; -GVAR(NotificationClass) +GVAR(NotificationService) diff --git a/arma/client/addons/org/XEH_PREP.hpp b/arma/client/addons/org/XEH_PREP.hpp index 7d71bae..fb83b48 100644 --- a/arma/client/addons/org/XEH_PREP.hpp +++ b/arma/client/addons/org/XEH_PREP.hpp @@ -1,4 +1,4 @@ PREP(handleUIEvents); -PREP(initClass); +PREP(initRepository); PREP(initUIBridge); PREP(openUI); diff --git a/arma/client/addons/org/XEH_postInitClient.sqf b/arma/client/addons/org/XEH_postInitClient.sqf index 3f9e4d9..5ddcfc0 100644 --- a/arma/client/addons/org/XEH_postInitClient.sqf +++ b/arma/client/addons/org/XEH_postInitClient.sqf @@ -1,26 +1,31 @@ #include "script_component.hpp" -if (isNil QGVAR(OrgClass)) then { call FUNC(initClass); }; +if (isNil QGVAR(OrgRepository)) then { call FUNC(initRepository); }; if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initUIBridge); }; [QGVAR(initOrg), { - GVAR(OrgClass) call ["init", []]; + GVAR(OrgRepository) call ["init", []]; }] call CFUNC(addEventHandler); [QGVAR(responseInitOrg), { params [["_data", createHashMap, [createHashMap]]]; - GVAR(OrgClass) call ["sync", [_data, true]]; - GVAR(OrgUIBridge) call ["refreshPortal", []]; + GVAR(OrgRepository) call ["markLoaded", []]; }] call CFUNC(addEventHandler); [QGVAR(responseSyncOrg), { params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]]; - GVAR(OrgClass) call ["sync", [_data, _jip]]; + GVAR(OrgRepository) call ["markLoaded", []]; GVAR(OrgUIBridge) call ["refreshPortal", []]; }] call CFUNC(addEventHandler); +[QGVAR(responseHydrateOrg), { + params [["_payload", createHashMap, [createHashMap]], ["_bridgeEvent", "org::sync", [""]]]; + + GVAR(OrgUIBridge) call ["handleHydrateResponse", [_payload, _bridgeEvent]]; +}] call CFUNC(addEventHandler); + [QGVAR(responseCreateOrg), { params [["_payload", createHashMap, [createHashMap]]]; @@ -46,7 +51,7 @@ if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initUIBridge); }; }] call CFUNC(addEventHandler); [{ - EGVAR(actor,ActorClass) get "isLoaded"; + EGVAR(locker,VARepository) get "isLoaded"; }, { [QGVAR(initOrg), []] call CFUNC(localEvent); }] call CFUNC(waitUntilAndExecute); diff --git a/arma/client/addons/org/XEH_preStart.sqf b/arma/client/addons/org/XEH_preStart.sqf index 0228885..a51262a 100644 --- a/arma/client/addons/org/XEH_preStart.sqf +++ b/arma/client/addons/org/XEH_preStart.sqf @@ -1,3 +1,2 @@ #include "script_component.hpp" - #include "XEH_PREP.hpp" diff --git a/arma/client/addons/org/functions/fnc_handleUIEvents.sqf b/arma/client/addons/org/functions/fnc_handleUIEvents.sqf index a9657c3..6563ac6 100644 --- a/arma/client/addons/org/functions/fnc_handleUIEvents.sqf +++ b/arma/client/addons/org/functions/fnc_handleUIEvents.sqf @@ -1,19 +1,24 @@ #include "..\script_component.hpp" /* + * File: fnc_handleUIEvents.sqf * Author: IDSolutions - * Handles the UI events. + * Date: 2026-03-27 + * Public: No + * + * Description: + * Handles the org UI events. * * Arguments: - * None + * 0: [CONTROL] - The control that triggered the event + * 1: [BOOL] - Whether the event is from a confirm dialog + * 2: [STRING] - The message containing the event data * * Return Value: - * None + * UI events handled [BOOL] * * Example: * call forge_client_org_fnc_handleUIEvents; - * - * Public: No */ params ["_control", "_isConfirmDialog", "_message"]; diff --git a/arma/client/addons/org/functions/fnc_initClass.sqf b/arma/client/addons/org/functions/fnc_initClass.sqf deleted file mode 100644 index dab354d..0000000 --- a/arma/client/addons/org/functions/fnc_initClass.sqf +++ /dev/null @@ -1,181 +0,0 @@ -#include "..\script_component.hpp" - -/* - * File: fnc_initClass.sqf - * Author: IDSolutions - * Date: 2026-02-13 - * Last Update: 2026-02-13 - * Public: No - * - * Description: - * Initializes the org class. - * - * Arguments: - * None - * - * Return Value: - * Org class object [HASHMAP OBJECT] - * - * Examples: - * call forge_client_org_fnc_initClass - */ - -#pragma hemtt ignore_variables ["_self"] -GVAR(OrgBaseClass) = compileFinal createHashMapFromArray [ - ["#type", "OrgBaseClass"], - ["#create", compileFinal { - _self set ["uid", getPlayerUID player]; - _self set ["org", createHashMap]; - _self set ["isLoaded", false]; - _self set ["lastSave", time]; - - private _org = createHashMap; - _org set ["id", ""]; - _org set ["owner", ""]; - _org set ["name", ""]; - _org set ["funds", 0]; - _org set ["reputation", 0]; - _org set ["credit_lines", createHashMap]; - _org set ["assets", createHashMap]; - _org set ["fleet", createHashMap]; - _org set ["members", createHashMap]; - - _self set ["org", _org]; - }], - ["init", compileFinal { - private _uid = _self get "uid"; - private _org = _self get "org"; - - [SRPC(org,requestInitOrg), [_uid, _org]] call CFUNC(serverEvent); - - systemChat format ["Org loaded for %1", (name player)]; - diag_log "[FORGE:Client:Org] Org Class Initialized!"; - }], - ["save", compileFinal { - params [["_sync", false, [false]]]; - - private _uid = _self get "uid"; - [SRPC(org,requestSaveOrg), [_uid, _sync]] call CFUNC(serverEvent); - - _self set ["lastSave", time]; - }], - ["sync", compileFinal { - params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]]; - - private _isLoaded = _self get "isLoaded"; - private _org = _self get "org"; - - { _org set [_x, _y]; } forEach _data; - _self set ["org", _org]; - - if !(_isLoaded) then { _self set ["isLoaded", true]; }; - diag_log "[FORGE:Client:Org] Sync completed"; - }], - ["buildPortalPayload", compileFinal { - private _orgData = _self get "org"; - - private _name = _orgData get "name"; - private _id = _orgData get "id"; - private _ownerUid = _orgData get "owner"; - private _funds = _orgData get "funds"; - private _reputation = _orgData get "reputation"; - private _creditLinesRaw = _orgData getOrDefault ["credit_lines", createHashMap]; - private _assetsRaw = _orgData get "assets"; - private _fleetRaw = _orgData get "fleet"; - private _membersRaw = _orgData get "members"; - private _isDefaultOrg = (_orgData getOrDefault ["default", false]) - || {toLower _id isEqualTo "default"} - || {toLower _ownerUid isEqualTo "server"}; - - private _playerName = name player; - private _playerUid = getPlayerUID player; - private _playerVar = vehicleVarName player; - private _sessionRole = "Member"; - private _sessionIsCeo = _isDefaultOrg && {_playerVar isEqualTo "ceo"}; - private _ownerName = ["", "Server"] select (toLower _ownerUid isEqualTo "server"); - - private _membersList = []; - { - private _memberData = _y; - private _memberName = _memberData getOrDefault ["name", "Unknown"]; - private _memberUid = _memberData getOrDefault ["uid", ""]; - - if (_memberUid isEqualTo _ownerUid && {_ownerName isEqualTo ""}) then { _ownerName = _memberName; }; - if (_memberUid isEqualTo _playerUid) then { _sessionRole = "Member"; }; - - _membersList pushBack (createHashMapFromArray [ - ["uid", _memberUid], - ["name", _memberName] - ]); - } forEach _membersRaw; - - if (_ownerName isEqualTo "" && { _ownerUid isEqualTo _playerUid }) then { _ownerName = _playerName; }; - if (_ownerName isEqualTo "" && { _ownerUid isNotEqualTo "" }) then { _ownerName = "Unknown Owner"; }; - if (_ownerUid isEqualTo _playerUid) then { _sessionRole = "Leader"; }; - - 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])] - ]); - } forEach _assetsRaw; - - private _fleetList = []; - { - private _vehicleData = _y; - _fleetList pushBack (createHashMapFromArray [ - ["name", _vehicleData getOrDefault ["name", "Unknown Vehicle"]], - ["type", _vehicleData getOrDefault ["type", "other"]], - ["status", _vehicleData getOrDefault ["status", "Unknown"]], - ["damage", _vehicleData getOrDefault ["damage", "0%"]] - ]); - } forEach _fleetRaw; - - private _creditLinesList = []; - { - private _creditLineData = _y; - _creditLinesList pushBack (createHashMapFromArray [ - ["uid", _creditLineData getOrDefault ["uid", _x]], - ["member", _creditLineData getOrDefault ["name", "Unknown Member"]], - ["amount", _creditLineData getOrDefault ["amount", 0]] - ]); - } forEach _creditLinesRaw; - - createHashMapFromArray [ - ["session", createHashMapFromArray [ - ["actorName", _playerName], - ["actorUid", _playerUid], - ["role", _sessionRole], - ["ceo", _sessionIsCeo] - ]], - ["portalData", createHashMapFromArray [ - ["org", createHashMapFromArray [ - ["name", _name], - ["tag", _id], - ["owner", _ownerName], - ["ownerUid", _ownerUid], - ["isDefault", _isDefaultOrg] - ]], - ["funds", _funds], - ["reputation", _reputation], - ["creditLines", _creditLinesList], - ["members", _membersList], - ["fleet", _fleetList], - ["assets", _assetsList], - ["activity", []] - ]] - ] - }], - ["get", compileFinal { - params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]]; - - private _org = _self get "org"; - _org getOrDefault [_key, _default]; - }] -]; - -GVAR(OrgClass) = createHashMapObject [GVAR(OrgBaseClass)]; -GVAR(OrgClass) diff --git a/arma/client/addons/org/functions/fnc_initRepository.sqf b/arma/client/addons/org/functions/fnc_initRepository.sqf new file mode 100644 index 0000000..1a6ca54 --- /dev/null +++ b/arma/client/addons/org/functions/fnc_initRepository.sqf @@ -0,0 +1,44 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initRepository.sqf + * Author: IDSolutions + * Date: 2026-03-27 + * Public: No + * + * Description: + * Initializes the org repository for client org lifecycle state. + * + * Arguments: + * None + * + * Return Value: + * Org repository object [HASHMAP OBJECT] + * + * Example: + * call forge_client_org_fnc_initRepository; + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(OrgRepositoryBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "OrgRepositoryBaseClass"], + ["#create", compileFinal { + _self set ["uid", getPlayerUID player]; + _self set ["isLoaded", false]; + _self set ["lastSave", time]; + }], + ["init", compileFinal { + [SRPC(org,requestInitOrg), [getPlayerUID player]] call CFUNC(serverEvent); + _self set ["lastSave", time]; + + systemChat format ["Org loaded for %1", name player]; + diag_log "[FORGE:Client:Org] Org Repository Initialized!"; + }], + ["markLoaded", compileFinal { + if !(_self getOrDefault ["isLoaded", false]) then { _self set ["isLoaded", true]; }; + true + }] +]; + +GVAR(OrgRepository) = createHashMapObject [GVAR(OrgRepositoryBaseClass)]; +GVAR(OrgRepository) diff --git a/arma/client/addons/org/functions/fnc_initUIBridge.sqf b/arma/client/addons/org/functions/fnc_initUIBridge.sqf index cfc5875..a5593df 100644 --- a/arma/client/addons/org/functions/fnc_initUIBridge.sqf +++ b/arma/client/addons/org/functions/fnc_initUIBridge.sqf @@ -50,20 +50,42 @@ GVAR(OrgUIBridgeBaseClass) = compileFinal createHashMapFromArray [ _self call ["setActiveBrowserControl", [_control]]; _control }], + ["hasOpenScreen", compileFinal { + private _screen = _self call ["getScreen", []]; + private _control = _self call ["getActiveBrowserControl", []]; + + !(isNull _control) && { _screen call ["isReady", []] } + }], + ["requestHydrate", compileFinal { + params [["_bridgeEvent", "org::sync", [""]]]; + + if !(_self call ["hasOpenScreen", []]) exitWith { false }; + + private _event = _bridgeEvent; + if !(_event in ["org::login::success", "org::create::success", "org::sync"]) then { + _event = "org::sync"; + }; + + [SRPC(org,requestHydrateOrg), [getPlayerUID player, _event]] call CFUNC(serverEvent); + true + }], + ["handleHydrateResponse", compileFinal { + params [["_payload", createHashMap, [createHashMap]], ["_bridgeEvent", "org::sync", [""]]]; + + if !(_self call ["hasOpenScreen", []]) exitWith { false }; + + private _event = _bridgeEvent; + if !(_event in ["org::login::success", "org::create::success", "org::sync"]) then { + _event = "org::sync"; + }; + + _self call ["sendEvent", [_event, _payload, _self call ["getActiveBrowserControl", []]]] + }], ["handleLoginRequest", compileFinal { params [["_control", controlNull, [controlNull]]]; - private _orgData = GVAR(OrgClass) get "org"; - private _orgId = _orgData getOrDefault ["id", ""]; - private _orgName = _orgData getOrDefault ["name", ""]; - - if (_orgId isEqualTo "" && { _orgName isEqualTo "" }) exitWith { - _self call ["sendEvent", ["org::login::failure", createHashMapFromArray [ - ["message", "No organization data is available for this player."] - ], _control]]; - }; - - _self call ["sendEvent", ["org::login::success", GVAR(OrgClass) call ["buildPortalPayload", []], _control]]; + _self call ["setActiveBrowserControl", [_control]]; + _self call ["requestHydrate", ["org::login::success"]]; }], ["handleCreateRequest", compileFinal { params [["_control", controlNull, [controlNull]], ["_data", createHashMap, [createHashMap]]]; @@ -91,11 +113,11 @@ GVAR(OrgUIBridgeBaseClass) = compileFinal createHashMapFromArray [ ], _control]]; }; - private _orgData = _payload getOrDefault ["org", createHashMap]; - GVAR(OrgClass) call ["sync", [_orgData, true]]; + if !(isNull _control) then { + _self call ["setActiveBrowserControl", [_control]]; + }; - if (isNull _control) exitWith {}; - _self call ["sendEvent", ["org::create::success", GVAR(OrgClass) call ["buildPortalPayload", []], _control]]; + _self call ["requestHydrate", ["org::create::success"]]; }], ["handleDisbandResponse", compileFinal { params [["_payload", createHashMap, [createHashMap]]]; @@ -155,10 +177,7 @@ GVAR(OrgUIBridgeBaseClass) = compileFinal createHashMapFromArray [ [SRPC(org,requestAssignCreditLine), [getPlayerUID player, _memberUid, _memberName, _amount]] call CFUNC(serverEvent); }], ["refreshPortal", compileFinal { - private _control = _self call ["getActiveBrowserControl", []]; - if (isNull _control) exitWith { false }; - - _self call ["sendEvent", ["org::sync", GVAR(OrgClass) call ["buildPortalPayload", []], _control]] + _self call ["requestHydrate", ["org::sync"]] }] ]; diff --git a/arma/client/addons/org/functions/fnc_openUI.sqf b/arma/client/addons/org/functions/fnc_openUI.sqf index 7506dd4..e146d45 100644 --- a/arma/client/addons/org/functions/fnc_openUI.sqf +++ b/arma/client/addons/org/functions/fnc_openUI.sqf @@ -1,19 +1,22 @@ #include "..\script_component.hpp" /* + * File: fnc_openUI.sqf * Author: IDSolutions - * Opens the player interaction interface. + * Date: 2026-03-27 + * Public: No + * + * Description: + * Opens the org UI. * * Arguments: * None * * Return Value: - * None + * UI opened [BOOL] * * Example: * call forge_client_org_fnc_openUI; - * - * Public: No */ private _display = createDialog ["RscOrg", true]; diff --git a/arma/client/addons/org/ui/_site/org-ui.js b/arma/client/addons/org/ui/_site/org-ui.js index 375a7ff..38b3707 100644 --- a/arma/client/addons/org/ui/_site/org-ui.js +++ b/arma/client/addons/org/ui/_site/org-ui.js @@ -1 +1 @@ -!function(){const e=window.ForgeWebUI,n=window.RegistryApp=window.RegistryApp||{},r=window.OrgPortal=window.OrgPortal||{};n.runtime=e,r.runtime=e,window.AppRuntime=e}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{createSignal:n}=e.runtime;e.store=new class{constructor(){[this.getView,this.setView]=n("home"),[this.getIsAuthenticating,this.setIsAuthenticating]=n(!1),[this.getLoginError,this.setLoginError]=n(""),[this.getIsCreating,this.setIsCreating]=n(!1),[this.getCreateError,this.setCreateError]=n("")}startLogin(){this.setLoginError(""),this.setIsAuthenticating(!0)}startCreate(){this.setCreateError(""),this.setIsCreating(!0)}failLogin(e){this.setIsAuthenticating(!1),this.setLoginError(e||"Authentication failed.")}failCreate(e){this.setIsCreating(!1),this.setCreateError(e||"Organization registration failed.")}hydratePortal(e){const n=window.OrgPortal&&window.OrgPortal.data?window.OrgPortal.data:null,r=window.OrgPortal&&window.OrgPortal.store?window.OrgPortal.store:null,t=e&&e.portalData?e.portalData:null,a=e&&e.session?e.session:null;return!!(n&&"function"==typeof n.applyLoginPayload&&r&&"function"==typeof r.hydrateFromPayload&&t&&a)&&(n.applyLoginPayload(e),r.hydrateFromPayload(e),!0)}completeLogin(e){this.hydratePortal(e)?(this.setLoginError(""),this.setIsAuthenticating(!1),this.setView("portal")):this.failLogin("Login response was missing portal data.")}completeCreate(e){this.hydratePortal(e)?(this.setCreateError(""),this.setIsCreating(!1),this.setView("portal")):this.failCreate("Organization registration response was missing portal data.")}}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},n=e.store,r=window.ForgeWebUI.createBridge({closeEvent:"org::close",globalName:"ForgeBridge",readyEvent:"org::ready"});function t(e,n){return r.send(e,n)}r.on("org::login::success",e=>{n.completeLogin(e)}),r.on("org::login::failure",e=>{n.failLogin(e.message||"Authentication failed.")}),r.on("org::create::success",e=>{n.completeCreate(e)}),r.on("org::create::failure",e=>{n.failCreate(e.message||"Organization registration failed.")}),r.on("org::sync",e=>{n&&"function"==typeof n.hydratePortal&&n.hydratePortal(e)}),r.on("org::credit::success",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("success",e.message||"Credit line assigned.")}),r.on("org::credit::failure",e=>{const n=window.OrgPortal;n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Unable to assign credit line.")}),r.on("org::member::creditUpdated",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setCreditLines(n=>{const r={amount:e.amount||0,member:e.memberName||"",uid:e.memberUid||""},t=n.findIndex(e=>e.uid===r.uid);return-1===t?[...n,r]:n.map((e,n)=>n===t?r:e)})}),r.on("org::disband::success",()=>{const e=window.OrgPortal;e&&e.store&&(e.store.setModal(null),e.store.setOrgDisbanded(!0))}),r.on("org::disband::failure",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Organization disbanding failed.")}),r.on("org::leave::success",e=>{const r=window.OrgPortal;r&&r.store&&r.store.setModal(null),n.failLogin(e.message||"You have left the organization."),n.setView("home")}),r.on("org::leave::failure",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Unable to leave the organization.")}),r.on("org::portal::revoked",e=>{const r=window.OrgPortal;r&&r.store&&r.store.setModal(null),n.failLogin(e.message||"Organization access is no longer available."),n.setView("home")}),e.bridge={close:r.close,ready:r.ready,receive:r.receive,requestLogin:function(e){n.startLogin(),t("org::login::request",e)||n.failLogin("Arma login bridge is unavailable.")},requestCreateOrg:function(e){n.startCreate(),t("org::create::request",e)||n.failCreate("Arma registration bridge is unavailable.")},requestDisbandOrg:function(){if(t("org::disband::request",{}))return;const e=window.OrgPortal;e&&e.actions&&e.actions.showTreasuryNotice("error","Arma disband bridge is unavailable.")},requestLeaveOrg:function(){if(t("org::leave::request",{}))return;const e=window.OrgPortal;e&&e.actions&&e.actions.showTreasuryNotice("error","Arma leave bridge is unavailable.")},requestCreditLine:function(e){if(t("org::credit::request",e))return!0;const n=window.OrgPortal;return n&&n.actions&&n.actions.showTreasuryNotice("error","Arma credit line bridge is unavailable."),!1},sendEvent:t}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},n={type:"Organization",status:"Operational",headquarters:"ArmA Verse"};function r(e){return JSON.parse(JSON.stringify(e))}function t(e,n){Object.keys(e).forEach(n=>delete e[n]),Object.assign(e,r(n))}function a(e,n){e.splice(0,e.length,...r(n))}e.data={portalData:{org:Object.assign({name:"",tag:"",owner:"",ownerUid:"",isDefault:!1},n),funds:0,reputation:0,creditLines:[],members:[],fleet:[],assets:[],activity:[],roadmap:[{name:"Contracts Board",status:"Planned",detail:"Track payouts, assignments, and claim approvals."},{name:"Diplomacy",status:"Future Review",detail:"Possible future module pending a full design and scope review."},{name:"Logistics Queue",status:"Future Review",detail:"Possible future module pending a full design and scope review."},{name:"Permissions",status:"Future Review",detail:"Possible future module pending a full design and scope review."}]},session:{actorName:"",actorUid:"",role:"",ceo:!1},applyLoginPayload(e){t(this.portalData.org,Object.assign({},e.portalData.org||{},n)),this.portalData.funds=e.portalData.funds||0,this.portalData.reputation=e.portalData.reputation||0,a(this.portalData.creditLines,e.portalData.creditLines||[]),a(this.portalData.members,e.portalData.members||[]),a(this.portalData.fleet,e.portalData.fleet||[]),a(this.portalData.assets,e.portalData.assets||[]),a(this.portalData.activity,e.portalData.activity||[]),a(this.portalData.roadmap,e.portalData.roadmap||[]),t(this.session,e.session||{})}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{createSignal:n}=window.RegistryApp.runtime,{portalData:r}=e.data;e.store=new class{constructor(){[this.getFunds,this.setFunds]=n(r.funds),[this.getReputation,this.setReputation]=n(r.reputation),[this.getMembers,this.setMembers]=n([...r.members]),[this.getCreditLines,this.setCreditLines]=n([...r.creditLines]),[this.getFleet,this.setFleet]=n([...r.fleet]),[this.getAssets,this.setAssets]=n([...r.assets]),[this.getActivity,this.setActivity]=n([...r.activity]),[this.getTreasuryNotice,this.setTreasuryNotice]=n({type:"",text:""}),[this.getModal,this.setModal]=n(null),[this.getOrgDisbanded,this.setOrgDisbanded]=n(!1)}hydrateFromPayload(e){const n=e.portalData||{};this.setFunds(n.funds||0),this.setReputation(n.reputation||0),this.setMembers([...n.members||[]]),this.setCreditLines([...n.creditLines||[]]),this.setFleet([...n.fleet||[]]),this.setAssets([...n.assets||[]]),this.setActivity([...n.activity||[]])}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{portalData:n,session:r}=e.data;e.getters=new class{formatCurrency(e){return"$"+Number(e||0).toLocaleString()}formatVehicleType(e){return e?e.charAt(0).toUpperCase()+e.slice(1):""}formatAssetType(e){return e?e.charAt(0).toUpperCase()+e.slice(1):""}formatDisplayName(e){return e?String(e).trim().split(/\s+/).map(e=>e?e.charAt(0).toUpperCase()+e.slice(1).toLowerCase():"").join(" "):""}getAssetReadiness(){const r=e.store?e.store.getFleet():n.fleet;if(0===r.length)return null;const t=r.reduce((e,n)=>e+(100-parseInt(n.damage,10)),0);return Math.round(t/r.length)}getNormalizedRole(){return String(r.role||"").trim().toUpperCase()}isDefaultOrg(){return!0===n.org.isDefault||"DEFAULT"===String(n.org.tag||"").trim().toUpperCase()}isOrgOwner(){const e=String(n.org.ownerUid||n.org.owner||"").trim().toLowerCase(),t=String(r.actorUid||"").trim().toLowerCase();return e&&t?t===e:String(r.actorName||"").trim().toLowerCase()===String(n.org.owner||"").trim().toLowerCase()}isSessionCeo(){return!0===r.ceo}isOrgLeaderOrCeo(){return this.isOrgOwner()||"LEADER"===this.getNormalizedRole()||this.isDefaultOrg()&&this.isSessionCeo()}canManageMembers(){return this.isOrgLeaderOrCeo()}canManageTreasury(){return this.isOrgLeaderOrCeo()}canDisbandOrg(){return this.isOrgOwner()&&!this.isDefaultOrg()}canLeaveOrg(){return!this.isDefaultOrg()&&!this.isOrgOwner()}getMemberName(e){return String(e&&"object"==typeof e?e.name||"":e||"")}getMemberUid(e){return e&&"object"==typeof e?String(e.uid||""):""}isOwnerMember(e){return this.getMemberName(e).trim().toLowerCase()===String(n.org.owner||"").trim().toLowerCase()}isCurrentMember(e){const n=this.getMemberUid(e).trim().toLowerCase(),t=String(r.actorUid||"").trim().toLowerCase();return n&&t?n===t:this.getMemberName(e).trim().toLowerCase()===String(r.actorName||"").trim().toLowerCase()}isProtectedMember(e){return this.isOwnerMember(e)||this.isCurrentMember(e)}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{portalData:n}=e.data,r=e.store,t=e.getters,a=window.RegistryApp.store;e.actions=new class{constructor(){this.treasuryNoticeTimer=null}showTreasuryNotice(e,n){r.setTreasuryNotice({type:e,text:n}),this.treasuryNoticeTimer&&clearTimeout(this.treasuryNoticeTimer),this.treasuryNoticeTimer=setTimeout(()=>{r.setTreasuryNotice({type:"",text:""}),this.treasuryNoticeTimer=null},3500)}parseAmount(e){const n=Number(e);return Number.isFinite(n)?Math.round(n):0}getInputValue(e){const n=document.getElementById(e);return n?n.value:""}closePortal(){const e=window.RegistryApp?window.RegistryApp.bridge:null;e&&"function"==typeof e.close?e.close({}):a&&a.setView("home")}openModal(e){"payroll"!==e&&"transfer"!==e&&"credit"!==e||t.canManageTreasury()?("disband"!==e||t.canDisbandOrg())&&("leave"!==e||t.canLeaveOrg())&&r.setModal({type:e}):this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions.")}closeModal(){r.setModal(null)}removeMember(e){if(!t.canManageMembers())return!1;if(t.isProtectedMember(e))return!1;const n=t.getMemberUid(e),a=t.getMemberName(e);return r.setMembers(e=>e.filter(e=>n?e.uid!==n:e.name!==a)),r.setCreditLines(e=>e.filter(e=>n?e.uid!==n:e.member!==a)),!0}disbandOrganization(){if(!t.canDisbandOrg())return!1;const e=window.RegistryApp?window.RegistryApp.bridge:null;return e&&"function"==typeof e.requestDisbandOrg?(this.closeModal(),e.requestDisbandOrg(),!0):(this.showTreasuryNotice("error","Disband bridge is unavailable."),!1)}leaveOrganization(){if(!t.canLeaveOrg())return!1;const e=window.RegistryApp?window.RegistryApp.bridge:null;return e&&"function"==typeof e.requestLeaveOrg?(this.closeModal(),e.requestLeaveOrg(),!0):(this.showTreasuryNotice("error","Leave bridge is unavailable."),!1)}runPayroll(e){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;const n=r.getMembers(),a=r.getFunds();if(0===n.length)return this.showTreasuryNotice("error","No members available for payroll."),!1;if(e<=0)return this.showTreasuryNotice("error","Enter a valid payroll amount."),!1;const o=e*n.length;return o>a?(this.showTreasuryNotice("error","Insufficient org funds for payroll."),!1):(r.setFunds(a-o),this.showTreasuryNotice("success",`Payroll sent to ${n.length} members for ${t.formatCurrency(o)}.`),!0)}sendFundsToMember(e,n){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;const a=r.getFunds();return e?n<=0?(this.showTreasuryNotice("error","Enter a valid transfer amount."),!1):n>a?(this.showTreasuryNotice("error","Insufficient org funds for this transfer."),!1):(r.setFunds(a-n),this.showTreasuryNotice("success",`${t.formatCurrency(n)} sent to ${e}.`),!0):(this.showTreasuryNotice("error","Select a member to receive funds."),!1)}grantCreditLine(e,n){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;if(!e)return this.showTreasuryNotice("error","Select a member for the credit line."),!1;if(n<=0)return this.showTreasuryNotice("error","Enter a valid credit line amount."),!1;const a=r.getMembers().find(n=>t.getMemberUid(n)===e),o=a?t.getMemberName(a):"";if(!o)return this.showTreasuryNotice("error","Selected member was not found in the organization roster."),!1;const i=window.RegistryApp?window.RegistryApp.bridge:null;return i&&"function"==typeof i.requestCreditLine?i.requestCreditLine({memberUid:e,memberName:o,amount:n}):(this.showTreasuryNotice("error","Credit line bridge is unavailable."),!1)}}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-navbar",o=`[${a}]`,i=`\n${o} {\n background: var(--bg-surface);\n border-bottom: 1px solid var(--border);\n box-shadow: var(--shadow);\n}\n\n${o} .app-navbar-inner {\n display: flex;\n justify-content: space-between;\n align-items: center;\n max-width: 1200px;\n width: 100%;\n margin: 0 auto;\n padding: 1rem 2rem;\n box-sizing: border-box;\n}\n\n${o} .app-navbar-brand {\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n}\n\n${o} .app-navbar-kicker {\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n font-weight: 600;\n}\n\n${o} .app-navbar-title {\n font-size: 1.25rem;\n font-weight: 700;\n color: var(--primary-hover);\n letter-spacing: -0.025em;\n}\n\n${o} .app-navbar-actions {\n display: flex;\n align-items: center;\n gap: 1.5rem;\n}\n\n${o} .app-navbar-view {\n font-size: 0.8rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--text-muted);\n font-weight: 600;\n}\n\n${o} .app-close-btn {\n background: transparent;\n color: var(--text-muted);\n border: 1px solid var(--border);\n padding: 0.5rem 1rem;\n font-size: 0.85rem;\n}\n\n${o} .app-close-btn:hover {\n background: var(--bg-surface-hover);\n color: var(--primary-hover);\n border-color: var(--primary);\n transform: none;\n box-shadow: none;\n}\n\n@media (max-width: 960px) {\n ${o} .app-navbar-inner {\n flex-direction: column;\n align-items: flex-start;\n padding: 1rem 1.5rem;\n }\n\n ${o} .app-navbar-actions {\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.Navbar=function({kicker:e="ORBIS",title:n="",viewLabel:o="",actionLabel:s="",onAction:l=null}){return t("shared-navbar",i),r("nav",{className:"app-navbar",[a]:""},r("div",{className:"app-navbar-inner"},r("div",{className:"app-navbar-brand"},r("span",{className:"app-navbar-kicker"},e),r("span",{className:"app-navbar-title"},n)),r("div",{className:"app-navbar-actions"},r("span",{className:"app-navbar-view"},o),s&&"function"==typeof l?r("button",{type:"button",className:"app-close-btn",onClick:l},s):null)))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Header=function({title:e,subtitle:n="Organization Registration & Management Portal",onTitleClick:t=null}){return r("div",{className:"header"},r("h1",{style:{cursor:t?"pointer":"default"},onClick:t},e),r("p",null,n))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.OrgPortal=window.OrgPortal||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Hero=function({className:e="",kicker:n="",title:t="",subtitle:a="",meta:o=""}){const i=["card org-panel org-span-12 org-page-header",e].filter(Boolean).join(" ");return r("section",{className:i},r("div",{className:"org-page-heading"},r("span",{className:"org-page-kicker"},n),r("h1",{className:"org-page-title"},t),r("p",{className:"org-page-subtitle"},a),r("span",{className:"org-page-meta"},o)))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Footer=function({sections:e=[]}){return r("div",{className:"footer"},r("div",{className:"wrapper"},...e.map(e=>r("div",null,r("h3",null,e.title),r("ul",{style:{listStyleType:"none",padding:0}},...(e.items||[]).map(e=>r("li",null,e)))))))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-modal",o=`[${a}]`,i=`\n${o} {\n position: fixed;\n inset: 0;\n background: rgb(15 23 42 / 0.38);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 1.5rem;\n z-index: 20;\n}\n\n${o} .app-modal-card {\n width: min(100%, 30rem);\n margin-bottom: 0;\n text-align: left;\n}\n\n${o} .app-modal-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 1rem;\n margin-bottom: 1rem;\n}\n\n${o} .app-modal-title {\n margin: 0;\n color: var(--primary-hover);\n font-size: 1.45rem;\n}\n\n${o} .app-modal-close {\n width: 2.25rem;\n height: 2.25rem;\n padding: 0;\n background: var(--bg-surface);\n color: var(--text-main);\n border: 1px solid var(--border);\n box-shadow: none;\n transform: none;\n}\n\n${o} .app-modal-close:hover {\n background: var(--bg-surface-hover);\n color: var(--text-main);\n box-shadow: none;\n transform: none;\n}\n\n${o} .app-modal-form {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n${o} .app-modal-form label {\n display: block;\n margin-bottom: 0.5rem;\n color: var(--text-muted);\n font-weight: 500;\n font-size: 0.9rem;\n}\n\n${o} .app-modal-form input,\n${o} .app-modal-form select {\n width: 100%;\n padding: 0.75rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: var(--bg-app);\n color: var(--text-main);\n font-family: inherit;\n font-size: 1rem;\n box-sizing: border-box;\n transition: border-color 0.2s, box-shadow 0.2s;\n}\n\n${o} .app-modal-form input:focus,\n${o} .app-modal-form select:focus {\n outline: none;\n border-color: var(--primary);\n box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12);\n}\n\n${o} .app-modal-form input:disabled,\n${o} .app-modal-form select:disabled {\n background: #f1f5f9;\n color: var(--text-muted);\n cursor: not-allowed;\n}\n\n${o} .app-modal-actions {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 0.75rem;\n margin-top: 0.5rem;\n}\n\n${o} .app-modal-actions button + button,\n${o} .app-modal-danger-actions button + button {\n margin-left: 0;\n}\n\n${o} .app-modal-danger {\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid #fecaca;\n border-radius: var(--radius);\n background: #fff1f2;\n align-items: flex-start;\n}\n\n${o} .app-modal-danger p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .app-modal-danger-actions {\n display: flex;\n flex-wrap: wrap;\n gap: 0.75rem;\n}\n\n@media (max-width: 960px) {\n ${o} .app-modal-head,\n ${o} .app-modal-danger {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.Modal=function({title:e="",body:n=null,onClose:o=null}){return t("shared-modal",i),r("div",{className:"app-modal-backdrop",[a]:"",onClick:e=>{e.target===e.currentTarget&&o&&o()}},r("div",{className:"card app-modal-card"},r("div",{className:"app-modal-head"},r("div",null,r("h2",{className:"app-modal-title"},e)),r("button",{type:"button",className:"app-modal-close",onClick:o,"aria-label":"Close dialog"},"x")),n))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-panel-card",o=`[${a}]`,i=`\n${o} {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n}\n\n${o} .org-panel-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 1rem;\n margin-bottom: 1.5rem;\n}\n\n${o} .org-panel-body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-height: 0;\n}\n\n${o} .org-eyebrow {\n font-size: 0.8rem;\n font-weight: 700;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-muted);\n margin-bottom: 0.4rem;\n}\n\n${o} .org-panel-title {\n margin: 0;\n color: var(--primary-hover);\n font-size: 1.45rem;\n}\n\n${o} .org-panel-subtitle {\n margin: 0.35rem 0 0;\n color: var(--text-muted);\n font-size: 0.95rem;\n}\n\n@media (max-width: 960px) {\n ${o} .org-panel-head {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.PanelCard=function({className:e="",eyebrow:n="",title:o="",subtitle:s="",headerExtras:l=null,body:d=null,rootProps:c={}}){const m=["card org-panel",e].filter(Boolean).join(" ");return t("shared-panel-card",i),r("section",{className:m,[a]:"",...c},r("div",{className:"org-panel-head"},r("div",null,n?r("div",{className:"org-eyebrow"},n):null,r("h2",{className:"org-panel-title"},o),s?r("p",{className:"org-panel-subtitle"},s):null),l),r("div",{className:"org-panel-body"},d))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-metric-card",a=`[${t}]`,o=`\n${a} {\n display: flex;\n flex-direction: column;\n gap: 0.45rem;\n padding: 1rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);\n}\n\n${a}:nth-child(4n + 2),\n${a}:nth-child(4n + 3) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(226 232 240) 100%);\n border-color: rgb(100 116 139 / 0.35);\n box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6);\n}\n\n${a} .org-metric-label {\n font-size: 0.76rem;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--text-muted);\n}\n\n${a} .org-metric-value {\n font-size: 1.8rem;\n color: var(--primary-hover);\n line-height: 1.1;\n}\n\n${a}:nth-child(4n + 2) .org-metric-value,\n${a}:nth-child(4n + 3) .org-metric-value {\n color: #334155;\n}\n\n${a} .org-metric-note {\n color: var(--text-muted);\n font-size: 0.9rem;\n}\n\n@media (max-width: 960px) {\n ${a}:nth-child(4n + 3) {\n background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);\n border-color: var(--border);\n box-shadow: none;\n }\n\n ${a}:nth-child(4n + 3) .org-metric-value {\n color: var(--primary-hover);\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.MetricCard=function(e,a,i){return r("portal-metric-card",o),n("div",{className:"org-metric-card",[t]:""},n("span",{className:"org-metric-label"},e),n("strong",{className:"org-metric-value"},a),n("span",{className:"org-metric-note"},i))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-simple-stat",a=`[${t}]`,o=`\n${a} {\n display: flex;\n flex-direction: column;\n gap: 0.2rem;\n min-width: 90px;\n}\n\n${a} .org-simple-label {\n font-size: 0.72rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${a} .org-simple-value {\n font-size: 0.95rem;\n color: var(--text-main);\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.SimpleStat=function(e,a){return r("portal-simple-stat",o),n("div",{className:"org-simple-stat",[t]:""},n("span",{className:"org-simple-label"},e),n("strong",{className:"org-simple-value"},a))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.store,o=e.getters,i="data-ui-overview-card",s=`[${i}]`,l=`\n${s} .org-hero-grid {\n display: grid;\n grid-template-columns: 1.3fr 1fr;\n gap: 1.5rem;\n align-items: start;\n}\n\n${s} .org-summary {\n margin: 0;\n font-size: 1.05rem;\n color: var(--text-main);\n}\n\n${s} .org-meta-row {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 1rem;\n margin-top: 1.5rem;\n}\n\n${s} .org-meta-item {\n display: flex;\n flex-direction: column;\n gap: 0.4rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${s} .org-meta-item:nth-child(even) {\n background: linear-gradient(180deg, rgb(241 245 249) 0%, rgb(226 232 240) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${s} .org-meta-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${s} .org-meta-value {\n font-size: 1rem;\n font-weight: 600;\n color: var(--primary-hover);\n}\n\n${s} .org-metric-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${s} .org-hero-grid,\n ${s} .org-meta-row,\n ${s} .org-metric-grid {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.OverviewCard=function(){const s=e.componentFns.MetricCard,d=window.SharedUI.componentFns.PanelCard,c=o.getAssetReadiness(),m=t.org.headquarters||"ArmA Verse",g=a.getAssets().length,p=a.getFleet().length,u=a.getFunds(),f=a.getMembers().length,b=a.getReputation();return r("portal-overview-card",l),d({className:"org-span-12",eyebrow:t.org.tag,title:"Organization Overview",rootProps:{[i]:""},body:n("div",{className:"org-hero-grid"},n("div",{className:"org-hero-copy"},n("p",{className:"org-summary"},t.org.type," operating from ",m,". Treasury, fleet status, inventory, and roster management are surfaced here first."),n("div",{className:"org-meta-row"},n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Director"),n("span",{className:"org-meta-value"},o.formatDisplayName(t.org.owner))),n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Active Members"),n("span",{className:"org-meta-value"},`${f} total`)),n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Fleet Readiness"),n("span",{className:"org-meta-value"},null===c?"N/A":`${c}%`)))),n("div",{className:"org-metric-grid"},s("Org Funds",o.formatCurrency(u),"Organization treasury balance"),s("Reputation",b,"Organization standing"),s("Asset Lines",g,"Tracked supply and equipment entries"),s("Fleet Vehicles",p,"Tracked air, ground, and naval vehicles")))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.getters,o="data-ui-fleet-card",i=`[${o}]`,s=`\n${i} .org-simple-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${i} .org-simple-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${i} .org-simple-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${i} .org-simple-name {\n color: var(--primary-hover);\n}\n\n${i} .org-simple-meta {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${i} .org-simple-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.FleetCard=function(){const t=window.SharedUI.componentFns.PanelCard,i=e.componentFns.SimpleStat,l=e.store.getFleet();return r("portal-fleet-card",s),t({className:"org-scroll-panel org-span-7",title:"Fleet",subtitle:"Individual vehicles with type, status, and overall damage.",rootProps:{[o]:""},body:n("div",{className:"org-simple-list"},...l.map(e=>n("article",{className:"org-simple-row"},n("strong",{className:"org-simple-name"},e.name),n("div",{className:"org-simple-meta"},i("Type",a.formatVehicleType(e.type)),i("Status",e.status),i("Damage",e.damage)))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r,createSignal:t}=e.runtime,{portalData:a}=e.data,o=e.store,i=e.getters,s=e.actions,l="data-ui-treasury-card",d=`[${l}]`,[c,m]=t("overview"),[g,p]=t(!1),u=`\n${d} .org-treasury-menu {\n position: relative;\n}\n\n${d} .org-menu-btn {\n width: 2.75rem;\n height: 2.75rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 1px solid var(--border);\n background: #f8fafc;\n color: var(--text-muted);\n}\n\n${d} .org-menu-btn:hover {\n color: var(--primary-hover);\n border-color: rgb(148 163 184 / 0.65);\n}\n\n${d} .org-menu-btn svg {\n width: 1.1rem;\n height: 1.1rem;\n}\n\n${d} .org-menu-dropdown {\n position: absolute;\n top: calc(100% + 0.6rem);\n right: 0;\n min-width: 10.5rem;\n padding: 0.45rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #fff;\n box-shadow: 0 12px 28px rgb(15 23 42 / 0.12);\n display: flex;\n flex-direction: column;\n gap: 0.35rem;\n z-index: 5;\n}\n\n${d} .org-menu-option + .org-menu-option {\n margin-left: 0;\n}\n\n${d} .org-menu-option {\n width: 100%;\n justify-content: flex-start;\n background: transparent;\n color: var(--text-main);\n border: 1px solid transparent;\n}\n\n${d} .org-menu-option:hover {\n background: #f8fafc;\n border-color: rgb(148 163 184 / 0.35);\n}\n\n${d} .org-menu-option.is-active {\n background: rgb(226 232 240 / 0.7);\n color: var(--primary-hover);\n border-color: rgb(148 163 184 / 0.35);\n}\n\n${d} .org-finance-meta {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n margin-bottom: 1.5rem;\n}\n\n${d} .org-finance-meta > div {\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n display: flex;\n flex-direction: column;\n gap: 0.4rem;\n}\n\n${d} .org-meta-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${d} .org-action-grid {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n margin-bottom: 1rem;\n}\n\n${d} .org-action-grid button + button {\n margin-left: 0;\n}\n\n${d} .org-action-grid button {\n width: 100%;\n}\n\n${d} .org-access-note {\n margin: 0 0 1rem;\n color: var(--text-muted);\n font-size: 0.95rem;\n}\n\n${d} .org-credit-summary {\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n padding: 0.85rem 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${d} .org-credit-summary strong {\n font-size: 1rem;\n}\n\n${d} .org-credit-summary span:last-child {\n font-size: 0.92rem;\n line-height: 1.45;\n}\n\n${d} .org-credit-lines-list {\n display: flex;\n flex-direction: column;\n gap: 0.85rem;\n}\n\n${d} .org-treasury-body {\n display: flex;\n flex: 1;\n flex-direction: column;\n gap: 1rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${d} .org-credit-line-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${d} .org-credit-line-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${d} .org-credit-line-member {\n display: flex;\n flex-direction: column;\n gap: 0.3rem;\n}\n\n${d} .org-credit-line-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${d} .org-credit-line-empty {\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n color: var(--text-muted);\n}\n\n@media (max-width: 960px) {\n ${d} .org-finance-meta {\n grid-template-columns: 1fr;\n }\n\n ${d} .org-credit-line-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.TreasuryCard=function(){const e=window.SharedUI.componentFns.PanelCard,t=o.getCreditLines(),a=o.getReputation(),d=i.canManageTreasury(),f=c(),b=g(),w=1===t.length?"1 active credit line":`${t.length} active credit lines`;return r("portal-treasury-card",u),e({className:"org-scroll-panel org-span-5",title:"Treasury",subtitle:"Organization funds, reputation and payouts.",headerExtras:n("div",{className:"org-treasury-menu"},n("button",{type:"button",className:"org-menu-btn",title:"Treasury views","aria-label":"Treasury views",onClick:()=>p(e=>!e)},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"},n("line",{x1:"4",y1:"7",x2:"20",y2:"7"}),n("line",{x1:"4",y1:"12",x2:"20",y2:"12"}),n("line",{x1:"4",y1:"17",x2:"20",y2:"17"}))),b?n("div",{className:"org-menu-dropdown"},n("button",{type:"button",className:"overview"===f?"org-menu-option is-active":"org-menu-option",onClick:()=>{m("overview"),p(!1)}},"Overview"),n("button",{type:"button",className:"credit"===f?"org-menu-option is-active":"org-menu-option",onClick:()=>{m("credit"),p(!1)}},"Credit Lines")):null),rootProps:{[l]:""},body:n("div",{className:"org-treasury-body"},"credit"===f?t.length>0?n("div",{className:"org-credit-lines-list"},...t.map(e=>n("article",{className:"org-credit-line-row"},n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Member"),n("strong",null,e.member)),n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Amount"),n("strong",null,i.formatCurrency(e.amount)))))):n("div",{className:"org-credit-line-empty"},"No active credit lines."):n("div",null,n("div",{className:"org-finance-meta"},n("div",null,n("span",{className:"org-meta-label"},"Funds"),n("strong",null,i.formatCurrency(o.getFunds()))),n("div",null,n("span",{className:"org-meta-label"},"Reputation"),n("strong",null,`${a}`))),d?n("div",{className:"org-action-grid"},n("button",{type:"button",onClick:()=>s.openModal("payroll")},"Run Payroll"),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>s.openModal("transfer")},"Send Funds"),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>s.openModal("credit")},"Credit Line")):n("p",{className:"org-access-note"},"Only the organization leader or CEO can manage treasury actions."),n("div",{className:"org-credit-summary"},n("span",{className:"org-meta-label"},"Credit Line Status"),n("strong",null,w),n("span",null,t.length>0?"Open the Credit Lines tab to review assigned members and amounts.":"Assign a credit line to create the first approved member limit."))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.getters,o="data-ui-assets-card",i=`[${o}]`,s=`\n${i} .org-simple-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${i} .org-simple-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${i} .org-simple-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${i} .org-simple-name {\n color: var(--primary-hover);\n}\n\n${i} .org-simple-meta {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${i} .org-simple-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.AssetsCard=function(){const t=window.SharedUI.componentFns.PanelCard,i=e.componentFns.SimpleStat,l=e.store.getAssets();return r("portal-assets-card",s),t({className:"org-scroll-panel org-span-7",title:"Assets",subtitle:"Inventory supplies and equipment with quantity totals.",rootProps:{[o]:""},body:n("div",{className:"org-simple-list"},...l.map(e=>n("article",{className:"org-simple-row"},n("strong",{className:"org-simple-name"},e.name),n("div",{className:"org-simple-meta"},i("Type",a.formatAssetType(e.type)),i("Quantity",e.quantity)))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.getters,o=e.actions,i="data-ui-members-card",s=`[${i}]`,l=`\n${s} .org-name-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${s} .org-name-row {\n display: flex;\n align-items: center;\n justify-content: flex-start;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${s} .org-name-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${s} .org-name-row button {\n margin-left: auto;\n}\n\n@media (max-width: 960px) {\n ${s} .org-name-row {\n flex-direction: column;\n align-items: flex-start;\n }\n\n ${s} .org-name-row button {\n margin-left: 0;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.MembersCard=function(){const e=window.SharedUI.componentFns.PanelCard,s=t.getMembers(),d=a.canManageMembers();return r("portal-members-card",l),e({className:"org-scroll-panel org-span-5",title:"Members",subtitle:"Current roster listing. The organization owner and your own member entry cannot be removed.",rootProps:{[i]:""},body:n("div",{className:"org-name-list"},...s.map(e=>{const r=d&&!a.isProtectedMember(e);return n("article",{className:"org-name-row"},n("strong",null,e.name),r?n("button",{type:"button",className:"org-danger-btn org-icon-btn",title:`Remove ${e.name}`,"aria-label":`Remove ${e.name}`,onClick:()=>o.removeMember(e)},n("svg",{className:"org-icon",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"},n("path",{d:"M9 3h6"}),n("path",{d:"M4 7h16"}),n("path",{d:"M6 7l1 13h10l1-13"}),n("path",{d:"M10 11v6"}),n("path",{d:"M14 11v6"}))):null)}))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a="data-ui-activity-card",o=`[${a}]`,i=`\n${o} .org-activity-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${o} .org-activity-row {\n padding: 1rem;\n border: 1px solid var(--border);\n border-left: 3px solid #94a3b8;\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${o} .org-activity-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n border-left-color: #64748b;\n}\n\n${o} .org-activity-row p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .org-activity-time {\n display: inline-block;\n margin-bottom: 0.35rem;\n color: var(--text-muted);\n font-size: 0.8rem;\n font-weight: 700;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.ActivityCard=function(){const t=window.SharedUI.componentFns.PanelCard,o=e.store.getActivity();return r("portal-activity-card",i),t({className:"org-scroll-panel org-span-6",title:"Command Feed",subtitle:"Recent organization-level actions and updates.",rootProps:{[a]:""},body:n("div",{className:"org-activity-list"},...o.map(e=>n("article",{className:"org-activity-row"},n("span",{className:"org-activity-time"},e.time),n("p",null,e.text))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-future-card",a=[{name:"Contracts Board",status:"Planned",detail:"Track payouts, assignments, and claim approvals."},{name:"Diplomacy",status:"Future Review",detail:"Possible future module pending a full design and scope review."},{name:"Logistics Queue",status:"Future Review",detail:"Possible future module pending a full design and scope review."},{name:"Permissions",status:"Future Review",detail:"Possible future module pending a full design and scope review."}],o=`[${t}]`,i=`\n${o} .org-roadmap-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${o} .org-roadmap-card {\n padding: 1rem;\n display: flex;\n flex-direction: column;\n gap: 0.7rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${o} .org-roadmap-card:nth-child(4n + 2),\n${o} .org-roadmap-card:nth-child(4n + 3) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(100 116 139 / 0.4);\n}\n\n${o} .org-roadmap-card p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .org-list-tag {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0.2rem 0.55rem;\n border-radius: 999px;\n font-size: 0.72rem;\n font-weight: 700;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n background: #e2e8f0;\n color: var(--primary-hover);\n}\n\n${o} .org-roadmap-card:nth-child(4n + 2) .org-list-tag,\n${o} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {\n background: #cbd5e1;\n color: #1e293b;\n}\n\n@media (max-width: 960px) {\n ${o} .org-roadmap-grid {\n grid-template-columns: 1fr;\n }\n\n ${o} .org-roadmap-card:nth-child(4n + 3) {\n background: #f8fafc;\n border-color: var(--border);\n }\n\n ${o} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {\n background: #e2e8f0;\n color: var(--primary-hover);\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.FutureCard=function(){const e=window.SharedUI.componentFns.PanelCard;return r("portal-future-card",i),e({className:"org-scroll-panel org-span-6",title:"Expansion Slots",subtitle:"Potential modules are tagged by status such as Planned, In Design, In Review, and Future Review.",rootProps:{[t]:""},body:n("div",{className:"org-roadmap-grid"},...a.map(e=>n("article",{className:"org-roadmap-card"},n("span",{className:"org-list-tag"},e.status),n("strong",null,e.name),n("p",null,e.detail))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.getters,a=e.actions,o="data-ui-danger-card",i=`[${o}]`,s=`\n${i} {\n border-color: #fecaca;\n background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%);\n}\n\n${i} .org-danger-copy {\n margin-bottom: 1rem;\n}\n\n${i} .org-danger-copy strong,\n${i} .org-danger-copy p {\n display: block;\n}\n\n${i} .org-danger-copy p {\n margin: 0.4rem 0 0;\n color: var(--text-muted);\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.DangerCard=function(){const e=window.SharedUI.componentFns.PanelCard;return r("portal-danger-card",s),t.canDisbandOrg()?e({className:"org-span-12 org-danger-panel",title:"Organization Controls",subtitle:"Leader-only actions for membership and permanent organization removal.",rootProps:{[o]:""},body:n("div",null,n("div",{className:"org-danger-copy"},n("strong",null,"Disband organization"),n("p",null,"This removes the organization and revokes access to the portal for all members.")),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.openModal("disband")},"Disband Organization"))}):null}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n}=e.runtime,{portalData:r}=e.data,t=e.store,a=e.actions;e.componentFns=e.componentFns||{},e.componentFns.ModalLayer=function(){const e=window.SharedUI.componentFns.Modal,o=t.getModal();if(!o)return null;const i=t.getMembers(),s=0===i.length?{disabled:!0}:{};let l="",d=null;return"payroll"===o.type?(l="Run Payroll",d=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Amount Per Member"),n("input",{id:"treasury-payroll-amount",type:"number",min:"1",placeholder:"500",autofocus:"true"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",onClick:()=>{a.runPayroll(a.parseAmount(a.getInputValue("treasury-payroll-amount")))&&a.closeModal()}},"Run Payroll")))):"transfer"===o.type?(l="Send Funds",d=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Member"),n("select",{id:"treasury-transfer-member",...s},...i.map(e=>n("option",{value:e.name},e.name)))),n("div",null,n("label",null,"Amount"),n("input",{id:"treasury-transfer-amount",type:"number",min:"1",placeholder:"1500"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",...s,onClick:()=>{a.sendFundsToMember(String(a.getInputValue("treasury-transfer-member")||""),a.parseAmount(a.getInputValue("treasury-transfer-amount")))&&a.closeModal()}},"Send Funds")))):"credit"===o.type?(l="Assign Credit Line",d=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Member"),n("select",{id:"treasury-credit-member",...s},...i.map(e=>n("option",{value:e.uid},e.name)))),n("div",null,n("label",null,"Credit Amount"),n("input",{id:"treasury-credit-amount",type:"number",min:"1",placeholder:"5000"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",...s,onClick:()=>{a.grantCreditLine(String(a.getInputValue("treasury-credit-member")||""),a.parseAmount(a.getInputValue("treasury-credit-amount")))&&a.closeModal()}},"Assign Credit Line")))):"disband"===o.type?(l="Disband Organization",d=n("div",{className:"app-modal-danger"},n("p",null,"This action is permanent. Disband ",r.org.name,"?"),n("div",{className:"app-modal-danger-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.disbandOrganization()},"Confirm Disband")))):"leave"===o.type&&(l="Leave Organization",d=n("div",{className:"app-modal-danger"},n("p",null,"Leave ",r.org.name," and return to the default organization?"),n("div",{className:"app-modal-danger-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.leaveOrganization()},"Confirm Leave")))),e({title:l,body:d,onClose:()=>a.closeModal()})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n}=e.runtime,{portalData:r}=e.data,t=window.RegistryApp.store;e.componentFns=e.componentFns||{},e.componentFns.DisbandedView=function(){return(0,window.SharedUI.componentFns.PanelCard)({className:"org-span-12 org-empty-state",eyebrow:"Organization Removed",title:r.org.name,body:n("div",null,n("p",{className:"org-summary"},"This organization has been disbanded. Member access, assets, and fleet management are no longer available from this portal preview."),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>t.setView("home")},"Return to Registry"))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t,session:a}=e.data,o=e.store,i="[data-ui-portal-view]";r("portal-view",`\n ${i} {\n --org-row-card-max-height: 36rem;\n }\n\n ${i} .org-toast-stack {\n position: fixed;\n top: 1.5rem;\n right: 2rem;\n z-index: 20;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n pointer-events: none;\n }\n\n ${i} .org-toast {\n max-width: 24rem;\n padding: 0.9rem 1rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: #fff;\n box-shadow: 0 12px 28px rgb(15 23 42 / 0.14);\n font-size: 0.92rem;\n pointer-events: auto;\n }\n\n ${i} .org-toast.is-success {\n background: #ecfdf5;\n border-color: #bbf7d0;\n color: #166534;\n }\n\n ${i} .org-toast.is-error {\n background: #fef2f2;\n border-color: #fecaca;\n color: #991b1b;\n }\n\n ${i} .org-dashboard-grid {\n display: grid;\n grid-template-columns: repeat(12, minmax(0, 1fr));\n gap: 1.5rem;\n align-items: stretch;\n }\n\n ${i} .org-panel {\n margin-bottom: 0;\n text-align: left;\n }\n\n ${i} .org-scroll-panel {\n display: flex;\n flex-direction: column;\n min-height: 0;\n max-height: var(--org-row-card-max-height);\n overflow: hidden;\n }\n\n ${i} .org-island-root {\n display: flex;\n align-self: stretch;\n min-height: 0;\n min-width: 0;\n }\n\n ${i} .org-island-root > .org-panel {\n height: 100%;\n width: 100%;\n }\n\n ${i} .org-span-12 {\n grid-column: span 12;\n }\n\n ${i} .org-span-7 {\n grid-column: span 7;\n }\n\n ${i} .org-span-6 {\n grid-column: span 6;\n }\n\n ${i} .org-span-5 {\n grid-column: span 5;\n }\n\n @media (max-width: 960px) {\n ${i} .org-toast-stack {\n top: 1rem;\n right: 1rem;\n left: 1rem;\n }\n\n ${i} .org-toast {\n max-width: none;\n }\n\n ${i} .org-span-12,\n ${i} .org-span-7,\n ${i} .org-span-6,\n ${i} .org-span-5 {\n grid-column: span 12;\n }\n\n ${i} .org-scroll-panel {\n max-height: none;\n }\n\n }\n `),e.components=e.components||{},e.componentFns=e.componentFns||{},e.componentFns.TreasuryNoticeLayer=function(){const e=o.getTreasuryNotice();return e.text?n("div",{className:"org-toast-stack"},n("div",{className:"error"===e.type?"org-toast is-error":"org-toast is-success"},e.text)):null},e.components.App=function(){const r=window.SharedUI.componentFns.Hero,i=window.SharedUI.componentFns.Footer,s=e.componentFns.FutureCard,l=e.componentFns.DangerCard,d=e.componentFns.DisbandedView,c=[{title:"Organization Controls",items:["Roster Management","Fleet Assignment","Treasury Permissions","Asset Registry"]},{title:"Planned Extensions",items:["Contracts Board","Diplomacy Layer","Procurement Queue","Reputation History"]}];return o.getOrgDisbanded()?n("main",{"data-ui-portal-view":""},n("div",{className:"container"},n("div",{className:"org-dashboard-grid"},r({kicker:t.org.tag,title:t.org.name,subtitle:"Player organization command portal",meta:`${a.actorName} - ${a.role}`}),d())),n("div",{id:"org-portal-modal-root"}),i({sections:c})):n("main",{"data-ui-portal-view":""},n("div",{id:"org-portal-toast-root"}),n("div",{className:"container"},n("div",{className:"org-dashboard-grid"},r({kicker:t.org.tag,title:t.org.name,subtitle:"Player organization command portal",meta:`${a.actorName} - ${a.role}`}),n("div",{className:"org-island-root org-span-12",id:"org-overview-card-root"}),n("div",{className:"org-island-root org-span-7",id:"org-fleet-card-root"}),n("div",{className:"org-island-root org-span-5",id:"org-treasury-card-root"}),n("div",{className:"org-island-root org-span-5",id:"org-members-card-root"}),n("div",{className:"org-island-root org-span-7",id:"org-assets-card-root"}),n("div",{className:"org-island-root org-span-6",id:"org-activity-card-root"}),s(),l())),n("div",{id:"org-portal-modal-root"}),i({sections:c}))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.bridge,o="data-ui-registration-view",i=`[${o}]`,s=`\n${i} {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2rem;\n align-items: center;\n width: 100%;\n}\n\n${i} .info-panel {\n text-align: left;\n padding: 1rem;\n}\n\n${i} .create-feature-list {\n text-align: left;\n margin-top: 1.5rem;\n list-style-type: none;\n padding: 0;\n}\n\n${i} .create-feature-item {\n margin-bottom: 0.5rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n${i} .create-feature-icon {\n width: 1.2rem;\n height: 1.2rem;\n flex-shrink: 0;\n}\n\n${i} .price-tag {\n margin-top: 2rem;\n padding: 1rem;\n background: var(--bg-app);\n border-radius: var(--radius);\n border: 1px solid var(--border);\n}\n\n${i} .price-label {\n display: block;\n font-size: 0.9rem;\n color: var(--text-muted);\n}\n\n${i} .price-value {\n display: block;\n font-size: 2rem;\n font-weight: 700;\n color: var(--primary);\n}\n\n${i} .form-panel {\n margin: 0;\n}\n\n${i} .app-form {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n text-align: left;\n}\n\n${i} .app-form label {\n display: block;\n margin-bottom: 0.5rem;\n color: var(--text-muted);\n font-weight: 500;\n font-size: 0.9rem;\n}\n\n${i} .app-form input,\n${i} .app-form select {\n width: 100%;\n padding: 0.75rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: var(--bg-app);\n color: var(--text-main);\n font-family: inherit;\n font-size: 1rem;\n box-sizing: border-box;\n transition: border-color 0.2s;\n}\n\n${i} .app-form input:focus,\n${i} .app-form select:focus {\n outline: none;\n border-color: var(--primary);\n box-shadow: 0 0 0 2px rgb(59 130 246 / 0.1);\n}\n\n${i} .form-actions {\n margin-top: 1rem;\n display: flex;\n flex-direction: column;\n gap: 1rem;\n align-items: center;\n}\n\n${i} .submit-btn {\n width: 100%;\n}\n\n${i} .cancel-link {\n font-size: 0.9rem;\n color: var(--text-muted);\n cursor: pointer;\n text-decoration: underline;\n}\n\n${i} .cancel-link:hover {\n color: var(--primary);\n}\n\n${i} .form-feedback {\n padding: 0.85rem 1rem;\n border-radius: var(--radius);\n font-size: 0.92rem;\n}\n\n${i} .form-feedback.is-error {\n background: #fef2f2;\n border: 1px solid #fecaca;\n color: #991b1b;\n}\n\n@media (max-width: 960px) {\n ${i} {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.RegistrationView=function(){const e=t.getIsCreating(),i=t.getCreateError();r("main-registration-view",s);return n("div",{className:"split-container",[o]:""},n("div",{className:"info-panel"},n("h2",null,"Registration Details"),n("p",null,"Complete the form to add your organization to the Global Organization Registry."),n("ul",{className:"create-feature-list"},n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Official Organization Designator"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Secure Comms Channel"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Deployment Roster Access"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"After-Action Report Tools")),n("div",{className:"price-tag"},n("span",{className:"price-label"},"Registration Fee"),n("span",{className:"price-value"},"$50,000"))),n("div",{className:"form-panel card"},n("h2",null,"Organization Registration"),n("div",{className:"app-form"},n("div",null,n("label",null,"Organization Name"),n("input",{id:"org-create-name",type:"text",placeholder:"e.g. Task Force 141"})),n("div",null,n("label",null,"Organization Type"),n("select",{id:"org-create-type"},n("option",{value:"infantry"},"Infantry / Milsim"),n("option",{value:"aviation"},"Aviation Wing"),n("option",{value:"pmc"},"Private Military Company"),n("option",{value:"support"},"Logistics & Support"))),n("div",{className:"form-actions"},i?n("div",{className:"form-feedback is-error"},i):null,n("button",{type:"button",className:"submit-btn",disabled:e,onClick:()=>{const e={orgName:String(document.getElementById("org-create-name")?.value||"").trim(),type:String(document.getElementById("org-create-type")?.value||"")};a&&"function"==typeof a.requestCreateOrg?a.requestCreateOrg(e):t.failCreate("Registration bridge is not available.")}},e?"Submitting Registration...":"Submit Registration"),n("span",{className:"cancel-link",onClick:()=>t.setView("home")},"Cancel / Return to Main")))))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.bridge,o="data-ui-home-view",i=`[${o}]`,s=`\n${i} {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2rem;\n margin-bottom: 2rem;\n}\n\n${i} .home-feedback {\n padding: 0.85rem 1rem;\n border-radius: var(--radius);\n font-size: 0.92rem;\n background: #fef2f2;\n border: 1px solid #fecaca;\n color: #991b1b;\n}\n\n@media (max-width: 960px) {\n ${i} {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.HomeView=function(){const e=t.getIsAuthenticating(),i=t.getLoginError();return r("main-home-view",s),n("div",{className:"content",[o]:""},n("div",{className:"card"},n("h2",null,"Create Organization"),n("p",null,"Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Receive your official unit designator and TO&E authorization instantly."),n("button",{onClick:()=>t.setView("create")},"Register")),n("div",{className:"card"},n("h2",null,"Organization Portal"),n("p",null,"Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink."),i?n("div",{className:"home-feedback"},i):null,n("button",{disabled:e,onClick:()=>{a?a.requestLogin({}):t.failLogin("Login bridge is not available.")}},e?"Opening Portal...":"Login")))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n}=e.runtime,r=e.store;e.components=e.components||{},e.components.App=function(){const t=window.SharedUI.componentFns.Navbar,a=window.SharedUI.componentFns.Header,o=window.SharedUI.componentFns.Footer,i=window.SharedUI.componentFns.WindowTitleBar,s=e.componentFns.HomeView,l=e.componentFns.RegistrationView,d=window.OrgPortal&&window.OrgPortal.components?window.OrgPortal.components.App:null,c=r.getView(),m=window.OrgPortal&&window.OrgPortal.getters?window.OrgPortal.getters:null,g=window.OrgPortal&&window.OrgPortal.actions?window.OrgPortal.actions:null,p="create"===c?"Organization Registration":"portal"===c?"Organization Portal":"Entry Hub";function u(){e.bridge&&"function"==typeof e.bridge.close?e.bridge.close({}):r.setView("home")}if("portal"===c&&d){const e=m&&"function"==typeof m.canLeaveOrg&&m.canLeaveOrg();return n("div",{className:"app-shell"},i({kicker:"FORGE ORBIS",title:"Global Organization Network",onClose:u,closeLabel:"Close organization interface"}),t({title:"Global Organization Network",viewLabel:p,actionLabel:e?"Leave Organization":"",onAction:e&&g&&"function"==typeof g.openModal?()=>g.openModal("leave"):null}),n("div",{id:"org-portal-frame-root"}))}let f;return"home"===c?f=s():"create"===c&&(f=l()),n("div",{className:"app-shell"},i({kicker:"FORGE ORBIS",title:"Global Organization Network",onClose:u,closeLabel:"Close organization interface"}),n("main",null,t({title:"Global Organization Network",viewLabel:p}),n("div",{className:"container"},a({title:"Global Organization Network",onTitleClick:()=>r.setView("home")}),f),o({sections:[{title:"Registry Resources",items:["Registration Guidelines","Tax & Fee Schedule","Legal Compliance","Trademark Database"]},{title:"Bureau Support",items:["Office: Sector 7 Admin Block","Hours: 0800 - 1600 (GST)","Helpdesk: 555-01-REGISTRY","support@org-bureau.gov"]}]})))}}(),function(){const e=window.ForgeWebUI,n=window.RegistryApp,r=window.OrgPortal,t=[{id:"org-portal-frame-root",preserveScroll:!0,render:()=>r.components.App()},{id:"org-portal-toast-root",preserveScroll:!1,render:()=>r.componentFns.TreasuryNoticeLayer()},{id:"org-overview-card-root",preserveScroll:!1,render:()=>r.componentFns.OverviewCard()},{id:"org-fleet-card-root",preserveScroll:!0,render:()=>r.componentFns.FleetCard()},{id:"org-treasury-card-root",preserveScroll:!1,render:()=>r.componentFns.TreasuryCard()},{id:"org-members-card-root",preserveScroll:!0,render:()=>r.componentFns.MembersCard()},{id:"org-assets-card-root",preserveScroll:!0,render:()=>r.componentFns.AssetsCard()},{id:"org-activity-card-root",preserveScroll:!0,render:()=>r.componentFns.ActivityCard()},{id:"org-portal-modal-root",preserveScroll:!1,render:()=>r.componentFns.ModalLayer()}];e.createApp({name:"org",root:"#app",setup({root:r}){const a=function(){const n=new Map;return{sync:function(){t.forEach(r=>{const t=document.getElementById(r.id),a=n.get(r.id);if(!t)return void(a&&(a.handle.dispose(),n.delete(r.id)));if(a&&a.container===t)return;a&&a.handle.dispose();const o=e.mount(t,r.render,{preserveScroll:r.preserveScroll});n.set(r.id,{container:t,handle:o})})}}}();e.mount(r,()=>n.components.App(),{preserveScroll:!1}),n.bridge.ready({loaded:!0}),e.effect(()=>{n.store.getView(),requestAnimationFrame(()=>{a.sync()})})}}).start()}(); \ No newline at end of file +!function(){const e=window.ForgeWebUI,n=window.RegistryApp=window.RegistryApp||{},r=window.OrgPortal=window.OrgPortal||{};n.runtime=e,r.runtime=e,window.AppRuntime=e}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{createSignal:n}=e.runtime;e.store=new class{constructor(){[this.getView,this.setView]=n("home"),[this.getIsAuthenticating,this.setIsAuthenticating]=n(!1),[this.getLoginError,this.setLoginError]=n(""),[this.getIsCreating,this.setIsCreating]=n(!1),[this.getCreateError,this.setCreateError]=n("")}startLogin(){this.setLoginError(""),this.setIsAuthenticating(!0)}startCreate(){this.setCreateError(""),this.setIsCreating(!0)}failLogin(e){this.setIsAuthenticating(!1),this.setLoginError(e||"Authentication failed.")}failCreate(e){this.setIsCreating(!1),this.setCreateError(e||"Organization registration failed.")}hydratePortal(e){const n=window.OrgPortal&&window.OrgPortal.data?window.OrgPortal.data:null,r=window.OrgPortal&&window.OrgPortal.store?window.OrgPortal.store:null,t=e&&e.portalData?e.portalData:null,a=e&&e.session?e.session:null;return!!(n&&"function"==typeof n.applyLoginPayload&&r&&"function"==typeof r.hydrateFromPayload&&t&&a)&&(n.applyLoginPayload(e),r.hydrateFromPayload(e),!0)}completeLogin(e){this.hydratePortal(e)?(this.setLoginError(""),this.setIsAuthenticating(!1),this.setView("portal")):this.failLogin("Login response was missing portal data.")}completeCreate(e){this.hydratePortal(e)?(this.setCreateError(""),this.setIsCreating(!1),this.setView("portal")):this.failCreate("Organization registration response was missing portal data.")}}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},n=e.store,r=window.ForgeWebUI.createBridge({closeEvent:"org::close",globalName:"ForgeBridge",readyEvent:"org::ready"});function t(e,n){return r.send(e,n)}r.on("org::login::success",e=>{n.completeLogin(e)}),r.on("org::login::failure",e=>{n.failLogin(e.message||"Authentication failed.")}),r.on("org::create::success",e=>{n.completeCreate(e)}),r.on("org::create::failure",e=>{n.failCreate(e.message||"Organization registration failed.")}),r.on("org::sync",e=>{n&&"function"==typeof n.hydratePortal&&n.hydratePortal(e)}),r.on("org::credit::success",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("success",e.message||"Credit line assigned.")}),r.on("org::credit::failure",e=>{const n=window.OrgPortal;n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Unable to assign credit line.")}),r.on("org::member::creditUpdated",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setCreditLines(n=>{const r={amount:e.availableAmount||e.amount||0,amountDue:e.amountDue||0,approvedAmount:e.approvedAmount||e.availableAmount||e.amount||0,availableAmount:e.availableAmount||e.amount||0,interestRate:e.interestRate||.1,member:e.memberName||"",outstandingPrincipal:e.outstandingPrincipal||0,uid:e.memberUid||""},t=n.findIndex(e=>e.uid===r.uid);return-1===t?[...n,r]:n.map((e,n)=>n===t?r:e)})}),r.on("org::disband::success",()=>{const e=window.OrgPortal;e&&e.store&&(e.store.setModal(null),e.store.setOrgDisbanded(!0))}),r.on("org::disband::failure",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Organization disbanding failed.")}),r.on("org::leave::success",e=>{const r=window.OrgPortal;r&&r.store&&r.store.setModal(null),n.failLogin(e.message||"You have left the organization."),n.setView("home")}),r.on("org::leave::failure",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Unable to leave the organization.")}),r.on("org::portal::revoked",e=>{const r=window.OrgPortal;r&&r.store&&r.store.setModal(null),n.failLogin(e.message||"Organization access is no longer available."),n.setView("home")}),e.bridge={close:r.close,ready:r.ready,receive:r.receive,requestLogin:function(e){n.startLogin(),t("org::login::request",e)||n.failLogin("Arma login bridge is unavailable.")},requestCreateOrg:function(e){n.startCreate(),t("org::create::request",e)||n.failCreate("Arma registration bridge is unavailable.")},requestDisbandOrg:function(){if(t("org::disband::request",{}))return;const e=window.OrgPortal;e&&e.actions&&e.actions.showTreasuryNotice("error","Arma disband bridge is unavailable.")},requestLeaveOrg:function(){if(t("org::leave::request",{}))return;const e=window.OrgPortal;e&&e.actions&&e.actions.showTreasuryNotice("error","Arma leave bridge is unavailable.")},requestCreditLine:function(e){if(t("org::credit::request",e))return!0;const n=window.OrgPortal;return n&&n.actions&&n.actions.showTreasuryNotice("error","Arma credit line bridge is unavailable."),!1},sendEvent:t}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},n={type:"Organization",status:"Operational",headquarters:"ArmA Verse"};function r(e){return JSON.parse(JSON.stringify(e))}function t(e,n){Object.keys(e).forEach(n=>delete e[n]),Object.assign(e,r(n))}function a(e,n){e.splice(0,e.length,...r(n))}function o(e){if(e&&"object"==typeof e&&!Array.isArray(e))return e;if(Array.isArray(e)){if(e.every(e=>Array.isArray(e)&&e.length>=2&&"string"==typeof e[0]))return Object.fromEntries(e)}if("string"==typeof e&&""!==e.trim())try{return o(JSON.parse(e))}catch(n){return e}return e}function i(e){return(Array.isArray(e)?e:e&&"object"==typeof e?Object.values(e):[]).map(o).filter(Boolean)}e.data={portalData:{org:Object.assign({name:"",tag:"",owner:"",ownerUid:"",isDefault:!1},n),funds:0,reputation:0,creditLines:[],members:[],fleet:[],assets:[],activity:[],roadmap:[{name:"Contracts Board",status:"Planned",detail:"Track payouts, assignments, and claim approvals."},{name:"Diplomacy",status:"Future Review",detail:"Possible future module pending a full design and scope review."},{name:"Logistics Queue",status:"Future Review",detail:"Possible future module pending a full design and scope review."},{name:"Permissions",status:"Future Review",detail:"Possible future module pending a full design and scope review."}]},session:{actorName:"",actorUid:"",role:"",ceo:!1},applyLoginPayload(e){t(this.portalData.org,Object.assign({},e.portalData.org||{},n)),this.portalData.funds=e.portalData.funds||0,this.portalData.reputation=e.portalData.reputation||0,a(this.portalData.creditLines,i(e.portalData.creditLines)),a(this.portalData.members,i(e.portalData.members)),a(this.portalData.fleet,i(e.portalData.fleet)),a(this.portalData.assets,i(e.portalData.assets)),a(this.portalData.activity,i(e.portalData.activity)),a(this.portalData.roadmap,i(e.portalData.roadmap)),t(this.session,e.session||{})}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{createSignal:n}=window.RegistryApp.runtime,{portalData:r}=e.data;function t(e){if(e&&"object"==typeof e&&!Array.isArray(e))return e;if(Array.isArray(e)){if(e.every(e=>Array.isArray(e)&&e.length>=2&&"string"==typeof e[0]))return Object.fromEntries(e)}if("string"==typeof e&&""!==e.trim())try{return t(JSON.parse(e))}catch(n){return e}return e}function a(e){return(Array.isArray(e)?e:e&&"object"==typeof e?Object.values(e):[]).map(t).filter(Boolean)}e.store=new class{constructor(){[this.getFunds,this.setFunds]=n(r.funds),[this.getReputation,this.setReputation]=n(r.reputation),[this.getMembers,this.setMembers]=n([...r.members]),[this.getCreditLines,this.setCreditLines]=n([...r.creditLines]),[this.getFleet,this.setFleet]=n([...r.fleet]),[this.getAssets,this.setAssets]=n([...r.assets]),[this.getActivity,this.setActivity]=n([...r.activity]),[this.getTreasuryNotice,this.setTreasuryNotice]=n({type:"",text:""}),[this.getModal,this.setModal]=n(null),[this.getOrgDisbanded,this.setOrgDisbanded]=n(!1)}hydrateFromPayload(e){const n=e.portalData||{};this.setFunds(n.funds||0),this.setReputation(n.reputation||0),this.setMembers([...a(n.members)]),this.setCreditLines([...a(n.creditLines)]),this.setFleet([...a(n.fleet)]),this.setAssets([...a(n.assets)]),this.setActivity([...a(n.activity)])}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{portalData:n,session:r}=e.data;e.getters=new class{formatCurrency(e){return"$"+Number(e||0).toLocaleString()}formatVehicleType(e){return e?e.charAt(0).toUpperCase()+e.slice(1):""}formatAssetType(e){return e?e.charAt(0).toUpperCase()+e.slice(1):""}formatDisplayName(e){return e?String(e).trim().split(/\s+/).map(e=>e?e.charAt(0).toUpperCase()+e.slice(1).toLowerCase():"").join(" "):""}getAssetReadiness(){const r=e.store?e.store.getFleet():n.fleet;if(0===r.length)return null;const t=r.reduce((e,n)=>e+(100-parseInt(n.damage,10)),0);return Math.round(t/r.length)}getNormalizedRole(){return String(r.role||"").trim().toUpperCase()}isDefaultOrg(){return!0===n.org.isDefault||"DEFAULT"===String(n.org.tag||"").trim().toUpperCase()}isOrgOwner(){const e=String(n.org.ownerUid||n.org.owner||"").trim().toLowerCase(),t=String(r.actorUid||"").trim().toLowerCase();return e&&t?t===e:String(r.actorName||"").trim().toLowerCase()===String(n.org.owner||"").trim().toLowerCase()}isSessionCeo(){return!0===r.ceo}isOrgLeaderOrCeo(){return this.isOrgOwner()||"LEADER"===this.getNormalizedRole()||this.isDefaultOrg()&&this.isSessionCeo()}canManageMembers(){return this.isOrgLeaderOrCeo()}canManageTreasury(){return this.isOrgLeaderOrCeo()}canDisbandOrg(){return this.isOrgOwner()&&!this.isDefaultOrg()}canLeaveOrg(){return!this.isDefaultOrg()&&!this.isOrgOwner()}getMemberName(e){return String(e&&"object"==typeof e?e.name||"":e||"")}getMemberUid(e){return e&&"object"==typeof e?String(e.uid||""):""}isOwnerMember(e){return this.getMemberName(e).trim().toLowerCase()===String(n.org.owner||"").trim().toLowerCase()}isCurrentMember(e){const n=this.getMemberUid(e).trim().toLowerCase(),t=String(r.actorUid||"").trim().toLowerCase();return n&&t?n===t:this.getMemberName(e).trim().toLowerCase()===String(r.actorName||"").trim().toLowerCase()}isProtectedMember(e){return this.isOwnerMember(e)||this.isCurrentMember(e)}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{portalData:n}=e.data,r=e.store,t=e.getters,a=window.RegistryApp.store;e.actions=new class{constructor(){this.treasuryNoticeTimer=null}showTreasuryNotice(e,n){r.setTreasuryNotice({type:e,text:n}),this.treasuryNoticeTimer&&clearTimeout(this.treasuryNoticeTimer),this.treasuryNoticeTimer=setTimeout(()=>{r.setTreasuryNotice({type:"",text:""}),this.treasuryNoticeTimer=null},3500)}parseAmount(e){const n=Number(e);return Number.isFinite(n)?Math.round(n):0}getInputValue(e){const n=document.getElementById(e);return n?n.value:""}closePortal(){const e=window.RegistryApp?window.RegistryApp.bridge:null;e&&"function"==typeof e.close?e.close({}):a&&a.setView("home")}openModal(e){"payroll"!==e&&"transfer"!==e&&"credit"!==e||t.canManageTreasury()?("disband"!==e||t.canDisbandOrg())&&("leave"!==e||t.canLeaveOrg())&&r.setModal({type:e}):this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions.")}closeModal(){r.setModal(null)}removeMember(e){if(!t.canManageMembers())return!1;if(t.isProtectedMember(e))return!1;const n=t.getMemberUid(e),a=t.getMemberName(e);return r.setMembers(e=>e.filter(e=>n?e.uid!==n:e.name!==a)),r.setCreditLines(e=>e.filter(e=>n?e.uid!==n:e.member!==a)),!0}disbandOrganization(){if(!t.canDisbandOrg())return!1;const e=window.RegistryApp?window.RegistryApp.bridge:null;return e&&"function"==typeof e.requestDisbandOrg?(this.closeModal(),e.requestDisbandOrg(),!0):(this.showTreasuryNotice("error","Disband bridge is unavailable."),!1)}leaveOrganization(){if(!t.canLeaveOrg())return!1;const e=window.RegistryApp?window.RegistryApp.bridge:null;return e&&"function"==typeof e.requestLeaveOrg?(this.closeModal(),e.requestLeaveOrg(),!0):(this.showTreasuryNotice("error","Leave bridge is unavailable."),!1)}runPayroll(e){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;const n=r.getMembers(),a=r.getFunds();if(0===n.length)return this.showTreasuryNotice("error","No members available for payroll."),!1;if(e<=0)return this.showTreasuryNotice("error","Enter a valid payroll amount."),!1;const o=e*n.length;return o>a?(this.showTreasuryNotice("error","Insufficient org funds for payroll."),!1):(r.setFunds(a-o),this.showTreasuryNotice("success",`Payroll sent to ${n.length} members for ${t.formatCurrency(o)}.`),!0)}sendFundsToMember(e,n){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;const a=r.getFunds();return e?n<=0?(this.showTreasuryNotice("error","Enter a valid transfer amount."),!1):n>a?(this.showTreasuryNotice("error","Insufficient org funds for this transfer."),!1):(r.setFunds(a-n),this.showTreasuryNotice("success",`${t.formatCurrency(n)} sent to ${e}.`),!0):(this.showTreasuryNotice("error","Select a member to receive funds."),!1)}grantCreditLine(e,n){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;if(!e)return this.showTreasuryNotice("error","Select a member for the credit line."),!1;if(n<=0)return this.showTreasuryNotice("error","Enter a valid credit line amount."),!1;const a=r.getMembers().find(n=>t.getMemberUid(n)===e),o=a?t.getMemberName(a):"";if(!o)return this.showTreasuryNotice("error","Selected member was not found in the organization roster."),!1;const i=window.RegistryApp?window.RegistryApp.bridge:null;return i&&"function"==typeof i.requestCreditLine?i.requestCreditLine({memberUid:e,memberName:o,amount:n}):(this.showTreasuryNotice("error","Credit line bridge is unavailable."),!1)}}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-navbar",o=`[${a}]`,i=`\n${o} {\n background: var(--bg-surface);\n border-bottom: 1px solid var(--border);\n box-shadow: var(--shadow);\n}\n\n${o} .app-navbar-inner {\n display: flex;\n justify-content: space-between;\n align-items: center;\n max-width: 1200px;\n width: 100%;\n margin: 0 auto;\n padding: 1rem 2rem;\n box-sizing: border-box;\n}\n\n${o} .app-navbar-brand {\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n}\n\n${o} .app-navbar-kicker {\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n font-weight: 600;\n}\n\n${o} .app-navbar-title {\n font-size: 1.25rem;\n font-weight: 700;\n color: var(--primary-hover);\n letter-spacing: -0.025em;\n}\n\n${o} .app-navbar-actions {\n display: flex;\n align-items: center;\n gap: 1.5rem;\n}\n\n${o} .app-navbar-view {\n font-size: 0.8rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--text-muted);\n font-weight: 600;\n}\n\n${o} .app-close-btn {\n background: transparent;\n color: var(--text-muted);\n border: 1px solid var(--border);\n padding: 0.5rem 1rem;\n font-size: 0.85rem;\n}\n\n${o} .app-close-btn:hover {\n background: var(--bg-surface-hover);\n color: var(--primary-hover);\n border-color: var(--primary);\n transform: none;\n box-shadow: none;\n}\n\n@media (max-width: 960px) {\n ${o} .app-navbar-inner {\n flex-direction: column;\n align-items: flex-start;\n padding: 1rem 1.5rem;\n }\n\n ${o} .app-navbar-actions {\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.Navbar=function({kicker:e="ORBIS",title:n="",viewLabel:o="",actionLabel:s="",onAction:l=null}){return t("shared-navbar",i),r("nav",{className:"app-navbar",[a]:""},r("div",{className:"app-navbar-inner"},r("div",{className:"app-navbar-brand"},r("span",{className:"app-navbar-kicker"},e),r("span",{className:"app-navbar-title"},n)),r("div",{className:"app-navbar-actions"},r("span",{className:"app-navbar-view"},o),s&&"function"==typeof l?r("button",{type:"button",className:"app-close-btn",onClick:l},s):null)))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Header=function({title:e,subtitle:n="Organization Registration & Management Portal",onTitleClick:t=null}){return r("div",{className:"header"},r("h1",{style:{cursor:t?"pointer":"default"},onClick:t},e),r("p",null,n))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.OrgPortal=window.OrgPortal||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Hero=function({className:e="",kicker:n="",title:t="",subtitle:a="",meta:o=""}){const i=["card org-panel org-span-12 org-page-header",e].filter(Boolean).join(" ");return r("section",{className:i},r("div",{className:"org-page-heading"},r("span",{className:"org-page-kicker"},n),r("h1",{className:"org-page-title"},t),r("p",{className:"org-page-subtitle"},a),r("span",{className:"org-page-meta"},o)))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Footer=function({sections:e=[]}){return r("div",{className:"footer"},r("div",{className:"wrapper"},...e.map(e=>r("div",null,r("h3",null,e.title),r("ul",{style:{listStyleType:"none",padding:0}},...(e.items||[]).map(e=>r("li",null,e)))))))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-modal",o=`[${a}]`,i=`\n${o} {\n position: fixed;\n inset: 0;\n background: rgb(15 23 42 / 0.38);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 1.5rem;\n z-index: 20;\n}\n\n${o} .app-modal-card {\n width: min(100%, 30rem);\n margin-bottom: 0;\n text-align: left;\n}\n\n${o} .app-modal-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 1rem;\n margin-bottom: 1rem;\n}\n\n${o} .app-modal-title {\n margin: 0;\n color: var(--primary-hover);\n font-size: 1.45rem;\n}\n\n${o} .app-modal-close {\n width: 2.25rem;\n height: 2.25rem;\n padding: 0;\n background: var(--bg-surface);\n color: var(--text-main);\n border: 1px solid var(--border);\n box-shadow: none;\n transform: none;\n}\n\n${o} .app-modal-close:hover {\n background: var(--bg-surface-hover);\n color: var(--text-main);\n box-shadow: none;\n transform: none;\n}\n\n${o} .app-modal-form {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n${o} .app-modal-form label {\n display: block;\n margin-bottom: 0.5rem;\n color: var(--text-muted);\n font-weight: 500;\n font-size: 0.9rem;\n}\n\n${o} .app-modal-form input,\n${o} .app-modal-form select {\n width: 100%;\n padding: 0.75rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: var(--bg-app);\n color: var(--text-main);\n font-family: inherit;\n font-size: 1rem;\n box-sizing: border-box;\n transition: border-color 0.2s, box-shadow 0.2s;\n}\n\n${o} .app-modal-form input:focus,\n${o} .app-modal-form select:focus {\n outline: none;\n border-color: var(--primary);\n box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12);\n}\n\n${o} .app-modal-form input:disabled,\n${o} .app-modal-form select:disabled {\n background: #f1f5f9;\n color: var(--text-muted);\n cursor: not-allowed;\n}\n\n${o} .app-modal-actions {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 0.75rem;\n margin-top: 0.5rem;\n}\n\n${o} .app-modal-actions button + button,\n${o} .app-modal-danger-actions button + button {\n margin-left: 0;\n}\n\n${o} .app-modal-danger {\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid #fecaca;\n border-radius: var(--radius);\n background: #fff1f2;\n align-items: flex-start;\n}\n\n${o} .app-modal-danger p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .app-modal-danger-actions {\n display: flex;\n flex-wrap: wrap;\n gap: 0.75rem;\n}\n\n@media (max-width: 960px) {\n ${o} .app-modal-head,\n ${o} .app-modal-danger {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.Modal=function({title:e="",body:n=null,onClose:o=null}){return t("shared-modal",i),r("div",{className:"app-modal-backdrop",[a]:"",onClick:e=>{e.target===e.currentTarget&&o&&o()}},r("div",{className:"card app-modal-card"},r("div",{className:"app-modal-head"},r("div",null,r("h2",{className:"app-modal-title"},e)),r("button",{type:"button",className:"app-modal-close",onClick:o,"aria-label":"Close dialog"},"x")),n))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-panel-card",o=`[${a}]`,i=`\n${o} {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n}\n\n${o} .org-panel-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 1rem;\n margin-bottom: 1.5rem;\n}\n\n${o} .org-panel-body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-height: 0;\n}\n\n${o} .org-eyebrow {\n font-size: 0.8rem;\n font-weight: 700;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-muted);\n margin-bottom: 0.4rem;\n}\n\n${o} .org-panel-title {\n margin: 0;\n color: var(--primary-hover);\n font-size: 1.45rem;\n}\n\n${o} .org-panel-subtitle {\n margin: 0.35rem 0 0;\n color: var(--text-muted);\n font-size: 0.95rem;\n}\n\n@media (max-width: 960px) {\n ${o} .org-panel-head {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.PanelCard=function({className:e="",eyebrow:n="",title:o="",subtitle:s="",headerExtras:l=null,body:d=null,rootProps:c={}}){const m=["card org-panel",e].filter(Boolean).join(" ");return t("shared-panel-card",i),r("section",{className:m,[a]:"",...c},r("div",{className:"org-panel-head"},r("div",null,n?r("div",{className:"org-eyebrow"},n):null,r("h2",{className:"org-panel-title"},o),s?r("p",{className:"org-panel-subtitle"},s):null),l),r("div",{className:"org-panel-body"},d))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-metric-card",a=`[${t}]`,o=`\n${a} {\n display: flex;\n flex-direction: column;\n gap: 0.45rem;\n padding: 1rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);\n}\n\n${a}:nth-child(4n + 2),\n${a}:nth-child(4n + 3) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(226 232 240) 100%);\n border-color: rgb(100 116 139 / 0.35);\n box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6);\n}\n\n${a} .org-metric-label {\n font-size: 0.76rem;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--text-muted);\n}\n\n${a} .org-metric-value {\n font-size: 1.8rem;\n color: var(--primary-hover);\n line-height: 1.1;\n}\n\n${a}:nth-child(4n + 2) .org-metric-value,\n${a}:nth-child(4n + 3) .org-metric-value {\n color: #334155;\n}\n\n${a} .org-metric-note {\n color: var(--text-muted);\n font-size: 0.9rem;\n}\n\n@media (max-width: 960px) {\n ${a}:nth-child(4n + 3) {\n background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);\n border-color: var(--border);\n box-shadow: none;\n }\n\n ${a}:nth-child(4n + 3) .org-metric-value {\n color: var(--primary-hover);\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.MetricCard=function(e,a,i){return r("portal-metric-card",o),n("div",{className:"org-metric-card",[t]:""},n("span",{className:"org-metric-label"},e),n("strong",{className:"org-metric-value"},a),n("span",{className:"org-metric-note"},i))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-simple-stat",a=`[${t}]`,o=`\n${a} {\n display: flex;\n flex-direction: column;\n gap: 0.2rem;\n min-width: 90px;\n}\n\n${a} .org-simple-label {\n font-size: 0.72rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${a} .org-simple-value {\n font-size: 0.95rem;\n color: var(--text-main);\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.SimpleStat=function(e,a){return r("portal-simple-stat",o),n("div",{className:"org-simple-stat",[t]:""},n("span",{className:"org-simple-label"},e),n("strong",{className:"org-simple-value"},a))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.store,o=e.getters,i="data-ui-overview-card",s=`[${i}]`,l=`\n${s} .org-hero-grid {\n display: grid;\n grid-template-columns: 1.3fr 1fr;\n gap: 1.5rem;\n align-items: start;\n}\n\n${s} .org-summary {\n margin: 0;\n font-size: 1.05rem;\n color: var(--text-main);\n}\n\n${s} .org-meta-row {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 1rem;\n margin-top: 1.5rem;\n}\n\n${s} .org-meta-item {\n display: flex;\n flex-direction: column;\n gap: 0.4rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${s} .org-meta-item:nth-child(even) {\n background: linear-gradient(180deg, rgb(241 245 249) 0%, rgb(226 232 240) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${s} .org-meta-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${s} .org-meta-value {\n font-size: 1rem;\n font-weight: 600;\n color: var(--primary-hover);\n}\n\n${s} .org-metric-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${s} .org-hero-grid,\n ${s} .org-meta-row,\n ${s} .org-metric-grid {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.OverviewCard=function(){const s=e.componentFns.MetricCard,d=window.SharedUI.componentFns.PanelCard,c=o.getAssetReadiness(),m=t.org.headquarters||"ArmA Verse",g=a.getAssets().length,p=a.getFleet().length,u=a.getFunds(),f=a.getMembers().length,b=a.getReputation();return r("portal-overview-card",l),d({className:"org-span-12",eyebrow:t.org.tag,title:"Organization Overview",rootProps:{[i]:""},body:n("div",{className:"org-hero-grid"},n("div",{className:"org-hero-copy"},n("p",{className:"org-summary"},t.org.type," operating from ",m,". Treasury, fleet status, inventory, and roster management are surfaced here first."),n("div",{className:"org-meta-row"},n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Director"),n("span",{className:"org-meta-value"},o.formatDisplayName(t.org.owner))),n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Active Members"),n("span",{className:"org-meta-value"},`${f} total`)),n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Fleet Readiness"),n("span",{className:"org-meta-value"},null===c?"N/A":`${c}%`)))),n("div",{className:"org-metric-grid"},s("Org Funds",o.formatCurrency(u),"Organization treasury balance"),s("Reputation",b,"Organization standing"),s("Asset Lines",g,"Tracked supply and equipment entries"),s("Fleet Vehicles",p,"Tracked air, ground, and naval vehicles")))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.getters,o="data-ui-fleet-card",i=`[${o}]`,s=`\n${i} .org-simple-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${i} .org-simple-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${i} .org-simple-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${i} .org-simple-name {\n color: var(--primary-hover);\n}\n\n${i} .org-simple-meta {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${i} .org-simple-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.FleetCard=function(){const t=window.SharedUI.componentFns.PanelCard,i=e.componentFns.SimpleStat,l=e.store.getFleet();return r("portal-fleet-card",s),t({className:"org-scroll-panel org-span-7",title:"Fleet",subtitle:"Individual vehicles with type, status, and overall damage.",rootProps:{[o]:""},body:n("div",{className:"org-simple-list"},...l.map(e=>n("article",{className:"org-simple-row"},n("strong",{className:"org-simple-name"},e.name),n("div",{className:"org-simple-meta"},i("Type",a.formatVehicleType(e.type)),i("Status",e.status),i("Damage",e.damage)))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r,createSignal:t}=e.runtime,{portalData:a}=e.data,o=e.store,i=e.getters,s=e.actions,l="data-ui-treasury-card",d=`[${l}]`,[c,m]=t("overview"),[g,p]=t(!1),u=`\n${d} .org-treasury-menu {\n position: relative;\n}\n\n${d} .org-menu-btn {\n width: 2.75rem;\n height: 2.75rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 1px solid var(--border);\n background: #f8fafc;\n color: var(--text-muted);\n}\n\n${d} .org-menu-btn:hover {\n color: var(--primary-hover);\n border-color: rgb(148 163 184 / 0.65);\n}\n\n${d} .org-menu-btn svg {\n width: 1.1rem;\n height: 1.1rem;\n}\n\n${d} .org-menu-dropdown {\n position: absolute;\n top: calc(100% + 0.6rem);\n right: 0;\n min-width: 10.5rem;\n padding: 0.45rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #fff;\n box-shadow: 0 12px 28px rgb(15 23 42 / 0.12);\n display: flex;\n flex-direction: column;\n gap: 0.35rem;\n z-index: 5;\n}\n\n${d} .org-menu-option + .org-menu-option {\n margin-left: 0;\n}\n\n${d} .org-menu-option {\n width: 100%;\n justify-content: flex-start;\n background: transparent;\n color: var(--text-main);\n border: 1px solid transparent;\n}\n\n${d} .org-menu-option:hover {\n background: #f8fafc;\n border-color: rgb(148 163 184 / 0.35);\n}\n\n${d} .org-menu-option.is-active {\n background: rgb(226 232 240 / 0.7);\n color: var(--primary-hover);\n border-color: rgb(148 163 184 / 0.35);\n}\n\n${d} .org-finance-meta {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n margin-bottom: 1.5rem;\n}\n\n${d} .org-finance-meta > div {\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n display: flex;\n flex-direction: column;\n gap: 0.4rem;\n}\n\n${d} .org-meta-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${d} .org-action-grid {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n margin-bottom: 1rem;\n}\n\n${d} .org-action-grid button + button {\n margin-left: 0;\n}\n\n${d} .org-action-grid button {\n width: 100%;\n}\n\n${d} .org-access-note {\n margin: 0 0 1rem;\n color: var(--text-muted);\n font-size: 0.95rem;\n}\n\n${d} .org-credit-summary {\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n padding: 0.85rem 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${d} .org-credit-summary strong {\n font-size: 1rem;\n}\n\n${d} .org-credit-summary span:last-child {\n font-size: 0.92rem;\n line-height: 1.45;\n}\n\n${d} .org-credit-lines-list {\n display: flex;\n flex-direction: column;\n gap: 0.85rem;\n}\n\n${d} .org-treasury-body {\n display: flex;\n flex: 1;\n flex-direction: column;\n gap: 1rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${d} .org-credit-line-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${d} .org-credit-line-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${d} .org-credit-line-member {\n display: flex;\n flex-direction: column;\n gap: 0.3rem;\n}\n\n${d} .org-credit-line-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${d} .org-credit-line-empty {\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n color: var(--text-muted);\n}\n\n@media (max-width: 960px) {\n ${d} .org-finance-meta {\n grid-template-columns: 1fr;\n }\n\n ${d} .org-credit-line-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.TreasuryCard=function(){const e=window.SharedUI.componentFns.PanelCard,t=o.getCreditLines(),a=o.getReputation(),d=i.canManageTreasury(),f=c(),b=g(),w=t.reduce((e,n)=>e+Number(n.availableAmount||n.amount||0),0),h=t.reduce((e,n)=>e+Number(n.amountDue||0),0),y=1===t.length?"1 active credit line":`${t.length} active credit lines`;return r("portal-treasury-card",u),e({className:"org-scroll-panel org-span-5",title:"Treasury",subtitle:"Organization funds, reputation and payouts.",headerExtras:n("div",{className:"org-treasury-menu"},n("button",{type:"button",className:"org-menu-btn",title:"Treasury views","aria-label":"Treasury views",onClick:()=>p(e=>!e)},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"},n("line",{x1:"4",y1:"7",x2:"20",y2:"7"}),n("line",{x1:"4",y1:"12",x2:"20",y2:"12"}),n("line",{x1:"4",y1:"17",x2:"20",y2:"17"}))),b?n("div",{className:"org-menu-dropdown"},n("button",{type:"button",className:"overview"===f?"org-menu-option is-active":"org-menu-option",onClick:()=>{m("overview"),p(!1)}},"Overview"),n("button",{type:"button",className:"credit"===f?"org-menu-option is-active":"org-menu-option",onClick:()=>{m("credit"),p(!1)}},"Credit Lines")):null),rootProps:{[l]:""},body:n("div",{className:"org-treasury-body"},"credit"===f?t.length>0?n("div",{className:"org-credit-lines-list"},...t.map(e=>n("article",{className:"org-credit-line-row"},n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Member"),n("strong",null,e.member)),n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Available"),n("strong",null,i.formatCurrency(e.availableAmount||e.amount))),n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Amount Due"),n("strong",null,i.formatCurrency(e.amountDue))),n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Interest"),n("strong",null,`${Math.round(100*Number(e.interestRate||0))}%`))))):n("div",{className:"org-credit-line-empty"},"No active credit lines."):n("div",null,n("div",{className:"org-finance-meta"},n("div",null,n("span",{className:"org-meta-label"},"Funds"),n("strong",null,i.formatCurrency(o.getFunds()))),n("div",null,n("span",{className:"org-meta-label"},"Reputation"),n("strong",null,`${a}`)),n("div",null,n("span",{className:"org-meta-label"},"Reserved Credit"),n("strong",null,i.formatCurrency(w))),n("div",null,n("span",{className:"org-meta-label"},"Outstanding Due"),n("strong",null,i.formatCurrency(h)))),d?n("div",{className:"org-action-grid"},n("button",{type:"button",onClick:()=>s.openModal("payroll")},"Run Payroll"),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>s.openModal("transfer")},"Send Funds"),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>s.openModal("credit")},"Credit Line")):n("p",{className:"org-access-note"},"Only the organization leader or CEO can manage treasury actions."),n("div",{className:"org-credit-summary"},n("span",{className:"org-meta-label"},"Credit Line Status"),n("strong",null,y),n("span",null,t.length>0?"Open the Credit Lines tab to review reserved balances, due amounts, and member exposure.":"Assign a credit line to create the first approved member limit."))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.getters,o="data-ui-assets-card",i=`[${o}]`,s=`\n${i} .org-simple-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${i} .org-simple-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${i} .org-simple-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${i} .org-simple-name {\n color: var(--primary-hover);\n}\n\n${i} .org-simple-meta {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${i} .org-simple-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.AssetsCard=function(){const t=window.SharedUI.componentFns.PanelCard,i=e.componentFns.SimpleStat,l=e.store.getAssets();return r("portal-assets-card",s),t({className:"org-scroll-panel org-span-7",title:"Assets",subtitle:"Inventory supplies and equipment with quantity totals.",rootProps:{[o]:""},body:n("div",{className:"org-simple-list"},...l.map(e=>n("article",{className:"org-simple-row"},n("strong",{className:"org-simple-name"},e.name),n("div",{className:"org-simple-meta"},i("Type",a.formatAssetType(e.type)),i("Quantity",e.quantity)))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.getters,o=e.actions,i="data-ui-members-card",s=`[${i}]`,l=`\n${s} .org-name-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${s} .org-name-row {\n display: flex;\n align-items: center;\n justify-content: flex-start;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${s} .org-name-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${s} .org-name-row button {\n margin-left: auto;\n}\n\n@media (max-width: 960px) {\n ${s} .org-name-row {\n flex-direction: column;\n align-items: flex-start;\n }\n\n ${s} .org-name-row button {\n margin-left: 0;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.MembersCard=function(){const e=window.SharedUI.componentFns.PanelCard,s=t.getMembers(),d=a.canManageMembers();return r("portal-members-card",l),e({className:"org-scroll-panel org-span-5",title:"Members",subtitle:"Current roster listing. The organization owner and your own member entry cannot be removed.",rootProps:{[i]:""},body:n("div",{className:"org-name-list"},...s.map(e=>{const r=d&&!a.isProtectedMember(e);return n("article",{className:"org-name-row"},n("strong",null,e.name),r?n("button",{type:"button",className:"org-danger-btn org-icon-btn",title:`Remove ${e.name}`,"aria-label":`Remove ${e.name}`,onClick:()=>o.removeMember(e)},n("svg",{className:"org-icon",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"},n("path",{d:"M9 3h6"}),n("path",{d:"M4 7h16"}),n("path",{d:"M6 7l1 13h10l1-13"}),n("path",{d:"M10 11v6"}),n("path",{d:"M14 11v6"}))):null)}))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a="data-ui-activity-card",o=`[${a}]`,i=`\n${o} .org-activity-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${o} .org-activity-row {\n padding: 1rem;\n border: 1px solid var(--border);\n border-left: 3px solid #94a3b8;\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${o} .org-activity-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n border-left-color: #64748b;\n}\n\n${o} .org-activity-row p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .org-activity-time {\n display: inline-block;\n margin-bottom: 0.35rem;\n color: var(--text-muted);\n font-size: 0.8rem;\n font-weight: 700;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.ActivityCard=function(){const t=window.SharedUI.componentFns.PanelCard,o=e.store.getActivity();return r("portal-activity-card",i),t({className:"org-scroll-panel org-span-6",title:"Command Feed",subtitle:"Recent organization-level actions and updates.",rootProps:{[a]:""},body:n("div",{className:"org-activity-list"},...o.map(e=>n("article",{className:"org-activity-row"},n("span",{className:"org-activity-time"},e.time),n("p",null,e.text))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-future-card",a=[{name:"Contracts Board",status:"Planned",detail:"Track payouts, assignments, and claim approvals."},{name:"Diplomacy",status:"Future Review",detail:"Possible future module pending a full design and scope review."},{name:"Logistics Queue",status:"Future Review",detail:"Possible future module pending a full design and scope review."},{name:"Permissions",status:"Future Review",detail:"Possible future module pending a full design and scope review."}],o=`[${t}]`,i=`\n${o} .org-roadmap-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${o} .org-roadmap-card {\n padding: 1rem;\n display: flex;\n flex-direction: column;\n gap: 0.7rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${o} .org-roadmap-card:nth-child(4n + 2),\n${o} .org-roadmap-card:nth-child(4n + 3) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(100 116 139 / 0.4);\n}\n\n${o} .org-roadmap-card p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .org-list-tag {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0.2rem 0.55rem;\n border-radius: 999px;\n font-size: 0.72rem;\n font-weight: 700;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n background: #e2e8f0;\n color: var(--primary-hover);\n}\n\n${o} .org-roadmap-card:nth-child(4n + 2) .org-list-tag,\n${o} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {\n background: #cbd5e1;\n color: #1e293b;\n}\n\n@media (max-width: 960px) {\n ${o} .org-roadmap-grid {\n grid-template-columns: 1fr;\n }\n\n ${o} .org-roadmap-card:nth-child(4n + 3) {\n background: #f8fafc;\n border-color: var(--border);\n }\n\n ${o} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {\n background: #e2e8f0;\n color: var(--primary-hover);\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.FutureCard=function(){const e=window.SharedUI.componentFns.PanelCard;return r("portal-future-card",i),e({className:"org-scroll-panel org-span-6",title:"Expansion Slots",subtitle:"Potential modules are tagged by status such as Planned, In Design, In Review, and Future Review.",rootProps:{[t]:""},body:n("div",{className:"org-roadmap-grid"},...a.map(e=>n("article",{className:"org-roadmap-card"},n("span",{className:"org-list-tag"},e.status),n("strong",null,e.name),n("p",null,e.detail))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.getters,a=e.actions,o="data-ui-danger-card",i=`[${o}]`,s=`\n${i} {\n border-color: #fecaca;\n background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%);\n}\n\n${i} .org-danger-copy {\n margin-bottom: 1rem;\n}\n\n${i} .org-danger-copy strong,\n${i} .org-danger-copy p {\n display: block;\n}\n\n${i} .org-danger-copy p {\n margin: 0.4rem 0 0;\n color: var(--text-muted);\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.DangerCard=function(){const e=window.SharedUI.componentFns.PanelCard;return r("portal-danger-card",s),t.canDisbandOrg()?e({className:"org-span-12 org-danger-panel",title:"Organization Controls",subtitle:"Leader-only actions for membership and permanent organization removal.",rootProps:{[o]:""},body:n("div",null,n("div",{className:"org-danger-copy"},n("strong",null,"Disband organization"),n("p",null,"This removes the organization and revokes access to the portal for all members.")),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.openModal("disband")},"Disband Organization"))}):null}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n}=e.runtime,{portalData:r}=e.data,t=e.store,a=e.actions;e.componentFns=e.componentFns||{},e.componentFns.ModalLayer=function(){const e=window.SharedUI.componentFns.Modal,o=t.getModal();if(!o)return null;const i=t.getMembers(),s=0===i.length?{disabled:!0}:{};let l="",d=null;return"payroll"===o.type?(l="Run Payroll",d=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Amount Per Member"),n("input",{id:"treasury-payroll-amount",type:"number",min:"1",placeholder:"500",autofocus:"true"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",onClick:()=>{a.runPayroll(a.parseAmount(a.getInputValue("treasury-payroll-amount")))&&a.closeModal()}},"Run Payroll")))):"transfer"===o.type?(l="Send Funds",d=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Member"),n("select",{id:"treasury-transfer-member",...s},...i.map(e=>n("option",{value:e.name},e.name)))),n("div",null,n("label",null,"Amount"),n("input",{id:"treasury-transfer-amount",type:"number",min:"1",placeholder:"1500"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",...s,onClick:()=>{a.sendFundsToMember(String(a.getInputValue("treasury-transfer-member")||""),a.parseAmount(a.getInputValue("treasury-transfer-amount")))&&a.closeModal()}},"Send Funds")))):"credit"===o.type?(l="Assign Credit Line",d=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Member"),n("select",{id:"treasury-credit-member",...s},...i.map(e=>n("option",{value:e.uid},e.name)))),n("div",null,n("label",null,"Credit Amount"),n("input",{id:"treasury-credit-amount",type:"number",min:"1",placeholder:"5000"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",...s,onClick:()=>{a.grantCreditLine(String(a.getInputValue("treasury-credit-member")||""),a.parseAmount(a.getInputValue("treasury-credit-amount")))&&a.closeModal()}},"Assign Credit Line")))):"disband"===o.type?(l="Disband Organization",d=n("div",{className:"app-modal-danger"},n("p",null,"This action is permanent. Disband ",r.org.name,"?"),n("div",{className:"app-modal-danger-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.disbandOrganization()},"Confirm Disband")))):"leave"===o.type&&(l="Leave Organization",d=n("div",{className:"app-modal-danger"},n("p",null,"Leave ",r.org.name," and return to the default organization?"),n("div",{className:"app-modal-danger-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.leaveOrganization()},"Confirm Leave")))),e({title:l,body:d,onClose:()=>a.closeModal()})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n}=e.runtime,{portalData:r}=e.data,t=window.RegistryApp.store;e.componentFns=e.componentFns||{},e.componentFns.DisbandedView=function(){return(0,window.SharedUI.componentFns.PanelCard)({className:"org-span-12 org-empty-state",eyebrow:"Organization Removed",title:r.org.name,body:n("div",null,n("p",{className:"org-summary"},"This organization has been disbanded. Member access, assets, and fleet management are no longer available from this portal preview."),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>t.setView("home")},"Return to Registry"))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t,session:a}=e.data,o=e.store,i="[data-ui-portal-view]";r("portal-view",`\n ${i} {\n --org-row-card-max-height: 36rem;\n }\n\n ${i} .org-toast-stack {\n position: fixed;\n top: 1.5rem;\n right: 2rem;\n z-index: 20;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n pointer-events: none;\n }\n\n ${i} .org-toast {\n max-width: 24rem;\n padding: 0.9rem 1rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: #fff;\n box-shadow: 0 12px 28px rgb(15 23 42 / 0.14);\n font-size: 0.92rem;\n pointer-events: auto;\n }\n\n ${i} .org-toast.is-success {\n background: #ecfdf5;\n border-color: #bbf7d0;\n color: #166534;\n }\n\n ${i} .org-toast.is-error {\n background: #fef2f2;\n border-color: #fecaca;\n color: #991b1b;\n }\n\n ${i} .org-dashboard-grid {\n display: grid;\n grid-template-columns: repeat(12, minmax(0, 1fr));\n gap: 1.5rem;\n align-items: stretch;\n }\n\n ${i} .org-panel {\n margin-bottom: 0;\n text-align: left;\n }\n\n ${i} .org-scroll-panel {\n display: flex;\n flex-direction: column;\n min-height: 0;\n max-height: var(--org-row-card-max-height);\n overflow: hidden;\n }\n\n ${i} .org-island-root {\n display: flex;\n align-self: stretch;\n min-height: 0;\n min-width: 0;\n }\n\n ${i} .org-island-root > .org-panel {\n height: 100%;\n width: 100%;\n }\n\n ${i} .org-span-12 {\n grid-column: span 12;\n }\n\n ${i} .org-span-7 {\n grid-column: span 7;\n }\n\n ${i} .org-span-6 {\n grid-column: span 6;\n }\n\n ${i} .org-span-5 {\n grid-column: span 5;\n }\n\n @media (max-width: 960px) {\n ${i} .org-toast-stack {\n top: 1rem;\n right: 1rem;\n left: 1rem;\n }\n\n ${i} .org-toast {\n max-width: none;\n }\n\n ${i} .org-span-12,\n ${i} .org-span-7,\n ${i} .org-span-6,\n ${i} .org-span-5 {\n grid-column: span 12;\n }\n\n ${i} .org-scroll-panel {\n max-height: none;\n }\n\n }\n `),e.components=e.components||{},e.componentFns=e.componentFns||{},e.componentFns.TreasuryNoticeLayer=function(){const e=o.getTreasuryNotice();return e.text?n("div",{className:"org-toast-stack"},n("div",{className:"error"===e.type?"org-toast is-error":"org-toast is-success"},e.text)):null},e.components.App=function(){const r=window.SharedUI.componentFns.Hero,i=window.SharedUI.componentFns.Footer,s=e.componentFns.FutureCard,l=e.componentFns.DangerCard,d=e.componentFns.DisbandedView,c=[{title:"Organization Controls",items:["Roster Management","Fleet Assignment","Treasury Permissions","Asset Registry"]},{title:"Planned Extensions",items:["Contracts Board","Diplomacy Layer","Procurement Queue","Reputation History"]}];return o.getOrgDisbanded()?n("main",{"data-ui-portal-view":""},n("div",{className:"container"},n("div",{className:"org-dashboard-grid"},r({kicker:t.org.tag,title:t.org.name,subtitle:"Player organization command portal",meta:`${a.actorName} - ${a.role}`}),d())),n("div",{id:"org-portal-modal-root"}),i({sections:c})):n("main",{"data-ui-portal-view":""},n("div",{id:"org-portal-toast-root"}),n("div",{className:"container"},n("div",{className:"org-dashboard-grid"},r({kicker:t.org.tag,title:t.org.name,subtitle:"Player organization command portal",meta:`${a.actorName} - ${a.role}`}),n("div",{className:"org-island-root org-span-12",id:"org-overview-card-root"}),n("div",{className:"org-island-root org-span-7",id:"org-fleet-card-root"}),n("div",{className:"org-island-root org-span-5",id:"org-treasury-card-root"}),n("div",{className:"org-island-root org-span-5",id:"org-members-card-root"}),n("div",{className:"org-island-root org-span-7",id:"org-assets-card-root"}),n("div",{className:"org-island-root org-span-6",id:"org-activity-card-root"}),s(),l())),n("div",{id:"org-portal-modal-root"}),i({sections:c}))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.bridge,o="data-ui-registration-view",i=`[${o}]`,s=`\n${i} {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2rem;\n align-items: center;\n width: 100%;\n}\n\n${i} .info-panel {\n text-align: left;\n padding: 1rem;\n}\n\n${i} .create-feature-list {\n text-align: left;\n margin-top: 1.5rem;\n list-style-type: none;\n padding: 0;\n}\n\n${i} .create-feature-item {\n margin-bottom: 0.5rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n${i} .create-feature-icon {\n width: 1.2rem;\n height: 1.2rem;\n flex-shrink: 0;\n}\n\n${i} .price-tag {\n margin-top: 2rem;\n padding: 1rem;\n background: var(--bg-app);\n border-radius: var(--radius);\n border: 1px solid var(--border);\n}\n\n${i} .price-label {\n display: block;\n font-size: 0.9rem;\n color: var(--text-muted);\n}\n\n${i} .price-value {\n display: block;\n font-size: 2rem;\n font-weight: 700;\n color: var(--primary);\n}\n\n${i} .form-panel {\n margin: 0;\n}\n\n${i} .app-form {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n text-align: left;\n}\n\n${i} .app-form label {\n display: block;\n margin-bottom: 0.5rem;\n color: var(--text-muted);\n font-weight: 500;\n font-size: 0.9rem;\n}\n\n${i} .app-form input,\n${i} .app-form select {\n width: 100%;\n padding: 0.75rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: var(--bg-app);\n color: var(--text-main);\n font-family: inherit;\n font-size: 1rem;\n box-sizing: border-box;\n transition: border-color 0.2s;\n}\n\n${i} .app-form input:focus,\n${i} .app-form select:focus {\n outline: none;\n border-color: var(--primary);\n box-shadow: 0 0 0 2px rgb(59 130 246 / 0.1);\n}\n\n${i} .form-actions {\n margin-top: 1rem;\n display: flex;\n flex-direction: column;\n gap: 1rem;\n align-items: center;\n}\n\n${i} .submit-btn {\n width: 100%;\n}\n\n${i} .cancel-link {\n font-size: 0.9rem;\n color: var(--text-muted);\n cursor: pointer;\n text-decoration: underline;\n}\n\n${i} .cancel-link:hover {\n color: var(--primary);\n}\n\n${i} .form-feedback {\n padding: 0.85rem 1rem;\n border-radius: var(--radius);\n font-size: 0.92rem;\n}\n\n${i} .form-feedback.is-error {\n background: #fef2f2;\n border: 1px solid #fecaca;\n color: #991b1b;\n}\n\n@media (max-width: 960px) {\n ${i} {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.RegistrationView=function(){const e=t.getIsCreating(),i=t.getCreateError();r("main-registration-view",s);return n("div",{className:"split-container",[o]:""},n("div",{className:"info-panel"},n("h2",null,"Registration Details"),n("p",null,"Complete the form to add your organization to the Global Organization Registry."),n("ul",{className:"create-feature-list"},n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Official Organization Designator"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Secure Comms Channel"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Deployment Roster Access"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"After-Action Report Tools")),n("div",{className:"price-tag"},n("span",{className:"price-label"},"Registration Fee"),n("span",{className:"price-value"},"$50,000"))),n("div",{className:"form-panel card"},n("h2",null,"Organization Registration"),n("div",{className:"app-form"},n("div",null,n("label",null,"Organization Name"),n("input",{id:"org-create-name",type:"text",placeholder:"e.g. Task Force 141"})),n("div",null,n("label",null,"Organization Type"),n("select",{id:"org-create-type"},n("option",{value:"infantry"},"Infantry / Milsim"),n("option",{value:"aviation"},"Aviation Wing"),n("option",{value:"pmc"},"Private Military Company"),n("option",{value:"support"},"Logistics & Support"))),n("div",{className:"form-actions"},i?n("div",{className:"form-feedback is-error"},i):null,n("button",{type:"button",className:"submit-btn",disabled:e,onClick:()=>{const e={orgName:String(document.getElementById("org-create-name")?.value||"").trim(),type:String(document.getElementById("org-create-type")?.value||"")};a&&"function"==typeof a.requestCreateOrg?a.requestCreateOrg(e):t.failCreate("Registration bridge is not available.")}},e?"Submitting Registration...":"Submit Registration"),n("span",{className:"cancel-link",onClick:()=>t.setView("home")},"Cancel / Return to Main")))))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.bridge,o="data-ui-home-view",i=`[${o}]`,s=`\n${i} {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2rem;\n margin-bottom: 2rem;\n}\n\n${i} .home-feedback {\n padding: 0.85rem 1rem;\n border-radius: var(--radius);\n font-size: 0.92rem;\n background: #fef2f2;\n border: 1px solid #fecaca;\n color: #991b1b;\n}\n\n@media (max-width: 960px) {\n ${i} {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.HomeView=function(){const e=t.getIsAuthenticating(),i=t.getLoginError();return r("main-home-view",s),n("div",{className:"content",[o]:""},n("div",{className:"card"},n("h2",null,"Create Organization"),n("p",null,"Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Receive your official unit designator and TO&E authorization instantly."),n("button",{onClick:()=>t.setView("create")},"Register")),n("div",{className:"card"},n("h2",null,"Organization Portal"),n("p",null,"Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink."),i?n("div",{className:"home-feedback"},i):null,n("button",{disabled:e,onClick:()=>{a?a.requestLogin({}):t.failLogin("Login bridge is not available.")}},e?"Opening Portal...":"Login")))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n}=e.runtime,r=e.store;e.components=e.components||{},e.components.App=function(){const t=window.SharedUI.componentFns.Navbar,a=window.SharedUI.componentFns.Header,o=window.SharedUI.componentFns.Footer,i=window.SharedUI.componentFns.WindowTitleBar,s=e.componentFns.HomeView,l=e.componentFns.RegistrationView,d=window.OrgPortal&&window.OrgPortal.components?window.OrgPortal.components.App:null,c=r.getView(),m=window.OrgPortal&&window.OrgPortal.getters?window.OrgPortal.getters:null,g=window.OrgPortal&&window.OrgPortal.actions?window.OrgPortal.actions:null,p="create"===c?"Organization Registration":"portal"===c?"Organization Portal":"Entry Hub";function u(){e.bridge&&"function"==typeof e.bridge.close?e.bridge.close({}):r.setView("home")}if("portal"===c&&d){const e=m&&"function"==typeof m.canLeaveOrg&&m.canLeaveOrg();return n("div",{className:"app-shell"},i({kicker:"FORGE ORBIS",title:"Global Organization Network",onClose:u,closeLabel:"Close organization interface"}),t({title:"Global Organization Network",viewLabel:p,actionLabel:e?"Leave Organization":"",onAction:e&&g&&"function"==typeof g.openModal?()=>g.openModal("leave"):null}),n("div",{id:"org-portal-frame-root"}))}let f;return"home"===c?f=s():"create"===c&&(f=l()),n("div",{className:"app-shell"},i({kicker:"FORGE ORBIS",title:"Global Organization Network",onClose:u,closeLabel:"Close organization interface"}),n("main",null,t({title:"Global Organization Network",viewLabel:p}),n("div",{className:"container"},a({title:"Global Organization Network",onTitleClick:()=>r.setView("home")}),f),o({sections:[{title:"Registry Resources",items:["Registration Guidelines","Tax & Fee Schedule","Legal Compliance","Trademark Database"]},{title:"Bureau Support",items:["Office: Sector 7 Admin Block","Hours: 0800 - 1600 (GST)","Helpdesk: 555-01-REGISTRY","support@org-bureau.gov"]}]})))}}(),function(){const e=window.ForgeWebUI,n=window.RegistryApp,r=window.OrgPortal,t=[{id:"org-portal-frame-root",preserveScroll:!0,render:()=>r.components.App()},{id:"org-portal-toast-root",preserveScroll:!1,render:()=>r.componentFns.TreasuryNoticeLayer()},{id:"org-overview-card-root",preserveScroll:!1,render:()=>r.componentFns.OverviewCard()},{id:"org-fleet-card-root",preserveScroll:!0,render:()=>r.componentFns.FleetCard()},{id:"org-treasury-card-root",preserveScroll:!1,render:()=>r.componentFns.TreasuryCard()},{id:"org-members-card-root",preserveScroll:!0,render:()=>r.componentFns.MembersCard()},{id:"org-assets-card-root",preserveScroll:!0,render:()=>r.componentFns.AssetsCard()},{id:"org-activity-card-root",preserveScroll:!0,render:()=>r.componentFns.ActivityCard()},{id:"org-portal-modal-root",preserveScroll:!1,render:()=>r.componentFns.ModalLayer()}];e.createApp({name:"org",root:"#app",setup({root:r}){const a=function(){const n=new Map;return{sync:function(){t.forEach(r=>{const t=document.getElementById(r.id),a=n.get(r.id);if(!t)return void(a&&(a.handle.dispose(),n.delete(r.id)));if(a&&a.container===t)return;a&&a.handle.dispose();const o=e.mount(t,r.render,{preserveScroll:r.preserveScroll});n.set(r.id,{container:t,handle:o})})}}}();e.mount(r,()=>n.components.App(),{preserveScroll:!1}),n.bridge.ready({loaded:!0}),e.effect(()=>{n.store.getView(),requestAnimationFrame(()=>{a.sync()})})}}).start()}(); \ No newline at end of file diff --git a/arma/client/addons/org/ui/src/bridge.js b/arma/client/addons/org/ui/src/bridge.js index cfa10ae..d5fab25 100644 --- a/arma/client/addons/org/ui/src/bridge.js +++ b/arma/client/addons/org/ui/src/bridge.js @@ -136,8 +136,18 @@ OrgPortal.store.setCreditLines((currentLines) => { const nextLine = { - amount: payloadData.amount || 0, + amount: payloadData.availableAmount || payloadData.amount || 0, + amountDue: payloadData.amountDue || 0, + approvedAmount: + payloadData.approvedAmount || + payloadData.availableAmount || + payloadData.amount || + 0, + availableAmount: + payloadData.availableAmount || payloadData.amount || 0, + interestRate: payloadData.interestRate || 0.1, member: payloadData.memberName || "", + outstandingPrincipal: payloadData.outstandingPrincipal || 0, uid: payloadData.memberUid || "", }; const matchIndex = currentLines.findIndex( diff --git a/arma/client/addons/org/ui/src/components/portal/treasuryCard.js b/arma/client/addons/org/ui/src/components/portal/treasuryCard.js index 0de5144..6b95c49 100644 --- a/arma/client/addons/org/ui/src/components/portal/treasuryCard.js +++ b/arma/client/addons/org/ui/src/components/portal/treasuryCard.js @@ -215,6 +215,15 @@ ${scopeSelector} .org-credit-line-empty { const allowTreasuryActions = getters.canManageTreasury(); const activeTab = getTreasuryTab(); const isMenuOpen = getTreasuryMenuOpen(); + const totalReserved = creditLines.reduce( + (sum, line) => + sum + Number(line.availableAmount || line.amount || 0), + 0, + ); + const totalDue = creditLines.reduce( + (sum, line) => sum + Number(line.amountDue || 0), + 0, + ); const activeCreditLabel = creditLines.length === 1 ? "1 active credit line" @@ -331,16 +340,59 @@ ${scopeSelector} .org-credit-line-empty { className: "org-credit-line-label", }, - "Amount", + "Available", ), h( "strong", null, getters.formatCurrency( - line.amount, + line.availableAmount || + line.amount, ), ), ), + h( + "div", + { + className: + "org-credit-line-member", + }, + h( + "span", + { + className: + "org-credit-line-label", + }, + "Amount Due", + ), + h( + "strong", + null, + getters.formatCurrency( + line.amountDue, + ), + ), + ), + h( + "div", + { + className: + "org-credit-line-member", + }, + h( + "span", + { + className: + "org-credit-line-label", + }, + "Interest", + ), + h( + "strong", + null, + `${Math.round(Number(line.interestRate || 0) * 100)}%`, + ), + ), ), ), ) @@ -379,6 +431,34 @@ ${scopeSelector} .org-credit-line-empty { ), h("strong", null, `${reputation}`), ), + h( + "div", + null, + h( + "span", + { className: "org-meta-label" }, + "Reserved Credit", + ), + h( + "strong", + null, + getters.formatCurrency(totalReserved), + ), + ), + h( + "div", + null, + h( + "span", + { className: "org-meta-label" }, + "Outstanding Due", + ), + h( + "strong", + null, + getters.formatCurrency(totalDue), + ), + ), ), allowTreasuryActions ? h( @@ -432,7 +512,7 @@ ${scopeSelector} .org-credit-line-empty { "span", null, creditLines.length > 0 - ? "Open the Credit Lines tab to review assigned members and amounts." + ? "Open the Credit Lines tab to review reserved balances, due amounts, and member exposure." : "Assign a credit line to create the first approved member limit.", ), ), diff --git a/arma/client/addons/org/ui/src/portal/data.js b/arma/client/addons/org/ui/src/portal/data.js index 72b4c3b..a544baf 100644 --- a/arma/client/addons/org/ui/src/portal/data.js +++ b/arma/client/addons/org/ui/src/portal/data.js @@ -19,6 +19,45 @@ target.splice(0, target.length, ...cloneValue(source)); } + function normalizeRecord(value) { + if (value && typeof value === "object" && !Array.isArray(value)) { + return value; + } + + if (Array.isArray(value)) { + const isEntryArray = value.every( + (entry) => + Array.isArray(entry) && + entry.length >= 2 && + typeof entry[0] === "string", + ); + + if (isEntryArray) { + return Object.fromEntries(value); + } + } + + if (typeof value === "string" && value.trim() !== "") { + try { + return normalizeRecord(JSON.parse(value)); + } catch (_error) { + return value; + } + } + + return value; + } + + function normalizeCollection(value) { + const source = Array.isArray(value) + ? value + : value && typeof value === "object" + ? Object.values(value) + : []; + + return source.map(normalizeRecord).filter(Boolean); + } + OrgPortal.data = { portalData: { org: Object.assign( @@ -80,25 +119,28 @@ this.portalData.reputation = payload.portalData.reputation || 0; replaceArray( this.portalData.creditLines, - payload.portalData.creditLines || [], + normalizeCollection(payload.portalData.creditLines), ); replaceArray( this.portalData.members, - payload.portalData.members || [], + normalizeCollection(payload.portalData.members), + ); + replaceArray( + this.portalData.fleet, + normalizeCollection(payload.portalData.fleet), ); - replaceArray(this.portalData.fleet, payload.portalData.fleet || []); replaceArray( this.portalData.assets, - payload.portalData.assets || [], + normalizeCollection(payload.portalData.assets), ); replaceArray( this.portalData.activity, - payload.portalData.activity || [], + normalizeCollection(payload.portalData.activity), ); replaceArray( this.portalData.roadmap, - payload.portalData.roadmap || [], + normalizeCollection(payload.portalData.roadmap), ); replaceObject(this.session, payload.session || {}); diff --git a/arma/client/addons/org/ui/src/portal/store.js b/arma/client/addons/org/ui/src/portal/store.js index d17a366..abdb361 100644 --- a/arma/client/addons/org/ui/src/portal/store.js +++ b/arma/client/addons/org/ui/src/portal/store.js @@ -3,6 +3,45 @@ const { createSignal } = window.RegistryApp.runtime; const { portalData } = OrgPortal.data; + function normalizeRecord(value) { + if (value && typeof value === "object" && !Array.isArray(value)) { + return value; + } + + if (Array.isArray(value)) { + const isEntryArray = value.every( + (entry) => + Array.isArray(entry) && + entry.length >= 2 && + typeof entry[0] === "string", + ); + + if (isEntryArray) { + return Object.fromEntries(value); + } + } + + if (typeof value === "string" && value.trim() !== "") { + try { + return normalizeRecord(JSON.parse(value)); + } catch (_error) { + return value; + } + } + + return value; + } + + function normalizeCollection(value) { + const source = Array.isArray(value) + ? value + : value && typeof value === "object" + ? Object.values(value) + : []; + + return source.map(normalizeRecord).filter(Boolean); + } + class OrgPortalStore { constructor() { [this.getFunds, this.setFunds] = createSignal(portalData.funds); @@ -37,11 +76,13 @@ this.setFunds(nextPortalData.funds || 0); this.setReputation(nextPortalData.reputation || 0); - this.setMembers([...(nextPortalData.members || [])]); - this.setCreditLines([...(nextPortalData.creditLines || [])]); - this.setFleet([...(nextPortalData.fleet || [])]); - this.setAssets([...(nextPortalData.assets || [])]); - this.setActivity([...(nextPortalData.activity || [])]); + this.setMembers([...normalizeCollection(nextPortalData.members)]); + this.setCreditLines([ + ...normalizeCollection(nextPortalData.creditLines), + ]); + this.setFleet([...normalizeCollection(nextPortalData.fleet)]); + this.setAssets([...normalizeCollection(nextPortalData.assets)]); + this.setActivity([...normalizeCollection(nextPortalData.activity)]); } } diff --git a/arma/client/addons/phone.7z b/arma/client/addons/phone.7z new file mode 100644 index 0000000000000000000000000000000000000000..e058c7c4c668375a984e36c27935825b56a12bdd GIT binary patch literal 691770 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�}vd*jUhJ9TsMFlU;Q??2#$);VE? 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 2Srw@Fy`_+r&EJo}(85%z_Q2?84^QZBa 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


-+8*i?}uqLqWXnB(&m*3v*r)2n^ac5?SM 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%0AChh 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

oyFqy|K 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

Nv9|Eu+#| zN(`Gkb$06>jOKa5u}D8i$Iv$W8tyrJ(JRyZXX*NY%u|LyezlxJm|#M1Mf?AKi-+au z57@%CyWg%%1}Pfujh{ z1TC%#8fP{N`D=J$WT@i!N2;7YS_-318vbSPs-&EjJd%b?P65K_POvq~UPlGPI;l-I zKhBGB25Lw99d5_d(6-2wB+A3X4(^sVQps@Tt`G~(8{$#_mw=zfw~3|0guphOM-Wf@ zj&MU|k-Y4MTrmKu4$0M}boW{6*Jfe$oUYEY`GSkG43$xO1GKgqSn;<%3<0|1?ANri zj+SVIZ9LtzhA1o)2cIHHA?5VRO6g2jRZ=(Y{eB9C z^n*tM3(84Pc$u_4qB7YW#jMwrpCn;@M$PdaPeHI$`wzZ7yr6g2P;?m_HE2txJ-*#o z8a`4Bh>o`uHK-vJ4s7CDSItsRGe1QaKGIU-c87&w2;OA%6S!$p4eRNO_y)w)2&T!g{qEBVE>x;+~xA* zj2|D(dG9zX7FqtAItwc44leC_V$DI^!40C42?TC;r}>k&4P7d!z5wtrXVc3pU~O2P zVziA+u;wI^lMG&Nv2RzZi&WqR-!tU0K|tHH+f8|TE* zfMGEGM-o2iM?Gdo{uX{U&F3{uCxysyI3;uia#TnXDSoH(EQ=+3c)fM+BYgQ9pv~ej z+*4dCP!3++D7<1bwN#;k?(6J>cg+}1oEUj>*Y|Esx|Er#B3k*WdSY&6SIv$KrwQ#s z*=mR85{670ge@_(`qX~d)gRKE=1K<^ZxE!z_}S@{ZrV2X)uq1kIiAFvWSY2X>!gJ@ zB0eoyvq3-^Qm(U?+2Tse=4uB#b_c{)WNa@_;s6o32r3z(A^n6tr zRqize=gpWhpY;l(ml}x?;wr^Gs@2-PZQsK8XqL|_ky?-`^PiPBF;lu^Knta9K4}ys zB&VI}-K|Jg#&`@Dxy1iv1fUJAp<*SyifXuwsQdS6EJA30^MnBBZbkWFw$D-a+#l7O z3w^~6CK%4EY!RS7FmR7@*3bb4ez@lTOBaO1zXCyw66&?Vg)TLhGfQu?=JBm(&eZXZ z*`6ZW>y*6dQgW~3;7MqwH4>cgskD!h#~Wz-=!z9&-)~jGud>mJh1Acem7NAE5iolr z;H?y-V)Sw&VAvy5PxN0caUbLSaEAXnth&`s%dZ78A-3Fq(PLF$?|KQo+)AFHm%xLp z$C8T8p7D7}K0VJytG^|zb!M?%&@8*Tu8suPPVQ4-K#gcO$2uYhL&B`wOXHvMln_qs z3Cq=+BacG}gv*@SOa7Tw40sg-@=TSASf}&62!7nCu9n+=v`;HW03c>?R~dJow5s|0 z!i?&YCrEw()NIPbv$r92vn+0b(eox_{$R%fDN_$fnF6yVD2guWxUF}@Tq!Y^B14Hr z_uaFzu`tbsA>b8r0*7J{Z*Al~GZA=i5l|yeQ@=j^I!q_O{oVjJGu?WEgq#Z4T?tIv z?o!P$xdD~=Dmh8y!iZwtcZn_Z?;dbCd_a&=HXJ%=Zrq**R|IX9aFd`4#?q`#UAv1g za_@pb%VNL8Qy(ZVmAXeyu)5y=(v+=9IS-)jp`cHP2HvZ)%d_POm8NR2kHVbIwW~7A z`OJaP(P2OzvUoBTC=k+oNA0yy-!40Kldey6YnFovMwH|bnR48ULU8|xUk84b%0D*u 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 z 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

    )!_MAbP$d)ZXSwAz6q0Sg}TU2o@W4 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 ^nblQ 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 a00AQd0RaVF06+i$47TnH0X4j{4FCYCc_0q} literal 0 HcmV?d00001 diff --git a/arma/client/addons/store/XEH_PREP.hpp b/arma/client/addons/store/XEH_PREP.hpp index 868066a..339e665 100644 --- a/arma/client/addons/store/XEH_PREP.hpp +++ b/arma/client/addons/store/XEH_PREP.hpp @@ -1,5 +1,3 @@ -PREP(buildUIPayload); PREP(handleUIEvents); -PREP(initClass); PREP(initUIBridge); PREP(openUI); diff --git a/arma/client/addons/store/XEH_postInitClient.sqf b/arma/client/addons/store/XEH_postInitClient.sqf index ac1eb81..44eb8c8 100644 --- a/arma/client/addons/store/XEH_postInitClient.sqf +++ b/arma/client/addons/store/XEH_postInitClient.sqf @@ -1,6 +1,5 @@ #include "script_component.hpp" -if (isNil QGVAR(StoreClass)) then { call FUNC(initClass); }; if (isNil QGVAR(StoreUIBridge)) then { call FUNC(initUIBridge); }; [QGVAR(responseCategory), { @@ -9,6 +8,12 @@ if (isNil QGVAR(StoreUIBridge)) then { call FUNC(initUIBridge); }; GVAR(StoreUIBridge) call ["handleCategoryResponse", [_payload]]; }] call CFUNC(addEventHandler); +[QGVAR(responseHydrateStore), { + params [["_payload", createHashMap, [createHashMap]], ["_bridgeEvent", "store::hydrate", [""]]]; + + GVAR(StoreUIBridge) call ["handleHydrateResponse", [_payload, _bridgeEvent]]; +}] call CFUNC(addEventHandler); + [QGVAR(responseCheckout), { params [["_payload", createHashMap, [createHashMap]]]; diff --git a/arma/client/addons/store/functions/fnc_buildUIPayload.sqf b/arma/client/addons/store/functions/fnc_buildUIPayload.sqf deleted file mode 100644 index 3b748ce..0000000 --- a/arma/client/addons/store/functions/fnc_buildUIPayload.sqf +++ /dev/null @@ -1,125 +0,0 @@ -#include "..\script_component.hpp" - -/* - * File: fnc_buildUIPayload.sqf - * Author: IDSolutions - * Date: 2026-03-13 - * Public: No - * - * Description: - * Builds the browser hydrate payload for the store UI from current client state. - * - * Arguments: - * None - * - * Return Value: - * Store UI payload [HASHMAP] - */ - -private _storeState = createHashMap; -private _budget = 50000; -private _creditLine = 0; -private _cashBalance = 0; -private _bankBalance = 0; -private _orgFunds = 0; -private _orgId = ""; -private _orgName = ""; -private _orgOwnerUid = ""; -private _orgCreditLines = createHashMap; -private _playerUid = getPlayerUID player; -private _playerVar = toLowerANSI (vehicleVarName player); -private _isOrgLeader = false; -private _isDefaultOrg = false; -private _isDefaultOrgCeo = false; - -if !(isNil QGVAR(StoreClass)) then { - _storeState = GVAR(StoreClass) call ["getStoreState", []]; - _budget = _storeState getOrDefault ["budget", _budget]; -}; - -if !(isNil QEGVAR(bank,BankClass)) then { - _cashBalance = EGVAR(bank,BankClass) call ["get", ["cash", 0]]; - _bankBalance = EGVAR(bank,BankClass) call ["get", ["bank", 0]]; -}; - -if !(isNil QEGVAR(org,OrgClass)) then { - _orgId = EGVAR(org,OrgClass) call ["get", ["id", ""]]; - _orgName = EGVAR(org,OrgClass) call ["get", ["name", ""]]; - _orgOwnerUid = EGVAR(org,OrgClass) call ["get", ["owner", ""]]; - _orgFunds = EGVAR(org,OrgClass) call ["get", ["funds", 0]]; - _orgCreditLines = EGVAR(org,OrgClass) call ["get", ["credit_lines", createHashMap]]; - _isDefaultOrg = (_orgId isEqualTo "default") || { toLowerANSI _orgOwnerUid isEqualTo "server" }; - _isOrgLeader = _orgOwnerUid isEqualTo _playerUid; - _isDefaultOrgCeo = _isDefaultOrg && { _playerVar isEqualTo "ceo" }; -}; - -if (_orgCreditLines isEqualType createHashMap) then { - private _playerCreditLine = _orgCreditLines getOrDefault [_playerUid, createHashMap]; - if (_playerCreditLine isEqualType createHashMap) then { - _creditLine = _playerCreditLine getOrDefault ["amount", 0]; - }; -}; - -private _canUseOrgFunds = _isOrgLeader || _isDefaultOrgCeo; -private _orgFundsEnabled = _canUseOrgFunds && { _orgFunds > 0 }; -private _paymentSources = [ - createHashMapFromArray [ - ["id", "cash"], - ["label", "Cash"], - ["balance", _cashBalance], - ["enabled", _cashBalance > 0], - ["detail", "Use on-hand cash carried by the player."] - ], - createHashMapFromArray [ - ["id", "bank"], - ["label", "Bank"], - ["balance", _bankBalance], - ["enabled", _bankBalance > 0], - ["detail", "Charge the player bank account."] - ], - createHashMapFromArray [ - ["id", "org_funds"], - ["label", "Org Funds"], - ["balance", _orgFunds], - ["enabled", _orgFundsEnabled], - ["detail", [ - "Only organization leaders or the default-org CEO can use treasury funds.", - [ - "Charge organization treasury funds.", - "No organization funds are currently available." - ] select _orgFundsEnabled - ] select _canUseOrgFunds] - ], - createHashMapFromArray [ - ["id", "credit_line"], - ["label", "Credit Line"], - ["balance", _creditLine], - ["enabled", _creditLine > 0], - ["detail", [ - "No approved credit line is assigned to this member.", - "Use the approved procurement credit line." - ] select (_creditLine > 0)] - ] -]; - -createHashMapFromArray [ - ["session", createHashMapFromArray [ - ["actorName", name player], - ["actorUid", _playerUid], - ["approval", "Field Access"], - ["orgId", _orgId], - ["orgName", _orgName], - ["orgLeader", _isOrgLeader], - ["defaultOrgCeo", _isDefaultOrgCeo], - ["canUseOrgFunds", _canUseOrgFunds] - ]], - ["storeConfig", createHashMapFromArray [ - ["budget", _budget], - ["creditLine", _creditLine], - ["availability", _storeState getOrDefault ["availability", "In-Stock"]], - ["moduleState", _storeState getOrDefault ["moduleState", "Preview"]], - ["paymentSources", _paymentSources], - ["defaultPaymentSource", "cash"] - ]], - ["cartItems", []] -] diff --git a/arma/client/addons/store/functions/fnc_initClass.sqf b/arma/client/addons/store/functions/fnc_initClass.sqf deleted file mode 100644 index f88d2ff..0000000 --- a/arma/client/addons/store/functions/fnc_initClass.sqf +++ /dev/null @@ -1,42 +0,0 @@ -#include "..\script_component.hpp" - -/* - * File: fnc_initClass.sqf - * Author: IDSolutions - * Date: 2026-01-28 - * Last Update: 2026-03-12 - * Public: Yes - * - * Description: - * Initializes the store class for managing store data. - * - * Arguments: - * None - * - * Return Value: - * Store class object [HASHMAP OBJECT] - * - * Example: - * call forge_client_store_fnc_initClass - */ - -#pragma hemtt ignore_variables ["_self"] -GVAR(StoreBaseClass) = compileFinal createHashMapFromArray [ - ["#type", "StoreBaseClass"], - ["#create", compileFinal { - _self set ["uid", getPlayerUID player]; - _self set ["store", createHashMapFromArray [ - ["budget", 50000], - ["availability", "In-Stock"], - ["moduleState", "Preview"] - ]]; - _self set ["isLoaded", false]; - _self set ["lastSave", time]; - }], - ["getStoreState", compileFinal { - _self getOrDefault ["store", createHashMap] - }] -]; - -GVAR(StoreClass) = createHashMapObject [GVAR(StoreBaseClass)]; -GVAR(StoreClass) diff --git a/arma/client/addons/store/functions/fnc_initUIBridge.sqf b/arma/client/addons/store/functions/fnc_initUIBridge.sqf index 2e707d0..e2e7b47 100644 --- a/arma/client/addons/store/functions/fnc_initUIBridge.sqf +++ b/arma/client/addons/store/functions/fnc_initUIBridge.sqf @@ -3,12 +3,20 @@ /* * File: fnc_initUIBridge.sqf * Author: IDSolutions - * Date: 2026-03-10 - * Last Update: 2026-03-12 + * Date: 2026-03-27 * Public: No * * Description: - * Initializes the store UI bridge for browser control state, event routing, and catalog queries. + * Initializes the store UI bridge for browser control state and store UI events. + * + * Arguments: + * None + * + * Return Value: + * Store UI bridge object [HASHMAP OBJECT] + * + * Example: + * call forge_client_store_fnc_initUIBridge; */ #pragma hemtt ignore_variables ["_self"] @@ -47,8 +55,12 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [ ["handleReady", compileFinal { params [["_control", controlNull, [controlNull]]]; - private _payload = call FUNC(buildUIPayload); - _self call ["sendBridgeEvent", ["store::hydrate", _payload, _control]]; + private _uid = getPlayerUID player; + if (_uid isEqualTo "") exitWith { + _self call ["sendBridgeEvent", ["store::hydrate", createHashMap, _control]]; + }; + + [SRPC(store,requestHydrateStore), [_uid, "store::hydrate"]] call CFUNC(serverEvent); }], ["handleCategoryRequest", compileFinal { params [["_data", createHashMap, [createHashMap]]]; @@ -79,8 +91,19 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [ _self call ["sendBridgeEvent", [_bridgeEvent, _payload]]; }], ["refreshStoreConfig", compileFinal { - private _payload = call FUNC(buildUIPayload); - _self call ["sendBridgeEvent", ["store::config::hydrate", _payload]]; + private _uid = getPlayerUID player; + if (_uid isEqualTo "") exitWith { false }; + + [SRPC(store,requestHydrateStore), [_uid, "store::config::hydrate"]] call CFUNC(serverEvent); + true + }], + ["handleHydrateResponse", compileFinal { + params [["_payload", createHashMap, [createHashMap]], ["_bridgeEvent", "store::hydrate", [""]]]; + + private _event = _bridgeEvent; + if !(_event in ["store::hydrate", "store::config::hydrate"]) then { _event = "store::hydrate"; }; + + _self call ["sendBridgeEvent", [_event, _payload]] }], ["handleCheckoutRequest", compileFinal { params [["_data", createHashMap, [createHashMap]]]; 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/actor/functions/fnc_initActorStore.sqf b/arma/server/addons/actor/functions/fnc_initActorStore.sqf index a82e17d..5dbba52 100644 --- a/arma/server/addons/actor/functions/fnc_initActorStore.sqf +++ b/arma/server/addons/actor/functions/fnc_initActorStore.sqf @@ -4,12 +4,13 @@ * File: fnc_initActorStore.sqf * Author: IDSolutions * Date: 2025-12-17 - * Last Update: 2026-02-13 + * Last Update: 2026-04-01 * Public: Yes * * Description: * Initializes the actor store for managing player actor data. - * Provides methods for creating, fetching, migrating, and validating actor data. + * Actor hot state is owned by the extension; SQF maintains a compatibility + * mirror for engine-adjacent consumers. * * Arguments: * None @@ -111,12 +112,112 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [ GVAR(Registry) = createHashMap; ["INFO", "Actor Store Initialized!"] call EFUNC(common,log); }], + ["cacheActor", compileFinal { + params [["_uid", "", [""]], ["_actor", createHashMap, [createHashMap]]]; + + if (_uid isEqualTo "" || { !(_actor isEqualType createHashMap) }) exitWith { createHashMap }; + + private _finalActor = GVAR(ActorModel) call ["migrate", [+_actor]]; + GVAR(Registry) set [_uid, _finalActor]; + _finalActor + }], + ["callHotActor", compileFinal { + params [["_function", "", [""]], ["_arguments", [], [[]]]]; + + if (_function isEqualTo "") exitWith { createHashMap }; + + [_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + if !(_isSuccess) exitWith { createHashMap }; + if !(_result isEqualType "") exitWith { createHashMap }; + if ((_result find "Error:") == 0) exitWith { + ["ERROR", format ["Actor extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log); + createHashMap + }; + + private _data = fromJSON _result; + if !(_data isEqualType createHashMap) exitWith { createHashMap }; + _data + }], + ["loadHotActor", compileFinal { + params [["_uid", "", [""]], ["_initialize", false, [false]]]; + + if (_uid isEqualTo "") exitWith { createHashMap }; + + private _command = ["actor:hot:get", "actor:hot:init"] select _initialize; + private _actor = _self call ["callHotActor", [_command, [_uid]]]; + if (_actor isEqualTo createHashMap) exitWith { _actor }; + + _self call ["cacheActor", [_uid, _actor]] + }], + ["normalizeGetArgs", compileFinal { + params ["_rawArguments"]; + + if ((_rawArguments param [0, createHashMap]) isEqualType createHashMap) exitWith { + [ + _rawArguments param [1, "", [""]], + _rawArguments param [2, "", [""]] + ] + }; + + [ + _rawArguments param [0, "", [""]], + _rawArguments param [1, "", [""]] + ] + }], + ["normalizeSetArgs", compileFinal { + params ["_rawArguments"]; + + if ((_rawArguments param [0, createHashMap]) isEqualType createHashMap) exitWith { + [ + _rawArguments param [2, "", [""]], + _rawArguments param [3, "", [""]], + _rawArguments param [4, nil, [0, "", [], false, createHashMap, objNull, grpNull]], + _rawArguments param [5, false, [false]] + ] + }; + + [ + _rawArguments param [0, "", [""]], + _rawArguments param [1, "", [""]], + _rawArguments param [2, nil, [0, "", [], false, createHashMap, objNull, grpNull]], + _rawArguments param [3, false, [false]] + ] + }], + ["normalizeMSetArgs", compileFinal { + params ["_rawArguments"]; + + if ((_rawArguments param [0, createHashMap]) isEqualType createHashMap) exitWith { + [ + _rawArguments param [2, "", [""]], + _rawArguments param [3, createHashMap, [createHashMap]], + _rawArguments param [4, false, [false]] + ] + }; + + [ + _rawArguments param [0, "", [""]], + _rawArguments param [1, createHashMap, [createHashMap]], + _rawArguments param [2, false, [false]] + ] + }], + ["normalizeUidArg", compileFinal { + params ["_rawArguments"]; + + if ((_rawArguments param [0, createHashMap]) isEqualType createHashMap) exitWith { + _rawArguments param [1, "", [""]] + }; + + _rawArguments param [0, "", [""]] + }], ["init", compileFinal { params [["_uid", "", [""]]]; private _player = [_uid] call EFUNC(common,getPlayer); private _cached = GVAR(Registry) getOrDefault [_uid, nil]; - if !(isNil { _cached }) exitWith { [CRPC(actor,responseInitActor), [_cached], _player] call CFUNC(targetEvent); _cached }; + if !(isNil { _cached }) exitWith { + [CRPC(actor,responseInitActor), [_cached], _player] call CFUNC(targetEvent); + _cached + }; ["actor:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; if !(_isSuccess) exitWith { @@ -124,52 +225,132 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [ private _fallbackActor = GVAR(ActorModel) call ["fromPlayer", [_player]]; _fallbackActor set ["uid", _uid]; - _fallbackActor = GVAR(ActorModel) call ["migrate", [_fallbackActor]]; + _fallbackActor = _self call ["cacheActor", [_uid, _fallbackActor]]; - GVAR(Registry) set [_uid, _fallbackActor]; [CRPC(actor,responseInitActor), [_fallbackActor], _player] call CFUNC(targetEvent); - _fallbackActor }; private _finalActor = createHashMap; - if (_result == "true") then { - _finalActor = _self call ["fetch", ["actor:get", _uid]]; + _finalActor = _self call ["loadHotActor", [_uid, true]]; ["INFO", format ["Found actor for %1", _uid]] call EFUNC(common,log); } else { _finalActor = GVAR(ActorModel) call ["fromPlayer", [_player]]; _finalActor set ["uid", _uid]; private _json = _self call ["toJSON", [_finalActor]]; - ["actor:create", [_uid, _json]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - if !(_isSuccess) exitWith { + ["actor:create", [_uid, _json]] call EFUNC(extension,extCall) params ["_createResult", "_createSuccess"]; + if (!_createSuccess) exitWith { ["ERROR", format ["Failed to create actor %1! Using fallback actor.", _uid]] call EFUNC(common,log); - _finalActor = GVAR(ActorModel) call ["migrate", [_finalActor]]; - GVAR(Registry) set [_uid, _finalActor]; + _finalActor = _self call ["cacheActor", [_uid, _finalActor]]; [CRPC(actor,responseInitActor), [_finalActor], _player] call CFUNC(targetEvent); - _finalActor }; + _finalActor = _self call ["loadHotActor", [_uid, true]]; ["INFO", format ["Created new actor for %1", _uid]] call EFUNC(common,log); }; - _finalActor = GVAR(ActorModel) call ["migrate", [_finalActor]]; - GVAR(Registry) set [_uid, _finalActor]; + if (_finalActor isEqualTo createHashMap) then { + _finalActor = GVAR(ActorModel) call ["fromPlayer", [_player]]; + _finalActor set ["uid", _uid]; + }; + + _finalActor = _self call ["cacheActor", [_uid, _finalActor]]; [CRPC(actor,responseInitActor), [_finalActor], _player] call CFUNC(targetEvent); _finalActor }], + ["get", compileFinal { + call (_self get "normalizeGetArgs") params ["_uid", "_field"]; + + private _actor = _self call ["loadHotActor", [_uid, false]]; + if (_actor isEqualTo createHashMap) then { + _actor = _self call ["loadHotActor", [_uid, true]]; + }; + + if (_field isEqualTo "") exitWith { _actor }; + _actor getOrDefault [_field, nil] + }], + ["override", compileFinal { + params [ + ["_uid", "", [""]], + ["_data", createHashMap, [createHashMap]], + ["_save", false, [false]] + ]; + + if (_uid isEqualTo "" || { !(_data isEqualType createHashMap) }) exitWith { createHashMap }; + + private _actor = _self call ["callHotActor", ["actor:hot:override", [_uid, toJSON _data]]]; + if (_save && { _actor isNotEqualTo createHashMap }) then { + private _savedActor = _self call ["callHotActor", ["actor:hot:save", [_uid]]]; + if (_savedActor isNotEqualTo createHashMap) then { + _actor = _savedActor; + } else { + _actor = createHashMap; + }; + }; + + if (_actor isEqualTo createHashMap) exitWith { _actor }; + _self call ["cacheActor", [_uid, _actor]] + }], + ["set", compileFinal { + call (_self get "normalizeSetArgs") params ["_uid", "_field", "_value", "_sync"]; + + if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap }; + + private _actor = _self call ["get", [_uid, ""]]; + if !(_actor isEqualType createHashMap) exitWith { createHashMap }; + + _actor set [_field, _value]; + private _updatedActor = _self call ["override", [_uid, _actor, _sync]]; + if !(_updatedActor isEqualType createHashMap) exitWith { createHashMap }; + if (_updatedActor isEqualTo createHashMap) exitWith { createHashMap }; + + createHashMapFromArray [[_field, _updatedActor getOrDefault [_field, _value]]] + }], + ["mset", compileFinal { + call (_self get "normalizeMSetArgs") params ["_uid", "_fieldValuePairs", "_sync"]; + + if (_uid isEqualTo "" || { !(_fieldValuePairs isEqualType createHashMap) }) exitWith { createHashMap }; + + private _actor = _self call ["get", [_uid, ""]]; + if !(_actor isEqualType createHashMap) exitWith { createHashMap }; + + { _actor set [_x, _y]; } forEach _fieldValuePairs; + private _updatedActor = _self call ["override", [_uid, _actor, _sync]]; + if !(_updatedActor isEqualType createHashMap) exitWith { createHashMap }; + if (_updatedActor isEqualTo createHashMap) exitWith { createHashMap }; + + +_fieldValuePairs + }], + ["save", compileFinal { + private _uid = call (_self get "normalizeUidArg"); + + if (_uid isEqualTo "") exitWith { createHashMap }; + private _actor = _self call ["callHotActor", ["actor:hot:save", [_uid]]]; + if (_actor isEqualTo createHashMap) exitWith { _actor }; + + _self call ["cacheActor", [_uid, _actor]] + }], + ["remove", compileFinal { + private _uid = call (_self get "normalizeUidArg"); + + if (_uid isEqualTo "") exitWith { false }; + + GVAR(Registry) deleteAt _uid; + ["actor:hot:remove", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + _isSuccess && { _result isEqualTo "OK" } + }], ["snapshot", compileFinal { params [["_uid", "", [""]]]; private _player = [_uid] call EFUNC(common,getPlayer); - private _existing = GVAR(Registry) getOrDefault [_uid, createHashMap]; - private _finalActor = +_existing; + private _finalActor = +(_self call ["get", [_uid, ""]]); - if (_finalActor isEqualTo createHashMap) then { + if (!(_finalActor isEqualType createHashMap) || (_finalActor isEqualTo createHashMap)) then { _finalActor = GVAR(ActorModel) call ["defaults", []]; _finalActor set ["uid", _uid]; }; @@ -187,10 +368,7 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [ ["WARNING", format ["No player object found for %1 during actor snapshot, using cached values.", _uid]] call EFUNC(common,log); }; - _finalActor = GVAR(ActorModel) call ["migrate", [_finalActor]]; - GVAR(Registry) set [_uid, _finalActor]; - - _finalActor + _self call ["override", [_uid, _finalActor, false]] }] ]; diff --git a/arma/server/addons/bank/XEH_PREP.hpp b/arma/server/addons/bank/XEH_PREP.hpp index fae036d..27385a0 100644 --- a/arma/server/addons/bank/XEH_PREP.hpp +++ b/arma/server/addons/bank/XEH_PREP.hpp @@ -1,2 +1,6 @@ PREP(initBank); -PREP(initBankStore); +PREP(initMessenger); +PREP(initModel); +PREP(initPayloadBuilder); +PREP(initSessionManager); +PREP(initStore); diff --git a/arma/server/addons/bank/XEH_preInit.sqf b/arma/server/addons/bank/XEH_preInit.sqf index b652346..33960da 100644 --- a/arma/server/addons/bank/XEH_preInit.sqf +++ b/arma/server/addons/bank/XEH_preInit.sqf @@ -13,99 +13,45 @@ PREP_RECOMPILE_END; GVAR(BankStore) call ["init", [_uid]]; }] call CFUNC(addEventHandler); -[QGVAR(requestGetBank), { - params [["_uid", "", [""]], ["_field", "", [""]]]; +[QGVAR(requestHydrateBank), { + params [["_uid", "", [""]], ["_mode", "bank", [""]], ["_resetAuthorization", false, [false]]]; if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID!" }; - - private _finalData = GVAR(BankStore) call ["get", [GVAR(Registry), _uid, _field]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(bank,responseSyncBank), [_finalData], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestSetBank), { - params [["_uid", "", [""]], ["_field", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "" || _field isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID or Key!" }; - - private _hashMap = GVAR(BankStore) call ["set", [GVAR(Registry), "bank:update", _uid, _field, _value, _sync]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(bank,responseSyncBank), [_hashMap], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestMSetBank), { - params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID!" }; - if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid field pairs!" }; - - private _hashMap = GVAR(BankStore) call ["mset", [GVAR(Registry), "bank:update", _uid, _fieldValuePairs, _sync]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(bank,responseSyncBank), [_hashMap], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestSaveBank), { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID!" }; - - private _finalData = GVAR(BankStore) call ["save", [GVAR(Registry), "bank:update", _uid]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(bank,responseSyncBank), [_finalData], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestRemoveBank), { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID!" }; - GVAR(BankStore) call ["remove", [GVAR(Registry), _uid]]; + GVAR(BankStore) call ["hydrateSession", [_uid, _mode, _resetAuthorization]]; }] call CFUNC(addEventHandler); [QGVAR(requestDeposit), { params [["_uid", "", [""]], ["_amount", 0, [0]]]; - if (_uid isEqualTo "" || _amount isEqualTo 0) exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID or Amount!" }; GVAR(BankStore) call ["deposit", [_uid, _amount]]; }] call CFUNC(addEventHandler); -[QGVAR(requestPayment), { - params [["_uid", "", [""]], ["_amount", 0, [0]]]; +[QGVAR(requestSubmitPin), { + params [["_uid", "", [""]], ["_pin", "", [""]]]; - if (_uid isEqualTo "" || _amount isEqualTo 0) exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID or Amount!" }; - GVAR(BankStore) call ["payment", [_uid, _amount]]; + GVAR(BankSessionManager) call ["submitPin", [_uid, _pin]]; }] call CFUNC(addEventHandler); [QGVAR(requestTransfer), { params [["_uid", "", [""]], ["_target", "", [""]], ["_from", "", [""]], ["_amount", 0, [0]]]; - if (_uid isEqualTo "" || _target isEqualTo "" || _from isEqualTo "" || _amount isEqualTo 0) exitWith { - diag_log "[FORGE:Server:Bank] Empty/Invalid UID, Target, From Account, or Amount!" - }; - - if (_uid isEqualTo _target) exitWith { - diag_log format ["[FORGE:Server:Bank] SECURITY: Player %1 attempted self-transfer!", _uid]; - - private _player = [_uid] call EFUNC(common,getPlayer); - [CRPC(notifications,recieveNotification), ["error", "Bank", "Cannot transfer to yourself!"], _player] call CFUNC(targetEvent); - }; - - GVAR(BankStore) call ["transfer", [_uid, _target, _from, _amount]]; + GVAR(BankStore) call ["transfer", [_uid, _target, _amount, createHashMapFromArray [["sourceField", _from]]]]; }] call CFUNC(addEventHandler); [QGVAR(requestWithdraw), { params [["_uid", "", [""]], ["_amount", 0, [0]]]; - if (_uid isEqualTo "" || _amount isEqualTo 0) exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID or Amount!" }; GVAR(BankStore) call ["withdraw", [_uid, _amount]]; }] call CFUNC(addEventHandler); [QGVAR(requestDepositEarnings), { params [["_uid", "", [""]], ["_amount", 0, [0]]]; - if (_uid isEqualTo "" || _amount isEqualTo 0) exitWith { diag_log "[FORGE:Server:Bank] Empty/Invalid UID or Amount!" }; GVAR(BankStore) call ["depositEarnings", [_uid, _amount]]; }] call CFUNC(addEventHandler); + +[QGVAR(requestRepayCreditLine), { + params [["_uid", "", [""]], ["_amount", 0, [0]]]; + + GVAR(BankStore) call ["repayCreditLine", [_uid, _amount]]; +}] call CFUNC(addEventHandler); diff --git a/arma/server/addons/bank/functions/fnc_initBankStore.sqf b/arma/server/addons/bank/functions/fnc_initBankStore.sqf deleted file mode 100644 index f130777..0000000 --- a/arma/server/addons/bank/functions/fnc_initBankStore.sqf +++ /dev/null @@ -1,326 +0,0 @@ -#include "..\script_component.hpp" - -/* - * File: fnc_initBankStore.sqf - * Author: IDSolutions - * Date: 2025-12-17 - * Last Update: 2026-02-17 - * Public: Yes - * - * Description: - * Initializes the bank store for managing player bank accounts. - * Provides methods for syncing, saving, and applying bank accounts to the player. - * - * Arguments: - * None - * - * Return Value: - * Bank store object [HASHMAP OBJECT] - * - * Example: - * call forge_server_bank_fnc_initBankStore - */ - -#pragma hemtt ignore_variables ["_self"] -GVAR(BankModel) = compileFinal createHashMapObject [[ - ["#type", "BankModel"], - ["defaults", compileFinal { - private _account = createHashMap; - - _account set ["uid", ""]; - _account set ["name", ""]; - _account set ["bank", 0]; - _account set ["cash", 0]; - _account set ["earnings", 0]; - _account set ["pin", 1234]; - _account set ["transactions", []]; - - _account - }], - ["fromPlayer", compileFinal { - params [["_player", objNull, [objNull]]]; - - if (_player isEqualTo objNull) exitWith { _self call ["defaults", []] }; - - private _account = _self call ["defaults", []]; - - _account set ["uid", getPlayerUID _player]; - _account set ["name", name _player]; - _account set ["bank", 0]; - _account set ["cash", 0]; - _account set ["earnings", 0]; - _account set ["pin", 1234]; - _account set ["transactions", []]; - - _account - }], - ["migrate", compileFinal { - params [["_account", createHashMap, [createHashMap]]]; - - private _defaults = _self call ["defaults", []]; - - { - if !(_x in _account) then { _account set [_x, _y]; }; - } forEach _defaults; - - _account - }], - ["validate", compileFinal { - params [["_account", createHashMap, [createHashMap]]]; - - private _uid = _account getOrDefault ["uid", ""]; - private _name = _account getOrDefault ["name", ""]; - private _bank = _account getOrDefault ["bank", 0]; - private _cash = _account getOrDefault ["cash", 0]; - private _earnings = _account getOrDefault ["earnings", 0]; - private _pin = _account getOrDefault ["pin", 1234]; - - [_uid, _name, _bank, _cash, _earnings, _pin] try { - if (_uid isEqualTo "" || !(_uid isEqualType "")) then { throw "Invalid UID!"; }; - if (_name isEqualTo "" || !(_name isEqualType "")) then { throw "Invalid Name!"; }; - if (_bank < 0 || !(_bank isEqualType 0)) then { throw "Invalid Bank!"; }; - if (_cash < 0 || !(_cash isEqualType 0)) then { throw "Invalid Cash!"; }; - if (_earnings < 0 || !(_earnings isEqualType 0)) then { throw "Invalid Earnings!"; }; - if (_pin < 1000 || _pin > 9999 || !(_pin isEqualType 0)) then { throw "Invalid Pin!"; }; - } catch { - ["ERROR", format ["Failed to validate account %1!", _exception]] call EFUNC(common,log); - false - }; - - true - }] -]]; - -GVAR(BankBaseStore) = compileFinal createHashMapFromArray [ - ["#base", EGVAR(common,BaseStore)], - ["#type", "BankBaseStore"], - ["#create", compileFinal { - GVAR(IndexRegistry) = createHashMap; - GVAR(Registry) = createHashMap; - ["INFO", "Bank Store Initialized!"] call EFUNC(common,log); - }], - ["init", compileFinal { - params [["_uid", "", [""]]]; - - private _player = [_uid] call EFUNC(common,getPlayer); - private _cached = GVAR(Registry) getOrDefault [_uid, nil]; - if !(isNil { _cached }) exitWith { [CRPC(bank,responseInitBank), [_cached], _player] call CFUNC(targetEvent); _cached }; - - ["bank:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - if !(_isSuccess) exitWith { - ["ERROR", format ["Failed to check if bank account %1 exists! Using fallback account.", _uid]] call EFUNC(common,log); - - private _fallbackAccount = GVAR(BankModel) call ["fromPlayer", [_player]]; - _fallbackAccount set ["uid", _uid]; - - private _regEntry = createHashMapFromArray [["uid", _uid], ["name", (name _player)]]; - GVAR(IndexRegistry) set [_uid, _regEntry]; - - GVAR(Registry) set [_uid, _fallbackAccount]; - [CRPC(bank,responseInitBank), [_fallbackAccount], _player] call CFUNC(targetEvent); - - _fallbackAccount - }; - - private _finalAccount = createHashMap; - - if (_result == "true") then { - _finalAccount = _self call ["fetch", ["bank:get", _uid]]; - ["INFO", format ["Found bank account for %1", _uid]] call EFUNC(common,log); - } else { - _finalAccount = GVAR(BankModel) call ["fromPlayer", [_player]]; - _finalAccount set ["uid", _uid]; - - private _json = _self call ["toJSON", [_finalAccount]]; - ["bank:create", [_uid, _json]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - if !(_isSuccess) exitWith { - ["ERROR", format ["Failed to create bank account %1! Using fallback account.", _uid]] call EFUNC(common,log); - - private _regEntry = createHashMapFromArray [["uid", _uid], ["name", (name _player)]]; - GVAR(IndexRegistry) set [_uid, _regEntry]; - - GVAR(Registry) set [_uid, _finalAccount]; - [CRPC(bank,responseInitBank), [_finalAccount], _player] call CFUNC(targetEvent); - - _finalAccount - }; - - ["INFO", format ["Created new bank account for %1", _uid]] call EFUNC(common,log); - }; - - - private _regEntry = createHashMapFromArray [["uid", _uid], ["name", (name _player)]]; - GVAR(IndexRegistry) set [_uid, _regEntry]; - - // _finalAccount = GVAR(BankModel) call ["migrate", [_finalAccount]]; - GVAR(Registry) set [_uid, _finalAccount]; - [CRPC(bank,responseInitBank), [_finalAccount], _player] call CFUNC(targetEvent); - - _finalAccount - }], - ["deposit", compileFinal { - params [["_uid", "", [""]], ["_amount", 0, [0]]]; - - ["INFO", format ["Deposit %1, for %2", _amount, _uid]] call EFUNC(common,log); - - private _account = GVAR(Registry) getOrDefault [_uid, nil]; - if (isNil "_account") exitWith { ["ERROR", "Empty/Invalid Account!"] call EFUNC(common,log); }; - - private _bank = _account getOrDefault ["bank", 0]; - private _cash = _account getOrDefault ["cash", 0]; - if (_cash < _amount) exitWith { ["WARNING", "Insufficient Funds!"] call EFUNC(common,log); }; - - private _finalAccount = createHashMapFromArray [["bank", (_bank + _amount)], ["cash", (_cash - _amount)]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - GVAR(Registry) set [_uid, _finalAccount]; - - [CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent); - [CRPC(notifications,recieveNotification), ["info", "Bank", format ["Deposited $%1", _amount]], _player] call CFUNC(targetEvent); - }], - ["payment", compileFinal { - params [["_uid", "", [""]], ["_amount", 0, [0]]]; - - ["INFO", format ["Payment %1, for %2", _amount, _uid]] call EFUNC(common,log); - - private _account = GVAR(Registry) getOrDefault [_uid, nil]; - if (isNil "_account") exitWith { ["ERROR", "Empty/Invalid Account!"] call EFUNC(common,log); }; - - private _bank = _account getOrDefault ["bank", 0]; - private _finalAccount = createHashMapFromArray [["bank", (_bank + _amount)]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - GVAR(Registry) set [_uid, _finalAccount]; - - [CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent); - [CRPC(notifications,recieveNotification), ["info", "Bank", format ["Paid $%1", _amount]], _player] call CFUNC(targetEvent); - }], - ["buildChargeResult", compileFinal { - params [["_message", "Unable to process bank payment.", [""]]]; - - createHashMapFromArray [ - ["success", false], - ["message", _message], - ["patch", createHashMap] - ] - }], - ["chargeCheckout", compileFinal { - params [ - ["_uid", "", [""]], - ["_source", "cash", [""]], - ["_amount", 0, [0]], - ["_commit", false, [false]] - ]; - - private _result = _self call ["buildChargeResult", []]; - private _field = switch (toLowerANSI _source) do { - case "cash": { "cash" }; - case "bank": { "bank" }; - default { "" }; - }; - - if (_field isEqualTo "") exitWith { - _result set ["message", "Selected bank payment source is unsupported."]; - _result - }; - - private _account = GVAR(Registry) getOrDefault [_uid, createHashMap]; - if (_account isEqualTo createHashMap) exitWith { - _result set ["message", "Bank account data is unavailable for checkout."]; - _result - }; - - private _balance = _account getOrDefault [_field, 0]; - if (_balance < _amount) exitWith { - private _message = [ - "Bank balance cannot cover this checkout.", - "Cash on hand cannot cover this checkout." - ] select (_field isEqualTo "cash"); - - _result set ["message", _message]; - _result - }; - - private _patch = createHashMapFromArray [[_field, (_balance - _amount)]]; - if (_commit) then { - _patch = _self call ["mset", [GVAR(Registry), "bank:update", _uid, _patch, false]]; - }; - - _result set ["success", true]; - _result set ["message", ""]; - _result set ["patch", _patch]; - _result - }], - ["transfer", compileFinal { - params [["_uid", "", [""]], ["_target", "", [""]], ["_from", "", [""]], ["_amount", 0, [0]]]; - - if (_uid isEqualTo _target) exitWith { ["WARNING", format ["Self-transfer attempt blocked for %1", _uid]] call EFUNC(common,log); }; - - private _account = GVAR(Registry) getOrDefault [_uid, nil]; - if (isNil "_account") exitWith { ["ERROR", "Empty/Invalid Account!"] call EFUNC(common,log); }; - - private _targetAccount = GVAR(Registry) getOrDefault [_target, nil]; - if (isNil "_targetAccount") exitWith { ["ERROR", "Empty/Invalid Target Account!"] call EFUNC(common,log); }; - - private _selected = _account getOrDefault [_from, 0]; - if (_selected < _amount) exitWith { ["WARNING", "Insufficient Funds!"] call EFUNC(common,log); }; - - private _targetBank = _targetAccount getOrDefault ["bank", 0]; - private _finalAccount = createHashMapFromArray [[_from, (_selected - _amount)]]; - private _finalTargetBank = createHashMapFromArray [["bank", (_targetBank + _amount)]]; - - GVAR(Registry) set [_uid, _finalAccount]; - GVAR(Registry) set [_target, _finalTargetBank]; - - private _player = [_uid] call EFUNC(common,getPlayer); - private _targetPlayer = [_target] call EFUNC(common,getPlayer); - - [CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent); - [CRPC(bank,responseSyncBank), [_finalTargetBank], _targetPlayer] call CFUNC(targetEvent); - [CRPC(notifications,recieveNotification), ["info", "Bank", format ["Transferred $%1 to %2", _amount, (name _targetPlayer)]], _player] call CFUNC(targetEvent); - [CRPC(notifications,recieveNotification), ["info", "Bank", format ["Received $%1 from %2", _amount, (name _player)]], _targetPlayer] call CFUNC(targetEvent); - }], - ["withdraw", compileFinal { - params [["_uid", "", [""]], ["_amount", 0, [0]]]; - - ["INFO", format ["Withdraw %1, for %2", _amount, _uid]] call EFUNC(common,log); - - private _account = GVAR(Registry) getOrDefault [_uid, nil]; - if (isNil "_account") exitWith { ["ERROR", "Empty/Invalid Account!"] call EFUNC(common,log); }; - - private _bank = _account getOrDefault ["bank", 0]; - private _cash = _account getOrDefault ["cash", 0]; - if (_bank < _amount) exitWith { ["WARNING", "Insufficient Funds!"] call EFUNC(common,log); }; - - private _finalAccount = createHashMapFromArray [["bank", (_bank - _amount)], ["cash", (_cash + _amount)]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - GVAR(Registry) set [_uid, _finalAccount]; - - [CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent); - [CRPC(notifications,recieveNotification), ["info", "Bank", format ["Withdrew $%1", _amount]], _player] call CFUNC(targetEvent); - }], - ["depositEarnings", compileFinal { - params [["_uid", "", [""]], ["_amount", 0, [0]]]; - - ["INFO", format ["Deposit Earnings %1, for %2", _amount, _uid]] call EFUNC(common,log); - - private _account = GVAR(Registry) getOrDefault [_uid, nil]; - if (isNil "_account") exitWith { ["ERROR", "Empty/Invalid Account!"] call EFUNC(common,log); }; - - private _bank = _account getOrDefault ["bank", 0]; - private _earnings = _account getOrDefault ["earnings", 0]; - if (_earnings < _amount) exitWith { ["WARNING", "Insufficient Earnings!"] call EFUNC(common,log); }; - - private _finalAccount = createHashMapFromArray [["bank", (_bank + _amount)], ["earnings", (_earnings - _amount)]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - GVAR(Registry) set [_uid, _finalAccount]; - - [CRPC(bank,responseSyncBank), [_finalAccount], _player] call CFUNC(targetEvent); - [CRPC(notifications,recieveNotification), ["info", "Bank", format ["Deposited $%1 from earnings", _amount]], _player] call CFUNC(targetEvent); - }] -]; - -GVAR(BankStore) = createHashMapObject [GVAR(BankBaseStore)]; -GVAR(BankStore) diff --git a/arma/server/addons/bank/functions/fnc_initMessenger.sqf b/arma/server/addons/bank/functions/fnc_initMessenger.sqf new file mode 100644 index 0000000..eafb94f --- /dev/null +++ b/arma/server/addons/bank/functions/fnc_initMessenger.sqf @@ -0,0 +1,75 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initMessenger.sqf + * Author: IDSolutions + * Date: 2026-03-16 + * Last Update: 2026-04-02 + * Public: No + * + * Description: + * Initializes the bank messenger for all server-to-client + * communication including account syncs, toast notifications, + * and inline bank UI notices. + * + * Parameter(s): + * None + * + * Returns: + * Messenger object [HASHMAP OBJECT] + * + * Example(s): + * call forge_server_bank_fnc_initMessenger + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(BankMessenger) = createHashMapObject [[ + ["#type", "BankMessenger"], + ["buildAccountPatch", compileFinal { + params [["_account", createHashMap, [createHashMap]]]; + + private _patch = createHashMap; + { + if (_x in _account) then { + _patch set [_x, _account get _x]; + }; + } forEach ["uid", "name", "bank", "cash", "earnings", "transactions"]; + + _patch + }], + ["sendAccountSync", compileFinal { + params [["_uid", "", [""]], ["_account", createHashMap, [createHashMap]], ["_event", CRPC(bank,responseSyncBank), [""]]]; + + if (_uid isEqualTo "" || { _account isEqualTo createHashMap }) exitWith { false }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (isNull _player) exitWith { false }; + + [_event, [_self call ["buildAccountPatch", [_account]]], _player] call CFUNC(targetEvent); + true + }], + ["sendNotification", compileFinal { + params [["_uid", "", [""]], ["_type", "info", [""]], ["_title", "Bank", [""]], ["_message", "", [""]]]; + + if (_uid isEqualTo "" || { _message isEqualTo "" }) exitWith { false }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (isNull _player) exitWith { false }; + + [CRPC(notifications,recieveNotification), [_type, _title, _message], _player] call CFUNC(targetEvent); + true + }], + ["sendAlert", compileFinal { + params [["_uid", "", [""]], ["_type", "error", [""]], ["_message", "", [""]]]; + + if (_uid isEqualTo "" || { _message isEqualTo "" }) exitWith { false }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (isNull _player) exitWith { false }; + + [CRPC(bank,responseBankNotice), [_type, _message], _player] call CFUNC(targetEvent); + true + }] +]]; + +GVAR(BankMessenger) diff --git a/arma/server/addons/bank/functions/fnc_initModel.sqf b/arma/server/addons/bank/functions/fnc_initModel.sqf new file mode 100644 index 0000000..3642fe3 --- /dev/null +++ b/arma/server/addons/bank/functions/fnc_initModel.sqf @@ -0,0 +1,67 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initModel.sqf + * Author: IDSolutions + * Date: 2026-03-16 + * Last Update: 2026-03-16 + * Public: No + * + * Description: + * Initializes the bank account data model. Provides default account + * schema, player-based account creation, schema migration for + * existing accounts. + * + * Parameter(s): + * None + * + * Returns: + * Bank model object [HASHMAP OBJECT] + * + * Example(s): + * call forge_server_bank_fnc_initModel + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(BankModel) = compileFinal createHashMapObject [[ + ["#type", "BankModel"], + ["defaults", compileFinal { + private _account = createHashMap; + + _account set ["uid", ""]; + _account set ["name", ""]; + _account set ["bank", 0]; + _account set ["cash", 0]; + _account set ["earnings", 0]; + _account set ["pin", 1234]; + _account set ["transactions", []]; + + _account + }], + ["fromPlayer", compileFinal { + params [["_player", objNull, [objNull]]]; + + if (_player isEqualTo objNull) exitWith { _self call ["defaults", []] }; + + private _account = _self call ["defaults", []]; + + _account set ["uid", getPlayerUID _player]; + _account set ["name", name _player]; + + _account + }], + ["migrate", compileFinal { + params [["_account", createHashMap, [createHashMap]]]; + + private _defaults = _self call ["defaults", []]; + { + if !(_x in _account) then { + _account set [_x, _y]; + }; + } forEach _defaults; + + _account + }] +]]; + +GVAR(BankModel) diff --git a/arma/server/addons/bank/functions/fnc_initPayloadBuilder.sqf b/arma/server/addons/bank/functions/fnc_initPayloadBuilder.sqf new file mode 100644 index 0000000..6cd9bcd --- /dev/null +++ b/arma/server/addons/bank/functions/fnc_initPayloadBuilder.sqf @@ -0,0 +1,151 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initPayloadBuilder.sqf + * Author: IDSolutions + * Date: 2026-04-02 + * Public: No + * + * Description: + * Initializes the bank payload builder for session/view shaping. + * Keeps hydrate/context construction out of BankStore so the store + * can focus on extension-backed account operations. + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(BankPayloadBuilder) = createHashMapObject [[ + ["#type", "BankPayloadBuilder"], + ["buildOperationContext", compileFinal { + params [["_uid", "", [""]], ["_modeOverride", "", [""]]]; + + private _session = GVAR(BankSessionManager) call ["getSessionState", [_uid]]; + private _mode = if (_modeOverride isEqualTo "") then { + _session getOrDefault ["mode", "bank"] + } else { + GVAR(BankSessionManager) call ["resolveMode", [_modeOverride]] + }; + + createHashMapFromArray [ + ["mode", _mode], + ["atmAuthorized", _session getOrDefault ["atmAuthorized", false]] + ] + }], + ["buildTransferContext", compileFinal { + params [["_uid", "", [""]], ["_from", "", [""]]]; + + private _context = _self call ["buildOperationContext", [_uid]]; + _context set ["fromField", _from]; + _context + }], + ["buildCheckoutContext", compileFinal { + params [["_source", "bank", [""]], ["_commit", false, [false]]]; + + createHashMapFromArray [ + ["commit", _commit], + ["sourceField", toLowerANSI _source] + ] + }], + ["resolveOrgState", compileFinal { + params [["_uid", "", [""]]]; + + private _defaultCreditLine = createHashMapFromArray [ + ["approvedAmount", 0], + ["availableAmount", 0], + ["outstandingPrincipal", 0], + ["interestRate", 0.1], + ["amountDue", 0] + ]; + private _defaultState = createHashMapFromArray [ + ["funds", 0], + ["name", ""], + ["creditLine", _defaultCreditLine] + ]; + if (_uid isEqualTo "") exitWith { _defaultState }; + + private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; + private _orgID = _actor getOrDefault ["organization", "default"]; + if (_orgID isEqualTo "") then { _orgID = "default"; }; + + private _org = EGVAR(org,OrgStore) call ["loadById", [_orgID]]; + if (_org isEqualTo createHashMap) then { + _org = EGVAR(org,OrgStore) call ["loadById", ["default"]]; + }; + if (_org isEqualTo createHashMap) exitWith { _defaultState }; + + private _creditLines = _org getOrDefault ["credit_lines", createHashMap]; + if !(_creditLines isEqualType createHashMap) then { + _creditLines = createHashMap; + }; + + private _creditLine = _creditLines getOrDefault [_uid, createHashMap]; + if !(_creditLine isEqualType createHashMap) then { + _creditLine = createHashMap; + }; + + createHashMapFromArray [ + ["funds", _org getOrDefault ["funds", 0]], + ["name", _org getOrDefault ["name", ""]], + ["creditLine", createHashMapFromArray [ + ["approvedAmount", _creditLine getOrDefault [ + "approved_amount", + _creditLine getOrDefault ["amount", 0] + ]], + ["availableAmount", _creditLine getOrDefault [ + "available_amount", + _creditLine getOrDefault ["amount", 0] + ]], + ["outstandingPrincipal", _creditLine getOrDefault ["outstanding_principal", 0]], + ["interestRate", _creditLine getOrDefault ["interest_rate", 0.1]], + ["amountDue", _creditLine getOrDefault ["amount_due", 0]] + ]] + ] + }], + ["buildTransferTargets", compileFinal { + params [["_sourceUid", "", [""]]]; + + private _targets = []; + { + if (isNull _x) then { continue; }; + private _targetUid = getPlayerUID _x; + private _targetName = name _x; + if (_targetUid isEqualTo "" || { _targetUid isEqualTo _sourceUid } || { _targetName isEqualTo "" }) then { continue; }; + _targets pushBack (createHashMapFromArray [["name", _targetName], ["uid", _targetUid]]); + } forEach allPlayers; + + private _targetPairs = _targets apply { [toLowerANSI (_x getOrDefault ["name", ""]), _x] }; + _targetPairs sort true; + _targetPairs apply { _x param [1, createHashMap] } + }], + ["buildHydratePayload", compileFinal { + params [["_uid", "", [""]], ["_mode", "", [""]], ["_resetAuthorization", false, [false]]]; + + if (_uid isEqualTo "") exitWith { createHashMap }; + + private _account = GVAR(BankStore) call ["get", [_uid, ""]]; + if (_account isEqualTo createHashMap) then { + _account = GVAR(BankStore) call ["init", [_uid]]; + }; + if (_account isEqualTo createHashMap) exitWith { createHashMap }; + + private _session = GVAR(BankSessionManager) call ["syncSessionMode", [_uid, _mode, _resetAuthorization]]; + private _orgState = _self call ["resolveOrgState", [_uid]]; + private _player = [_uid] call EFUNC(common,getPlayer); + private _playerName = if (isNull _player) then { _account getOrDefault ["name", "Unknown"] } else { name _player }; + + createHashMapFromArray [ + ["session", createHashMapFromArray [ + ["atmAuthorized", _session getOrDefault ["atmAuthorized", false]], + ["mode", _session getOrDefault ["mode", "bank"]], + ["orgFunds", _orgState getOrDefault ["funds", 0]], + ["orgName", _orgState getOrDefault ["name", ""]], + ["creditLine", _orgState getOrDefault ["creditLine", createHashMap]], + ["playerName", _playerName], + ["transferTargets", _self call ["buildTransferTargets", [_uid]]], + ["uid", _uid] + ]], + ["account", GVAR(BankMessenger) call ["buildAccountPatch", [_account]]] + ] + }] +]]; + +GVAR(BankPayloadBuilder) diff --git a/arma/server/addons/bank/functions/fnc_initSessionManager.sqf b/arma/server/addons/bank/functions/fnc_initSessionManager.sqf new file mode 100644 index 0000000..dc9077e --- /dev/null +++ b/arma/server/addons/bank/functions/fnc_initSessionManager.sqf @@ -0,0 +1,102 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initSessionManager.sqf + * Author: IDSolutions + * Date: 2026-03-16 + * Last Update: 2026-04-02 + * Public: No + * + * Description: + * Initializes the bank session manager for managing ATM/bank + * session state, mode resolution, and PIN authorization. + * + * Parameter(s): + * None + * + * Returns: + * Session manager object [HASHMAP OBJECT] + * + * Example(s): + * call forge_server_bank_fnc_initSessionManager + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(BankSessionManager) = createHashMapObject [[ + ["#type", "BankSessionManager"], + ["getSessionState", compileFinal { + params [["_uid", "", [""]]]; + + private _session = GVAR(SessionRegistry) getOrDefault [_uid, createHashMap]; + if (_session isEqualTo createHashMap) then { + _session = createHashMapFromArray [ + ["atmAuthorized", false], + ["mode", "bank"] + ]; + GVAR(SessionRegistry) set [_uid, _session]; + }; + + _session + }], + ["setSessionState", compileFinal { + params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]]]; + + if (_uid isEqualTo "") exitWith { createHashMap }; + + private _session = +(_self call ["getSessionState", [_uid]]); + { _session set [_x, _y]; } forEach _fieldValuePairs; + + GVAR(SessionRegistry) set [_uid, _session]; + _session + }], + ["resolveMode", compileFinal { + params [["_mode", "bank", [""]]]; + + private _finalMode = toLowerANSI _mode; + if !(_finalMode in ["atm", "bank"]) then { _finalMode = "bank"; }; + + _finalMode + }], + ["syncSessionMode", compileFinal { + params [["_uid", "", [""]], ["_mode", "", [""]], ["_resetAuthorization", false, [false]]]; + + private _current = _self call ["getSessionState", [_uid]]; + private _finalMode = if (_mode isEqualTo "") then { + _current getOrDefault ["mode", "bank"] + } else { + _self call ["resolveMode", [_mode]] + }; + private _atmAuthorized = _current getOrDefault ["atmAuthorized", false]; + + if (_finalMode isEqualTo "atm") then { + if (_resetAuthorization || { (_current getOrDefault ["mode", "bank"]) isNotEqualTo "atm" }) then { + _atmAuthorized = false; + }; + } else { + _atmAuthorized = false; + }; + + _self call ["setSessionState", [_uid, createHashMapFromArray [ + ["atmAuthorized", _atmAuthorized], + ["mode", _finalMode] + ]]] + }], + ["submitPin", compileFinal { + params [["_uid", "", [""]], ["_pin", "", [""]]]; + + if (_uid isEqualTo "") exitWith { false }; + + _self call ["setSessionState", [_uid, createHashMapFromArray [["atmAuthorized", false], ["mode", "atm"]]]]; + if !(GVAR(BankStore) call ["validatePin", [_uid, _pin]]) exitWith { + GVAR(BankStore) call ["hydrateSession", [_uid, "atm", false]]; + false + }; + + _self call ["setSessionState", [_uid, createHashMapFromArray [["atmAuthorized", true], ["mode", "atm"]]]]; + GVAR(BankMessenger) call ["sendNotification", [_uid, "info", "Bank", "ATM access granted."]]; + GVAR(BankStore) call ["hydrateSession", [_uid, "atm", false]]; + true + }] +]]; + +GVAR(BankSessionManager) diff --git a/arma/server/addons/bank/functions/fnc_initStore.sqf b/arma/server/addons/bank/functions/fnc_initStore.sqf new file mode 100644 index 0000000..507f236 --- /dev/null +++ b/arma/server/addons/bank/functions/fnc_initStore.sqf @@ -0,0 +1,495 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initStore.sqf + * Author: IDSolutions + * Date: 2025-12-17 + * Last Update: 2026-04-02 + * Public: No + * + * Description: + * Initializes the bank store for managing player bank accounts. + * Bank account truth lives in the extension hot cache; SQF handles + * session state, Arma-facing validation, and client messaging. + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(BankBaseStore) = compileFinal createHashMapFromArray [ + ["#base", EGVAR(common,BaseStore)], + ["#type", "BankBaseStore"], + ["#create", compileFinal { + GVAR(SessionRegistry) = createHashMap; + ["INFO", "Bank Store Initialized!"] call EFUNC(common,log); + }], + ["normalizeAccount", compileFinal { + params [["_uid", "", [""]], ["_account", createHashMap, [createHashMap]], ["_playerName", "", [""]]]; + + if (_uid isEqualTo "" || { !(_account isEqualType createHashMap) }) exitWith { createHashMap }; + + private _finalAccount = GVAR(BankModel) call ["migrate", [+_account]]; + if ((_finalAccount getOrDefault ["uid", ""]) isEqualTo "") then { + _finalAccount set ["uid", _uid]; + }; + if ((_finalAccount getOrDefault ["name", ""]) isEqualTo "" && { _playerName isNotEqualTo "" }) then { + _finalAccount set ["name", _playerName]; + }; + + _finalAccount + }], + ["callHotBank", compileFinal { + params [["_function", "", [""]], ["_arguments", [], [[]]]]; + + private _envelope = _self call ["callHotBankEnvelope", [_function, _arguments]]; + _envelope getOrDefault ["data", createHashMap] + }], + ["callHotBankEnvelope", compileFinal { + params [["_function", "", [""]], ["_arguments", [], [[]]]]; + + private _envelope = createHashMapFromArray [["data", createHashMap], ["error", ""]]; + + if (_function isEqualTo "") exitWith { _envelope }; + + [_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + if !(_isSuccess) exitWith { + _envelope set ["error", format ["Bank backend call '%1' failed.", _function]]; + _envelope + }; + if !(_result isEqualType "") exitWith { + _envelope set ["error", format ["Bank backend call '%1' returned an invalid response.", _function]]; + _envelope + }; + if ((_result find "Error:") == 0) exitWith { + ["ERROR", format ["Bank extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log); + _envelope set ["error", _result select [7]]; + _envelope + }; + + private _data = fromJSON _result; + if !(_data isEqualType createHashMap) exitWith { + _envelope set ["error", format ["Bank backend call '%1' returned unreadable JSON.", _function]]; + _envelope + }; + + _envelope set ["data", _data]; + _envelope + }], + ["loadHotBank", compileFinal { + params [["_uid", "", [""]], ["_initialize", false, [false]], ["_playerName", "", [""]]]; + + if (_uid isEqualTo "") exitWith { createHashMap }; + + private _command = ["bank:hot:get", "bank:hot:init"] select _initialize; + private _account = _self call ["callHotBank", [_command, [_uid]]]; + if (_account isEqualTo createHashMap) exitWith { _account }; + + _self call ["normalizeAccount", [_uid, _account, _playerName]] + }], + ["finalizeMutation", compileFinal { + params [ + ["_uid", "", [""]], + ["_result", createHashMap, [createHashMap]], + ["_save", false, [false]] + ]; + + if (_uid isEqualTo "" || { _result isEqualTo createHashMap }) exitWith { createHashMap }; + + private _account = _result getOrDefault ["account", createHashMap]; + private _patch = _result getOrDefault ["patch", createHashMap]; + + if !(_patch isEqualType createHashMap) then { + _patch = createHashMap; + }; + + if (_save && { _account isNotEqualTo createHashMap }) then { + private _savedAccount = _self call ["callHotBank", ["bank:hot:save", [_uid]]]; + if (_savedAccount isEqualTo createHashMap) exitWith { createHashMap }; + _account = _savedAccount; + }; + + if (_account isNotEqualTo createHashMap) then { + _self call ["normalizeAccount", [_uid, _account, ""]]; + }; + + _patch + }], + ["runMutation", compileFinal { + params [ + ["_uid", "", [""]], + ["_command", "", [""]], + ["_arguments", [], [[]]], + ["_save", false, [false]], + ["_notification", "", [""]] + ]; + + if (_uid isEqualTo "" || { _command isEqualTo "" }) exitWith { false }; + + private _envelope = _self call ["callHotBankEnvelope", [_command, _arguments]]; + private _result = _envelope getOrDefault ["data", createHashMap]; + private _finalPatch = _self call ["finalizeMutation", [_uid, _result, _save]]; + if (_finalPatch isEqualTo createHashMap) exitWith { + private _message = _envelope getOrDefault ["error", "Bank operation failed."]; + if (_message isNotEqualTo "") then { + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _message]]; + }; + false + }; + + GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalPatch]]; + if (_notification isNotEqualTo "") then { + GVAR(BankMessenger) call ["sendNotification", [_uid, "info", "Bank", _notification]]; + }; + + true + }], + ["chargeCheckout", compileFinal { + params [["_uid", "", [""]], ["_source", "cash", [""]], ["_amount", 0, [0]], ["_commit", false, [false]]]; + + private _result = createHashMapFromArray [["success", false], ["message", "Unable to process bank payment."], ["patch", createHashMap]]; + if (_uid isEqualTo "") exitWith { _result }; + + private _checkoutContext = GVAR(BankPayloadBuilder) call ["buildCheckoutContext", [_source, _commit]]; + private _envelope = _self call [ + "callHotBankEnvelope", + [ + "bank:hot:charge_checkout", + [_uid, str _amount, toJSON _checkoutContext] + ] + ]; + private _mutationResult = _envelope getOrDefault ["data", createHashMap]; + private _patch = _self call ["finalizeMutation", [_uid, _mutationResult, false]]; + if (_patch isEqualTo createHashMap) exitWith { + _result set ["message", _envelope getOrDefault ["error", "Bank checkout payment failed."]]; + _result + }; + + _result set ["success", true]; + _result set ["message", ""]; + _result set ["patch", _patch]; + _result + }], + ["repayCreditLine", compileFinal { + params [["_uid", "", [""]], ["_amount", 0, [0]]]; + + if (_uid isEqualTo "" || { _amount <= 0 }) exitWith { + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", "Enter a valid repayment amount."]]; + false + }; + + private _originalAccount = _self call ["loadHotBank", [_uid, false, ""]]; + if (_originalAccount isEqualTo createHashMap) then { + _originalAccount = _self call ["loadHotBank", [_uid, true, ""]]; + }; + if (_originalAccount isEqualTo createHashMap) exitWith { + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", "Bank account could not be loaded."]]; + false + }; + + private _checkoutContext = GVAR(BankPayloadBuilder) call ["buildCheckoutContext", ["bank", false]]; + private _previewEnvelope = _self call [ + "callHotBankEnvelope", + [ + "bank:hot:charge_checkout", + [_uid, str _amount, toJSON _checkoutContext] + ] + ]; + private _previewResult = _previewEnvelope getOrDefault ["data", createHashMap]; + private _bankPatch = _self call ["finalizeMutation", [_uid, _previewResult, false]]; + if (_bankPatch isEqualTo createHashMap) exitWith { + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _previewEnvelope getOrDefault ["error", "Credit repayment could not be funded from the bank account."]]]; + false + }; + + private _nextAccount = _previewResult getOrDefault ["account", createHashMap]; + if (_nextAccount isEqualTo createHashMap) exitWith { + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", "Bank repayment preview returned an invalid account state."]]; + false + }; + + private _overrideEnvelope = _self call [ + "callHotBankEnvelope", + ["bank:hot:override", [_uid, _self call ["toJSON", [_nextAccount]]]] + ]; + if ((_overrideEnvelope getOrDefault ["data", createHashMap]) isEqualTo createHashMap) exitWith { + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _overrideEnvelope getOrDefault ["error", "Credit repayment could not reserve bank funds."]]]; + false + }; + + private _orgResult = EGVAR(org,OrgStore) call ["repayCreditLine", [_uid, _amount]]; + if !(_orgResult getOrDefault ["success", false]) exitWith { + private _rollbackEnvelope = _self call [ + "callHotBankEnvelope", + ["bank:hot:override", [_uid, _self call ["toJSON", [_originalAccount]]]] + ]; + if ((_rollbackEnvelope getOrDefault ["data", createHashMap]) isEqualTo createHashMap) then { + ["ERROR", format ["Failed to roll back bank state for %1 after org credit repayment failure.", _uid]] call EFUNC(common,log); + }; + + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _orgResult getOrDefault ["message", "Credit repayment failed."]]]; + false + }; + + GVAR(BankMessenger) call ["sendAccountSync", [_uid, _bankPatch]]; + GVAR(BankMessenger) call ["sendNotification", [_uid, "info", "Bank", _orgResult getOrDefault ["message", format ["Repaid $%1 toward the organization credit line.", [_amount] call EFUNC(common,formatNumber)]]]]; + + private _orgPatch = _orgResult getOrDefault ["patch", createHashMap]; + if (_orgPatch isNotEqualTo createHashMap) then { + { + private _memberPlayer = [_x] call EFUNC(common,getPlayer); + if (_memberPlayer isNotEqualTo objNull) then { + [CRPC(org,responseSyncOrg), [_orgPatch], _memberPlayer] call CFUNC(targetEvent); + }; + } forEach (_orgResult getOrDefault ["memberUids", []]); + }; + + _self call ["hydrateSession", [_uid, "", false]]; + true + }], + ["deposit", compileFinal { + params [["_uid", "", [""]], ["_amount", 0, [0]]]; + + _self call [ + "runMutation", + [ + _uid, + "bank:hot:deposit", + [_uid, str _amount, toJSON (GVAR(BankPayloadBuilder) call ["buildOperationContext", [_uid]])], + false, + format ["Deposited $%1", [_amount] call EFUNC(common,formatNumber)] + ] + ] + }], + ["hydrateSession", compileFinal { + params [["_uid", "", [""]], ["_mode", "", [""]], ["_resetAuthorization", false, [false]]]; + + private _payload = GVAR(BankPayloadBuilder) call ["buildHydratePayload", [_uid, _mode, _resetAuthorization]]; + if (_payload isEqualTo createHashMap) exitWith { false }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (isNull _player) exitWith { false }; + + [CRPC(bank,responseHydrateBank), [_payload], _player] call CFUNC(targetEvent); + true + }], + ["init", compileFinal { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { createHashMap }; + + private _player = [_uid] call EFUNC(common,getPlayer); + private _playerName = if (isNull _player) then { "Unknown" } else { name _player }; + ["bank:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + if !(_isSuccess) exitWith { + ["ERROR", format ["Failed to check if bank account %1 exists in backend.", _uid]] call EFUNC(common,log); + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", "Bank backend is unavailable right now."]]; + createHashMap + }; + if !(_result isEqualType "") exitWith { + ["ERROR", format ["Bank exists check for %1 returned an invalid response.", _uid]] call EFUNC(common,log); + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", "Bank backend returned an invalid response."]]; + createHashMap + }; + if ((_result find "Error:") == 0) exitWith { + ["ERROR", format ["Bank exists check for %1 failed: %2", _uid, _result]] call EFUNC(common,log); + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _result select [7]]]; + createHashMap + }; + + private _finalAccount = createHashMap; + if (_result isEqualTo "true") then { + _finalAccount = _self call ["loadHotBank", [_uid, true, _playerName]]; + ["INFO", format ["Found bank account for %1", _uid]] call EFUNC(common,log); + } else { + _finalAccount = GVAR(BankModel) call ["fromPlayer", [_player]]; + _finalAccount set ["uid", _uid]; + if ((_finalAccount getOrDefault ["name", ""]) isEqualTo "") then { + _finalAccount set ["name", _playerName]; + }; + + private _json = _self call ["toJSON", [_finalAccount]]; + ["bank:create", [_uid, _json]] call EFUNC(extension,extCall) params ["_createResult", "_createSuccess"]; + if (!_createSuccess) exitWith { + ["ERROR", format ["Failed to create bank account %1 in backend.", _uid]] call EFUNC(common,log); + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", "Failed to create bank account in backend."]]; + createHashMap + }; + if !(_createResult isEqualType "") exitWith { + ["ERROR", format ["Bank create for %1 returned an invalid response.", _uid]] call EFUNC(common,log); + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", "Bank backend returned an invalid create response."]]; + createHashMap + }; + if ((_createResult find "Error:") == 0) exitWith { + ["ERROR", format ["Bank create for %1 failed: %2", _uid, _createResult]] call EFUNC(common,log); + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _createResult select [7]]]; + createHashMap + }; + + _finalAccount = _self call ["loadHotBank", [_uid, true, _playerName]]; + ["INFO", format ["Created new bank account for %1", _uid]] call EFUNC(common,log); + }; + + if (_finalAccount isEqualTo createHashMap) exitWith { + ["ERROR", format ["Failed to initialize bank hot state for %1.", _uid]] call EFUNC(common,log); + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", "Bank account hot state could not be initialized."]]; + createHashMap + }; + + _finalAccount = _self call ["normalizeAccount", [_uid, _finalAccount, _playerName]]; + GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalAccount, CRPC(bank,responseInitBank)]]; + _finalAccount + }], + ["get", compileFinal { + params [["_uid", "", [""]], ["_field", "", [""]]]; + + private _account = _self call ["loadHotBank", [_uid, false, ""]]; + if (_account isEqualTo createHashMap) then { + _account = _self call ["loadHotBank", [_uid, true, ""]]; + }; + + if (_field isEqualTo "") exitWith { _account }; + _account getOrDefault [_field, nil] + }], + ["set", compileFinal { + params [["_uid", "", [""]], ["_field", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]]; + + if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap }; + + _self call ["mset", [_uid, createHashMapFromArray [[_field, _value]], _sync]] + }], + ["mset", compileFinal { + params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; + + if (_uid isEqualTo "" || { !(_fieldValuePairs isEqualType createHashMap) }) exitWith { createHashMap }; + + private _result = _self call ["callHotBank", ["bank:hot:patch", [_uid, toJSON _fieldValuePairs]]]; + _self call ["finalizeMutation", [_uid, _result, _sync]] + }], + ["save", compileFinal { + params [["_uid", "", [""]]]; + if (_uid isEqualTo "") exitWith { createHashMap }; + + private _envelope = _self call ["callHotBankEnvelope", ["bank:hot:save", [_uid]]]; + private _account = _envelope getOrDefault ["data", createHashMap]; + if (_account isEqualTo createHashMap) exitWith { + private _message = _envelope getOrDefault ["error", "Bank save failed."]; + ["ERROR", format ["Failed to save bank account %1: %2", _uid, _message]] call EFUNC(common,log); + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _message]]; + createHashMap + }; + + _self call ["normalizeAccount", [_uid, _account, ""]] + }], + ["transfer", compileFinal { + params [["_uid", "", [""]], ["_target", "", [""]], ["_amount", 0, [0]], ["_context", createHashMap, [createHashMap]]]; + + private _transferContext = GVAR(BankPayloadBuilder) call ["buildTransferContext", [_uid, _context getOrDefault ["sourceField", "bank"]]]; + private _envelope = _self call [ + "callHotBankEnvelope", + [ + "bank:hot:transfer", + [_uid, _target, str _amount, toJSON _transferContext] + ] + ]; + private _result = _envelope getOrDefault ["data", createHashMap]; + if (_result isEqualTo createHashMap) exitWith { + private _message = _envelope getOrDefault ["error", "Bank transfer failed."]; + if (_message isNotEqualTo "") then { + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _message]]; + }; + false + }; + + private _sourceAccount = _result getOrDefault ["sourceAccount", createHashMap]; + private _targetAccount = _result getOrDefault ["targetAccount", createHashMap]; + private _finalSourcePatch = _result getOrDefault ["sourcePatch", createHashMap]; + private _finalTargetPatch = _result getOrDefault ["targetPatch", createHashMap]; + + if ( + _finalSourcePatch isEqualTo createHashMap + || { _finalTargetPatch isEqualTo createHashMap } + ) exitWith { + private _message = _envelope getOrDefault ["error", "Bank transfer failed."]; + if (_message isNotEqualTo "") then { + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _message]]; + }; + false + }; + + if (_sourceAccount isEqualType createHashMap && { _sourceAccount isNotEqualTo createHashMap }) then { + _self call ["normalizeAccount", [_uid, _sourceAccount, ""]]; + }; + if (_targetAccount isEqualType createHashMap && { _targetAccount isNotEqualTo createHashMap }) then { + _self call ["normalizeAccount", [_target, _targetAccount, ""]]; + }; + + GVAR(BankMessenger) call ["sendAccountSync", [_uid, _finalSourcePatch]]; + GVAR(BankMessenger) call ["sendAccountSync", [_target, _finalTargetPatch]]; + + private _contextTargetAccount = _context getOrDefault ["targetAccount", createHashMap]; + private _contextAccount = _context getOrDefault ["account", createHashMap]; + private _targetPlayer = [_target] call EFUNC(common,getPlayer); + private _targetName = if (isNull _targetPlayer) then { _contextTargetAccount getOrDefault ["name", "Recipient"] } else { name _targetPlayer }; + private _player = [_uid] call EFUNC(common,getPlayer); + private _playerName = if (isNull _player) then { _contextAccount getOrDefault ["name", "Unknown"] } else { name _player }; + + GVAR(BankMessenger) call ["sendNotification", [_uid, "info", "Bank", format ["Transferred $%1 to %2", [_amount] call EFUNC(common,formatNumber), _targetName]]]; + GVAR(BankMessenger) call ["sendNotification", [_target, "info", "Bank", format ["Received $%1 from %2", [_amount] call EFUNC(common,formatNumber), _playerName]]]; + true + }], + ["validatePin", compileFinal { + params [["_uid", "", [""]], ["_pin", "", [""]]]; + + if (_uid isEqualTo "") exitWith { false }; + + private _enteredPin = _pin; + if !(_enteredPin isEqualType "") then { + _enteredPin = str _enteredPin; + }; + + private _envelope = _self call [ + "callHotBankEnvelope", + [ + "bank:hot:validate_pin", + [_uid, _enteredPin, toJSON (GVAR(BankPayloadBuilder) call ["buildOperationContext", [_uid, "atm"]])] + ] + ]; + + private _message = _envelope getOrDefault ["error", ""]; + if (_message isNotEqualTo "") then { + GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _message]]; + false + } else { + true + } + }], + ["withdraw", compileFinal { + params [["_uid", "", [""]], ["_amount", 0, [0]]]; + + _self call [ + "runMutation", + [ + _uid, + "bank:hot:withdraw", + [_uid, str _amount, toJSON (GVAR(BankPayloadBuilder) call ["buildOperationContext", [_uid]])], + false, + format ["Withdrew $%1", [_amount] call EFUNC(common,formatNumber)] + ] + ] + }], + ["depositEarnings", compileFinal { + params [["_uid", "", [""]], ["_amount", 0, [0]]]; + + _self call [ + "runMutation", + [ + _uid, + "bank:hot:deposit_earnings", + [_uid, str _amount, toJSON (GVAR(BankPayloadBuilder) call ["buildOperationContext", [_uid]])], + false, + format ["Deposited $%1 from earnings", [_amount] call EFUNC(common,formatNumber)] + ] + ] + }] +]; + +GVAR(BankStore) = createHashMapObject [GVAR(BankBaseStore)]; +GVAR(BankStore) diff --git a/arma/server/addons/cad/$PBOPREFIX$ b/arma/server/addons/cad/$PBOPREFIX$ new file mode 100644 index 0000000..5062385 --- /dev/null +++ b/arma/server/addons/cad/$PBOPREFIX$ @@ -0,0 +1 @@ +forge\forge_server\addons\cad diff --git a/arma/server/addons/cad/CfgEventHandlers.hpp b/arma/server/addons/cad/CfgEventHandlers.hpp new file mode 100644 index 0000000..9b160c1 --- /dev/null +++ b/arma/server/addons/cad/CfgEventHandlers.hpp @@ -0,0 +1,5 @@ +class Extended_PreInit_EventHandlers { + class ADDON { + init = QUOTE(call COMPILE_SCRIPT(XEH_preInit)); + }; +}; diff --git a/arma/server/addons/cad/XEH_PREP.hpp b/arma/server/addons/cad/XEH_PREP.hpp new file mode 100644 index 0000000..3f556c5 --- /dev/null +++ b/arma/server/addons/cad/XEH_PREP.hpp @@ -0,0 +1,7 @@ +PREP(initActivityRepository); +PREP(initAssignmentRepository); +PREP(initCadStore); +PREP(initGroupRepository); +PREP(initPermissionService); +PREP(initPersistenceService); +PREP(initRequestRepository); diff --git a/arma/server/addons/cad/XEH_preInit.sqf b/arma/server/addons/cad/XEH_preInit.sqf new file mode 100644 index 0000000..187b250 --- /dev/null +++ b/arma/server/addons/cad/XEH_preInit.sqf @@ -0,0 +1,219 @@ +#include "script_component.hpp" + +PREP_RECOMPILE_START; +#include "XEH_PREP.hpp" +PREP_RECOMPILE_END; + +call FUNC(initCadStore); + +[QGVAR(requestHydrateCad), { + params [["_uid", "", [""]]]; + + private _player = GVAR(CadStore) call ["resolveRequestPlayer", [_uid, "CAD hydrate request received with empty UID."]]; + if (_player isEqualTo objNull) exitWith {}; + + private _payload = GVAR(CadStore) call ["buildHydratePayload", [_uid]]; + [CRPC(cad,responseHydrateCad), [_payload], _player] call CFUNC(targetEvent); +}] call CFUNC(addEventHandler); + +[QGVAR(requestAssignCadTask), { + params [ + ["_uid", "", [""]], + ["_taskID", "", [""]], + ["_groupID", "", [""]], + ["_note", "", [""]] + ]; + + if (_taskID isEqualTo "" || { _groupID isEqualTo "" }) exitWith { + ["WARNING", "Invalid CAD task assignment payload."] call EFUNC(common,log); + }; + + GVAR(CadStore) call ["dispatchRpcMutation", [ + _uid, + "Invalid CAD task assignment payload.", + CRPC(cad,responseCadAssignment), + "assignTaskToGroup", + [_uid, _taskID, _groupID, _note], + true, + false + ]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestCreateCadDispatchOrder), { + params [ + ["_uid", "", [""]], + ["_assigneeGroupID", "", [""]], + ["_targetGroupID", "", [""]], + ["_note", "", [""]], + ["_priority", "priority", [""]], + ["_request", createHashMap, [createHashMap]] + ]; + + if (_assigneeGroupID isEqualTo "" || { _targetGroupID isEqualTo "" }) exitWith { + ["WARNING", "Invalid CAD dispatch order payload."] call EFUNC(common,log); + }; + + GVAR(CadStore) call ["dispatchRpcMutation", [ + _uid, + "Invalid CAD dispatch order payload.", + CRPC(cad,responseCadAssignment), + "createDispatchOrder", + [_uid, _assigneeGroupID, _targetGroupID, _note, _priority, _request], + true, + false + ]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestSubmitCadSupportRequest), { + params [ + ["_uid", "", [""]], + ["_type", "", [""]], + ["_fields", createHashMap, [createHashMap]], + ["_priority", "priority", [""]] + ]; + + if (_type isEqualTo "") exitWith { + ["WARNING", "Invalid CAD support request payload."] call EFUNC(common,log); + }; + + GVAR(CadStore) call ["dispatchRpcMutation", [ + _uid, + "Invalid CAD support request payload.", + CRPC(cad,responseCadRequest), + "submitSupportRequest", + [_uid, _type, _fields, _priority], + true, + false + ]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestCloseCadSupportRequest), { + params [["_uid", "", [""]], ["_requestID", "", [""]]]; + + if (_requestID isEqualTo "") exitWith { + ["WARNING", "Invalid CAD support request close payload."] call EFUNC(common,log); + }; + + GVAR(CadStore) call ["dispatchRpcMutation", [ + _uid, + "Invalid CAD support request close payload.", + CRPC(cad,responseCadRequest), + "closeSupportRequest", + [_uid, _requestID], + true, + false + ]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestAcknowledgeCadTask), { + params [["_uid", "", [""]], ["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { + ["WARNING", "Invalid CAD acknowledge payload."] call EFUNC(common,log); + }; + + GVAR(CadStore) call ["dispatchRpcMutation", [ + _uid, + "Invalid CAD acknowledge payload.", + CRPC(cad,responseCadAssignment), + "acknowledgeTask", + [_uid, _taskID], + true, + false + ]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestCloseCadDispatchOrder), { + params [["_uid", "", [""]], ["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { + ["WARNING", "Invalid CAD dispatch order close payload."] call EFUNC(common,log); + }; + + GVAR(CadStore) call ["dispatchRpcMutation", [ + _uid, + "Invalid CAD dispatch order close payload.", + CRPC(cad,responseCadAssignment), + "closeDispatchOrder", + [_uid, _taskID], + true, + false + ]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestDeclineCadTask), { + params [["_uid", "", [""]], ["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { + ["WARNING", "Invalid CAD decline payload."] call EFUNC(common,log); + }; + + GVAR(CadStore) call ["dispatchRpcMutation", [ + _uid, + "Invalid CAD decline payload.", + CRPC(cad,responseCadAssignment), + "declineTask", + [_uid, _taskID], + true, + false + ]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestUpdateCadGroupStatus), { + params [["_uid", "", [""]], ["_groupID", "", [""]], ["_status", "", [""]]]; + + if (_groupID isEqualTo "" || { _status isEqualTo "" }) exitWith { + ["WARNING", "Invalid CAD group status payload."] call EFUNC(common,log); + }; + + GVAR(CadStore) call ["dispatchRpcMutation", [ + _uid, + "Invalid CAD group status payload.", + CRPC(cad,responseCadGroupUpdate), + "updateGroupStatus", + [_uid, _groupID, _status], + true, + true + ]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestUpdateCadGroupRole), { + params [["_uid", "", [""]], ["_groupID", "", [""]], ["_role", "", [""]]]; + + if (_groupID isEqualTo "" || { _role isEqualTo "" }) exitWith { + ["WARNING", "Invalid CAD group role payload."] call EFUNC(common,log); + }; + + GVAR(CadStore) call ["dispatchRpcMutation", [ + _uid, + "Invalid CAD group role payload.", + CRPC(cad,responseCadGroupUpdate), + "updateGroupRole", + [_uid, _groupID, _role], + true, + true + ]]; +}] call CFUNC(addEventHandler); + +[QGVAR(requestUpdateCadGroupProfile), { + params [ + ["_uid", "", [""]], + ["_groupID", "", [""]], + ["_status", "", [""]], + ["_role", "", [""]] + ]; + + if (_groupID isEqualTo "") exitWith { + ["WARNING", "Invalid CAD group profile payload."] call EFUNC(common,log); + }; + + GVAR(CadStore) call ["dispatchRpcMutation", [ + _uid, + "Invalid CAD group profile payload.", + CRPC(cad,responseCadGroupUpdate), + "updateGroupProfile", + [_uid, _groupID, _status, _role], + true, + true + ]]; +}] call CFUNC(addEventHandler); diff --git a/arma/server/addons/cad/config.cpp b/arma/server/addons/cad/config.cpp new file mode 100644 index 0000000..74d149d --- /dev/null +++ b/arma/server/addons/cad/config.cpp @@ -0,0 +1,23 @@ +#include "script_component.hpp" + +class CfgPatches { + class ADDON { + author = AUTHOR; + authors[] = {"IDSolutions"}; + url = ECSTRING(main,url); + name = COMPONENT_NAME; + requiredVersion = REQUIRED_VERSION; + requiredAddons[] = { + "forge_server_main", + "forge_server_common", + "forge_server_actor", + "forge_server_org", + "forge_server_task" + }; + units[] = {}; + weapons[] = {}; + VERSION_CONFIG; + }; +}; + +#include "CfgEventHandlers.hpp" diff --git a/arma/server/addons/cad/functions/fnc_initActivityRepository.sqf b/arma/server/addons/cad/functions/fnc_initActivityRepository.sqf new file mode 100644 index 0000000..3fab742 --- /dev/null +++ b/arma/server/addons/cad/functions/fnc_initActivityRepository.sqf @@ -0,0 +1,93 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initActivityRepository.sqf + * Author: IDSolutions + * Date: 2026-03-30 + * Public: No + * + * Description: + * Initializes the CAD activity repository for recent operational events. + * + * Arguments: + * None + * + * Return Value: + * CAD activity repository object [HASHMAP OBJECT] + * + * Example: + * call forge_server_cad_fnc_initActivityRepository + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(ActivityRepositoryBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "CadActivityRepositoryBaseClass"], + ["#create", compileFinal { + _self set ["activityRegistry", []]; + _self set ["persistenceLoaded", false]; + }], + ["restorePersistedActivity", compileFinal { + if (_self getOrDefault ["persistenceLoaded", false]) exitWith { true }; + + private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; + if (_persistenceService isEqualTo createHashMap) exitWith { false }; + + private _result = _persistenceService call ["loadActivity", []]; + if !(_result getOrDefault ["success", false]) exitWith { false }; + + _self set ["activityRegistry", +(_result getOrDefault ["data", []])]; + _self set ["persistenceLoaded", true]; + true + }], + ["appendEntry", compileFinal { + params [["_entry", createHashMap, [createHashMap]]]; + + if (_entry isEqualTo createHashMap) exitWith { false }; + + _self call ["restorePersistedActivity", []]; + + private _activityRegistry = +(_self getOrDefault ["activityRegistry", []]); + private _finalEntry = +_entry; + if ((_finalEntry getOrDefault ["timestamp", -1]) < 0) then { + _finalEntry set ["timestamp", serverTime]; + }; + + _activityRegistry pushBack _finalEntry; + + if ((count _activityRegistry) > 50) then { + _activityRegistry deleteRange [0, (count _activityRegistry) - 50]; + }; + + _self set ["activityRegistry", _activityRegistry]; + private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; + if (_persistenceService isNotEqualTo createHashMap) then { + _persistenceService call ["appendActivity", [_finalEntry]]; + }; + true + }], + ["appendActivity", compileFinal { + params [ + ["_type", "", [""]], + ["_message", "", [""]], + ["_taskID", "", [""]], + ["_groupID", "", [""]], + ["_actorUid", "", [""]] + ]; + + if (_type isEqualTo "" || { _message isEqualTo "" }) exitWith { false }; + private _entry = createHashMapFromArray [ + ["type", _type], + ["message", _message], + ["taskId", _taskID], + ["groupId", _groupID], + ["actorUid", _actorUid] + ]; + _self call ["appendEntry", [_entry]] + }], + ["getActivity", compileFinal { + _self call ["restorePersistedActivity", []]; + +(_self getOrDefault ["activityRegistry", []]) + }] +]; + +createHashMapObject [GVAR(ActivityRepositoryBaseClass)] diff --git a/arma/server/addons/cad/functions/fnc_initAssignmentRepository.sqf b/arma/server/addons/cad/functions/fnc_initAssignmentRepository.sqf new file mode 100644 index 0000000..cb3d5be --- /dev/null +++ b/arma/server/addons/cad/functions/fnc_initAssignmentRepository.sqf @@ -0,0 +1,549 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initAssignmentRepository.sqf + * Author: IDSolutions + * Date: 2026-03-30 + * Public: No + * + * Description: + * Initializes the CAD assignment repository for contract assignment + * state and dispatcher/group-leader task actions. + * + * Arguments: + * None + * + * Return Value: + * CAD assignment repository object [HASHMAP OBJECT] + * + * Example: + * call forge_server_cad_fnc_initAssignmentRepository + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "CadAssignmentRepositoryBaseClass"], + ["#create", compileFinal { + _self set ["assignmentRegistry", createHashMap]; + _self set ["dispatchOrderRegistry", createHashMap]; + _self set ["persistenceLoaded", false]; + }], + ["pruneAssignments", compileFinal { + _self call ["restorePersistedState", []]; + + private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap]; + private _dispatchOrderRegistry = _self getOrDefault ["dispatchOrderRegistry", createHashMap]; + private _keysToRemove = []; + + { + if ((_dispatchOrderRegistry getOrDefault [_x, createHashMap]) isNotEqualTo createHashMap) then { + continue; + }; + + private _status = EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]; + if !(_status in ["active", ""]) then { + _keysToRemove pushBack _x; + }; + } forEach _assignmentRegistry; + + { + _assignmentRegistry deleteAt _x; + } forEach _keysToRemove; + + _self set ["assignmentRegistry", _assignmentRegistry]; + + private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; + if (_persistenceService isNotEqualTo createHashMap) then { + { + _persistenceService call ["deleteAssignment", [_x]]; + } forEach _keysToRemove; + }; + + count _keysToRemove + }], + ["getAssignments", compileFinal { + _self call ["restorePersistedState", []]; + values (_self getOrDefault ["assignmentRegistry", createHashMap]) + }], + ["isDispatchOrder", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + ((_self getOrDefault ["dispatchOrderRegistry", createHashMap]) getOrDefault [_taskID, createHashMap]) isNotEqualTo createHashMap + }], + ["restorePersistedState", compileFinal { + if (_self getOrDefault ["persistenceLoaded", false]) exitWith { true }; + + private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; + if (_persistenceService isEqualTo createHashMap) exitWith { false }; + + private _assignmentsResult = _persistenceService call ["loadAssignments", []]; + if !(_assignmentsResult getOrDefault ["success", false]) exitWith { false }; + + private _ordersResult = _persistenceService call ["loadDispatchOrders", []]; + if !(_ordersResult getOrDefault ["success", false]) exitWith { false }; + + private _assignmentRegistry = +(_assignmentsResult getOrDefault ["data", createHashMap]); + private _dispatchOrderRegistry = +(_ordersResult getOrDefault ["data", createHashMap]); + + _self set ["assignmentRegistry", _assignmentRegistry]; + _self set ["dispatchOrderRegistry", _dispatchOrderRegistry]; + _self set ["persistenceLoaded", true]; + + { + if ((_y getOrDefault ["state", ""]) isNotEqualTo "acknowledged") then { continue; }; + if (((_y getOrDefault ["acknowledgedByUid", ""]) isEqualTo "")) then { continue; }; + if ((_dispatchOrderRegistry getOrDefault [_x, createHashMap]) isNotEqualTo createHashMap) then { continue; }; + if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) isNotEqualTo "active") then { continue; }; + EGVAR(task,TaskStore) call ["bindTaskOwnership", [_x, _y getOrDefault ["acknowledgedByUid", ""]]]; + } forEach _assignmentRegistry; + + true + }], + ["buildDispatchOrderEntry", compileFinal { + params [ + ["_taskID", "", [""]], + ["_order", createHashMap, [createHashMap]], + ["_assignmentRegistry", createHashMap, [createHashMap]], + ["_groupRepository", createHashMap, [createHashMap]] + ]; + + if (_taskID isEqualTo "" || { _order isEqualTo createHashMap }) exitWith { createHashMap }; + + private _entry = +_order; + private _targetGroupID = _order getOrDefault ["targetGroupId", ""]; + if (_targetGroupID isNotEqualTo "") then { + private _targetGroup = _groupRepository call ["getGroupRecord", [_targetGroupID]]; + if (_targetGroup isNotEqualTo createHashMap) then { + private _targetCallsign = _targetGroup getOrDefault ["callsign", _targetGroupID]; + _entry set ["targetGroupCallsign", _targetCallsign]; + _entry set ["position", +(_targetGroup getOrDefault ["position", _entry getOrDefault ["position", []]])]; + _entry set ["title", format ["Backup %1", _targetCallsign]]; + + if ((_order getOrDefault ["note", ""]) isEqualTo "") then { + _entry set ["description", format ["Dispatch order to back up %1 at its current position.", _targetCallsign]]; + }; + }; + }; + + private _assignment = _assignmentRegistry getOrDefault [_taskID, createHashMap]; + _entry set ["taskId", _taskID]; + _entry set ["taskID", _taskID]; + _entry set ["type", _entry getOrDefault ["type", "dispatch_order"]]; + _entry set ["isDispatchOrder", true]; + _entry set ["assignedGroupId", _assignment getOrDefault ["groupId", ""]]; + _entry set ["assignmentState", [_assignment getOrDefault ["state", ""], "unassigned"] select (_assignment isEqualTo createHashMap)]; + _entry + }], + ["assignTaskToGroup", compileFinal { + params [ + ["_requesterUid", "", [""]], + ["_taskID", "", [""]], + ["_groupID", "", [""]], + ["_note", "", [""]] + ]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", "Unable to assign task."], + ["assignment", createHashMap] + ]; + + _self call ["restorePersistedState", []]; + + private _permissionService = _self getOrDefault ["permissionService", createHashMap]; + if !(_permissionService call ["canDispatch", [_requesterUid]]) exitWith { + _result set ["message", "You are not authorized to assign contracts."]; + _result + }; + + private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap]; + private _dispatchOrderRegistry = _self getOrDefault ["dispatchOrderRegistry", createHashMap]; + private _isDispatchOrder = (_dispatchOrderRegistry getOrDefault [_taskID, createHashMap]) isNotEqualTo createHashMap; + + if (!_isDispatchOrder && { (EGVAR(task,TaskStore) call ["getTaskStatus", [_taskID]]) isNotEqualTo "active" }) exitWith { + _result set ["message", "Task is no longer active."]; + _result + }; + + private _existingAssignment = +(_assignmentRegistry getOrDefault [_taskID, createHashMap]); + if ( + _existingAssignment isNotEqualTo createHashMap + && { (_existingAssignment getOrDefault ["state", ""]) in ["assigned", "acknowledged"] } + ) exitWith { + _result set ["message", ["Task is already assigned and must be declined or completed before reassignment.", "Dispatch order is already assigned and must be declined or closed before reassignment."] select _isDispatchOrder]; + _result set ["assignment", _existingAssignment]; + _result + }; + + private _groupRepository = _self getOrDefault ["groupRepository", createHashMap]; + private _groupRecord = _groupRepository call ["getGroupRecord", [_groupID]]; + if (_groupRecord isEqualTo createHashMap) exitWith { + _result set ["message", "Selected group is unavailable."]; + _result + }; + + private _leaderUid = _groupRecord getOrDefault ["leaderUid", ""]; + if (_leaderUid isEqualTo "") exitWith { + _result set ["message", "Selected group has no online leader."]; + _result + }; + + private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer); + private _assignment = createHashMapFromArray [ + ["taskId", _taskID], + ["groupId", _groupID], + ["groupCallsign", _groupRecord getOrDefault ["callsign", _groupID]], + ["assignedByUid", _requesterUid], + ["assignedByName", ["Dispatcher", name _requesterPlayer] select (_requesterPlayer isNotEqualTo objNull)], + ["assignedAt", serverTime], + ["state", "assigned"], + ["note", _note] + ]; + + private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; + if (_persistenceService isEqualTo createHashMap) exitWith { + _result set ["message", "CAD extension state is unavailable."]; + _result + }; + + private _assignResult = _persistenceService call ["assignAssignment", [_taskID, _assignment]]; + if !(_assignResult getOrDefault ["success", false]) exitWith { + _result set ["message", "CAD extension rejected the assignment."]; + _result + }; + + private _assignData = +(_assignResult getOrDefault ["data", createHashMap]); + _assignment = +(_assignData getOrDefault ["assignment", createHashMap]); + if (_assignment isEqualTo createHashMap) exitWith { + _result set ["message", "CAD extension returned an invalid assignment."]; + _result + }; + + _assignmentRegistry set [_taskID, _assignment]; + _self set ["assignmentRegistry", _assignmentRegistry]; + + private _activityEntry = +(_assignData getOrDefault ["activity", createHashMap]); + if (_activityEntry isNotEqualTo createHashMap) then { + private _activityRepository = _self getOrDefault ["activityRepository", createHashMap]; + _activityRepository call ["appendEntry", [_activityEntry]]; + }; + + _result set ["success", true]; + _result set ["message", _assignData getOrDefault ["message", ["Task assigned.", "Dispatch order assigned."] select _isDispatchOrder]]; + _result set ["assignment", _assignment]; + _result set ["leaderUid", _leaderUid]; + _result set ["isDispatchOrder", _isDispatchOrder]; + _result + }], + ["createDispatchOrder", compileFinal { + params [ + ["_requesterUid", "", [""]], + ["_assigneeGroupID", "", [""]], + ["_targetGroupID", "", [""]], + ["_note", "", [""]], + ["_priority", "priority", [""]], + ["_request", createHashMap, [createHashMap]] + ]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", "Unable to create dispatch order."], + ["assignment", createHashMap], + ["order", createHashMap] + ]; + + _self call ["restorePersistedState", []]; + + private _permissionService = _self getOrDefault ["permissionService", createHashMap]; + if !(_permissionService call ["canDispatch", [_requesterUid]]) exitWith { + _result set ["message", "You are not authorized to create dispatch orders."]; + _result + }; + + if (_assigneeGroupID isEqualTo "" || { _targetGroupID isEqualTo "" }) exitWith { + _result set ["message", "Assignee and target groups are required."]; + _result + }; + + if (_assigneeGroupID isEqualTo _targetGroupID) exitWith { + _result set ["message", "Assignee and target groups must be different."]; + _result + }; + + private _groupRepository = _self getOrDefault ["groupRepository", createHashMap]; + private _assigneeGroup = _groupRepository call ["getGroupRecord", [_assigneeGroupID]]; + if (_assigneeGroup isEqualTo createHashMap) exitWith { + _result set ["message", "Selected assignee group is unavailable."]; + _result + }; + + private _assigneeLeaderUid = _assigneeGroup getOrDefault ["leaderUid", ""]; + if (_assigneeLeaderUid isEqualTo "") exitWith { + _result set ["message", "Selected assignee group has no online leader."]; + _result + }; + + private _targetGroup = _groupRepository call ["getGroupRecord", [_targetGroupID]]; + if (_targetGroup isEqualTo createHashMap) exitWith { + _result set ["message", "Selected target group is unavailable."]; + _result + }; + + private _validPriorities = ["routine", "priority", "emergency"]; + private _finalPriority = toLowerANSI _priority; + if !(_finalPriority in _validPriorities) then { + _finalPriority = "priority"; + }; + + private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer); + private _dispatchContext = createHashMapFromArray [ + ["assigneeGroupId", _assigneeGroupID], + ["assigneeGroupCallsign", _assigneeGroup getOrDefault ["callsign", _assigneeGroupID]], + ["targetGroupId", _targetGroupID], + ["targetGroupCallsign", _targetGroup getOrDefault ["callsign", _targetGroupID]], + ["targetPosition", +(_targetGroup getOrDefault ["position", []])], + ["createdByUid", _requesterUid], + ["createdByName", ["Dispatcher", name _requesterPlayer] select (_requesterPlayer isNotEqualTo objNull)], + ["requestId", _request getOrDefault ["requestId", ""]], + ["requestType", _request getOrDefault ["type", ""]], + ["requestTitle", _request getOrDefault ["title", ""]], + ["requestSummary", _request getOrDefault ["summary", ""]], + ["requestFields", +(_request getOrDefault ["fields", createHashMap])], + ["note", _note], + ["priority", _finalPriority], + ["createdAt", serverTime] + ]; + + private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; + if (_persistenceService isEqualTo createHashMap) exitWith { + _result set ["message", "CAD extension state is unavailable."]; + _result + }; + + private _createResult = _persistenceService call ["createDispatchOrderFromContext", [_dispatchContext]]; + if !(_createResult getOrDefault ["success", false]) exitWith { + _result set ["message", "CAD extension rejected the dispatch order."]; + _result + }; + + private _createData = +(_createResult getOrDefault ["data", createHashMap]); + private _taskID = _createData getOrDefault ["taskId", ""]; + private _order = +(_createData getOrDefault ["order", createHashMap]); + private _assignment = +(_createData getOrDefault ["assignment", createHashMap]); + if (_taskID isEqualTo "" || { _order isEqualTo createHashMap } || { _assignment isEqualTo createHashMap }) exitWith { + _result set ["message", "CAD extension returned an invalid dispatch order."]; + _result + }; + + private _dispatchOrderRegistry = _self getOrDefault ["dispatchOrderRegistry", createHashMap]; + _dispatchOrderRegistry set [_taskID, _order]; + _self set ["dispatchOrderRegistry", _dispatchOrderRegistry]; + + private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap]; + _assignmentRegistry set [_taskID, _assignment]; + _self set ["assignmentRegistry", _assignmentRegistry]; + + private _activityEntry = +(_createData getOrDefault ["activity", createHashMap]); + if (_activityEntry isNotEqualTo createHashMap) then { + private _activityRepository = _self getOrDefault ["activityRepository", createHashMap]; + _activityRepository call ["appendEntry", [_activityEntry]]; + }; + + _result set ["success", true]; + _result set ["message", _createData getOrDefault ["message", "Dispatch order created."]]; + _result set ["assignment", _assignment]; + _result set ["order", _order]; + _result set ["leaderUid", _assigneeLeaderUid]; + _result set ["isDispatchOrder", true]; + _result + }], + ["closeDispatchOrder", compileFinal { + params [["_requesterUid", "", [""]], ["_taskID", "", [""]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", "Unable to close dispatch order."], + ["assignment", createHashMap] + ]; + + _self call ["restorePersistedState", []]; + + private _permissionService = _self getOrDefault ["permissionService", createHashMap]; + if !(_permissionService call ["canDispatch", [_requesterUid]]) exitWith { + _result set ["message", "You are not authorized to close dispatch orders."]; + _result + }; + + private _dispatchOrderRegistry = _self getOrDefault ["dispatchOrderRegistry", createHashMap]; + private _order = +(_dispatchOrderRegistry getOrDefault [_taskID, createHashMap]); + if (_order isEqualTo createHashMap) exitWith { + _result set ["message", "Dispatch order could not be resolved."]; + _result + }; + + private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap]; + private _assignment = +(_assignmentRegistry getOrDefault [_taskID, createHashMap]); + + private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; + if (_persistenceService isEqualTo createHashMap) exitWith { + _result set ["message", "CAD extension state is unavailable."]; + _result + }; + + private _closeResult = _persistenceService call ["closeDispatchOrder", [_taskID]]; + if !(_closeResult getOrDefault ["success", false]) exitWith { + _result set ["message", "CAD extension rejected the dispatch order close."]; + _result + }; + + private _closeData = +(_closeResult getOrDefault ["data", createHashMap]); + _order = +(_closeData getOrDefault ["order", _order]); + _assignment = +(_closeData getOrDefault ["assignment", _assignment]); + + _dispatchOrderRegistry deleteAt _taskID; + _self set ["dispatchOrderRegistry", _dispatchOrderRegistry]; + _assignmentRegistry deleteAt _taskID; + _self set ["assignmentRegistry", _assignmentRegistry]; + + private _activityEntry = +(_closeData getOrDefault ["activity", createHashMap]); + if (_activityEntry isNotEqualTo createHashMap) then { + _activityEntry set ["actorUid", _requesterUid]; + private _activityRepository = _self getOrDefault ["activityRepository", createHashMap]; + _activityRepository call ["appendEntry", [_activityEntry]]; + }; + + _result set ["success", true]; + _result set ["message", _closeData getOrDefault ["message", "Dispatch order closed."]]; + _result set ["assignment", _assignment]; + _result set ["isDispatchOrder", true]; + _result + }], + ["applyAssignmentTransition", compileFinal { + params [["_requesterUid", "", [""]], ["_taskID", "", [""]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", "Unable to update task assignment."], + ["assignment", createHashMap] + ]; + + private _transition = _this param [2, "acknowledge", [""]]; + + _self call ["restorePersistedState", []]; + + private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap]; + private _assignment = +(_assignmentRegistry getOrDefault [_taskID, createHashMap]); + private _isDispatchOrder = _self call ["isDispatchOrder", [_taskID]]; + if (_assignment isEqualTo createHashMap) exitWith { + _result set ["message", "Task is not assigned."]; + _result + }; + + private _groupID = _assignment getOrDefault ["groupId", ""]; + private _groupRepository = _self getOrDefault ["groupRepository", createHashMap]; + if !(_groupRepository call ["isGroupLeader", [_requesterUid, _groupID]]) exitWith { + _result set ["message", format ["Only the assigned group leader can %1 this task.", _transition]]; + _result + }; + + switch (_transition) do { + case "acknowledge": { + if (!_isDispatchOrder) then { + private _bindResult = EGVAR(task,TaskStore) call ["bindTaskOwnership", [_taskID, _requesterUid]]; + if !(_bindResult getOrDefault ["success", false]) exitWith { + _result set ["message", _bindResult getOrDefault ["message", "Failed to bind task ownership."]]; + _result + }; + }; + }; + case "decline": { + if (!_isDispatchOrder) then { + EGVAR(task,TaskStore) call ["releaseTaskOwnership", [_taskID]]; + }; + }; + }; + + if (_result getOrDefault ["success", false]) exitWith { _result }; + + private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; + if (_persistenceService isEqualTo createHashMap) exitWith { + _result set ["message", "CAD extension state is unavailable."]; + _result + }; + + private _patch = switch (_transition) do { + case "decline": { + createHashMapFromArray [ + ["state", "declined"], + ["declinedAt", serverTime], + ["declinedByUid", _requesterUid] + ] + }; + default { + createHashMapFromArray [ + ["state", "acknowledged"], + ["acknowledgedAt", serverTime], + ["acknowledgedByUid", _requesterUid] + ] + }; + }; + private _transitionResult = switch (_transition) do { + case "decline": { _persistenceService call ["declineAssignment", [_taskID, _patch]] }; + default { _persistenceService call ["acknowledgeAssignment", [_taskID, _patch]] }; + }; + if !(_transitionResult getOrDefault ["success", false]) exitWith { + _result set ["message", switch (_transition) do { + case "decline": { "CAD extension rejected the decline." }; + default { "CAD extension rejected the acknowledgement." }; + }]; + _result + }; + + private _transitionData = +(_transitionResult getOrDefault ["data", createHashMap]); + _assignment = +(_transitionData getOrDefault ["assignment", createHashMap]); + if (_assignment isEqualTo createHashMap) exitWith { + _result set ["message", "CAD extension returned an invalid assignment."]; + _result + }; + + switch (_transition) do { + case "decline": { + _assignmentRegistry deleteAt _taskID; + }; + default { + _assignmentRegistry set [_taskID, _assignment]; + }; + }; + _self set ["assignmentRegistry", _assignmentRegistry]; + + private _activityEntry = +(_transitionData getOrDefault ["activity", createHashMap]); + if (_activityEntry isNotEqualTo createHashMap) then { + if (_isDispatchOrder) then { + _activityEntry set ["type", format ["dispatch_order_%1", _transition]]; + _activityEntry set ["message", format ["%1 %2d %3.", _requesterUid, _transition, _taskID]]; + }; + + private _activityRepository = _self getOrDefault ["activityRepository", createHashMap]; + _activityRepository call ["appendEntry", [_activityEntry]]; + }; + + _result set ["success", true]; + _result set ["message", switch (_transition) do { + case "decline": { [_transitionData getOrDefault ["message", "Task declined and returned to the contract board."], "Dispatch order declined and returned to the dispatch board."] select _isDispatchOrder }; + default { [_transitionData getOrDefault ["message", "Task acknowledged."], "Dispatch order acknowledged."] select _isDispatchOrder }; + }]; + _result set ["assignment", _assignment]; + _result set ["isDispatchOrder", _isDispatchOrder]; + _result + }], + ["acknowledgeTask", compileFinal { + _self call ["applyAssignmentTransition", [_this # 0, _this # 1, "acknowledge"]] + }], + ["declineTask", compileFinal { + _self call ["applyAssignmentTransition", [_this # 0, _this # 1, "decline"]] + }] +]; + +createHashMapObject [GVAR(AssignmentRepositoryBaseClass)] diff --git a/arma/server/addons/cad/functions/fnc_initCadStore.sqf b/arma/server/addons/cad/functions/fnc_initCadStore.sqf new file mode 100644 index 0000000..07db1f0 --- /dev/null +++ b/arma/server/addons/cad/functions/fnc_initCadStore.sqf @@ -0,0 +1,250 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initCadStore.sqf + * Author: IDSolutions + * Date: 2026-03-29 + * Public: Yes + * + * Description: + * Initializes the CAD store as a coordinator over activity, group, + * assignment, and permission domain objects. + * + * Arguments: + * None + * + * Return Value: + * CAD store object [HASHMAP OBJECT] + * + * Example: + * call forge_server_cad_fnc_initCadStore + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(CadStoreBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "CadStoreBaseClass"], + ["#create", compileFinal { + private _activityRepository = call FUNC(initActivityRepository); + private _permissionService = call FUNC(initPermissionService); + private _groupRepository = call FUNC(initGroupRepository); + private _assignmentRepository = call FUNC(initAssignmentRepository); + private _persistenceService = call FUNC(initPersistenceService); + private _requestRepository = call FUNC(initRequestRepository); + + _groupRepository set ["activityRepository", _activityRepository]; + _groupRepository set ["assignmentRepository", _assignmentRepository]; + _groupRepository set ["permissionService", _permissionService]; + _groupRepository set ["persistenceService", _persistenceService]; + + _assignmentRepository set ["activityRepository", _activityRepository]; + _assignmentRepository set ["groupRepository", _groupRepository]; + _assignmentRepository set ["permissionService", _permissionService]; + _assignmentRepository set ["persistenceService", _persistenceService]; + + _requestRepository set ["activityRepository", _activityRepository]; + _requestRepository set ["groupRepository", _groupRepository]; + _requestRepository set ["permissionService", _permissionService]; + _requestRepository set ["persistenceService", _persistenceService]; + + _activityRepository set ["persistenceService", _persistenceService]; + + _self set ["ActivityRepository", _activityRepository]; + _self set ["PermissionService", _permissionService]; + _self set ["GroupRepository", _groupRepository]; + _self set ["AssignmentRepository", _assignmentRepository]; + _self set ["PersistenceService", _persistenceService]; + _self set ["RequestRepository", _requestRepository]; + + ["INFO", "CAD Store Initialized!"] call EFUNC(common,log); + }], + ["notifyPlayer", compileFinal { + params [ + ["_uid", "", [""]], + ["_type", "info", [""]], + ["_title", "CAD", [""]], + ["_message", "", [""]] + ]; + + if (_uid isEqualTo "" || { _message isEqualTo "" }) exitWith { false }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (_player isEqualTo objNull) exitWith { false }; + + [CRPC(notifications,recieveNotification), [_type, _title, _message], _player] call CFUNC(targetEvent); + true + }], + ["resolveRequestPlayer", compileFinal { + params [ + ["_uid", "", [""]], + ["_warning", "Invalid CAD payload.", [""]] + ]; + + if (_uid isEqualTo "") exitWith { + ["WARNING", _warning] call EFUNC(common,log); + objNull + }; + + [_uid] call EFUNC(common,getPlayer) + }], + ["sendRpcResult", compileFinal { + params [ + ["_player", objNull, [objNull]], + ["_responseRpc", "", [""]], + ["_result", createHashMap, [createHashMap]], + ["_invalidateOnSuccess", false, [false]], + ["_requireChanged", false, [false]] + ]; + + if (_player isEqualTo objNull || { _responseRpc isEqualTo "" }) exitWith {}; + + [_responseRpc, [_result], _player] call CFUNC(targetEvent); + + if ( + _invalidateOnSuccess + && { _result getOrDefault ["success", false] } + && { !_requireChanged || { _result getOrDefault ["changed", true] } } + ) then { + [CRPC(cad,invalidateCadState), []] call CFUNC(globalEvent); + }; + }], + ["dispatchRpcMutation", compileFinal { + params [ + ["_uid", "", [""]], + ["_warning", "Invalid CAD payload.", [""]], + ["_responseRpc", "", [""]], + ["_method", "", [""]], + ["_arguments", [], [[]]], + ["_invalidateOnSuccess", false, [false]], + ["_requireChanged", false, [false]] + ]; + + private _player = _self call ["resolveRequestPlayer", [_uid, _warning]]; + if (_player isEqualTo objNull || { _method isEqualTo "" }) exitWith { createHashMap }; + + private _result = _self call [_method, _arguments]; + _self call ["sendRpcResult", [_player, _responseRpc, _result, _invalidateOnSuccess, _requireChanged]]; + _result + }], + ["notifyAssignmentLeader", compileFinal { + params [["_result", createHashMap, [createHashMap]]]; + + if !(_result getOrDefault ["success", false]) exitWith { false }; + + private _leaderUid = _result getOrDefault ["leaderUid", ""]; + if (_leaderUid isEqualTo "") exitWith { false }; + + private _message = if (_result getOrDefault ["isDispatchOrder", false]) then { + private _order = _result getOrDefault ["order", createHashMap]; + if (_order isEqualTo createHashMap) then { + private _assignment = _result getOrDefault ["assignment", createHashMap]; + private _taskID = _assignment getOrDefault ["taskId", ""]; + _order = (_self get "AssignmentRepository") call ["buildDispatchOrderEntry", [ + _taskID, + ((_self get "AssignmentRepository") getOrDefault ["dispatchOrderRegistry", createHashMap]) getOrDefault [_taskID, createHashMap], + (_self get "AssignmentRepository") getOrDefault ["assignmentRegistry", createHashMap], + _self get "GroupRepository" + ]]; + }; + + format ["Dispatch order assigned: %1. Open CAD to review and acknowledge.", _order getOrDefault ["title", "Dispatch Order"]] + } else { + private _assignment = _result getOrDefault ["assignment", createHashMap]; + format ["Contract assigned: %1. Open CAD to review and acknowledge.", _assignment getOrDefault ["taskId", "Task"]] + }; + + _self call ["notifyPlayer", [ + _leaderUid, + "info", + "Tasks", + _message + ]] + }], + ["assignTaskToGroup", compileFinal { + private _result = (_self get "AssignmentRepository") call ["assignTaskToGroup", _this]; + if !(_result getOrDefault ["success", false]) exitWith { _result }; + + _self call ["notifyAssignmentLeader", [_result]]; + _result + }], + ["createDispatchOrder", compileFinal { + private _result = (_self get "AssignmentRepository") call ["createDispatchOrder", _this]; + if !(_result getOrDefault ["success", false]) exitWith { _result }; + + _self call ["notifyAssignmentLeader", [_result]]; + _result + }], + ["closeDispatchOrder", compileFinal { + (_self get "AssignmentRepository") call ["closeDispatchOrder", _this] + }], + ["submitSupportRequest", compileFinal { + (_self get "RequestRepository") call ["submitRequest", _this] + }], + ["closeSupportRequest", compileFinal { + (_self get "RequestRepository") call ["closeRequest", _this] + }], + ["acknowledgeTask", compileFinal { + (_self get "AssignmentRepository") call ["acknowledgeTask", _this] + }], + ["declineTask", compileFinal { + (_self get "AssignmentRepository") call ["declineTask", _this] + }], + ["updateGroupStatus", compileFinal { + (_self get "GroupRepository") call ["updateGroupStatus", _this] + }], + ["updateGroupRole", compileFinal { + (_self get "GroupRepository") call ["updateGroupRole", _this] + }], + ["updateGroupProfile", compileFinal { + (_self get "GroupRepository") call ["updateGroupProfile", _this] + }], + ["buildHydratePayload", compileFinal { + params [["_uid", "", [""]]]; + + private _permissionService = _self get "PermissionService"; + private _groupRepository = _self get "GroupRepository"; + + private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; + if (_actor isEqualTo createHashMap && { _uid isNotEqualTo "" }) then { + _actor = EGVAR(actor,ActorStore) call ["init", [_uid]]; + }; + + private _groupID = _groupRepository call ["getPlayerGroupId", [_uid]]; + private _session = createHashMapFromArray [ + ["uid", _uid], + ["orgId", _actor getOrDefault ["organization", "default"]], + ["isDispatcher", _permissionService call ["canDispatch", [_uid]]], + ["groupId", _groupID], + ["isLeader", _groupRepository call ["isGroupLeader", [_uid, _groupID]]] + ]; + private _seed = createHashMapFromArray [ + ["groups", _groupRepository call ["buildGroups", []]], + ["activeTasks", EGVAR(task,TaskStore) call ["getActiveTaskCatalog", []]], + ["session", _session] + ]; + private _emptyPayload = createHashMapFromArray [ + ["groups", _seed get "groups"], + ["contracts", []], + ["requests", []], + ["assignments", []], + ["activity", []], + ["session", _session] + ]; + private _persistenceService = _self getOrDefault ["PersistenceService", createHashMap]; + + if (_persistenceService isEqualTo createHashMap) exitWith { + ["WARNING", "CAD hydrate extension state is unavailable; returning seed-only payload."] call EFUNC(common,log); + _emptyPayload + }; + + private _hydrateResult = _persistenceService call ["buildHydratePayload", [_seed]]; + if (_hydrateResult getOrDefault ["success", false]) exitWith { + _hydrateResult getOrDefault ["data", createHashMap] + }; + + ["WARNING", "CAD hydrate failed in the extension; returning seed-only payload."] call EFUNC(common,log); + _emptyPayload + }] +]; + +GVAR(CadStore) = createHashMapObject [GVAR(CadStoreBaseClass)]; +GVAR(CadStore) diff --git a/arma/server/addons/cad/functions/fnc_initGroupRepository.sqf b/arma/server/addons/cad/functions/fnc_initGroupRepository.sqf new file mode 100644 index 0000000..3a119a7 --- /dev/null +++ b/arma/server/addons/cad/functions/fnc_initGroupRepository.sqf @@ -0,0 +1,341 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initGroupRepository.sqf + * Author: IDSolutions + * Date: 2026-03-30 + * Public: No + * + * Description: + * Initializes the CAD group repository for live group state, roles, + * and dispatcher/leader-managed group profiles. + * + * Arguments: + * None + * + * Return Value: + * CAD group repository object [HASHMAP OBJECT] + * + * Example: + * call forge_server_cad_fnc_initGroupRepository + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(GroupRepositoryBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "CadGroupRepositoryBaseClass"], + ["#create", compileFinal { + _self set ["groupRegistry", createHashMap]; + _self set ["groupProfileRegistry", createHashMap]; + _self set ["validStatuses", [ + "available", + "en_route", + "on_task", + "holding", + "danger", + "unavailable" + ]]; + _self set ["validRoles", [ + "infantry", + "recon", + "armor", + "air", + "logistics", + "support" + ]]; + }], + ["resolveGroupId", compileFinal { + params [["_group", grpNull, [grpNull]]]; + + if (isNull _group) exitWith { "" }; + + private _leader = leader _group; + private _leaderUid = if (isNull _leader) then { "" } else { getPlayerUID _leader }; + if (_leaderUid isNotEqualTo "") exitWith { format ["group:%1", _leaderUid] }; + + private _groupLabel = groupId _group; + if (_groupLabel isNotEqualTo "") exitWith { format ["group:%1", _groupLabel] }; + + str _group + }], + ["getCurrentTaskIdForGroup", compileFinal { + params [["_groupID", "", [""]]]; + + if (_groupID isEqualTo "") exitWith { "" }; + + private _assignmentRepository = _self getOrDefault ["assignmentRepository", createHashMap]; + private _assignmentRegistry = _assignmentRepository getOrDefault ["assignmentRegistry", createHashMap]; + private _dispatchOrderRegistry = _assignmentRepository getOrDefault ["dispatchOrderRegistry", createHashMap]; + private _taskID = ""; + + { + if ((_y getOrDefault ["groupId", ""]) isNotEqualTo _groupID) then { continue; }; + if !((_y getOrDefault ["state", ""]) in ["assigned", "acknowledged"]) then { continue; }; + private _dispatchOrder = +(_dispatchOrderRegistry getOrDefault [_x, createHashMap]); + if (_dispatchOrder isEqualTo createHashMap) then { + if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) isNotEqualTo "active") then { continue; }; + _taskID = _x; + } else { + _taskID = _dispatchOrder getOrDefault ["title", _x]; + }; + + } forEach _assignmentRegistry; + + _taskID + }], + ["syncGroups", compileFinal { + private _assignmentRepository = _self getOrDefault ["assignmentRepository", createHashMap]; + if (_assignmentRepository isNotEqualTo createHashMap) then { + _assignmentRepository call ["restorePersistedState", []]; + }; + + private _liveGroups = []; + + { + private _group = _x; + if (side _group isNotEqualTo west) then { continue; }; + + private _members = allPlayers select { group _x isEqualTo _group }; + if (_members isEqualTo []) then { continue; }; + + private _leader = leader _group; + if (isNull _leader || { !isPlayer _leader }) then { + _leader = _members # 0; + }; + + private _groupID = _self call ["resolveGroupId", [_group]]; + if (_groupID isEqualTo "") then { continue; }; + + private _leaderUid = getPlayerUID _leader; + private _actor = EGVAR(actor,Registry) getOrDefault [_leaderUid, createHashMap]; + if (_actor isEqualTo createHashMap && { _leaderUid isNotEqualTo "" }) then { + _actor = EGVAR(actor,ActorStore) call ["init", [_leaderUid]]; + }; + + private _orgID = _actor getOrDefault ["organization", "default"]; + if (_orgID isEqualTo "") then { _orgID = "default"; }; + private _memberUids = []; + private _memberRoster = []; + + { + private _memberUid = getPlayerUID _x; + private _memberState = toLowerANSI (lifeState _x); + + if (_memberUid isNotEqualTo "") then { + _memberUids pushBack _memberUid; + }; + + _memberRoster pushBack (createHashMapFromArray [ + ["uid", _memberUid], + ["name", name _x], + ["lifeState", _memberState], + ["isLeader", _x isEqualTo _leader] + ]); + } forEach _members; + + _liveGroups pushBack (createHashMapFromArray [ + ["groupId", _groupID], + ["callsign", [groupId _group, _groupID] select ((groupId _group) isEqualTo "")], + ["leaderUid", _leaderUid], + ["leaderName", name _leader], + ["memberUids", _memberUids], + ["members", _memberRoster], + ["orgId", _orgID], + ["role", "infantry"], + ["status", "available"], + ["position", getPosATL _leader], + ["currentTaskId", _self call ["getCurrentTaskIdForGroup", [_groupID]]], + ["lastUpdate", serverTime] + ]); + } forEach allGroups; + + private _mergedGroups = _liveGroups; + private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; + if (_persistenceService isNotEqualTo createHashMap) then { + private _buildResult = _persistenceService call ["buildGroups", [_liveGroups]]; + if (_buildResult getOrDefault ["success", false]) then { + _mergedGroups = +(_buildResult getOrDefault ["data", _liveGroups]); + }; + }; + + private _nextRegistry = createHashMap; + private _profileRegistry = createHashMap; + { + if !(_x isEqualType createHashMap) then { continue; }; + private _groupID = _x getOrDefault ["groupId", ""]; + if (_groupID isEqualTo "") then { continue; }; + + private _groupRecord = +_x; + _nextRegistry set [_groupID, _groupRecord]; + _profileRegistry set [_groupID, createHashMapFromArray [ + ["groupId", _groupID], + ["role", _groupRecord getOrDefault ["role", "infantry"]], + ["status", _groupRecord getOrDefault ["status", "available"]] + ]]; + } forEach _mergedGroups; + + _self set ["groupProfileRegistry", _profileRegistry]; + _self set ["groupRegistry", _nextRegistry]; + _nextRegistry + }], + ["getGroupRecord", compileFinal { + params [["_groupID", "", [""]]]; + + if (_groupID isEqualTo "") exitWith { createHashMap }; + + private _groupRegistry = _self call ["syncGroups", []]; + +(_groupRegistry getOrDefault [_groupID, createHashMap]) + }], + ["getPlayerGroupId", compileFinal { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { "" }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (_player isEqualTo objNull) exitWith { "" }; + + _self call ["resolveGroupId", [group _player]] + }], + ["isGroupLeader", compileFinal { + params [["_uid", "", [""]], ["_groupID", "", [""]]]; + + if (_uid isEqualTo "" || { _groupID isEqualTo "" }) exitWith { false }; + + private _groupRecord = _self call ["getGroupRecord", [_groupID]]; + (_groupRecord getOrDefault ["leaderUid", ""]) isEqualTo _uid + }], + ["buildGroups", compileFinal { + private _groupRegistry = _self call ["syncGroups", []]; + private _groups = []; + + { + _groups pushBack +_y; + } forEach _groupRegistry; + + _groups + }], + ["applyGroupProfileUpdate", compileFinal { + params [ + ["_requesterUid", "", [""]], + ["_groupID", "", [""]], + ["_status", "", [""]], + ["_role", "", [""]], + ["_mode", "profile", [""]] + ]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", "Unable to update group profile."], + ["changed", false], + ["group", createHashMap] + ]; + + private _finalStatus = toLowerANSI _status; + private _finalRole = toLowerANSI _role; + private _hasStatus = _finalStatus isNotEqualTo ""; + private _hasRole = _finalRole isNotEqualTo ""; + + if (_mode isEqualTo "status" && !_hasStatus) exitWith { + _result set ["message", "Invalid group status."]; + _result + }; + + if (_mode isEqualTo "role" && !_hasRole) exitWith { + _result set ["message", "Invalid group role."]; + _result + }; + + if (_mode isEqualTo "profile" && !(_hasStatus || _hasRole)) exitWith { + _result set ["message", "No group changes were provided."]; + _result + }; + + if (_hasStatus && !(_finalStatus in (_self getOrDefault ["validStatuses", []]))) exitWith { + _result set ["message", "Invalid group status."]; + _result + }; + + if (_hasRole && !(_finalRole in (_self getOrDefault ["validRoles", []]))) exitWith { + _result set ["message", "Invalid group role."]; + _result + }; + + private _permissionService = _self getOrDefault ["permissionService", createHashMap]; + private _isAuthorized = (_self call ["isGroupLeader", [_requesterUid, _groupID]]) || { _permissionService call ["canDispatch", [_requesterUid]] }; + if !_isAuthorized exitWith { + _result set ["message", "You are not authorized to update that group."]; + _result + }; + + private _groupRegistry = _self call ["syncGroups", []]; + private _groupRecord = +(_groupRegistry getOrDefault [_groupID, createHashMap]); + if (_groupRecord isEqualTo createHashMap) exitWith { + _result set ["message", "Group could not be resolved."]; + _result + }; + + private _didChangeStatus = _hasStatus && { (_groupRecord getOrDefault ["status", ""]) isNotEqualTo _finalStatus }; + private _didChangeRole = _hasRole && { (_groupRecord getOrDefault ["role", ""]) isNotEqualTo _finalRole }; + private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; + if (_persistenceService isEqualTo createHashMap) exitWith { + _result set ["message", "CAD extension state is unavailable."]; + _result + }; + + private _updateContext = createHashMapFromArray [ + ["groupId", _groupID], + ["groupCallsign", _groupRecord getOrDefault ["callsign", _groupID]], + ["requesterUid", _requesterUid], + ["currentRole", _groupRecord getOrDefault ["role", "infantry"]], + ["currentStatus", _groupRecord getOrDefault ["status", "available"]], + ["role", [_finalRole, ""] select !_hasRole], + ["status", [_finalStatus, ""] select !_hasStatus], + ["mode", _mode] + ]; + + private _profileResult = _persistenceService call ["updateGroupProfileFromContext", [_updateContext]]; + if !(_profileResult getOrDefault ["success", false]) exitWith { + _result set ["message", "CAD extension rejected the group profile update."]; + _result + }; + + private _profileData = +(_profileResult getOrDefault ["data", createHashMap]); + private _profile = +(_profileData getOrDefault ["profile", createHashMap]); + if (_profile isEqualTo createHashMap) exitWith { + _result set ["message", "CAD extension returned an invalid group profile."]; + _result + }; + + _groupRecord set ["role", _profile getOrDefault ["role", _groupRecord getOrDefault ["role", "infantry"]]]; + _groupRecord set ["status", _profile getOrDefault ["status", _groupRecord getOrDefault ["status", "available"]]]; + _groupRecord set ["lastUpdate", serverTime]; + + private _profileRegistry = _self getOrDefault ["groupProfileRegistry", createHashMap]; + _groupRegistry set [_groupID, _groupRecord]; + _self set ["groupRegistry", _groupRegistry]; + _profileRegistry set [_groupID, _profile]; + _self set ["groupProfileRegistry", _profileRegistry]; + + private _activityEntry = +(_profileData getOrDefault ["activity", createHashMap]); + if (_activityEntry isNotEqualTo createHashMap) then { + private _activityRepository = _self getOrDefault ["activityRepository", createHashMap]; + _activityRepository call ["appendEntry", [_activityEntry]]; + }; + + _result set ["success", true]; + _result set ["message", _profileData getOrDefault ["message", "Group profile updated."]]; + _result set ["changed", _profileData getOrDefault ["changed", (_didChangeStatus || _didChangeRole)]]; + _result set ["group", _groupRecord]; + _result + }], + ["updateGroupStatus", compileFinal { + _self call ["applyGroupProfileUpdate", [_this # 0, _this # 1, _this # 2, "", "status"]] + }], + ["updateGroupRole", compileFinal { + _self call ["applyGroupProfileUpdate", [_this # 0, _this # 1, "", _this # 2, "role"]] + }], + ["updateGroupProfile", compileFinal { + _self call ["applyGroupProfileUpdate", [_this # 0, _this # 1, _this # 2, _this # 3, "profile"]] + }] +]; + +createHashMapObject [GVAR(GroupRepositoryBaseClass)] diff --git a/arma/server/addons/cad/functions/fnc_initPermissionService.sqf b/arma/server/addons/cad/functions/fnc_initPermissionService.sqf new file mode 100644 index 0000000..07fea10 --- /dev/null +++ b/arma/server/addons/cad/functions/fnc_initPermissionService.sqf @@ -0,0 +1,48 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initPermissionService.sqf + * Author: IDSolutions + * Date: 2026-03-30 + * Public: No + * + * Description: + * Initializes the CAD permission service for dispatcher authorization checks. + * + * Arguments: + * None + * + * Return Value: + * CAD permission service object [HASHMAP OBJECT] + * + * Example: + * call forge_server_cad_fnc_initPermissionService + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(PermissionServiceBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "CadPermissionServiceBaseClass"], + ["canDispatch", compileFinal { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { false }; + + private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; + if (_actor isEqualTo createHashMap) exitWith { false }; + + private _orgID = _actor getOrDefault ["organization", "default"]; + private _org = EGVAR(org,OrgStore) call ["loadById", [_orgID]]; + if (_org isEqualTo createHashMap) exitWith { false }; + + private _owner = _org getOrDefault ["owner", ""]; + if (_owner isEqualTo _uid) exitWith { true }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (_player isEqualTo objNull) exitWith { false }; + + private _playerVar = toLowerANSI (vehicleVarName _player); + (_orgID isEqualTo "default") && { _playerVar in ["ceo", "dispatch"] } + }] +]; + +createHashMapObject [GVAR(PermissionServiceBaseClass)] diff --git a/arma/server/addons/cad/functions/fnc_initPersistenceService.sqf b/arma/server/addons/cad/functions/fnc_initPersistenceService.sqf new file mode 100644 index 0000000..dd4c053 --- /dev/null +++ b/arma/server/addons/cad/functions/fnc_initPersistenceService.sqf @@ -0,0 +1,209 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initPersistenceService.sqf + * Author: IDSolutions + * Date: 2026-03-31 + * Public: No + * + * Description: + * Initializes the CAD extension-state service that bridges live SQF + * state to the Rust extension for hot CAD storage and recent history. + * + * Arguments: + * None + * + * Return Value: + * CAD persistence service object [HASHMAP OBJECT] + * + * Example: + * call forge_server_cad_fnc_initPersistenceService + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(PersistenceServiceBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "CadPersistenceServiceBaseClass"], + ["makeResult", compileFinal { + params [ + ["_success", false, [false]], + ["_data", nil, [createHashMap, []]] + ]; + + createHashMapFromArray [ + ["success", _success], + ["data", _data] + ] + }], + ["loadObject", compileFinal { + params [["_function", "", [""]], ["_arguments", [], [[]]]]; + + private _result = _self call ["makeResult", [false, createHashMap]]; + if (_function isEqualTo "") exitWith { _result }; + + [_function, _arguments] call EFUNC(extension,extCall) params ["_payload", "_isSuccess"]; + if (!_isSuccess || { !(_payload isEqualType "") } || { (_payload find "Error:") == 0 }) exitWith { + _result + }; + + private _data = fromJSON _payload; + if !(_data isEqualType createHashMap) exitWith { _result }; + + _result set ["success", true]; + _result set ["data", _data]; + _result + }], + ["loadCollection", compileFinal { + params [["_function", "", [""]], ["_arguments", [], [[]]]]; + + private _result = _self call ["makeResult", [false, []]]; + if (_function isEqualTo "") exitWith { _result }; + + [_function, _arguments] call EFUNC(extension,extCall) params ["_payload", "_isSuccess"]; + if (!_isSuccess || { !(_payload isEqualType "") } || { (_payload find "Error:") == 0 }) exitWith { + _result + }; + + private _data = fromJSON _payload; + if !(_data isEqualType []) exitWith { _result }; + + _result set ["success", true]; + _result set ["data", _data]; + _result + }], + ["loadRegistry", compileFinal { + params [["_function", "", [""]], ["_idField", "", [""]]]; + + private _result = _self call ["makeResult", [false, createHashMap]]; + if (_function isEqualTo "" || { _idField isEqualTo "" }) exitWith { _result }; + + private _collectionResult = _self call ["loadCollection", [_function, []]]; + if !(_collectionResult getOrDefault ["success", false]) exitWith { _result }; + + private _registry = createHashMap; + { + if !(_x isEqualType createHashMap) then { continue; }; + private _entryId = _x getOrDefault [_idField, ""]; + if (_entryId isEqualTo "") then { continue; }; + _registry set [_entryId, +_x]; + } forEach (_collectionResult getOrDefault ["data", []]); + + _result set ["success", true]; + _result set ["data", _registry]; + _result + }], + ["saveEntry", compileFinal { + params [ + ["_function", "", [""]], + ["_entryID", "", [""]], + ["_entry", createHashMap, [createHashMap]] + ]; + + if (_function isEqualTo "" || { _entryID isEqualTo "" } || { _entry isEqualTo createHashMap }) exitWith { false }; + + [_function, [_entryID, toJSON _entry]] call EFUNC(extension,extCall) params ["_payload", "_isSuccess"]; + _isSuccess && { !(_payload isEqualType "") || { (_payload find "Error:") != 0 } } + }], + ["deleteEntry", compileFinal { + params [["_function", "", [""]], ["_entryID", "", [""]]]; + + if (_function isEqualTo "" || { _entryID isEqualTo "" }) exitWith { false }; + + [_function, [_entryID]] call EFUNC(extension,extCall) params ["_payload", "_isSuccess"]; + _isSuccess && { !(_payload isEqualType "") || { (_payload find "Error:") != 0 } } + }], + ["appendActivity", compileFinal { + params [["_entry", createHashMap, [createHashMap]]]; + + if (_entry isEqualTo createHashMap) exitWith { false }; + + ["cad:activity:append", [toJSON _entry]] call EFUNC(extension,extCall) params ["_payload", "_isSuccess"]; + _isSuccess && { !(_payload isEqualType "") || { (_payload find "Error:") != 0 } } + }], + ["loadActivity", compileFinal { + _self call ["loadCollection", ["cad:activity:recent", [str 50]]] + }], + ["buildHydratePayload", compileFinal { + _self call ["loadObject", ["cad:view:hydrate", [toJSON (_this # 0)]]] + }], + ["loadAssignments", compileFinal { + _self call ["loadRegistry", ["cad:assignments:list", "taskId"]] + }], + ["assignAssignment", compileFinal { + _self call ["loadObject", ["cad:assignments:assign", [_this # 0, toJSON (_this # 1)]]] + }], + ["acknowledgeAssignment", compileFinal { + _self call ["loadObject", ["cad:assignments:acknowledge", [_this # 0, toJSON (_this # 1)]]] + }], + ["declineAssignment", compileFinal { + _self call ["loadObject", ["cad:assignments:decline", [_this # 0, toJSON (_this # 1)]]] + }], + ["saveAssignment", compileFinal { + _self call ["saveEntry", ["cad:assignments:upsert", _this # 0, _this # 1]] + }], + ["deleteAssignment", compileFinal { + _self call ["deleteEntry", ["cad:assignments:delete", _this # 0]] + }], + ["loadDispatchOrders", compileFinal { + _self call ["loadRegistry", ["cad:orders:list", "taskID"]] + }], + ["createDispatchOrder", compileFinal { + params [ + ["_orderSeed", createHashMap, [createHashMap]], + ["_assignmentSeed", createHashMap, [createHashMap]] + ]; + + _self call ["loadObject", ["cad:orders:create", [toJSON (createHashMapFromArray [ + ["order", _orderSeed], + ["assignment", _assignmentSeed] + ])]]] + }], + ["createDispatchOrderFromContext", compileFinal { + _self call ["loadObject", ["cad:orders:create_from_context", [toJSON (_this # 0)]]] + }], + ["closeDispatchOrder", compileFinal { + _self call ["loadObject", ["cad:orders:close", [_this # 0]]] + }], + ["saveDispatchOrder", compileFinal { + _self call ["saveEntry", ["cad:orders:upsert", _this # 0, _this # 1]] + }], + ["deleteDispatchOrder", compileFinal { + _self call ["deleteEntry", ["cad:orders:delete", _this # 0]] + }], + ["loadRequests", compileFinal { + _self call ["loadRegistry", ["cad:requests:list", "requestId"]] + }], + ["submitSupportRequest", compileFinal { + _self call ["loadObject", ["cad:requests:submit", [toJSON (_this # 0)]]] + }], + ["submitSupportRequestFromContext", compileFinal { + _self call ["loadObject", ["cad:requests:submit_from_context", [toJSON (_this # 0)]]] + }], + ["closeSupportRequest", compileFinal { + _self call ["loadObject", ["cad:requests:close", [_this # 0]]] + }], + ["saveRequest", compileFinal { + _self call ["saveEntry", ["cad:requests:upsert", _this # 0, _this # 1]] + }], + ["deleteRequest", compileFinal { + _self call ["deleteEntry", ["cad:requests:delete", _this # 0]] + }], + ["loadGroupProfiles", compileFinal { + _self call ["loadRegistry", ["cad:profiles:list", "groupId"]] + }], + ["buildGroups", compileFinal { + _self call ["loadCollection", ["cad:groups:build", [toJSON (createHashMapFromArray [ + ["liveGroups", _this # 0] + ])]]] + }], + ["updateGroupProfileFromContext", compileFinal { + _self call ["loadObject", ["cad:profiles:update_from_context", [toJSON (_this # 0)]]] + }], + ["saveGroupProfile", compileFinal { + _self call ["saveEntry", ["cad:profiles:upsert", _this # 0, _this # 1]] + }], + ["deleteGroupProfile", compileFinal { + _self call ["deleteEntry", ["cad:profiles:delete", _this # 0]] + }] +]; + +createHashMapObject [GVAR(PersistenceServiceBaseClass)] diff --git a/arma/server/addons/cad/functions/fnc_initRequestRepository.sqf b/arma/server/addons/cad/functions/fnc_initRequestRepository.sqf new file mode 100644 index 0000000..7a6f89b --- /dev/null +++ b/arma/server/addons/cad/functions/fnc_initRequestRepository.sqf @@ -0,0 +1,208 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initRequestRepository.sqf + * Author: IDSolutions + * Date: 2026-03-31 + * Public: No + * + * Description: + * Initializes the CAD request repository for structured support + * requests submitted by groups and triaged by dispatch. + * + * Arguments: + * None + * + * Return Value: + * CAD request repository object [HASHMAP OBJECT] + * + * Example: + * call forge_server_cad_fnc_initRequestRepository + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(RequestRepositoryBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "CadRequestRepositoryBaseClass"], + ["#create", compileFinal { + _self set ["requestRegistry", createHashMap]; + _self set ["persistenceLoaded", false]; + _self set ["validTypes", [ + "medevac_9line", + "ace_lace", + "fire_support", + "air_support", + "logreq" + ]]; + _self set ["validPriorities", [ + "routine", + "priority", + "emergency" + ]]; + }], + ["restorePersistedState", compileFinal { + if (_self getOrDefault ["persistenceLoaded", false]) exitWith { true }; + + private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; + if (_persistenceService isEqualTo createHashMap) exitWith { false }; + + private _result = _persistenceService call ["loadRequests", []]; + if !(_result getOrDefault ["success", false]) exitWith { false }; + + private _requestRegistry = +(_result getOrDefault ["data", createHashMap]); + + _self set ["requestRegistry", _requestRegistry]; + _self set ["persistenceLoaded", true]; + true + }], + ["submitRequest", compileFinal { + params [ + ["_requesterUid", "", [""]], + ["_type", "", [""]], + ["_fields", createHashMap, [createHashMap]], + ["_priority", "priority", [""]] + ]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", "Unable to submit support request."], + ["request", createHashMap] + ]; + + _self call ["restorePersistedState", []]; + + private _finalType = toLowerANSI _type; + if !(_finalType in (_self getOrDefault ["validTypes", []])) exitWith { + _result set ["message", "Invalid support request type."]; + _result + }; + + private _groupRepository = _self getOrDefault ["groupRepository", createHashMap]; + private _groupID = _groupRepository call ["getPlayerGroupId", [_requesterUid]]; + if (_groupID isEqualTo "") exitWith { + _result set ["message", "You are not currently assigned to a group."]; + _result + }; + + if !(_groupRepository call ["isGroupLeader", [_requesterUid, _groupID]]) exitWith { + _result set ["message", "Only the current group leader can submit support requests."]; + _result + }; + + private _groupRecord = _groupRepository call ["getGroupRecord", [_groupID]]; + if (_groupRecord isEqualTo createHashMap) exitWith { + _result set ["message", "Your group could not be resolved."]; + _result + }; + + private _validPriorities = _self getOrDefault ["validPriorities", []]; + private _finalPriority = toLowerANSI _priority; + if !(_finalPriority in _validPriorities) then { + _finalPriority = "priority"; + }; + + private _requestContext = createHashMapFromArray [ + ["type", _finalType], + ["fields", +_fields], + ["groupId", _groupID], + ["groupCallsign", _groupRecord getOrDefault ["callsign", _groupID]], + ["submittedByUid", _requesterUid], + ["submittedByName", _groupRecord getOrDefault ["leaderName", _requesterUid]], + ["priority", _finalPriority], + ["position", +(_groupRecord getOrDefault ["position", []])], + ["createdAt", serverTime] + ]; + + private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; + if (_persistenceService isEqualTo createHashMap) exitWith { + _result set ["message", "CAD extension state is unavailable."]; + _result + }; + + private _submitResult = _persistenceService call ["submitSupportRequestFromContext", [_requestContext]]; + if !(_submitResult getOrDefault ["success", false]) exitWith { + _result set ["message", "CAD extension rejected the support request."]; + _result + }; + + private _submitData = +(_submitResult getOrDefault ["data", createHashMap]); + private _request = +(_submitData getOrDefault ["request", createHashMap]); + private _requestID = _request getOrDefault ["requestId", ""]; + if (_requestID isEqualTo "") exitWith { + _result set ["message", "CAD extension returned an invalid support request."]; + _result + }; + + private _requestRegistry = _self getOrDefault ["requestRegistry", createHashMap]; + _requestRegistry set [_requestID, _request]; + _self set ["requestRegistry", _requestRegistry]; + + private _activityEntry = +(_submitData getOrDefault ["activity", createHashMap]); + if (_activityEntry isNotEqualTo createHashMap) then { + private _activityRepository = _self getOrDefault ["activityRepository", createHashMap]; + _activityRepository call ["appendEntry", [_activityEntry]]; + }; + + _result set ["success", true]; + _result set ["message", _submitData getOrDefault ["message", "Support request submitted."]]; + _result set ["request", _request]; + _result + }], + ["closeRequest", compileFinal { + params [["_requesterUid", "", [""]], ["_requestID", "", [""]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", "Unable to close support request."], + ["request", createHashMap] + ]; + + _self call ["restorePersistedState", []]; + + private _requestRegistry = _self getOrDefault ["requestRegistry", createHashMap]; + private _request = +(_requestRegistry getOrDefault [_requestID, createHashMap]); + if (_request isEqualTo createHashMap) exitWith { + _result set ["message", "Support request could not be resolved."]; + _result + }; + + private _permissionService = _self getOrDefault ["permissionService", createHashMap]; + private _groupRepository = _self getOrDefault ["groupRepository", createHashMap]; + private _groupID = _request getOrDefault ["groupId", ""]; + private _isAuthorized = (_permissionService call ["canDispatch", [_requesterUid]]) || { _groupRepository call ["isGroupLeader", [_requesterUid, _groupID]] }; + if !_isAuthorized exitWith { + _result set ["message", "You are not authorized to close that support request."]; + _result + }; + + private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; + if (_persistenceService isEqualTo createHashMap) exitWith { + _result set ["message", "CAD extension state is unavailable."]; + _result + }; + + private _closeResult = _persistenceService call ["closeSupportRequest", [_requestID]]; + if !(_closeResult getOrDefault ["success", false]) exitWith { + _result set ["message", "CAD extension rejected the support request close."]; + _result + }; + + private _closeData = +(_closeResult getOrDefault ["data", createHashMap]); + _request = +(_closeData getOrDefault ["request", _request]); + _requestRegistry deleteAt _requestID; + _self set ["requestRegistry", _requestRegistry]; + + private _activityEntry = +(_closeData getOrDefault ["activity", createHashMap]); + if (_activityEntry isNotEqualTo createHashMap) then { + _activityEntry set ["actorUid", _requesterUid]; + private _activityRepository = _self getOrDefault ["activityRepository", createHashMap]; + _activityRepository call ["appendEntry", [_activityEntry]]; + }; + + _result set ["success", true]; + _result set ["message", _closeData getOrDefault ["message", "Support request closed."]]; + _result set ["request", _request]; + _result + }] +]; + +createHashMapObject [GVAR(RequestRepositoryBaseClass)] diff --git a/arma/server/addons/cad/script_component.hpp b/arma/server/addons/cad/script_component.hpp new file mode 100644 index 0000000..e5f508d --- /dev/null +++ b/arma/server/addons/cad/script_component.hpp @@ -0,0 +1,9 @@ +#define COMPONENT cad +#define COMPONENT_BEAUTIFIED CAD +#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/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..d6edd84 100644 --- a/arma/server/addons/economy/functions/fnc_initMEconomyStore.sqf +++ b/arma/server/addons/economy/functions/fnc_initMEconomyStore.sqf @@ -69,9 +69,12 @@ GVAR(MEconomyStore) = createHashMapObject [[ if (isNull _unit) exitWith { ["WARNING", format ["Invalid unit provided: %1", (name _unit)], nil, nil] call EFUNC(common,log); }; private _uid = getPlayerUID _unit; - private _account = EGVAR(bank,Registry) get _uid; + private _account = EGVAR(bank,BankStore) call ["get", [_uid, ""]]; + if (_account isEqualTo createHashMap) then { + _account = EGVAR(bank,BankStore) call ["init", [_uid]]; + }; - if (isNil "_account") exitWith { ["ERROR", format ["No account found for %1. UID: %2", (name _unit), _uid], nil, nil] call EFUNC(common,log); }; + if (_account isEqualTo createHashMap) exitWith { ["ERROR", format ["No account found for %1. UID: %2", (name _unit), _uid], nil, nil] call EFUNC(common,log); }; private _bank = _account get "bank"; private _cash = _account get "cash"; @@ -80,7 +83,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/extension/XEH_PREP.hpp b/arma/server/addons/extension/XEH_PREP.hpp index c9a683a..7fa64fc 100644 --- a/arma/server/addons/extension/XEH_PREP.hpp +++ b/arma/server/addons/extension/XEH_PREP.hpp @@ -1,2 +1,3 @@ PREP(extCall); PREP(setHandler); +PREP(transport); diff --git a/arma/server/addons/extension/functions/fnc_extCall.sqf b/arma/server/addons/extension/functions/fnc_extCall.sqf index b2bab27..e91ae1a 100644 --- a/arma/server/addons/extension/functions/fnc_extCall.sqf +++ b/arma/server/addons/extension/functions/fnc_extCall.sqf @@ -4,7 +4,7 @@ * File: fnc_extCall.sqf * Author: IDSolutions * Date: 2026-01-03 - * Last Update: 2026-01-03 + * Last Update: 2026-04-01 * Public: No * * Description: @@ -27,14 +27,91 @@ params [["_function", "", [""]], ["_arguments", [], [[]]]]; ["INFO", format ["Calling function: %1", _function], nil, nil] call EFUNC(common,log); private _functionLower = toLower _function; +private _chunkPrefix = "FORGE_TRANSPORT_CHUNK:"; +private _chunkPrefixLength = count toArray _chunkPrefix; +private _unsupportedRoutePrefix = "Error: Unsupported transport route"; +private _requestChunkSize = 12000; +private _transportResponseFunctions = [ + "actor:get", + "actor:create", + "actor:update", + "actor:hot:init", + "actor:hot:get", + "actor:hot:save", + "bank:get", + "bank:create", + "bank:update", + "bank:hot:init", + "bank:hot:get", + "bank:hot:save", + "cad:view:hydrate", + "cad:groups:build", + "cad:assignments:list", + "cad:orders:list", + "cad:requests:list", + "cad:activity:recent", + "org:members:get", + "org:assets:get", + "org:fleet:get" +]; private _requiresRedis = !(_functionLower in ["status", "version"]) && (_functionLower find "icom:" == 0) && (_functionLower find "terrain:" == 0); -if (_requiresRedis) then { - ("forge_server" callExtension ["status", []]) params ["_redisStatus", "_statusExtCode", "_statusArmaCode"]; +private _callExtensionCommand = { + params [["_command", "", [""]], ["_commandArguments", [], [[]]]]; + + ("forge_server" callExtension [_command, _commandArguments]) params [ + "_response", + "_responseExtCode", + "_responseArmaCode" + ]; + + private _responseSuccess = true; + + if (_responseArmaCode != 0 && _responseArmaCode != 301) then { + _responseSuccess = false; + + private _armaCodeMessage = createHashMapFromArray [ + [101, "SYNTAX_ERROR_WRONG_PARAMS_SIZE"], + [102, "SYNTAX_ERROR_WRONG_PARAMS_TYPE"], + [201, "PARAMS_ERROR_TOO_MANY_ARGS"], + [400, "EXTENSION_LOAD_FAILED"], + [403, "EXTENSION_BLOCKED_BY_BATTLEYE"], + [404, "EXTENSION_NOT_FOUND"] + ] getOrDefault [_responseArmaCode, format ["UNKNOWN_%1", _responseArmaCode]]; + + ["WARNING", format ["Arma error: %1", _armaCodeMessage], nil, nil] call EFUNC(common,log); + }; + + if (_responseExtCode != 0) then { + _responseSuccess = false; + + if (_responseExtCode == -1) exitWith { + ["WARNING", "Extension not available", nil, nil] call EFUNC(common,log); + [_response, false] + }; + + if (_responseExtCode == 9) exitWith { + ["WARNING", format ["Extension error: %1", _response], nil, nil] call EFUNC(common,log); + [_response, false] + }; + + ["WARNING", format ["Extension error: %1", _responseExtCode], nil, nil] call EFUNC(common,log); + }; + + [_response, _responseSuccess] +}; + +private _checkRedisAvailability = { + ("forge_server" callExtension ["status", []]) params [ + "_redisStatus", + "_statusExtCode", + "_statusArmaCode" + ]; private _statusSuccess = (_statusExtCode == 0) && (_statusArmaCode == 0 || _statusArmaCode == 301); + if (!_statusSuccess) exitWith { ["WARNING", "Unable to determine Redis status before extension call", nil, nil] call EFUNC(common,log); ["Error: Redis status check failed", false] @@ -44,32 +121,81 @@ if (_requiresRedis) then { ["WARNING", format ["Blocked extension call '%1' because Redis status is '%2'", _function, _redisStatus], nil, nil] call EFUNC(common,log); [format ["Error: Redis is %1", _redisStatus], false] }; + + ["", true] }; -("forge_server" callExtension [_function, _arguments]) params ["_result", "_extCode", "_armaCode"]; +private _buildTransportArgumentsJson = { + params [["_rawArguments", [], [[]]]]; -private _success = true; + private _stringArguments = _rawArguments apply { + if (_x isEqualType "") exitWith { _x }; + if (_x isEqualType true) exitWith { ["false", "true"] select _x }; + str _x + }; -if (_armaCode != 0 && _armaCode != 301) then { - _success = false; - private _armaCodeMessage = createHashMapFromArray [ - [101, "SYNTAX_ERROR_WRONG_PARAMS_SIZE"], - [102, "SYNTAX_ERROR_WRONG_PARAMS_TYPE"], - [201, "PARAMS_ERROR_TOO_MANY_ARGS"], - // [301, "EXECUTION_WARNING_TAKES_TOO_LONG"], - [400, "EXTENSION_LOAD_FAILED"], - [403, "EXTENSION_BLOCKED_BY_BATTLEYE"], - [404, "EXTENSION_NOT_FOUND"] - ] getOrDefault [_armaCode, format ["UNKNOWN_%1", _armaCode]]; - ["WARNING", format ["Arma error: %1", _armaCodeMessage], nil, nil] call EFUNC(common,log); + if !(_stringArguments isEqualType []) then { + _stringArguments = [_stringArguments]; + }; + + private _encodedArguments = []; + { + _encodedArguments pushBack (toJSON _x); + } forEach _stringArguments; + + format ["[%1]", _encodedArguments joinString ","] }; -if (_extCode != 0) then { - _success = false; - if (_extCode == -1) exitWith { ["WARNING", "Extension not available", nil, nil] call EFUNC(common,log); }; - if (_extCode == 9) exitWith { ["WARNING", format ["Extension error: %1", _result], nil, nil] call EFUNC(common,log); }; +if (_requiresRedis) exitWith { + [_function, _arguments] call _checkRedisAvailability params ["_redisResult", "_redisSuccess"]; + if (!_redisSuccess) exitWith { [_redisResult, false] }; - ["WARNING", format ["Extension error: %1", _extCode], nil, nil] call EFUNC(common,log); + if (_functionLower in ["status", "version"]) exitWith { + [_function, _arguments] call _callExtensionCommand + }; + + [_function, _arguments] call _callExtensionCommand }; -[_result, _success] +if (_functionLower in ["status", "version"]) exitWith { + [_function, _arguments] call _callExtensionCommand +}; + +private _argumentsJson = [_arguments] call _buildTransportArgumentsJson; +private _usesTransportResponse = _functionLower in _transportResponseFunctions; +private _usesChunkedRequest = (count toArray _argumentsJson) > _requestChunkSize; + +if !(_usesTransportResponse || { _usesChunkedRequest }) exitWith { + [_function, _arguments] call _callExtensionCommand +}; + +private _transportCommand = "transport:invoke"; +private _transportArguments = [_function, _argumentsJson]; + +if (_usesChunkedRequest) then { + ["stage", _function, _argumentsJson, _requestChunkSize, _callExtensionCommand] call FUNC(transport) params [ + "_stagedTransportCommand", + "_stagedTransportArguments", + "_stageSuccess" + ]; + + if (!_stageSuccess) exitWith { + ["Error: Failed to stage chunked extension request", false] + }; + + _transportCommand = _stagedTransportCommand; + _transportArguments = _stagedTransportArguments; +}; + +[_transportCommand, _transportArguments] call _callExtensionCommand params ["_result", "_success"]; + +if ( + _success + && { _result isEqualType "" } + && { (_result find _unsupportedRoutePrefix) == 0 } + && { !_usesChunkedRequest } +) exitWith { + [_function, _arguments] call _callExtensionCommand +}; + +["assemble", _result, _success, _chunkPrefix, _chunkPrefixLength, _callExtensionCommand] call FUNC(transport) diff --git a/arma/server/addons/extension/functions/fnc_transport.sqf b/arma/server/addons/extension/functions/fnc_transport.sqf new file mode 100644 index 0000000..a86494c --- /dev/null +++ b/arma/server/addons/extension/functions/fnc_transport.sqf @@ -0,0 +1,115 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_transport.sqf + * Author: IDSolutions + * Date: 2026-04-01 + * Public: No + * + * Description: + * Shared transport helper for staging oversized requests and assembling + * chunked responses. + * + * Parameter(s): + * 0: Mode + * "stage": 1=function, 2=argumentsJson, 3=chunkSize, 4=invoker + * "assemble": 1=response, 2=success, 3=chunkPrefix, 4=chunkPrefixLength, 5=invoker + * + * Returns: + * Depends on mode. + */ + +params [["_mode", "", [""]]]; + +switch (_mode) do { + case "stage": { + _this params [ + "_mode", + ["_transportFunction", "", [""]], + ["_argumentsJson", "", [""]], + ["_requestChunkSize", 12000, [0]], + ["_callExtensionCommand", {}, [{}]] + ]; + + private _transferID = format [ + "req_%1_%2", + floor (diag_tickTime * 1000), + floor (random 1000000000) + ]; + + for "_offset" from 0 to ((count toArray _argumentsJson) - 1) step _requestChunkSize do { + private _chunk = _argumentsJson select [_offset, _requestChunkSize]; + + ["transport:request:append", [_transferID, _chunk]] call _callExtensionCommand params [ + "_appendResult", + "_appendSuccess" + ]; + + if (!_appendSuccess || { !(_appendResult isEqualType "") } || { (_appendResult find "Error:") == 0 }) exitWith { + _transferID = ""; + }; + }; + + if (_transferID isEqualTo "") exitWith { + ["", [], false] + }; + + [ + "transport:invoke_stored", + [_transportFunction, _transferID], + true + ] + }; + + case "assemble": { + _this params [ + "_mode", + ["_response", "", [""]], + ["_responseSuccess", false, [true]], + ["_chunkPrefix", "", [""]], + ["_chunkPrefixLength", 0, [0]], + ["_callExtensionCommand", {}, [{}]] + ]; + + if !(_responseSuccess && { _response isEqualType "" } && { (_response find _chunkPrefix) == 0 }) exitWith { + [_response, _responseSuccess] + }; + + private _chunkEnvelope = fromJSON (_response select [_chunkPrefixLength]); + if !(_chunkEnvelope isEqualType createHashMap) exitWith { + ["Error: Invalid extension chunk envelope", false] + }; + + private _transferID = _chunkEnvelope getOrDefault ["transferId", ""]; + private _chunkCount = _chunkEnvelope getOrDefault ["chunkCount", 0]; + + if (_transferID isEqualTo "" || { !(_chunkCount isEqualType 0) } || { _chunkCount < 1 }) exitWith { + ["Error: Invalid extension chunk metadata", false] + }; + + private _assembledResponse = ""; + private _chunkReadSuccess = true; + + for "_index" from 0 to (_chunkCount - 1) do { + ["transport:response:get", [_transferID, str _index]] call _callExtensionCommand params [ + "_chunkResult", + "_chunkSuccess" + ]; + + if (!_chunkSuccess || { !(_chunkResult isEqualType "") } || { (_chunkResult find "Error:") == 0 }) exitWith { + _chunkReadSuccess = false; + _assembledResponse = "Error: Failed to retrieve chunked extension response"; + }; + + _assembledResponse = _assembledResponse + _chunkResult; + }; + + ["transport:response:clear", [_transferID]] call _callExtensionCommand; + + [_assembledResponse, _chunkReadSuccess] + }; + + default { + ["Error: Unsupported extension transport mode", false] + }; +}; diff --git a/arma/server/addons/garage/XEH_preInit.sqf b/arma/server/addons/garage/XEH_preInit.sqf index 109ae47..8222cf3 100644 --- a/arma/server/addons/garage/XEH_preInit.sqf +++ b/arma/server/addons/garage/XEH_preInit.sqf @@ -13,58 +13,17 @@ PREP_RECOMPILE_END; GVAR(GarageStore) call ["init", [_uid]]; }] call CFUNC(addEventHandler); -[QGVAR(requestGetGarage), { - params [["_uid", "", [""]], ["_field", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid UID!" }; - - private _finalData = GVAR(GarageStore) call ["get", [GVAR(Registry), "garage:get", _uid, _field]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(garage,responseSyncGarage), [_finalData], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestSetGarage), { - params [["_uid", "", [""]], ["_key", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "" || _key isEqualTo "") exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid UID or Key!" }; - - private _hashMap = GVAR(GarageStore) call ["set", [GVAR(Registry), "garage:update", _uid, _key, _value, _sync]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(garage,responseSyncGarage), [_hashMap], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestMSetGarage), { - params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid UID!" }; - if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid field pairs!" }; - - private _hashMap = GVAR(GarageStore) call ["mset", [GVAR(Registry), "garage:update", _uid, _fieldValuePairs, _sync]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(garage,responseSyncGarage), [_hashMap], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - [QGVAR(requestSaveGarage), { params [["_uid", "", [""]]]; if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid UID!" }; - private _finalData = GVAR(GarageStore) call ["save", [GVAR(Registry), "garage:update", _uid]]; + private _finalData = GVAR(GarageStore) call ["save", [_uid]]; private _player = [_uid] call EFUNC(common,getPlayer); [CRPC(garage,responseSyncGarage), [_finalData], _player] call CFUNC(targetEvent); }] call CFUNC(addEventHandler); -[QGVAR(requestRemoveGarage), { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Garage] Empty/Invalid UID!" }; - GVAR(GarageStore) call ["remove", [GVAR(Registry), _uid]]; -}] call CFUNC(addEventHandler); - [QGVAR(requestStoreVehicle), { params [ ["_uid", "", [""]], @@ -90,18 +49,15 @@ PREP_RECOMPILE_END; ["hit_points", fromJSON _hitPointsJson] ]); - ["garage:add", [_uid, _payloadJson]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - if !(_isSuccess) exitWith { + private _garage = GVAR(GarageStore) call ["storeVehicle", [_uid, _payloadJson]]; + if (_garage isEqualTo createHashMap) exitWith { [CRPC(garage,responseGarageAction), [createHashMapFromArray [ ["action", "store"], ["success", false], - ["message", format ["Failed to store vehicle: %1", _result]] + ["message", "Failed to store vehicle."] ]], _player] call CFUNC(targetEvent); }; - private _garage = fromJSON _result; - GVAR(Registry) set [_uid, _garage]; - [CRPC(garage,responseSyncGarage), [_garage], _player] call CFUNC(targetEvent); [CRPC(garage,responseGarageAction), [createHashMapFromArray [ ["action", "store"], @@ -123,18 +79,15 @@ PREP_RECOMPILE_END; }; private _payloadJson = toJSON (createHashMapFromArray [["plate", _plate]]); - ["garage:remove", [_uid, _payloadJson]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - if !(_isSuccess) exitWith { + private _garage = GVAR(GarageStore) call ["retrieveVehicle", [_uid, _payloadJson]]; + if (_garage isEqualTo createHashMap) exitWith { [CRPC(garage,responseGarageAction), [createHashMapFromArray [ ["action", "retrieve"], ["success", false], - ["message", format ["Failed to retrieve vehicle: %1", _result]] + ["message", "Failed to retrieve vehicle."] ]], _player] call CFUNC(targetEvent); }; - private _garage = fromJSON _result; - GVAR(Registry) set [_uid, _garage]; - [CRPC(garage,responseSyncGarage), [_garage], _player] call CFUNC(targetEvent); [CRPC(garage,responseGarageAction), [createHashMapFromArray [ ["action", "retrieve"], @@ -150,54 +103,14 @@ PREP_RECOMPILE_END; GVAR(VGarageStore) call ["init", [_uid]]; }] call CFUNC(addEventHandler); -[QGVAR(requestGetVG), { - params [["_uid", "", [""]], ["_field", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid UID!" }; - - private _finalData = GVAR(VGarageStore) call ["get", [GVAR(VGRegistry), "owned:garage:fetch", _uid, _field]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(garage,responseSyncVG), [_finalData], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestSetVG), { - params [["_uid", "", [""]], ["_key", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "" || _key isEqualTo "") exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid UID or Key!" }; - - private _hashMap = GVAR(VGarageStore) call ["set", [GVAR(VGRegistry), "", _uid, _key, _value, _sync]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(garage,responseSyncVG), [_hashMap], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestMSetVG), { - params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid UID!" }; - if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid field pairs!" }; - - private _hashMap = GVAR(VGarageStore) call ["mset", [GVAR(VGRegistry), "", _uid, _fieldValuePairs, _sync]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(garage,responseSyncVG), [_hashMap], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - [QGVAR(requestSaveVG), { params [["_uid", "", [""]]]; if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid UID!" }; - private _finalData = GVAR(VGarageStore) call ["save", [GVAR(VGRegistry), "", _uid]]; + private _finalData = GVAR(VGarageStore) call ["save", [_uid]]; private _player = [_uid] call EFUNC(common,getPlayer); [CRPC(garage,responseSyncVG), [_finalData], _player] call CFUNC(targetEvent); }] call CFUNC(addEventHandler); -[QGVAR(requestRemoveVG), { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VGarage] Empty/Invalid UID!" }; - GVAR(VGarageStore) call ["remove", [GVAR(VGRegistry), _uid]]; -}] call CFUNC(addEventHandler); diff --git a/arma/server/addons/garage/functions/fnc_initGarageStore.sqf b/arma/server/addons/garage/functions/fnc_initGarageStore.sqf index b714040..6e35c3c 100644 --- a/arma/server/addons/garage/functions/fnc_initGarageStore.sqf +++ b/arma/server/addons/garage/functions/fnc_initGarageStore.sqf @@ -4,12 +4,12 @@ * File: fnc_initGarageStore.sqf * Author: IDSolutions * Date: 2025-12-17 - * Last Update: 2026-02-13 + * Last Update: 2026-04-01 * Public: No * * Description: * Initializes the Garage store for managing player vehicles. - * Provides methods for syncing, saving, and applying vehicles to the player's garage. + * Garage hot state is owned by the extension; SQF acts as a thin bridge. * * Arguments: * None @@ -26,50 +26,70 @@ GVAR(GarageBaseStore) = compileFinal createHashMapFromArray [ ["#base", EGVAR(common,BaseStore)], ["#type", "GarageBaseStore"], ["#create", compileFinal { - GVAR(Registry) = createHashMap; ["INFO", "Garage Store Initialized!"] call EFUNC(common,log); }], + ["callHotGarage", compileFinal { + params [["_function", "", [""]], ["_arguments", [], [[]]]]; + + if (_function isEqualTo "") exitWith { createHashMap }; + + [_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + if !(_isSuccess) exitWith { createHashMap }; + if !(_result isEqualType "") exitWith { createHashMap }; + if ((_result find "Error:") == 0) exitWith { + ["ERROR", format ["Garage extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log); + createHashMap + }; + + private _data = fromJSON _result; + if !(_data isEqualType createHashMap) exitWith { createHashMap }; + _data + }], + ["loadHotGarage", compileFinal { + params [["_uid", "", [""]], ["_initialize", false, [false]]]; + + if (_uid isEqualTo "") exitWith { createHashMap }; + + private _command = ["garage:hot:get", "garage:hot:init"] select _initialize; + _self call ["callHotGarage", [_command, [_uid]]] + }], ["init", compileFinal { params [["_uid", "", [""]]]; private _player = [_uid] call EFUNC(common,getPlayer); - private _cached = GVAR(Registry) getOrDefault [_uid, nil]; - if !(isNil { _cached }) exitWith { [CRPC(garage,responseInitGarage), [_cached], _player] call CFUNC(targetEvent); _cached }; + if (isNull _player) exitWith { createHashMap }; - ["garage:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - if !(_isSuccess) exitWith { - ["ERROR", format ["Failed to check if garage %1 exists! Using fallback garage.", _uid]] call EFUNC(common,log); - - private _fallbackGarage = createHashMap; - GVAR(Registry) set [_uid, _fallbackGarage]; - [CRPC(garage,responseInitGarage), [_fallbackGarage], _player] call CFUNC(targetEvent); - - _fallbackGarage + private _garage = _self call ["loadHotGarage", [_uid, true]]; + if (_garage isEqualTo createHashMap) then { + ["ERROR", format ["Failed to initialize garage for %1! Using fallback garage.", _uid]] call EFUNC(common,log); }; - private _finalGarage = createHashMap; + [CRPC(garage,responseInitGarage), [_garage], _player] call CFUNC(targetEvent); + _garage + }], + ["save", compileFinal { + params [["_uid", "", [""]]]; - if (_result == "true") then { - _finalGarage = _self call ["fetch", ["garage:get", _uid]]; - ["INFO", format ["Found garage for %1", _uid]] call EFUNC(common,log); - } else { - ["garage:create", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - if !(_isSuccess) exitWith { - ["ERROR", format ["Failed to create garage for %1! Using fallback garage.", _uid]] call EFUNC(common,log); + if (_uid isEqualTo "") exitWith { createHashMap }; + _self call ["callHotGarage", ["garage:hot:save", [_uid]]] + }], + ["storeVehicle", compileFinal { + params [ + ["_uid", "", [""]], + ["_payloadJson", "", [""]] + ]; - GVAR(Registry) set [_uid, _finalGarage]; - [CRPC(garage,responseInitGarage), [_finalGarage], _player] call CFUNC(targetEvent); + if (_uid isEqualTo "" || { _payloadJson isEqualTo "" }) exitWith { createHashMap }; + _self call ["callHotGarage", ["garage:hot:add", [_uid, _payloadJson]]] + }], + ["retrieveVehicle", compileFinal { + params [ + ["_uid", "", [""]], + ["_payloadJson", "", [""]] + ]; - _finalGarage - }; - - ["INFO", format ["Created new garage for %1", _uid]] call EFUNC(common,log); - }; - - GVAR(Registry) set [_uid, _finalGarage]; - [CRPC(garage,responseInitGarage), [_finalGarage], _player] call CFUNC(targetEvent); - - _finalGarage + if (_uid isEqualTo "" || { _payloadJson isEqualTo "" }) exitWith { createHashMap }; + _self call ["callHotGarage", ["garage:hot:remove_vehicle", [_uid, _payloadJson]]] }] ]; diff --git a/arma/server/addons/garage/functions/fnc_initVGStore.sqf b/arma/server/addons/garage/functions/fnc_initVGStore.sqf index 70e7d5b..a17b9c5 100644 --- a/arma/server/addons/garage/functions/fnc_initVGStore.sqf +++ b/arma/server/addons/garage/functions/fnc_initVGStore.sqf @@ -4,12 +4,12 @@ * File: fnc_initVGStore.sqf * Author: IDSolutions * Date: 2025-12-17 - * Last Update: 2026-02-13 + * Last Update: 2026-04-01 * Public: No * * Description: * Initializes the Virtual Garage store for managing player vehicle unlocks. - * Provides methods for syncing, saving, and applying virtual vehicles to BIS Garage. + * Virtual garage hot state is owned by the extension; SQF acts as a thin bridge. * * Arguments: * None @@ -42,108 +42,53 @@ GVAR(VGBaseStore) = compileFinal createHashMapFromArray [ ["#base", EGVAR(common,BaseStore)], ["#type", "VGBaseStore"], ["#create", compileFinal { - GVAR(VGRegistry) = createHashMap; ["INFO", "VGarage Store Initialized!"] call EFUNC(common,log); }], + ["callHotVGarage", compileFinal { + params [["_function", "", [""]], ["_arguments", [], [[]]]]; + + if (_function isEqualTo "") exitWith { createHashMap }; + + [_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + if !(_isSuccess) exitWith { createHashMap }; + if !(_result isEqualType "") exitWith { createHashMap }; + if ((_result find "Error:") == 0) exitWith { + ["ERROR", format ["VGarage extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log); + createHashMap + }; + + private _data = fromJSON _result; + if !(_data isEqualType createHashMap) exitWith { createHashMap }; + _data + }], + ["loadHotVGarage", compileFinal { + params [["_uid", "", [""]], ["_initialize", false, [false]]]; + + if (_uid isEqualTo "") exitWith { createHashMap }; + + private _command = ["owned:garage:hot:fetch", "owned:garage:hot:init"] select _initialize; + _self call ["callHotVGarage", [_command, [_uid]]] + }], ["init", compileFinal { params [["_uid", "", [""]]]; private _player = [_uid] call EFUNC(common,getPlayer); - private _cached = GVAR(VGRegistry) getOrDefault [_uid, nil]; - if !(isNil { _cached }) exitWith { - [CRPC(garage,responseInitVG), [_cached], _player] call CFUNC(targetEvent); - _cached + if (isNull _player) exitWith { createHashMap }; + + private _garage = _self call ["loadHotVGarage", [_uid, true]]; + if (_garage isEqualTo createHashMap) then { + _garage = GVAR(VGarageModel) call ["defaults", []]; + ["ERROR", format ["Failed to initialize virtual garage for %1! Using fallback virtual garage.", _uid]] call EFUNC(common,log); }; - ["owned:garage:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - if !(_isSuccess) exitWith { - ["ERROR", format ["Failed to check if virtual garage %1 exists! Using fallback virtual garage.", _uid]] call EFUNC(common,log); - - private _fallbackVGarage = GVAR(VGarageModel) call ["defaults", []]; - GVAR(VGRegistry) set [_uid, _fallbackVGarage]; - [CRPC(garage,responseInitVG), [_fallbackVGarage], _player] call CFUNC(targetEvent); - - _fallbackVGarage - }; - - private _finalVGarage = createHashMap; - - if (_result == "true") then { - _finalVGarage = _self call ["fetch", ["owned:garage:fetch", _uid]]; - ["INFO", format ["Found virtual garage for %1", _uid]] call EFUNC(common,log); - } else { - _finalVGarage = GVAR(VGarageModel) call ["defaults", []]; - - ["owned:garage:create", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - if !(_isSuccess) exitWith { - ["ERROR", format ["Failed to create virtual garage for %1! Using fallback virtual garage.", _uid]] call EFUNC(common,log); - - GVAR(VGRegistry) set [_uid, _finalVGarage]; - [CRPC(garage,responseInitVG), [_finalVGarage], _player] call CFUNC(targetEvent); - - _finalVGarage - }; - - ["INFO", format ["Created new virtual garage for %1", _uid]] call EFUNC(common,log); - }; - - GVAR(VGRegistry) set [_uid, _finalVGarage]; - [CRPC(garage,responseInitVG), [_finalVGarage], _player] call CFUNC(targetEvent); - - _finalVGarage + [CRPC(garage,responseInitVG), [_garage], _player] call CFUNC(targetEvent); + _garage }], - ["grantVehicles", compileFinal { - params [["_uid", "", [""]], ["_vehicles", [], [[]]], ["_commit", false, [false]]]; + ["save", compileFinal { + params [["_uid", "", [""]]]; - private _result = createHashMapFromArray [ - ["success", false], - ["message", "Virtual garage grant failed."], - ["patch", createHashMap], - ["granted", []], - ["garage", createHashMap] - ]; - - private _defaultGarage = GVAR(VGarageModel) call ["defaults", []]; - private _garage = +(GVAR(VGRegistry) getOrDefault [_uid, _defaultGarage]); - private _patch = createHashMap; - private _granted = []; - private _categoriesToSync = []; - - { - private _className = _x getOrDefault ["classname", ""]; - private _category = toLowerANSI (_x getOrDefault ["category", ""]); - - if (_className isEqualTo "") exitWith { - _result set ["message", "Vehicle checkout entry was missing a classname."]; - }; - - if !(_category in ["cars", "armor", "helis", "planes", "naval", "other"]) exitWith { - _result set ["message", format ["Vehicle category '%1' is unsupported.", _category]]; - }; - - private _categoryUnlocks = +(_garage getOrDefault [_category, []]); - _categoryUnlocks pushBackUnique _className; - _garage set [_category, _categoryUnlocks]; - _categoriesToSync pushBackUnique _category; - _granted pushBack (createHashMapFromArray [ - ["classname", _className], - ["category", _category] - ]); - } forEach _vehicles; - - { - private _category = _x; - _patch set [_category, _garage getOrDefault [_category, []]]; - } forEach _categoriesToSync; - - if (_commit) then { GVAR(VGRegistry) set [_uid, _garage]; }; - - _result set ["success", true]; - _result set ["message", ""]; - _result set ["patch", _patch]; - _result set ["granted", _granted]; - _result set ["garage", _garage]; - _result + if (_uid isEqualTo "") exitWith { createHashMap }; + _self call ["callHotVGarage", ["owned:garage:hot:save", [_uid]]] }] ]; diff --git a/arma/server/addons/locker/XEH_preInit.sqf b/arma/server/addons/locker/XEH_preInit.sqf index a1b5d92..bfd7343 100644 --- a/arma/server/addons/locker/XEH_preInit.sqf +++ b/arma/server/addons/locker/XEH_preInit.sqf @@ -13,46 +13,12 @@ PREP_RECOMPILE_END; GVAR(LockerStore) call ["init", [_uid]]; }] call CFUNC(addEventHandler); -[QGVAR(requestGetLocker), { - params [["_uid", "", [""]], ["_field", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID!" }; - - private _finalData = GVAR(LockerStore) call ["get", [GVAR(Registry), "locker:get", _uid, _field]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(locker,responseSyncLocker), [_finalData], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestSetLocker), { - params [["_uid", "", [""]], ["_field", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "" || _field isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID or Field!" }; - - private _hashMap = GVAR(LockerStore) call ["set", [GVAR(Registry), "locker:update", _uid, _field, _value, _sync]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(locker,responseSyncLocker), [_hashMap], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestMSetLocker), { - params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID!" }; - if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid field pairs!" }; - - private _hashMap = GVAR(LockerStore) call ["mset", [GVAR(Registry), "locker:update", _uid, _fieldValuePairs, _sync]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(locker,responseSyncLocker), [_hashMap], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - [QGVAR(requestSaveLocker), { params [["_uid", "", [""]]]; if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID!" }; - private _finalData = GVAR(LockerStore) call ["save", [GVAR(Registry), "locker:update", _uid]]; + private _finalData = GVAR(LockerStore) call ["save", [_uid]]; private _player = [_uid] call EFUNC(common,getPlayer); [CRPC(locker,responseSyncLocker), [_finalData], _player] call CFUNC(targetEvent); @@ -62,17 +28,10 @@ PREP_RECOMPILE_END; params [["_uid", "", [""]], ["_data", createHashMap, [createHashMap]]]; if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID!" }; - GVAR(Registry) set [_uid, _data]; + private _finalData = GVAR(LockerStore) call ["override", [_uid, _data, false]]; private _player = [_uid] call EFUNC(common,getPlayer); - [CRPC(locker,responseSyncLocker), [_data], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestRemoveLocker), { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Locker] Empty/Invalid UID!" }; - GVAR(LockerStore) call ["remove", [_uid]]; + [CRPC(locker,responseSyncLocker), [_finalData], _player] call CFUNC(targetEvent); }] call CFUNC(addEventHandler); [QGVAR(requestInitVA), { @@ -82,54 +41,14 @@ PREP_RECOMPILE_END; GVAR(VAStore) call ["init", [_uid]]; }] call CFUNC(addEventHandler); -[QGVAR(requestGetVA), { - params [["_uid", "", [""]], ["_field", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid UID!" }; - - private _finalData = GVAR(VAStore) call ["get", [GVAR(VARegistry), "owned:locker:fetch", _uid, _field]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(locker,responseSyncVA), [_finalData], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestSetVA), { - params [["_uid", "", [""]], ["_field", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "" || _field isEqualTo "") exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid UID or Field!" }; - - private _hashMap = GVAR(VAStore) call ["set", [GVAR(VARegistry), "owned:locker:update", _uid, _field, _value, _sync]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(locker,responseSyncVA), [_hashMap], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestMSetVA), { - params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid UID!" }; - if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid field pairs!" }; - - private _hashMap = GVAR(VAStore) call ["mset", [GVAR(VARegistry), "owned:locker:update", _uid, _fieldValuePairs, _sync]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(locker,responseSyncVA), [_hashMap], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - [QGVAR(requestSaveVA), { params [["_uid", "", [""]]]; if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid UID!" }; - private _finalData = GVAR(VAStore) call ["save", [GVAR(VARegistry), "owned:locker:update", _uid]]; + private _finalData = GVAR(VAStore) call ["save", [_uid]]; private _player = [_uid] call EFUNC(common,getPlayer); [CRPC(locker,responseSyncVA), [_finalData], _player] call CFUNC(targetEvent); }] call CFUNC(addEventHandler); -[QGVAR(requestRemoveVA), { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:VArsenal] Empty/Invalid UID!" }; - GVAR(VAStore) call ["remove", [GVAR(VARegistry), _uid]]; -}] call CFUNC(addEventHandler); diff --git a/arma/server/addons/locker/functions/fnc_initLockerStore.sqf b/arma/server/addons/locker/functions/fnc_initLockerStore.sqf index 352c940..a98e8e7 100644 --- a/arma/server/addons/locker/functions/fnc_initLockerStore.sqf +++ b/arma/server/addons/locker/functions/fnc_initLockerStore.sqf @@ -4,12 +4,12 @@ * File: fnc_initLockerStore.sqf * Author: IDSolutions * Date: 2025-12-17 - * Last Update: 2026-02-13 + * Last Update: 2026-04-01 * Public: No * * Description: * Initializes the Locker store for managing player locker items. - * Provides methods for syncing, saving, and applying locker items to the player's locker. + * Locker hot state is owned by the extension; SQF acts as a thin bridge. * * Arguments: * None @@ -26,112 +26,74 @@ GVAR(LockerBaseStore) = compileFinal createHashMapFromArray [ ["#base", EGVAR(common,BaseStore)], ["#type", "LockerBaseStore"], ["#create", compileFinal { - GVAR(Registry) = createHashMap; ["INFO", "Locker Store Initialized!"] call EFUNC(common,log); }], + ["callHotLocker", compileFinal { + params [["_function", "", [""]], ["_arguments", [], [[]]]]; + + if (_function isEqualTo "") exitWith { createHashMap }; + + [_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + if !(_isSuccess) exitWith { createHashMap }; + if !(_result isEqualType "") exitWith { createHashMap }; + if ((_result find "Error:") == 0) exitWith { + ["ERROR", format ["Locker extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log); + createHashMap + }; + + private _data = fromJSON _result; + if !(_data isEqualType createHashMap) exitWith { createHashMap }; + _data + }], + ["loadHotLocker", compileFinal { + params [["_uid", "", [""]], ["_initialize", false, [false]]]; + + if (_uid isEqualTo "") exitWith { createHashMap }; + + private _command = ["locker:hot:get", "locker:hot:init"] select _initialize; + _self call ["callHotLocker", [_command, [_uid]]] + }], ["init", compileFinal { params [["_uid", "", [""]]]; private _player = [_uid] call EFUNC(common,getPlayer); - private _cached = GVAR(Registry) getOrDefault [_uid, nil]; - if !(isNil { _cached }) exitWith { [CRPC(locker,responseInitLocker), [_cached], _player] call CFUNC(targetEvent); _cached }; + if (isNull _player) exitWith { createHashMap }; - ["locker:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - if !(_isSuccess) exitWith { - ["ERROR", format ["Failed to check if locker %1 exists! Using fallback locker.", _uid]] call EFUNC(common,log); - - private _fallbackLocker = createHashMap; - GVAR(Registry) set [_uid, _fallbackLocker]; - [CRPC(locker,responseInitLocker), [_fallbackLocker], _player] call CFUNC(targetEvent); - - _fallbackLocker + private _locker = _self call ["loadHotLocker", [_uid, true]]; + if (_locker isEqualTo createHashMap) then { + ["ERROR", format ["Failed to initialize locker for %1! Using fallback locker.", _uid]] call EFUNC(common,log); }; - private _finalLocker = createHashMap; - - if (_result == "true") then { - _finalLocker = _self call ["fetch", ["locker:get", _uid]]; - ["INFO", format ["Found locker for %1", _uid]] call EFUNC(common,log); - } else { - ["locker:create", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - if !(_isSuccess) exitWith { - ["ERROR", format ["Failed to create locker for %1! Using fallback locker.", _uid]] call EFUNC(common,log); - - GVAR(Registry) set [_uid, _finalLocker]; - [CRPC(locker,responseInitLocker), [_finalLocker], _player] call CFUNC(targetEvent); - - _finalLocker - }; - - ["INFO", format ["Created new locker for %1", _uid]] call EFUNC(common,log); - }; - - GVAR(Registry) set [_uid, _finalLocker]; - [CRPC(locker,responseInitLocker), [_finalLocker], _player] call CFUNC(targetEvent); - - _finalLocker + [CRPC(locker,responseInitLocker), [_locker], _player] call CFUNC(targetEvent); + _locker }], - ["grantItems", compileFinal { - params [["_uid", "", [""]], ["_items", [], [[]]], ["_commit", false, [false]]]; - - private _result = createHashMapFromArray [ - ["success", false], - ["message", "Locker grant failed."], - ["patch", createHashMap], - ["granted", []], - ["locker", createHashMap] + ["override", compileFinal { + params [ + ["_uid", "", [""]], + ["_data", createHashMap, [createHashMap]], + ["_save", false, [false]] ]; - private _locker = +(GVAR(Registry) getOrDefault [_uid, createHashMap]); - private _patch = createHashMap; - private _granted = []; + if (_uid isEqualTo "") exitWith { createHashMap }; + if !(_data isEqualType createHashMap) exitWith { createHashMap }; - { - private _className = _x getOrDefault ["classname", ""]; - private _category = toLowerANSI (_x getOrDefault ["category", ""]); - private _quantity = floor ((_x getOrDefault ["quantity", 0]) max 0); - private _lockerCategory = switch (_category) do { - case "item": { "item" }; - case "weapon": { "weapon" }; - case "magazine": { "magazine" }; - case "backpack": { "backpack" }; - default { "" }; + private _locker = _self call ["callHotLocker", ["locker:hot:override", [_uid, toJSON _data]]]; + if (_save && { _locker isNotEqualTo createHashMap }) then { + private _savedLocker = _self call ["callHotLocker", ["locker:hot:save", [_uid]]]; + if (_savedLocker isNotEqualTo createHashMap) then { + _locker = _savedLocker; + } else { + _locker = createHashMap; }; - - if (_className isEqualTo "" || { _lockerCategory isEqualTo "" } || { _quantity <= 0 }) exitWith { - _result set ["message", "Checkout item was missing a valid classname, category, or quantity."]; - _result set ["success", false]; - }; - - private _entry = +(_locker getOrDefault [_className, createHashMap]); - private _amount = _entry getOrDefault ["amount", 0]; - private _updatedEntry = createHashMapFromArray [ - ["amount", (_amount + _quantity)], - ["classname", _className], - ["category", _lockerCategory] - ]; - - _locker set [_className, _updatedEntry]; - _patch set [_className, _updatedEntry]; - _granted pushBack (createHashMapFromArray [ - ["classname", _className], - ["category", _lockerCategory], - ["quantity", _quantity] - ]); - } forEach _items; - - if ((count (keys _locker)) > 25) exitWith { - _result set ["message", "Locker capacity would exceed 25 unique items. Clear space before checkout."]; - _result }; - if (_commit) then { GVAR(Registry) set [_uid, _locker]; }; - _result set ["success", true]; - _result set ["message", ""]; - _result set ["patch", _patch]; - _result set ["granted", _granted]; - _result set ["locker", _locker]; - _result + _locker + }], + ["save", compileFinal { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { createHashMap }; + _self call ["callHotLocker", ["locker:hot:save", [_uid]]] }] ]; diff --git a/arma/server/addons/locker/functions/fnc_initVAStore.sqf b/arma/server/addons/locker/functions/fnc_initVAStore.sqf index 809fa95..567010b 100644 --- a/arma/server/addons/locker/functions/fnc_initVAStore.sqf +++ b/arma/server/addons/locker/functions/fnc_initVAStore.sqf @@ -4,12 +4,12 @@ * File: fnc_initVAStore.sqf * Author: IDSolutions * Date: 2025-12-17 - * Last Update: 2026-02-13 + * Last Update: 2026-04-01 * Public: No * * Description: * Initializes the Virtual Arsenal store for managing player arsenal unlocks. - * Provides methods for syncing, saving, and applying virtual items to BIS Arsenal. + * Virtual arsenal hot state is owned by the extension; SQF acts as a thin bridge. * * Arguments: * None @@ -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"]]; @@ -40,102 +40,53 @@ GVAR(VABaseStore) = compileFinal createHashMapFromArray [ ["#base", EGVAR(common,BaseStore)], ["#type", "VABaseStore"], ["#create", compileFinal { - GVAR(VARegistry) = createHashMap; ["INFO", "VArsenal Store Initialized!"] call EFUNC(common,log); }], + ["callHotVArsenal", compileFinal { + params [["_function", "", [""]], ["_arguments", [], [[]]]]; + + if (_function isEqualTo "") exitWith { createHashMap }; + + [_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + if !(_isSuccess) exitWith { createHashMap }; + if !(_result isEqualType "") exitWith { createHashMap }; + if ((_result find "Error:") == 0) exitWith { + ["ERROR", format ["VArsenal extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log); + createHashMap + }; + + private _data = fromJSON _result; + if !(_data isEqualType createHashMap) exitWith { createHashMap }; + _data + }], + ["loadHotVArsenal", compileFinal { + params [["_uid", "", [""]], ["_initialize", false, [false]]]; + + if (_uid isEqualTo "") exitWith { createHashMap }; + + private _command = ["owned:locker:hot:fetch", "owned:locker:hot:init"] select _initialize; + _self call ["callHotVArsenal", [_command, [_uid]]] + }], ["init", compileFinal { params [["_uid", "", [""]]]; private _player = [_uid] call EFUNC(common,getPlayer); - private _cached = GVAR(VARegistry) getOrDefault [_uid, nil]; - if !(isNil { _cached }) exitWith { - [CRPC(locker,responseInitVA), [_cached], _player] call CFUNC(targetEvent); - _cached + if (isNull _player) exitWith { createHashMap }; + + private _arsenal = _self call ["loadHotVArsenal", [_uid, true]]; + if (_arsenal isEqualTo createHashMap) then { + _arsenal = GVAR(VArsenalModel) call ["defaults", []]; + ["ERROR", format ["Failed to initialize virtual arsenal for %1! Using fallback virtual arsenal.", _uid]] call EFUNC(common,log); }; - ["owned:locker:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - if !(_isSuccess) exitWith { - ["ERROR", format ["Failed to check if virtual arsenal %1 exists! Using fallback virtual arsenal.", _uid]] call EFUNC(common,log); - - private _fallbackVArsenal = GVAR(VArsenalModel) call ["defaults", []]; - GVAR(VARegistry) set [_uid, _fallbackVArsenal]; - [CRPC(locker,responseInitVA), [_fallbackVArsenal], _player] call CFUNC(targetEvent); - - _fallbackVArsenal - }; - - private _finalVArsenal = createHashMap; - - if (_result == "true") then { - _finalVArsenal = _self call ["fetch", ["owned:locker:fetch", _uid]]; - ["INFO", format ["Found virtual arsenal for %1", _uid]] call EFUNC(common,log); - } else { - _finalVArsenal = GVAR(VArsenalModel) call ["defaults", []]; - - ["owned:locker:create", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - if !(_isSuccess) exitWith { - ["ERROR", format ["Failed to create virtual arsenal for %1! Using fallback virtual arsenal.", _uid]] call EFUNC(common,log); - - GVAR(VARegistry) set [_uid, _finalVArsenal]; - [CRPC(locker,responseInitVA), [_finalVArsenal], _player] call CFUNC(targetEvent); - - _finalVArsenal - }; - - ["INFO", format ["Created new virtual arsenal for %1", _uid]] call EFUNC(common,log); - }; - - GVAR(VARegistry) set [_uid, _finalVArsenal]; - [CRPC(locker,responseInitVA), [_finalVArsenal], _player] call CFUNC(targetEvent); - - _finalVArsenal + [CRPC(locker,responseInitVA), [_arsenal], _player] call CFUNC(targetEvent); + _arsenal }], - ["unlockItems", compileFinal { - params [["_uid", "", [""]], ["_items", [], [[]]], ["_commit", false, [false]]]; + ["save", compileFinal { + params [["_uid", "", [""]]]; - private _result = createHashMapFromArray [ - ["success", false], - ["message", "VA unlock failed."], - ["patch", createHashMap], - ["arsenal", createHashMap] - ]; - - private _defaultArsenal = GVAR(VArsenalModel) call ["defaults", []]; - private _arsenal = +(GVAR(VARegistry) getOrDefault [_uid, _defaultArsenal]); - private _patch = createHashMap; - private _categoriesToSync = []; - - { - private _item = _x; - private _className = _item getOrDefault ["classname", ""]; - private _category = toLowerANSI (_item getOrDefault ["category", ""]); - private _arsenalCategory = switch (_category) do { - case "item": { "items" }; - case "weapon": { "weapons" }; - case "magazine": { "magazines" }; - case "backpack": { "backpacks" }; - default { "items" }; - }; - - private _categoryUnlocks = +(_arsenal getOrDefault [_arsenalCategory, []]); - _categoryUnlocks pushBackUnique _className; - _arsenal set [_arsenalCategory, _categoryUnlocks]; - _categoriesToSync pushBackUnique _arsenalCategory; - } forEach _items; - - { - private _category = _x; - private _categoryUnlocks = _arsenal getOrDefault [_category, []]; - _patch set [_category, _categoryUnlocks]; - } forEach _categoriesToSync; - - if (_commit) then { GVAR(VARegistry) set [_uid, _arsenal]; }; - - _result set ["success", true]; - _result set ["message", ""]; - _result set ["patch", _patch]; - _result set ["arsenal", _arsenal]; - _result + if (_uid isEqualTo "") exitWith { createHashMap }; + _self call ["callHotVArsenal", ["owned:locker:hot:save", [_uid]]] }] ]; diff --git a/arma/server/addons/main/XEH_PREP.hpp b/arma/server/addons/main/XEH_PREP.hpp index 2e61ebc..3a1cf90 100644 --- a/arma/server/addons/main/XEH_PREP.hpp +++ b/arma/server/addons/main/XEH_PREP.hpp @@ -1 +1,2 @@ PREP(initStores); +PREP(saveHotState); diff --git a/arma/server/addons/main/XEH_preInit.sqf b/arma/server/addons/main/XEH_preInit.sqf index dcc00c2..929f1ff 100644 --- a/arma/server/addons/main/XEH_preInit.sqf +++ b/arma/server/addons/main/XEH_preInit.sqf @@ -4,6 +4,8 @@ PREP_RECOMPILE_START; #include "XEH_PREP.hpp" PREP_RECOMPILE_END; +GVAR(PlayerBootstrapRegistry) = createHashMap; + ["forge_icom_event", { params [["_event", "", [""]], ["_data", createHashMap, [createHashMap]]]; @@ -54,3 +56,23 @@ addMissionEventHandler ["ExtensionCallback", { }]); }; }]; + +addMissionEventHandler ["PlayerConnected", { + params ["_id", "_uid", "_name", "_jip", "_owner", "_idStr"]; +}]; + +addMissionEventHandler ["PlayerDisconnected", { + params ["_id", "_uid", "_name", "_jip", "_owner", "_idStr"]; + + if (_uid isEqualTo "") exitWith {}; + + [_uid] call FUNC(saveHotState); +}]; + +addMissionEventHandler ["Ended", { + [""] call FUNC(saveHotState); +}]; + +addMissionEventHandler ["MPEnded", { + [""] call FUNC(saveHotState); +}]; diff --git a/arma/server/addons/main/functions/fnc_initStores.sqf b/arma/server/addons/main/functions/fnc_initStores.sqf index 9093c32..69f5259 100644 --- a/arma/server/addons/main/functions/fnc_initStores.sqf +++ b/arma/server/addons/main/functions/fnc_initStores.sqf @@ -23,7 +23,11 @@ if (isNil QEGVAR(common,BaseStore)) then { call EFUNC(common,baseStore); }; if (isNil QEGVAR(actor,ActorStore)) then { call EFUNC(actor,initActorStore); }; // Bank -if (isNil QEGVAR(bank,BankStore)) then { call EFUNC(bank,initBankStore); }; +if (isNil QEGVAR(bank,BankSessionManager)) then { call EFUNC(bank,initSessionManager); }; +if (isNil QEGVAR(bank,BankMessenger)) then { call EFUNC(bank,initMessenger); }; +if (isNil QEGVAR(bank,BankModel)) then { call EFUNC(bank,initModel); }; +if (isNil QEGVAR(bank,BankPayloadBuilder)) then { call EFUNC(bank,initPayloadBuilder); }; +if (isNil QEGVAR(bank,BankStore)) then { call EFUNC(bank,initStore); }; // Garage if (isNil QEGVAR(garage,GarageStore)) then { call EFUNC(garage,initGarageStore); }; @@ -38,6 +42,7 @@ if (isNil QEGVAR(locker,LockerStore)) then { call EFUNC(locker,initLockerStore); if (isNil QEGVAR(locker,VAStore)) then { call EFUNC(locker,initVAStore); }; // Org +if (isNil QEGVAR(org,OrgPayloadBuilder)) then { call EFUNC(org,initPayloadBuilder); }; if (isNil QEGVAR(org,OrgStore)) then { call EFUNC(org,initOrgStore); }; // Store diff --git a/arma/server/addons/main/functions/fnc_saveHotState.sqf b/arma/server/addons/main/functions/fnc_saveHotState.sqf new file mode 100644 index 0000000..5fbfb98 --- /dev/null +++ b/arma/server/addons/main/functions/fnc_saveHotState.sqf @@ -0,0 +1,84 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_saveHotState.sqf + * Author: IDSolutions + * Date: 2026-04-01 + * Public: No + * + * Description: + * Flushes extension-backed hot state for a single UID or every known UID. + * + * Arguments: + * 0: UID to flush. Empty string flushes all known players. + * + * Return Value: + * True if the flush routine completed. + */ + +params [["_uid", "", [""]]]; + +private _uids = []; +if (_uid isEqualTo "") then { + { + if (isNull _x) then { continue; }; + private _playerUid = getPlayerUID _x; + if (_playerUid isNotEqualTo "") then { + _uids pushBackUnique _playerUid; + }; + } forEach allPlayers; + + if !(isNil QEGVAR(actor,Registry)) then { + { + if (_x isNotEqualTo "") then { + _uids pushBackUnique _x; + }; + } forEach keys EGVAR(actor,Registry); + }; +} else { + _uids pushBack _uid; +}; + +{ + private _flushUid = _x; + if (_flushUid isEqualTo "") then { continue; }; + + private _orgID = "default"; + if !(isNil QEGVAR(org,OrgStore)) then { + _orgID = EGVAR(org,OrgStore) call ["resolveOrgIdForUid", [_flushUid]]; + if (_orgID isEqualTo "") then { + _orgID = "default"; + }; + }; + + if !(isNil QEGVAR(actor,ActorStore)) then { + EGVAR(actor,ActorStore) call ["snapshot", [_flushUid]]; + EGVAR(actor,ActorStore) call ["save", [_flushUid]]; + }; + + if !(isNil QEGVAR(bank,BankStore)) then { + EGVAR(bank,BankStore) call ["save", [_flushUid]]; + }; + + if !(isNil QEGVAR(locker,LockerStore)) then { + EGVAR(locker,LockerStore) call ["save", [_flushUid]]; + }; + + if !(isNil QEGVAR(locker,VAStore)) then { + EGVAR(locker,VAStore) call ["save", [_flushUid]]; + }; + + if !(isNil QEGVAR(garage,GarageStore)) then { + EGVAR(garage,GarageStore) call ["save", [_flushUid]]; + }; + + if !(isNil QEGVAR(garage,VGarageStore)) then { + EGVAR(garage,VGarageStore) call ["save", [_flushUid]]; + }; + + if !(isNil QEGVAR(org,OrgStore)) then { + EGVAR(org,OrgStore) call ["saveById", [_orgID]]; + }; +} forEach _uids; + +true diff --git a/arma/server/addons/org/XEH_PREP.hpp b/arma/server/addons/org/XEH_PREP.hpp index dc78ebd..6a5b9f9 100644 --- a/arma/server/addons/org/XEH_PREP.hpp +++ b/arma/server/addons/org/XEH_PREP.hpp @@ -1,3 +1,2 @@ +PREP(initPayloadBuilder); PREP(initOrgStore); -PREP(memberService); -PREP(treasuryService); diff --git a/arma/server/addons/org/XEH_preInit.sqf b/arma/server/addons/org/XEH_preInit.sqf index 41fab57..fcc4c80 100644 --- a/arma/server/addons/org/XEH_preInit.sqf +++ b/arma/server/addons/org/XEH_preInit.sqf @@ -14,6 +14,24 @@ PREP_RECOMPILE_END; GVAR(OrgStore) call ["init", [_uid]]; }] call CFUNC(addEventHandler); +[QGVAR(requestHydrateOrg), { + params [["_uid", "", [""]], ["_bridgeEvent", "org::sync", [""]]]; + + if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" }; + + if !(_bridgeEvent in ["org::login::success", "org::create::success", "org::sync"]) then { + _bridgeEvent = "org::sync"; + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (_player isEqualTo objNull) exitWith {}; + + private _payload = GVAR(OrgStore) call ["buildPortalPayload", [_uid]]; + if (_payload isEqualTo createHashMap) exitWith {}; + + [CRPC(org,responseHydrateOrg), [_payload, _bridgeEvent], _player] call CFUNC(targetEvent); +}] call CFUNC(addEventHandler); + [QGVAR(requestCreateOrg), { params [["_uid", "", [""]], ["_orgName", "", [""]]]; @@ -34,41 +52,6 @@ PREP_RECOMPILE_END; [CRPC(org,responseCreateOrg), [_result], _player] call CFUNC(targetEvent); }] call CFUNC(addEventHandler); -[QGVAR(requestGetOrg), { - params [["_uid", "", [""]], ["_field", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" }; - - private _index = GVAR(IndexRegistry) get _uid; - private _key = _index get "orgID"; - private _finalData = GVAR(OrgStore) call ["get", [GVAR(Registry), _key, _field]]; - private _player = [_uid] call EFUNC(common,getPlayer); - - [CRPC(org,responseSyncOrg), [_finalData], _player] call CFUNC(targetEvent); -}] call CFUNC(addEventHandler); - -[QGVAR(requestSetOrg), { - params [["_uid", "", [""]], ["_field", "", [""]], ["_value", nil, [[], "", 0, false, createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "" || _field isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID or Field!" }; - - private _index = GVAR(IndexRegistry) get _uid; - private _key = _index get "orgID"; - GVAR(OrgStore) call ["set", [GVAR(Registry), "org:update", _key, _field, _value, _sync]]; -}] call CFUNC(addEventHandler); - -[QGVAR(requestMSetOrg), { - params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" }; - if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid field pairs!" }; - - private _index = GVAR(IndexRegistry) get _uid; - private _key = _index get "orgID"; - - GVAR(OrgStore) call ["mset", [GVAR(Registry), "org:update", _key, _fieldValuePairs, _sync]]; -}] call CFUNC(addEventHandler); - [QGVAR(requestAssignCreditLine), { params [ ["_uid", "", [""]], @@ -102,26 +85,6 @@ PREP_RECOMPILE_END; ]], _requester] call CFUNC(targetEvent); }] call CFUNC(addEventHandler); -[QGVAR(requestSaveOrg), { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" }; - - private _index = GVAR(IndexRegistry) get _uid; - private _key = _index get "orgID"; - GVAR(OrgStore) call ["save", [GVAR(Registry), "org:update", _key]]; -}] call CFUNC(addEventHandler); - -[QGVAR(requestRemoveOrg), { - params [["_uid", "", [""]]]; - - if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Org] Empty/Invalid UID!" }; - - private _index = GVAR(IndexRegistry) get _uid; - private _key = _index get "orgID"; - GVAR(OrgStore) call ["delete", [_key]]; -}] call CFUNC(addEventHandler); - [QGVAR(requestLeaveOrg), { params [["_uid", "", [""]]]; diff --git a/arma/server/addons/org/functions/fnc_initOrgStore.sqf b/arma/server/addons/org/functions/fnc_initOrgStore.sqf index 7e73c65..230564e 100644 --- a/arma/server/addons/org/functions/fnc_initOrgStore.sqf +++ b/arma/server/addons/org/functions/fnc_initOrgStore.sqf @@ -4,12 +4,12 @@ * File: fnc_initOrgStore.sqf * Author: IDSolutions * Date: 2026-02-13 - * Last Update: 2026-03-13 + * Last Update: 2026-04-01 * Public: Yes * * Description: * Initializes the org store for managing player organizations. - * Provides methods for creating, fetching, and updating organizations. + * Org hot state is owned by the extension; SQF acts as the bridge. * * Arguments: * None @@ -21,9 +21,6 @@ * call forge_server_org_fnc_initOrgStore */ -if (isNil QGVAR(OrgMembershipService)) then { call FUNC(memberService); }; -if (isNil QGVAR(OrgTreasuryService)) then { call FUNC(treasuryService); }; - #pragma hemtt ignore_variables ["_self"] GVAR(OrgModel) = compileFinal createHashMapObject [[ ["#type", "OrgModel"], @@ -50,6 +47,70 @@ 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]; + + private _creditLines = _org getOrDefault ["credit_lines", createHashMap]; + if !(_creditLines isEqualType createHashMap) then { + _creditLines = createHashMap; + }; + + { + if !(_y isEqualType createHashMap) then { continue; }; + + private _line = +_y; + private _legacyAmount = _line getOrDefault ["amount", 0]; + private _approvedAmount = _line getOrDefault ["approved_amount", _legacyAmount]; + private _availableAmount = _line getOrDefault ["available_amount", _approvedAmount]; + private _outstandingPrincipal = _line getOrDefault ["outstanding_principal", 0]; + private _interestRate = _line getOrDefault ["interest_rate", 0.1]; + private _amountDue = _line getOrDefault ["amount_due", 0]; + + _line set ["uid", _line getOrDefault ["uid", _x]]; + _line set ["approved_amount", _approvedAmount]; + _line set ["available_amount", _availableAmount]; + _line set ["outstanding_principal", _outstandingPrincipal]; + _line set ["interest_rate", _interestRate]; + _line set ["amount_due", _amountDue]; + _line set ["amount", _availableAmount]; + _creditLines set [_x, _line]; + } forEach _creditLines; + + _org set ["credit_lines", _creditLines]; + _org }], ["validate", compileFinal { @@ -82,8 +143,6 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ ["#base", EGVAR(common,BaseStore)], ["#type", "OrgBaseStore"], ["#create", compileFinal { - GVAR(IndexRegistry) = createHashMap; - GVAR(Registry) = createHashMap; ["INFO", "Org Store Initialized!"] call EFUNC(common,log); ["org:exists", ["default"]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; @@ -101,36 +160,168 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ ["fleet", createHashMap], ["members", createHashMap] ]; - GVAR(Registry) set ["default", _defaultOrg]; - _defaultOrg }; - private _defaultOrg = createHashMap; - if (_result == "true") then { - _defaultOrg = _self call ["fetch", ["org:get", "default"]]; - } else { - _defaultOrg set ["id", "default"]; - _defaultOrg set ["owner", "server"]; - _defaultOrg set ["name", "Forge Dynamics"]; - _defaultOrg set ["funds", 200000]; - _defaultOrg set ["reputation", 0]; - _defaultOrg set ["credit_lines", createHashMap]; + if (_result != "true") then { + private _defaultOrg = createHashMapFromArray [ + ["id", "default"], + ["owner", "server"], + ["name", "Forge Dynamics"], + ["funds", 200000], + ["reputation", 0], + ["credit_lines", createHashMap], + ["assets", createHashMap], + ["fleet", createHashMap], + ["members", createHashMap] + ]; private _defaultJson = _self call ["toJSON", [_defaultOrg]]; ["org:create", ["default", _defaultJson]] call EFUNC(extension,extCall); }; - GVAR(Registry) set ["default", _defaultOrg]; + private _loadedDefaultOrg = _self call ["loadHotOrg", ["default", true]]; + if (_loadedDefaultOrg isEqualTo createHashMap) then { + _loadedDefaultOrg = createHashMapFromArray [ + ["id", "default"], + ["owner", "server"], + ["name", "Forge Dynamics"], + ["funds", 200000], + ["reputation", 0], + ["credit_lines", createHashMap], + ["assets", createHashMap], + ["fleet", createHashMap], + ["members", createHashMap] + ]; + }; + + _loadedDefaultOrg }], - ["verifyMember", compileFinal { - GVAR(OrgMembershipService) call ["verifyMember", _this] + ["callHotOrg", compileFinal { + params [["_function", "", [""]], ["_arguments", [], [[]]]]; + + if (_function isEqualTo "") exitWith { createHashMap }; + + [_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + if !(_isSuccess) exitWith { createHashMap }; + if !(_result isEqualType "") exitWith { createHashMap }; + if ((_result find "Error:") == 0) exitWith { + ["ERROR", format ["Org extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log); + createHashMap + }; + + private _data = fromJSON _result; + if !(_data isEqualType createHashMap) exitWith { createHashMap }; + + _self call ["syncHotOrg", [_data]] }], - ["addMember", compileFinal { - GVAR(OrgMembershipService) call ["addMember", _this] + ["callHotOrgEnvelope", compileFinal { + params [["_function", "", [""]], ["_arguments", [], [[]]]]; + + if (_function isEqualTo "") exitWith { createHashMap }; + + [_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + if !(_isSuccess) exitWith { createHashMap }; + if !(_result isEqualType "") exitWith { createHashMap }; + if ((_result find "Error:") == 0) exitWith { + ["ERROR", format ["Org extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log); + createHashMap + }; + + private _data = fromJSON _result; + if !(_data isEqualType createHashMap) exitWith { createHashMap }; + + if ("org" in _data) then { + private _syncedOrg = _self call ["syncHotOrg", [_data getOrDefault ["org", createHashMap]]]; + if (_syncedOrg isNotEqualTo createHashMap) then { + _data set ["org", _syncedOrg]; + }; + }; + + _data }], - ["removeMember", compileFinal { - GVAR(OrgMembershipService) call ["removeMember", _this] + ["syncHotOrg", compileFinal { + params [["_org", createHashMap, [createHashMap]]]; + + if !(_org isEqualType createHashMap) exitWith { createHashMap }; + + private _migratedOrg = GVAR(OrgModel) call ["migrate", [+_org]]; + private _orgID = _migratedOrg getOrDefault ["id", ""]; + if (_orgID isEqualTo "") exitWith { createHashMap }; + + _migratedOrg + }], + ["resolveOrgIdForUid", compileFinal { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { "default" }; + + private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; + private _orgID = _actor getOrDefault ["organization", "default"]; + if (_orgID isEqualTo "") then { _orgID = "default"; }; + _orgID + }], + ["resolveActorName", compileFinal { + params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]]; + + private _memberName = _actor getOrDefault ["name", ""]; + if (_memberName isEqualTo "" && { _player isNotEqualTo objNull }) then { + _memberName = name _player; + }; + if (_memberName isEqualTo "") then { _memberName = "Unknown"; }; + _memberName + }], + ["applyActorOrganization", compileFinal { + params [["_uid", "", [""]], ["_orgID", "", [""]], ["_actor", createHashMap, [createHashMap]]]; + + if (_uid isEqualTo "" || { _orgID isEqualTo "" }) exitWith { createHashMap }; + + private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", _orgID, false]]; + private _updatedActor = EGVAR(actor,ActorStore) call ["get", [_uid, ""]]; + if ( + !(_updatedActor isEqualType createHashMap) + || { _updatedActor isEqualTo createHashMap } + || { (_updatedActor getOrDefault ["organization", ""]) isNotEqualTo _orgID } + ) then { + private _forcedActor = +_actor; + if !(_forcedActor isEqualType createHashMap) then { + _forcedActor = EGVAR(actor,ActorModel) call ["defaults", []]; + _forcedActor set ["uid", _uid]; + }; + + _forcedActor set ["organization", _orgID]; + _updatedActor = EGVAR(actor,ActorStore) call ["override", [_uid, _forcedActor, false]]; + if (_updatedActor isEqualType createHashMap && { _updatedActor isNotEqualTo createHashMap }) then { + _actorPatch = createHashMapFromArray [["organization", _orgID]]; + }; + }; + + if ( + !(_updatedActor isEqualType createHashMap) + || { _updatedActor isEqualTo createHashMap } + || { (_updatedActor getOrDefault ["organization", ""]) isNotEqualTo _orgID } + ) exitWith { createHashMap }; + + _actorPatch + }], + ["loadHotOrg", compileFinal { + params [["_orgID", "", [""]], ["_initialize", false, [false]]]; + + if (_orgID isEqualTo "") exitWith { createHashMap }; + + private _command = ["org:hot:get", "org:hot:init"] select _initialize; + _self call ["callHotOrg", [_command, [_orgID]]] + }], + ["get", compileFinal { + params [["_orgID", "", [""]], ["_field", "", [""]]]; + + private _org = _self call ["loadHotOrg", [_orgID, false]]; + if (_org isEqualTo createHashMap) then { + _org = _self call ["loadHotOrg", [_orgID, true]]; + }; + + if (_field isEqualTo "") exitWith { _org }; + _org getOrDefault [_field, createHashMap] }], ["delete", compileFinal { params [["_orgID", "", [""]]]; @@ -151,30 +342,312 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _result }; - GVAR(Registry) deleteAt _orgID; + ["org:hot:remove", [_orgID]] call EFUNC(extension,extCall); _result set ["success", true]; _result }], - ["restoreDefaultMembership", compileFinal { - GVAR(OrgMembershipService) call ["restoreDefaultMembership", _this] + ["ensureMember", compileFinal { + params [["_orgID", "", [""]], ["_uid", "", [""]], ["_memberName", "", [""]]]; + + if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap }; + + private _context = createHashMapFromArray [ + ["orgId", _orgID], + ["memberUid", _uid], + ["memberName", _memberName] + ]; + + _self call ["callHotOrg", ["org:hot:ensure_member", [toJSON _context]]] }], ["leave", compileFinal { - GVAR(OrgMembershipService) call ["leave", _this] + params [["_uid", "", [""]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", ""], + ["actorPatch", createHashMap], + ["notification", []] + ]; + + if (_uid isEqualTo "") exitWith { + _result set ["message", "A valid player UID is required."]; + _result + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; + private _orgID = _actor getOrDefault ["organization", "default"]; + private _memberName = _self call ["resolveActorName", [_uid, _player, _actor]]; + private _context = createHashMapFromArray [ + ["requesterUid", _uid], + ["requesterName", _memberName], + ["orgId", _orgID] + ]; + + private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:leave", [toJSON _context]]]; + if (_envelope isEqualTo createHashMap) exitWith { + _result set ["message", "Unable to leave the organization."]; + _result + }; + + private _actorOrg = _envelope getOrDefault ["actorOrganization", "default"]; + private _actorPatch = _self call ["applyActorOrganization", [_uid, _actorOrg, _actor]]; + if (_actorPatch isEqualTo createHashMap) exitWith { + _result set ["message", "Failed to restore default organization membership."]; + _result + }; + + _result set ["success", true]; + _result set ["message", _envelope getOrDefault ["message", "You returned to the default organization."]]; + _result set ["actorPatch", _actorPatch]; + _result set ["notification", ["info", "Organization Left", _result get "message", 6000]]; + _result }], ["disband", compileFinal { - GVAR(OrgMembershipService) call ["disband", _this] + params [["_uid", "", [""]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", ""], + ["members", []] + ]; + + if (_uid isEqualTo "") exitWith { + _result set ["message", "A valid player UID is required."]; + _result + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; + private _orgID = _actor getOrDefault ["organization", "default"]; + private _memberName = _self call ["resolveActorName", [_uid, _player, _actor]]; + private _context = createHashMapFromArray [ + ["requesterUid", _uid], + ["requesterName", _memberName], + ["orgId", _orgID] + ]; + + private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:disband", [toJSON _context]]]; + if (_envelope isEqualTo createHashMap) exitWith { + _result set ["message", "Failed to disband organization."]; + _result + }; + + private _memberResults = []; + { + private _memberUid = _x getOrDefault ["uid", ""]; + if (_memberUid isEqualTo "") then { continue; }; + + private _memberActor = EGVAR(actor,Registry) getOrDefault [_memberUid, createHashMap]; + private _actorPatch = _self call ["applyActorOrganization", [_memberUid, _x getOrDefault ["actorOrganization", "default"], _memberActor]]; + if (_actorPatch isEqualTo createHashMap) then { + ["WARNING", format ["Failed to restore actor organization for %1 after org disband.", _memberUid]] call EFUNC(common,log); + }; + + private _responseMessage = _x getOrDefault ["message", _envelope getOrDefault ["message", "Organization disbanded."]]; + private _notificationParams = [ + ["warning", "Organization Disbanded", _responseMessage, 6000], + ["success", "Organization Disbanded", _responseMessage, 6000] + ] select (_x getOrDefault ["requester", false]); + + _memberResults pushBack (createHashMapFromArray [ + ["uid", _memberUid], + ["requester", _x getOrDefault ["requester", false]], + ["message", _responseMessage], + ["notification", _notificationParams], + ["actorPatch", _actorPatch] + ]); + } forEach (_envelope getOrDefault ["members", []]); + + _result set ["success", true]; + _result set ["message", _envelope getOrDefault ["message", "Organization disbanded."]]; + _result set ["members", _memberResults]; + _result }], ["assignCreditLine", compileFinal { - GVAR(OrgTreasuryService) call ["assignCreditLine", _this] + params [["_requesterUid", "", [""]], ["_memberUid", "", [""]], ["_memberName", "", [""]], ["_amount", 0, [0]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", ""], + ["patch", createHashMap], + ["memberUids", []] + ]; + + if (_requesterUid isEqualTo "" || { _memberUid isEqualTo "" } || { _amount <= 0 }) exitWith { + _result set ["message", "A valid requester, member, and credit amount are required."]; + _result + }; + + private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap]; + private _orgID = _requesterActor getOrDefault ["organization", "default"]; + if (_orgID isEqualTo "") then { _orgID = "default"; }; + + private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer); + private _requesterIsDefaultOrgCeo = ( + _requesterPlayer isNotEqualTo objNull + && { _orgID isEqualTo "default" } + && { toLowerANSI (vehicleVarName _requesterPlayer) isEqualTo "ceo" } + ); + + private _context = createHashMapFromArray [ + ["requesterUid", _requesterUid], + ["orgId", _orgID], + ["requesterIsDefaultOrgCeo", _requesterIsDefaultOrgCeo], + ["memberUid", _memberUid], + ["memberName", _memberName], + ["amount", _amount] + ]; + + private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:assign_credit_line", [toJSON _context]]]; + if (_envelope isEqualTo createHashMap) exitWith { + _result set ["message", "Unable to assign credit line."]; + _result + }; + + _result set ["success", true]; + _result set ["message", _envelope getOrDefault ["message", "Credit line assigned."]]; + _result set ["patch", _envelope getOrDefault ["patch", createHashMap]]; + _result set ["memberUids", _envelope getOrDefault ["memberUids", []]]; + _result }], - ["buildChargeResult", compileFinal { - GVAR(OrgTreasuryService) call ["buildChargeResult", _this] + ["repayCreditLine", compileFinal { + params [["_requesterUid", "", [""]], ["_amount", 0, [0]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", ""], + ["patch", createHashMap], + ["memberUids", []] + ]; + + if (_requesterUid isEqualTo "" || { _amount <= 0 }) exitWith { + _result set ["message", "A valid repayment amount is required."]; + _result + }; + + private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap]; + private _orgID = _requesterActor getOrDefault ["organization", "default"]; + if (_orgID isEqualTo "") then { _orgID = "default"; }; + + private _context = createHashMapFromArray [ + ["requesterUid", _requesterUid], + ["orgId", _orgID], + ["amount", _amount] + ]; + + private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:repay_credit_line", [toJSON _context]]]; + if (_envelope isEqualTo createHashMap) exitWith { + _result set ["message", "Unable to apply credit repayment."]; + _result + }; + + _result set ["success", true]; + _result set ["message", _envelope getOrDefault ["message", "Credit repayment posted."]]; + _result set ["patch", _envelope getOrDefault ["patch", createHashMap]]; + _result set ["memberUids", _envelope getOrDefault ["memberUids", []]]; + _result + }], + ["buildPortalPayload", compileFinal { + params [["_uid", "", [""]]]; + + GVAR(OrgPayloadBuilder) call ["buildPortalPayload", [_uid]] }], ["chargeCheckout", compileFinal { - GVAR(OrgTreasuryService) call ["chargeCheckout", _this] + params [["_requesterUid", "", [""]], ["_requesterPlayer", objNull, [objNull]], ["_source", "org_funds", [""]], ["_amount", 0, [0]], ["_commit", false, [false]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", "Unable to process organization payment."], + ["patch", createHashMap], + ["memberUids", []] + ]; + + private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap]; + private _orgID = _requesterActor getOrDefault ["organization", "default"]; + if (_orgID isEqualTo "") then { _orgID = "default"; }; + + private _requesterIsDefaultOrgCeo = ( + _requesterPlayer isNotEqualTo objNull + && { _orgID isEqualTo "default" } + && { toLowerANSI (vehicleVarName _requesterPlayer) isEqualTo "ceo" } + ); + + private _context = createHashMapFromArray [ + ["requesterUid", _requesterUid], + ["orgId", _orgID], + ["requesterIsDefaultOrgCeo", _requesterIsDefaultOrgCeo], + ["source", _source], + ["amount", _amount], + ["commit", _commit] + ]; + + private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:charge_checkout", [toJSON _context]]]; + if (_envelope isEqualTo createHashMap) exitWith { _result }; + + _result set ["success", true]; + _result set ["message", _envelope getOrDefault ["message", ""]]; + _result set ["patch", _envelope getOrDefault ["patch", createHashMap]]; + _result set ["memberUids", _envelope getOrDefault ["memberUids", []]]; + _result + }], + ["saveById", compileFinal { + params [["_orgID", "", [""]]]; + + if (_orgID isEqualTo "") exitWith { createHashMap }; + + _self call ["callHotOrg", ["org:hot:save", [_orgID]]] + }], + ["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 _context = createHashMapFromArray [ + ["requesterUid", _requesterUid], + ["orgId", _resolvedOrgID], + ["commit", _commit] + ]; + private _assetSeeds = _assets apply { + createHashMapFromArray [ + ["classname", _x getOrDefault ["classname", ""]], + ["category", toLowerANSI (_x getOrDefault ["category", "items"])], + ["quantity", floor ((_x getOrDefault ["quantity", 0]) max 0)] + ] + }; + + private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:add_assets", [toJSON _context, toJSON _assetSeeds]]]; + if (_envelope isEqualTo createHashMap) exitWith { + _result set ["message", "Failed to update organization asset cache."]; + _result + }; + + _result set ["success", true]; + _result set ["message", _envelope getOrDefault ["message", ""]]; + _result set ["patch", _envelope getOrDefault ["patch", createHashMap]]; + _result set ["memberUids", _envelope getOrDefault ["memberUids", []]]; + _result }], ["addFleetVehicles", compileFinal { - params [["_requesterUid", "", [""]], ["_vehicles", [], [[]]], ["_commit", false, [false]]]; + params [["_requesterUid", "", [""]], ["_vehicles", [], [[]]], ["_commit", false, [false]], ["_orgID", "", [""]]]; private _result = createHashMapFromArray [ ["success", false], @@ -183,61 +656,41 @@ 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]; - if (_org isEqualTo createHashMap) exitWith { - _result set ["message", "Organization data is unavailable for fleet updates."]; + private _context = createHashMapFromArray [ + ["requesterUid", _requesterUid], + ["orgId", _resolvedOrgID], + ["commit", _commit] + ]; + private _fleetSeeds = _vehicles apply { + createHashMapFromArray [ + ["classname", _x getOrDefault ["classname", ""]], + ["category", toLowerANSI (_x getOrDefault ["category", "other"])] + ] + }; + + private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:add_fleet", [toJSON _context, toJSON _fleetSeeds]]]; + if (_envelope isEqualTo createHashMap) exitWith { + _result set ["message", "Failed to update organization fleet cache."]; _result }; - private _fleet = +(_org getOrDefault ["fleet", createHashMap]); - private _fleetIndex = count (keys _fleet); - - { - private _className = _x getOrDefault ["classname", ""]; - private _category = toLowerANSI (_x getOrDefault ["category", "other"]); - if (_className isEqualTo "") exitWith { - _result set ["message", "Vehicle fleet entry was missing a classname."]; - }; - - private _fleetKey = format ["%1_%2", _className, _fleetIndex]; - while { _fleetKey in (keys _fleet) } do { - _fleetIndex = _fleetIndex + 1; - _fleetKey = format ["%1_%2", _className, _fleetIndex]; - }; - - private _displayName = getText (configFile >> "CfgVehicles" >> _className >> "displayName"); - if (_displayName isEqualTo "") then { _displayName = _className; }; - - _fleet set [_fleetKey, createHashMapFromArray [ - ["classname", _className], - ["name", _displayName], - ["type", _category], - ["status", "Ready"], - ["damage", "0%"] - ]]; - - _fleetIndex = _fleetIndex + 1; - } forEach _vehicles; - - private _patch = createHashMapFromArray [["fleet", _fleet]]; - if (_commit) then { - _patch = _self call ["mset", [GVAR(Registry), "org:update", _orgID, _patch, false]]; - }; - _result set ["success", true]; - _result set ["message", ""]; - _result set ["patch", _patch]; - _result set ["memberUids", GVAR(OrgTreasuryService) call ["resolveOrgMemberUids", [_org, _requesterUid]]]; + _result set ["message", _envelope getOrDefault ["message", ""]]; + _result set ["patch", _envelope getOrDefault ["patch", createHashMap]]; + _result set ["memberUids", _envelope getOrDefault ["memberUids", []]]; _result }], ["loadById", compileFinal { @@ -245,33 +698,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ if (_orgID isEqualTo "") exitWith { createHashMap }; - private _cachedOrg = GVAR(Registry) getOrDefault [_orgID, createHashMap]; - if (_cachedOrg isNotEqualTo createHashMap) exitWith { _cachedOrg }; - - ["org:exists", [_orgID]] call EFUNC(extension,extCall) params ["_existsResult", "_existsSuccess"]; - if (!_existsSuccess || { _existsResult isNotEqualTo "true" }) exitWith { createHashMap }; - - private _org = _self call ["fetch", ["org:get", _orgID]]; - if (_org isEqualTo createHashMap) exitWith { _org }; - - _org = GVAR(OrgModel) call ["migrate", [_org]]; - - private _memberRows = _self call ["fetch", ["org:members:get", _orgID]]; - if !(_memberRows isEqualType []) then { - _memberRows = []; - }; - - private _memberMap = createHashMap; - { - private _memberUid = _x getOrDefault ["uid", ""]; - if (_memberUid isNotEqualTo "") then { - _memberMap set [_memberUid, _x]; - }; - } forEach _memberRows; - - _org set ["members", _memberMap]; - GVAR(Registry) set [_orgID, _org, true]; - _org + _self call ["loadHotOrg", [_orgID, true]] }], ["register", compileFinal { params [["_uid", "", [""]], ["_orgName", "", [""]]]; @@ -288,13 +715,8 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _result }; - private _player = [_uid] call EFUNC(common,getPlayer); private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; private _existingOrgID = _actor getOrDefault ["organization", ""]; - if (_existingOrgID isNotEqualTo "" && { toLower _existingOrgID isNotEqualTo "default" }) exitWith { - _result set ["message", "Player already belongs to an organization."]; - _result - }; private _orgID = _actor getOrDefault ["phone_number", ""]; if (_orgID isEqualTo "") exitWith { @@ -302,56 +724,29 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _result }; - ["org:exists", [_orgID]] call EFUNC(extension,extCall) params ["_existsResult", "_existsSuccess"]; - if (!_existsSuccess) exitWith { - _result set ["message", "Unable to verify organization ID availability."]; - _result - }; - - if (_existsResult isEqualTo "true") exitWith { - _result set ["message", "An organization already exists for this phone number."]; - _result - }; - - private _org = createHashMapFromArray [ - ["id", _orgID], - ["owner", _uid], - ["name", _orgName], - ["funds", 0], - ["reputation", 0], - ["credit_lines", createHashMap], - ["assets", createHashMap], - ["fleet", createHashMap], - ["members", createHashMap] + private _context = createHashMapFromArray [ + ["requesterUid", _uid], + ["requesterName", _self call ["resolveActorName", [_uid, [_uid] call EFUNC(common,getPlayer), _actor]]], + ["orgId", _orgID], + ["orgName", _orgName], + ["existingOrgId", _existingOrgID] ]; - private _json = _self call ["toJSON", [_org]]; - ["org:create", [_orgID, _json]] call EFUNC(extension,extCall) params ["_createResult", "_createSuccess"]; - if (!_createSuccess) exitWith { - _result set ["message", format ["Failed to create organization: %1", _createResult]]; + private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:register", [toJSON _context]]]; + if (_envelope isEqualTo createHashMap) exitWith { + _result set ["message", "Organization registration failed."]; _result }; - if (_createResult isNotEqualTo "") then { - _org = _self call ["toHashMap", [_createResult]]; + private _actorPatch = _self call ["applyActorOrganization", [_uid, _envelope getOrDefault ["actorOrganization", _orgID], _actor]]; + if (_actorPatch isEqualTo createHashMap) exitWith { + _result set ["message", "Failed to assign the player to the new organization."]; + _result }; - _org set ["members", createHashMap]; - _org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]]; - - if (toLower _existingOrgID isEqualTo "default") then { - private _defaultOrg = _self call ["removeMember", ["default", _uid]]; - if (_defaultOrg isEqualTo createHashMap) then { - ["WARNING", format ["Failed to remove %1 from default org members after creating org %2.", _uid, _orgID]] call EFUNC(common,log); - }; - }; - - private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", _orgID, true]]; - GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", _orgID]]]; - GVAR(Registry) set [_orgID, _org, true]; - _result set ["success", true]; - _result set ["org", _org]; + _result set ["message", _envelope getOrDefault ["message", ""]]; + _result set ["org", _envelope getOrDefault ["org", createHashMap]]; _result set ["actorPatch", _actorPatch]; _result }], @@ -365,53 +760,18 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _orgID = "default"; }; - private _cachedOrg = GVAR(Registry) getOrDefault [_orgID, nil]; - if !(isNil { _cachedOrg }) exitWith { - private _cachedOwner = _cachedOrg getOrDefault ["owner", ""]; - if (_orgID isEqualTo "default" || { _cachedOwner isEqualTo _uid }) then { - _cachedOrg = _self call ["verifyMember", [_cachedOrg, _orgID, _uid, _player, _actor]]; - }; - GVAR(Registry) set [_orgID, _cachedOrg, true]; - [CRPC(org,responseInitOrg), [_cachedOrg], _player] call CFUNC(targetEvent); - - _cachedOrg - }; - - ["org:exists", [_orgID]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - if !(_isSuccess) exitWith { - ["ERROR", format ["Failed to check for org %1! Using fallback org.", _orgID]] call EFUNC(common,log); - - private _fallbackOrg = GVAR(Registry) getOrDefault ["default", createHashMap]; - GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", _orgID]]]; - - if (_orgID isEqualTo "default") then { - _fallbackOrg = _self call ["verifyMember", [_fallbackOrg, _orgID, _uid, _player, _actor]]; - }; - - GVAR(Registry) set [_orgID, _fallbackOrg, true]; - [CRPC(org,responseInitOrg), [_fallbackOrg], _player] call CFUNC(targetEvent); - - _fallbackOrg - }; - - private _finalOrg = createHashMap; - if (_result == "true") then { - _finalOrg = _self call ["loadById", [_orgID]]; - ["INFO", format ["Found org for %1", _orgID]] call EFUNC(common,log); - } else { + private _finalOrg = _self call ["loadById", [_orgID]]; + if (_finalOrg isEqualTo createHashMap) then { ["WARNING", format ["No existing org found for %1, using default org.", _uid]] call EFUNC(common,log); - _finalOrg = GVAR(Registry) getOrDefault ["default", createHashMap]; + _finalOrg = _self call ["loadById", ["default"]]; _orgID = "default"; }; - GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", _orgID]]]; - - private _finalOwner = _finalOrg getOrDefault ["owner", ""]; - if (_orgID isEqualTo "default" || { _finalOwner isEqualTo _uid }) then { - _finalOrg = _self call ["verifyMember", [_finalOrg, _orgID, _uid, _player, _actor]]; + private _verifiedOrg = _self call ["ensureMember", [_orgID, _uid, _self call ["resolveActorName", [_uid, _player, _actor]]]]; + if (_verifiedOrg isNotEqualTo createHashMap) then { + _finalOrg = _verifiedOrg; }; - GVAR(Registry) set [_orgID, _finalOrg, true]; [CRPC(org,responseInitOrg), [_finalOrg], _player] call CFUNC(targetEvent); _finalOrg diff --git a/arma/server/addons/org/functions/fnc_initPayloadBuilder.sqf b/arma/server/addons/org/functions/fnc_initPayloadBuilder.sqf new file mode 100644 index 0000000..19fe960 --- /dev/null +++ b/arma/server/addons/org/functions/fnc_initPayloadBuilder.sqf @@ -0,0 +1,222 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initPayloadBuilder.sqf + * Author: IDSolutions + * Date: 2026-04-02 + * Public: No + * + * Description: + * Initializes the org payload builder for portal/read-model shaping. + * Keeps hydrate construction out of OrgStore so the store can focus on + * extension-backed org operations and actor coordination. + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(OrgPayloadBuilder) = createHashMapObject [[ + ["#type", "OrgPayloadBuilder"], + ["resolveOrgForUid", compileFinal { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { createHashMap }; + + private _orgID = GVAR(OrgStore) call ["resolveOrgIdForUid", [_uid]]; + private _org = GVAR(OrgStore) call ["loadById", [_orgID]]; + if (_org isEqualTo createHashMap) then { + _org = GVAR(OrgStore) call ["init", [_uid]]; + }; + + _org + }], + ["resolveOwnerName", compileFinal { + params [["_ownerUid", "", [""]], ["_uid", "", [""]], ["_playerName", "", [""]], ["_membersRaw", createHashMap, [createHashMap]]]; + + private _ownerName = ["", "Server"] select (toLowerANSI _ownerUid isEqualTo "server"); + { + private _memberData = _y; + private _memberUid = _memberData getOrDefault ["uid", ""]; + if (_memberUid isEqualTo _ownerUid && { _ownerName isEqualTo "" }) exitWith { + _ownerName = _memberData getOrDefault ["name", "Unknown"]; + }; + } forEach _membersRaw; + + if (_ownerName isEqualTo "" && { _ownerUid isEqualTo _uid }) then { _ownerName = _playerName; }; + if (_ownerName isEqualTo "" && { _ownerUid isNotEqualTo "" }) then { _ownerName = "Unknown Owner"; }; + if !(_ownerName isEqualType "") then { _ownerName = str _ownerName; }; + _ownerName + }], + ["buildMembersList", compileFinal { + params [["_membersRaw", createHashMap, [createHashMap]], ["_uid", "", [""]], ["_ownerUid", "", [""]]]; + + private _sessionRole = "Member"; + private _membersList = []; + + { + private _memberData = _y; + private _memberName = _memberData getOrDefault ["name", "Unknown"]; + private _memberUid = _memberData getOrDefault ["uid", ""]; + + if (_memberUid isEqualTo _uid) then { _sessionRole = "Member"; }; + if (_memberUid isEqualTo _ownerUid) then { _sessionRole = ["Member", "Leader"] select (_ownerUid isEqualTo _uid); }; + + _membersList pushBack [ + ["uid", _memberUid], + ["name", _memberName] + ]; + } forEach _membersRaw; + + createHashMapFromArray [ + ["members", _membersList], + ["sessionRole", _sessionRole] + ] + }], + ["resolveDisplayName", compileFinal { + params [["_className", "", [""]], ["_configRoots", [], [[]]]]; + + if (_className isEqualTo "") exitWith { "" }; + + private _displayName = _className; + { + private _cfg = _x >> _className; + if (isClass _cfg) exitWith { + private _resolvedName = getText (_cfg >> "displayName"); + if (_resolvedName isNotEqualTo "") then { _displayName = _resolvedName; }; + }; + } forEach _configRoots; + + _displayName + }], + ["buildAssetsList", compileFinal { + params [["_assetsRaw", createHashMap, [createHashMap]]]; + + private _assetsList = []; + { + private _category = _x; + { + private _assetData = _y; + private _className = _assetData getOrDefault ["classname", ""]; + private _displayName = _self call ["resolveDisplayName", [_className, [ + configFile >> "CfgWeapons", + configFile >> "CfgMagazines", + configFile >> "CfgVehicles", + configFile >> "CfgGlasses" + ]]]; + + _assetsList pushBack [ + ["name", _displayName], + ["type", _assetData getOrDefault ["type", _category]], + ["quantity", str (_assetData getOrDefault ["quantity", 0])] + ]; + } forEach _y; + } forEach _assetsRaw; + + _assetsList + }], + ["buildFleetList", compileFinal { + params [["_fleetRaw", createHashMap, [createHashMap]]]; + + private _fleetList = []; + { + private _vehicleData = _y; + _fleetList pushBack [ + ["name", _vehicleData getOrDefault ["name", "Unknown Vehicle"]], + ["type", _vehicleData getOrDefault ["type", "other"]], + ["status", _vehicleData getOrDefault ["status", "Unknown"]], + ["damage", _vehicleData getOrDefault ["damage", "0%"]] + ]; + } forEach _fleetRaw; + + _fleetList + }], + ["buildCreditLinesList", compileFinal { + params [["_creditLinesRaw", createHashMap, [createHashMap]]]; + + private _creditLinesList = []; + { + private _creditLineData = _y; + private _availableAmount = _creditLineData getOrDefault [ + "available_amount", + _creditLineData getOrDefault ["amount", 0] + ]; + _creditLinesList pushBack [ + ["uid", _creditLineData getOrDefault ["uid", _x]], + ["member", _creditLineData getOrDefault ["name", "Unknown Member"]], + ["approvedAmount", _creditLineData getOrDefault ["approved_amount", _availableAmount]], + ["availableAmount", _availableAmount], + ["outstandingPrincipal", _creditLineData getOrDefault ["outstanding_principal", 0]], + ["interestRate", _creditLineData getOrDefault ["interest_rate", 0.1]], + ["amountDue", _creditLineData getOrDefault ["amount_due", 0]], + ["amount", _availableAmount] + ]; + } forEach _creditLinesRaw; + + _creditLinesList + }], + ["buildPortalPayload", compileFinal { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { createHashMap }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (isNull _player) exitWith { createHashMap }; + + private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; + private _orgID = _actor getOrDefault ["organization", "default"]; + if (_orgID isEqualTo "") then { _orgID = "default"; }; + + private _org = _self call ["resolveOrgForUid", [_uid]]; + if (_org isEqualTo createHashMap) exitWith { createHashMap }; + + private _verifiedOrg = GVAR(OrgStore) call ["ensureMember", [_orgID, _uid, GVAR(OrgStore) call ["resolveActorName", [_uid, _player, _actor]]]]; + if (_verifiedOrg isNotEqualTo createHashMap) then { _org = _verifiedOrg; }; + + private _name = _org getOrDefault ["name", ""]; + private _id = _org getOrDefault ["id", _orgID]; + private _ownerUid = _org getOrDefault ["owner", ""]; + private _funds = _org getOrDefault ["funds", 0]; + private _reputation = _org getOrDefault ["reputation", 0]; + private _creditLinesRaw = _org getOrDefault ["credit_lines", createHashMap]; + private _assetsRaw = _org getOrDefault ["assets", createHashMap]; + private _fleetRaw = _org getOrDefault ["fleet", createHashMap]; + private _membersRaw = _org getOrDefault ["members", createHashMap]; + private _isDefaultOrg = (_org getOrDefault ["default", false]) + || { toLowerANSI _id isEqualTo "default" } + || { toLowerANSI _ownerUid isEqualTo "server" }; + + private _playerName = name _player; + private _playerVar = vehicleVarName _player; + private _sessionIsCeo = _isDefaultOrg && { _playerVar isEqualTo "ceo" }; + private _memberShape = _self call ["buildMembersList", [_membersRaw, _uid, _ownerUid]]; + private _sessionRole = _memberShape getOrDefault ["sessionRole", "Member"]; + private _ownerName = _self call ["resolveOwnerName", [_ownerUid, _uid, _playerName, _membersRaw]]; + + if (_ownerUid isEqualTo _uid) then { _sessionRole = "Leader"; }; + + createHashMapFromArray [ + ["session", createHashMapFromArray [ + ["actorName", _playerName], + ["actorUid", _uid], + ["role", _sessionRole], + ["ceo", _sessionIsCeo] + ]], + ["portalData", createHashMapFromArray [ + ["org", createHashMapFromArray [ + ["name", _name], + ["tag", _id], + ["owner", _ownerName], + ["ownerUid", _ownerUid], + ["isDefault", _isDefaultOrg] + ]], + ["funds", _funds], + ["reputation", _reputation], + ["creditLines", _self call ["buildCreditLinesList", [_creditLinesRaw]]], + ["members", _memberShape getOrDefault ["members", []]], + ["fleet", _self call ["buildFleetList", [_fleetRaw]]], + ["assets", _self call ["buildAssetsList", [_assetsRaw]]], + ["activity", []] + ]] + ] + }] +]]; + +GVAR(OrgPayloadBuilder) diff --git a/arma/server/addons/org/functions/fnc_memberService.sqf b/arma/server/addons/org/functions/fnc_memberService.sqf deleted file mode 100644 index 7e47105..0000000 --- a/arma/server/addons/org/functions/fnc_memberService.sqf +++ /dev/null @@ -1,243 +0,0 @@ -#include "..\script_component.hpp" - -#pragma hemtt ignore_variables ["_self"] -GVAR(OrgMembershipServiceBase) = compileFinal createHashMapFromArray [ - ["#type", "OrgMembershipService"], - ["buildMembershipResult", compileFinal { - params [["_message", "", [""]]]; - - createHashMapFromArray [ - ["success", false], - ["message", _message], - ["actorPatch", createHashMap] - ] - }], - ["verifyMember", compileFinal { - params [["_org", createHashMap, [createHashMap]], ["_orgID", "", [""]], ["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]]; - - if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { _org }; - - private _members = _org getOrDefault ["members", createHashMap]; - if ((_members getOrDefault [_uid, objNull]) isNotEqualTo objNull) exitWith { _org }; - - ["org:members:add", [_orgID, _uid]] call EFUNC(extension,extCall) params ["_memberResult", "_memberSuccess"]; - if (!_memberSuccess) then { - ["WARNING", format ["Failed to add %1 to org %2 members: %3", _uid, _orgID, _memberResult]] call EFUNC(common,log); - }; - - private _memberName = _actor getOrDefault ["name", ""]; - if (_memberName isEqualTo "" && { _player isNotEqualTo objNull }) then { - _memberName = name _player; - }; - if (_memberName isEqualTo "") then { - _memberName = "Unknown"; - }; - - private _updatedMembers = +_members; - _updatedMembers set [_uid, createHashMapFromArray [["uid", _uid], ["name", _memberName]]]; - _org set ["members", _updatedMembers]; - - _org - }], - ["addMember", compileFinal { - params [["_orgID", "", [""]], ["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]]; - - if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap }; - - private _org = GVAR(OrgStore) call ["loadById", [_orgID]]; - if (_org isEqualTo createHashMap) exitWith { _org }; - - _org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]]; - GVAR(Registry) set [_orgID, _org, true]; - - _org - }], - ["removeMember", compileFinal { - params [["_orgID", "", [""]], ["_uid", "", [""]]]; - - if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap }; - - private _org = GVAR(OrgStore) call ["loadById", [_orgID]]; - if (_org isEqualTo createHashMap) exitWith { _org }; - - ["org:members:remove", [_orgID, _uid]] call EFUNC(extension,extCall) params ["_memberResult", "_memberSuccess"]; - if (!_memberSuccess) exitWith { - ["WARNING", format ["Failed to remove %1 from org %2 members: %3", _uid, _orgID, _memberResult]] call EFUNC(common,log); - createHashMap - }; - - private _updatedMembers = +(_org getOrDefault ["members", createHashMap]); - _updatedMembers deleteAt _uid; - _org set ["members", _updatedMembers]; - GVAR(Registry) set [_orgID, _org, true]; - - _org - }], - ["restoreDefaultMembership", compileFinal { - params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]]; - - private _result = _self call ["buildMembershipResult", []]; - if (_uid isEqualTo "") exitWith { - _result set ["message", "A valid player UID is required."]; - _result - }; - - private _resolvedPlayer = _player; - if (_resolvedPlayer isEqualTo objNull) then { - _resolvedPlayer = [_uid] call EFUNC(common,getPlayer); - }; - - private _resolvedActor = EGVAR(actor,Registry) getOrDefault [_uid, _actor]; - private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", "default", true]]; - private _defaultActor = EGVAR(actor,Registry) getOrDefault [_uid, _resolvedActor]; - private _defaultOrg = _self call ["addMember", ["default", _uid, _resolvedPlayer, _defaultActor]]; - if (_defaultOrg isEqualTo createHashMap) exitWith { - _result set ["message", "Failed to restore default organization membership."]; - _result - }; - - GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", "default"]]]; - _result set ["success", true]; - _result set ["actorPatch", _actorPatch]; - _result - }], - ["leave", compileFinal { - params [["_uid", "", [""]]]; - - private _result = createHashMapFromArray [ - ["success", false], - ["message", ""], - ["actorPatch", createHashMap], - ["notification", []] - ]; - - if (_uid isEqualTo "") exitWith { - _result set ["message", "A valid player UID is required."]; - _result - }; - - private _player = [_uid] call EFUNC(common,getPlayer); - private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; - private _orgID = _actor getOrDefault ["organization", ""]; - if (_orgID isEqualTo "" || { toLower _orgID isEqualTo "default" }) exitWith { - _result set ["message", "You are already assigned to the default organization."]; - _result - }; - - private _org = GVAR(OrgStore) call ["loadById", [_orgID]]; - if (_org isEqualTo createHashMap) exitWith { - _result set ["message", "Unable to load organization data for leave request."]; - _result - }; - - private _ownerUid = _org getOrDefault ["owner", ""]; - if (_ownerUid isEqualTo _uid) exitWith { - _result set ["message", "Organization owners must disband the organization instead of leaving it."]; - _result - }; - - private _orgName = _org getOrDefault ["name", "Organization"]; - private _updatedOrg = _self call ["removeMember", [_orgID, _uid]]; - if (_updatedOrg isEqualTo createHashMap) exitWith { - _result set ["message", "Failed to remove you from the organization roster."]; - _result - }; - - private _defaultResult = _self call ["restoreDefaultMembership", [_uid, _player, _actor]]; - if !(_defaultResult getOrDefault ["success", false]) exitWith { - _result set ["message", _defaultResult getOrDefault ["message", "Failed to restore default organization membership."]]; - _result - }; - - private _message = format ["You left %1 and returned to the default organization.", _orgName]; - _result set ["success", true]; - _result set ["message", _message]; - _result set ["actorPatch", _defaultResult getOrDefault ["actorPatch", createHashMap]]; - _result set ["notification", ["info", "Organization Left", _message, 6000]]; - _result - }], - ["disband", compileFinal { - params [["_uid", "", [""]]]; - - private _result = createHashMapFromArray [ - ["success", false], - ["message", ""], - ["members", []] - ]; - - if (_uid isEqualTo "") exitWith { - _result set ["message", "A valid player UID is required."]; - _result - }; - - private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; - private _orgID = _actor getOrDefault ["organization", ""]; - if (_orgID isEqualTo "" || { toLower _orgID isEqualTo "default" }) exitWith { - _result set ["message", "Only active player organizations can be disbanded."]; - _result - }; - - private _org = GVAR(OrgStore) call ["loadById", [_orgID]]; - if (_org isEqualTo createHashMap) exitWith { - _result set ["message", "Unable to load organization data for disbanding."]; - _result - }; - - private _ownerUid = _org getOrDefault ["owner", ""]; - if (_ownerUid isEqualTo "" || { _ownerUid isNotEqualTo _uid }) exitWith { - _result set ["message", "Only the organization owner can disband this organization."]; - _result - }; - - private _orgName = _org getOrDefault ["name", "Organization"]; - private _memberMap = _org getOrDefault ["members", createHashMap]; - private _memberUids = keys _memberMap; - if !(_uid in _memberUids) then { - _memberUids pushBack _uid; - }; - - private _deleteResult = GVAR(OrgStore) call ["delete", [_orgID]]; - if !(_deleteResult getOrDefault ["success", false]) exitWith { - _result set ["message", _deleteResult getOrDefault ["message", "Failed to disband organization."]]; - _result - }; - - private _memberResults = []; - { - private _memberUid = _x; - if (_memberUid isNotEqualTo "") then { - private _memberPlayer = [_memberUid] call EFUNC(common,getPlayer); - private _memberActor = EGVAR(actor,Registry) getOrDefault [_memberUid, createHashMap]; - private _defaultResult = _self call ["restoreDefaultMembership", [_memberUid, _memberPlayer, _memberActor]]; - if !(_defaultResult getOrDefault ["success", false]) then { - ["WARNING", format ["Failed to restore default org for %1 after disbanding %2: %3", _memberUid, _orgID, _defaultResult getOrDefault ["message", "Unknown error."]]] call EFUNC(common,log); - }; - - private _responseMessage = [ - format ["%1 has been disbanded.", _orgName], - format ["Your organization, %1, has been disbanded.", _orgName] - ] select (_memberUid isEqualTo _uid); - - private _notificationParams = [ - ["warning", "Organization Disbanded", _responseMessage, 6000], - ["success", "Organization Disbanded", _responseMessage, 6000] - ] select (_memberUid isEqualTo _uid); - - _memberResults pushBack (createHashMapFromArray [ - ["uid", _memberUid], - ["requester", _memberUid isEqualTo _uid], - ["message", _responseMessage], - ["notification", _notificationParams], - ["actorPatch", _defaultResult getOrDefault ["actorPatch", createHashMap]] - ]); - }; - } forEach _memberUids; - - _result set ["success", true]; - _result set ["message", format ["%1 has been disbanded.", _orgName]]; - _result set ["members", _memberResults]; - _result - }] -]; - -GVAR(OrgMembershipService) = createHashMapObject [GVAR(OrgMembershipServiceBase)]; diff --git a/arma/server/addons/org/functions/fnc_treasuryService.sqf b/arma/server/addons/org/functions/fnc_treasuryService.sqf deleted file mode 100644 index 33cffe8..0000000 --- a/arma/server/addons/org/functions/fnc_treasuryService.sqf +++ /dev/null @@ -1,164 +0,0 @@ -#include "..\script_component.hpp" - -#pragma hemtt ignore_variables ["_self"] -GVAR(OrgTreasuryServiceBase) = compileFinal createHashMapFromArray [ - ["#type", "OrgTreasuryService"], - ["buildChargeResult", compileFinal { - params [["_message", "Unable to process organization payment.", [""]]]; - - createHashMapFromArray [ - ["success", false], - ["message", _message], - ["patch", createHashMap], - ["memberUids", []] - ] - }], - ["resolveOrgMemberUids", compileFinal { - params [["_org", createHashMap, [createHashMap]], ["_requesterUid", "", [""]]]; - - private _memberUids = keys (_org getOrDefault ["members", createHashMap]); - if !(_requesterUid in _memberUids) then { _memberUids pushBack _requesterUid; }; - - _memberUids - }], - ["canManageTreasury", compileFinal { - params [["_orgID", "", [""]], ["_org", createHashMap, [createHashMap]], ["_requesterUid", "", [""]], ["_requesterPlayer", objNull, [objNull]]]; - - private _ownerUid = _org getOrDefault ["owner", ""]; - private _isDefaultOrg = (_orgID isEqualTo "default") || { toLowerANSI _ownerUid isEqualTo "server" }; - private _isDefaultOrgCeo = _isDefaultOrg - && { _requesterPlayer isNotEqualTo objNull } - && { toLowerANSI (vehicleVarName _requesterPlayer) isEqualTo "ceo" }; - - (_ownerUid isEqualTo _requesterUid) || _isDefaultOrgCeo - }], - ["assignCreditLine", compileFinal { - params [["_requesterUid", "", [""]], ["_memberUid", "", [""]], ["_memberName", "", [""]], ["_amount", 0, [0]]]; - - private _result = createHashMapFromArray [ - ["success", false], - ["message", ""], - ["patch", createHashMap], - ["memberUids", []] - ]; - - if ( - _requesterUid isEqualTo "" - || { _memberUid isEqualTo "" } - || { _amount <= 0 } - ) exitWith { - _result set ["message", "A valid requester, member, and credit amount are required."]; - _result - }; - - private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap]; - private _orgID = _requesterActor getOrDefault ["organization", "default"]; - if (_orgID isEqualTo "") then { _orgID = "default"; }; - - private _org = GVAR(OrgStore) call ["loadById", [_orgID]]; - if (_org isEqualTo createHashMap) exitWith { - _result set ["message", "Unable to load organization data for credit line assignment."]; - _result - }; - - private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer); - if !(_self call ["canManageTreasury", [_orgID, _org, _requesterUid, _requesterPlayer]]) exitWith { - _result set ["message", "Only the organization leader or CEO can manage treasury actions."]; - _result - }; - - private _members = _org getOrDefault ["members", createHashMap]; - private _memberRecord = _members getOrDefault [_memberUid, createHashMap]; - if (_memberRecord isEqualTo createHashMap) exitWith { - _result set ["message", "Selected member was not found in the organization roster."]; - _result - }; - - private _resolvedMemberName = _memberRecord getOrDefault ["name", _memberName]; - if (_resolvedMemberName isEqualTo "") then { _resolvedMemberName = _memberName; }; - - private _creditLines = +(_org getOrDefault ["credit_lines", createHashMap]); - _creditLines set [_memberUid, createHashMapFromArray [ - ["uid", _memberUid], - ["name", _resolvedMemberName], - ["amount", _amount] - ]]; - - private _patch = GVAR(OrgStore) call ["set", [GVAR(Registry), "org:update", _orgID, "credit_lines", _creditLines, true]]; - 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 ["patch", _patch]; - _result set ["memberUids", _memberUids]; - _result - }], - ["chargeCheckout", compileFinal { - params [["_requesterUid", "", [""]], ["_requesterPlayer", objNull, [objNull]], ["_source", "org_funds", [""]], ["_amount", 0, [0]], ["_commit", false, [false]]]; - - private _result = _self call ["buildChargeResult", []]; - private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap]; - private _orgID = _requesterActor getOrDefault ["organization", "default"]; - if (_orgID isEqualTo "") then { _orgID = "default"; }; - - private _org = GVAR(Registry) getOrDefault [_orgID, createHashMap]; - if (_org isEqualTo createHashMap) exitWith { - _result set ["message", "Organization data is unavailable for checkout."]; - _result - }; - - private _memberUids = _self call ["resolveOrgMemberUids", [_org, _requesterUid]]; - - switch (toLowerANSI _source) do { - case "org_funds": { - if !(_self call ["canManageTreasury", [_orgID, _org, _requesterUid, _requesterPlayer]]) exitWith { - _result set ["message", "Only the organization leader or CEO can charge org funds."]; - _result - }; - - private _funds = _org getOrDefault ["funds", 0]; - if (_funds < _amount) exitWith { - _result set ["message", "Organization funds cannot cover this checkout."]; - _result - }; - - private _patch = createHashMapFromArray [["funds", (_funds - _amount)]]; - if (_commit) then { _patch = GVAR(OrgStore) call ["mset", [GVAR(Registry), "org:update", _orgID, _patch, false]]; }; - - _result set ["success", true]; - _result set ["message", ""]; - _result set ["patch", _patch]; - _result set ["memberUids", _memberUids]; - _result - }; - case "credit_line": { - private _creditLines = +(_org getOrDefault ["credit_lines", createHashMap]); - private _memberCredit = +(_creditLines getOrDefault [_requesterUid, createHashMap]); - private _creditAmount = _memberCredit getOrDefault ["amount", 0]; - if (_creditAmount < _amount) exitWith { - _result set ["message", "Assigned credit line cannot cover this checkout."]; - _result - }; - - _memberCredit set ["uid", _requesterUid]; - _memberCredit set ["amount", (_creditAmount - _amount)]; - _creditLines set [_requesterUid, _memberCredit]; - - private _patch = createHashMapFromArray [["credit_lines", _creditLines]]; - if (_commit) then { _patch = GVAR(OrgStore) call ["mset", [GVAR(Registry), "org:update", _orgID, _patch, false]]; }; - - _result set ["success", true]; - _result set ["message", ""]; - _result set ["patch", _patch]; - _result set ["memberUids", _memberUids]; - _result - }; - default { - _result set ["message", "Selected organization payment source is unsupported."]; - _result - }; - }; - }] -]; - -GVAR(OrgTreasuryService) = createHashMapObject [GVAR(OrgTreasuryServiceBase)]; diff --git a/arma/server/addons/phone.7z b/arma/server/addons/phone.7z new file mode 100644 index 0000000000000000000000000000000000000000..a8f16b59ac2699bbbfd628ddba1aafd139f416f5 GIT binary patch literal 6025 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 DC?2Pn literal 0 HcmV?d00001 diff --git a/arma/server/addons/store/XEH_preInit.sqf b/arma/server/addons/store/XEH_preInit.sqf index 54bfe32..ed11957 100644 --- a/arma/server/addons/store/XEH_preInit.sqf +++ b/arma/server/addons/store/XEH_preInit.sqf @@ -24,6 +24,26 @@ PREP_RECOMPILE_END; [CRPC(store,responseCategory), [_result], _player] call CFUNC(targetEvent); }] call CFUNC(addEventHandler); +[QGVAR(requestHydrateStore), { + params [["_uid", "", [""]], ["_bridgeEvent", "store::hydrate", [""]]]; + + if (_uid isEqualTo "") exitWith { + diag_log "[FORGE:Server:Store] Invalid hydrate request payload." + }; + + if !(_bridgeEvent in ["store::hydrate", "store::config::hydrate"]) then { + _bridgeEvent = "store::hydrate"; + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (_player isEqualTo objNull) exitWith {}; + + private _payload = GVAR(StoreStore) call ["buildHydratePayload", [_uid]]; + if (_payload isEqualTo createHashMap) exitWith {}; + + [CRPC(store,responseHydrateStore), [_payload, _bridgeEvent], _player] call CFUNC(targetEvent); +}] call CFUNC(addEventHandler); + [QGVAR(requestCheckout), { params [["_uid", "", [""]], ["_payloadJson", "", [""]]]; diff --git a/arma/server/addons/store/functions/fnc_initCatalogService.sqf b/arma/server/addons/store/functions/fnc_initCatalogService.sqf index 2e81ba4..a27f364 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]]]; @@ -399,12 +399,20 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ _resolved }], - ["calculateCheckoutTotal", compileFinal { + ["buildCheckoutRequest", compileFinal { params [["_items", [], [[]]], ["_vehicles", [], [[]]]]; - private _result = createHashMapFromArray [["success", false], ["total", 0], ["message", "Checkout total must be greater than zero."]]; + private _result = createHashMapFromArray [ + ["success", false], + ["total", 0], + ["message", "Checkout total must be greater than zero."], + ["items", []], + ["vehicles", []] + ]; private _total = 0; private _message = ""; + private _resolvedItems = []; + private _resolvedVehicles = []; { if (_message isEqualTo "") then { @@ -419,7 +427,14 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ if (_catalogEntry isEqualTo createHashMap) then { _message = format ["Unsupported store item: %1", _className]; } else { - _total = _total + ((_catalogEntry getOrDefault ["priceValue", 0]) * _quantity); + private _priceValue = _catalogEntry getOrDefault ["priceValue", 0]; + _total = _total + (_priceValue * _quantity); + _resolvedItems pushBack (createHashMapFromArray [ + ["classname", _className], + ["category", _catalogEntry getOrDefault ["category", "item"]], + ["priceValue", _priceValue], + ["quantity", _quantity] + ]); }; }; }; @@ -436,7 +451,13 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ if (_catalogEntry isEqualTo createHashMap) then { _message = format ["Unsupported store vehicle: %1", _className]; } else { - _total = _total + (_catalogEntry getOrDefault ["priceValue", 0]); + private _priceValue = _catalogEntry getOrDefault ["priceValue", 0]; + _total = _total + _priceValue; + _resolvedVehicles pushBack (createHashMapFromArray [ + ["classname", _className], + ["category", _catalogEntry getOrDefault ["category", _x getOrDefault ["category", "other"]]], + ["priceValue", _priceValue] + ]); }; }; }; @@ -452,7 +473,19 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ _result set ["success", true]; _result set ["total", floor _total]; _result set ["message", ""]; + _result set ["items", _resolvedItems]; + _result set ["vehicles", _resolvedVehicles]; _result + }], + ["calculateCheckoutTotal", compileFinal { + params [["_items", [], [[]]], ["_vehicles", [], [[]]]]; + + private _checkout = _self call ["buildCheckoutRequest", [_items, _vehicles]]; + createHashMapFromArray [ + ["success", _checkout getOrDefault ["success", false]], + ["total", _checkout getOrDefault ["total", 0]], + ["message", _checkout getOrDefault ["message", "Checkout total must be greater than zero."]] + ] }] ]; diff --git a/arma/server/addons/store/functions/fnc_initStoreStore.sqf b/arma/server/addons/store/functions/fnc_initStoreStore.sqf index 6e99752..61fb751 100644 --- a/arma/server/addons/store/functions/fnc_initStoreStore.sqf +++ b/arma/server/addons/store/functions/fnc_initStoreStore.sqf @@ -19,6 +19,135 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [ ["#create", compileFinal { ["INFO", "Store checkout service initialized!"] call EFUNC(common,log); }], + ["buildHydratePayload", compileFinal { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { createHashMap }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (isNull _player) exitWith { createHashMap }; + + private _budget = 50000; + private _creditLine = 0; + private _creditLineDue = 0; + private _cashBalance = 0; + private _bankBalance = 0; + private _orgFunds = 0; + private _orgName = ""; + private _orgOwnerUid = ""; + private _orgCreditLines = createHashMap; + private _playerVar = toLowerANSI (vehicleVarName _player); + private _isOrgLeader = false; + private _isDefaultOrg = false; + private _isDefaultOrgCeo = false; + + private _bankAccount = EGVAR(bank,BankStore) call ["get", [_uid, ""]]; + if (_bankAccount isEqualTo createHashMap) then { + _bankAccount = EGVAR(bank,BankStore) call ["init", [_uid]]; + }; + if (_bankAccount isNotEqualTo createHashMap) then { + _cashBalance = _bankAccount getOrDefault ["cash", 0]; + _bankBalance = _bankAccount getOrDefault ["bank", 0]; + }; + + private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; + private _orgId = _actor getOrDefault ["organization", "default"]; + if (_orgId isEqualTo "") then { _orgId = "default"; }; + + private _org = EGVAR(org,OrgStore) call ["loadById", [_orgId]]; + if (_org isEqualTo createHashMap) then { + _org = EGVAR(org,OrgStore) call ["loadById", ["default"]]; + _orgId = _org getOrDefault ["id", "default"]; + }; + + if (_org isNotEqualTo createHashMap) then { + _orgName = _org getOrDefault ["name", ""]; + _orgOwnerUid = _org getOrDefault ["owner", ""]; + _orgFunds = _org getOrDefault ["funds", 0]; + _orgCreditLines = _org getOrDefault ["credit_lines", createHashMap]; + _isDefaultOrg = (_orgId isEqualTo "default") || { toLowerANSI _orgOwnerUid isEqualTo "server" }; + _isOrgLeader = _orgOwnerUid isEqualTo _uid; + _isDefaultOrgCeo = _isDefaultOrg && { _playerVar isEqualTo "ceo" }; + }; + + if (_orgCreditLines isEqualType createHashMap) then { + private _playerCreditLine = _orgCreditLines getOrDefault [_uid, createHashMap]; + if (_playerCreditLine isEqualType createHashMap) then { + _creditLine = _playerCreditLine getOrDefault [ + "available_amount", + _playerCreditLine getOrDefault ["amount", 0] + ]; + _creditLineDue = _playerCreditLine getOrDefault ["amount_due", 0]; + }; + }; + + private _canUseOrgFunds = _isOrgLeader || _isDefaultOrgCeo; + private _orgFundsEnabled = _canUseOrgFunds && { _orgFunds > 0 }; + private _paymentSources = [ + createHashMapFromArray [ + ["id", "cash"], + ["label", "Cash"], + ["balance", _cashBalance], + ["enabled", _cashBalance > 0], + ["detail", "Use on-hand cash carried by the player."] + ], + createHashMapFromArray [ + ["id", "bank"], + ["label", "Bank"], + ["balance", _bankBalance], + ["enabled", _bankBalance > 0], + ["detail", "Charge the player bank account."] + ], + createHashMapFromArray [ + ["id", "org_funds"], + ["label", "Org Funds"], + ["balance", _orgFunds], + ["enabled", _orgFundsEnabled], + ["detail", [ + "Only organization leaders or the default-org CEO can use treasury funds.", + [ + "Charge organization treasury funds.", + "No organization funds are currently available." + ] select _orgFundsEnabled + ] select _canUseOrgFunds] + ], + createHashMapFromArray [ + ["id", "credit_line"], + ["label", "Credit Line"], + ["balance", _creditLine], + ["enabled", _creditLine > 0], + ["detail", [ + "No approved credit line is assigned to this member.", + format [ + "Use the approved procurement credit line. Outstanding due: $%1.", + [_creditLineDue] call EFUNC(common,formatNumber) + ] + ] select (_creditLine > 0)] + ] + ]; + + createHashMapFromArray [ + ["session", createHashMapFromArray [ + ["actorName", name _player], + ["actorUid", _uid], + ["approval", "Field Access"], + ["orgId", _orgId], + ["orgName", _orgName], + ["orgLeader", _isOrgLeader], + ["defaultOrgCeo", _isDefaultOrgCeo], + ["canUseOrgFunds", _canUseOrgFunds] + ]], + ["storeConfig", createHashMapFromArray [ + ["budget", _budget], + ["creditLine", _creditLine], + ["availability", "In-Stock"], + ["moduleState", "Preview"], + ["paymentSources", _paymentSources], + ["defaultPaymentSource", "cash"] + ]], + ["cartItems", []] + ] + }], ["buildResult", compileFinal { params [["_message", "Checkout failed.", [""]], ["_paymentMethod", "cash", [""]]]; @@ -37,47 +166,94 @@ 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]]]; + ["callCheckoutBackendEnvelope", compileFinal { + params [["_context", createHashMap, [createHashMap]]]; - private _result = _self call ["buildResult", ["Unable to process payment.", _paymentMethod]]; - private _payment = switch (toLowerANSI _paymentMethod) do { - case "cash"; - case "bank": { - EGVAR(bank,BankStore) call ["chargeCheckout", [_uid, _paymentMethod, _total, _commit]] - }; - case "org_funds"; - case "credit_line": { - EGVAR(org,OrgStore) call ["chargeCheckout", [_uid, _player, _paymentMethod, _total, _commit]] - }; - default { - createHashMapFromArray [ - ["success", false], - ["message", "Selected payment source is unsupported."], - ["patch", createHashMap], - ["memberUids", []] - ] - }; + private _envelope = createHashMapFromArray [["data", createHashMap], ["error", ""]]; + if (_context isEqualTo createHashMap) exitWith { + _envelope set ["error", "Checkout request was invalid."]; + _envelope }; - if !(_payment getOrDefault ["success", false]) exitWith { - _result set ["message", _payment getOrDefault ["message", "Unable to process payment."]]; - _result + ["store:checkout", [toJSON _context]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + if !(_isSuccess) exitWith { + _envelope set ["error", "Store backend call failed."]; + _envelope + }; + if !(_result isEqualType "") exitWith { + _envelope set ["error", "Store backend returned an invalid response."]; + _envelope + }; + if ((_result find "Error:") == 0) exitWith { + ["ERROR", format ["Store extension checkout failed: %1", _result]] call EFUNC(common,log); + _envelope set ["error", _result select [7]]; + _envelope }; - private _patch = _payment getOrDefault ["patch", createHashMap]; - if ((_paymentMethod isEqualTo "cash") || { _paymentMethod isEqualTo "bank" }) then { - _result set ["bankPatch", _patch]; - } else { - _result set ["orgPatch", _patch]; - _result set ["orgTargetUids", _payment getOrDefault ["memberUids", []]]; + private _data = fromJSON _result; + if !(_data isEqualType createHashMap) exitWith { + _envelope set ["error", "Store backend returned unreadable JSON."]; + _envelope }; - _result set ["success", true]; - _result set ["message", ""]; - _result + _envelope set ["data", _data]; + _envelope + }], + ["buildCheckoutContext", compileFinal { + params [ + ["_uid", "", [""]], + ["_player", objNull, [objNull]], + ["_paymentMethod", "cash", [""]], + ["_items", [], [[]]], + ["_vehicles", [], [[]]] + ]; + + if (_uid isEqualTo "" || { isNull _player }) exitWith { createHashMap }; + + private _orgID = EGVAR(org,OrgStore) call ["resolveOrgIdForUid", [_uid]]; + private _requesterIsDefaultOrgCeo = ( + _orgID isEqualTo "default" + && { toLowerANSI (vehicleVarName _player) isEqualTo "ceo" } + ); + + createHashMapFromArray [ + ["requesterUid", _uid], + ["requesterName", name _player], + ["orgId", _orgID], + ["requesterIsDefaultOrgCeo", _requesterIsDefaultOrgCeo], + ["paymentMethod", toLowerANSI _paymentMethod], + ["items", _items], + ["vehicles", _vehicles] + ] + }], + ["syncCheckoutResult", compileFinal { + params [["_player", objNull, [objNull]], ["_result", createHashMap, [createHashMap]]]; + + if (isNull _player || { _result isEqualTo createHashMap }) exitWith { false }; + + private _lockerPatch = _result getOrDefault ["lockerPatch", createHashMap]; + private _vaPatch = _result getOrDefault ["vaPatch", createHashMap]; + private _vgPatch = _result getOrDefault ["vgaragePatch", createHashMap]; + private _bankPatch = _result getOrDefault ["bankPatch", createHashMap]; + private _orgPatch = _result getOrDefault ["orgPatch", createHashMap]; + + if (keys _lockerPatch isNotEqualTo []) then { [CRPC(locker,responseSyncLocker), [_lockerPatch], _player] call CFUNC(targetEvent); }; + if (keys _vaPatch isNotEqualTo []) then { [CRPC(locker,responseSyncVA), [_vaPatch], _player] call CFUNC(targetEvent); }; + if (keys _vgPatch isNotEqualTo []) then { [CRPC(garage,responseSyncVG), [_vgPatch], _player] call CFUNC(targetEvent); }; + if (keys _bankPatch isNotEqualTo []) then { [CRPC(bank,responseSyncBank), [_bankPatch], _player] call CFUNC(targetEvent); }; + + if (keys _orgPatch isNotEqualTo []) then { + { + private _memberPlayer = [_x] call EFUNC(common,getPlayer); + if (_memberPlayer isNotEqualTo objNull) then { + [CRPC(org,responseSyncOrg), [_orgPatch], _memberPlayer] call CFUNC(targetEvent); + }; + } forEach (_result getOrDefault ["orgTargetUids", []]); + }; + + true }], ["checkout", compileFinal { params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_payloadJson", "", [""]]]; @@ -98,8 +274,8 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [ _result }; - private _priceResult = GVAR(StoreCatalogService) call ["calculateCheckoutTotal", [_items, _vehicles]]; - private _totalPrice = _priceResult getOrDefault ["total", 0]; + private _checkoutRequest = GVAR(StoreCatalogService) call ["buildCheckoutRequest", [_items, _vehicles]]; + private _totalPrice = _checkoutRequest getOrDefault ["total", 0]; _result set ["paymentMethod", _paymentMethod]; _result set ["chargedTotal", _totalPrice]; @@ -109,90 +285,41 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [ _result }; - if !(_priceResult getOrDefault ["success", false]) exitWith { - _result set ["message", _priceResult getOrDefault ["message", "Checkout total must be greater than zero."]]; + if !(_checkoutRequest getOrDefault ["success", false]) exitWith { + _result set ["message", _checkoutRequest getOrDefault ["message", "Checkout total must be greater than zero."]]; _result }; - private _lockerPreview = EGVAR(locker,LockerStore) call ["grantItems", [_uid, _items, false]]; - if !(_lockerPreview getOrDefault ["success", false]) exitWith { - _result set ["message", _lockerPreview getOrDefault ["message", "Locker grant failed."]]; + private _checkoutContext = _self call ["buildCheckoutContext", [ + _uid, + _player, + _paymentMethod, + _checkoutRequest getOrDefault ["items", []], + _checkoutRequest getOrDefault ["vehicles", []] + ]]; + if (_checkoutContext isEqualTo createHashMap) exitWith { + _result set ["message", "Checkout request context was invalid."]; _result }; - private _vaPreview = EGVAR(locker,VAStore) call ["unlockItems", [_uid, _items, false]]; - if !(_vaPreview getOrDefault ["success", false]) exitWith { - _result set ["message", _vaPreview getOrDefault ["message", "VA unlock failed."]]; + private _envelope = _self call ["callCheckoutBackendEnvelope", [_checkoutContext]]; + private _backendResult = _envelope getOrDefault ["data", createHashMap]; + if (_backendResult isEqualTo createHashMap) exitWith { + _result set ["message", _envelope getOrDefault ["error", "Checkout failed."]]; _result }; - private _vgPreview = EGVAR(garage,VGarageStore) call ["grantVehicles", [_uid, _vehicles, false]]; - if !(_vgPreview getOrDefault ["success", false]) exitWith { - _result set ["message", _vgPreview getOrDefault ["message", "Vehicle unlock failed."]]; - _result - }; - - private _orgFleetPreview = createHashMapFromArray [["success", true], ["message", ""], ["patch", createHashMap], ["memberUids", []]]; - if (_paymentMethod isEqualTo "org_funds" && { _vehicles isNotEqualTo [] }) then { - _orgFleetPreview = EGVAR(org,OrgStore) call ["addFleetVehicles", [_uid, _vehicles, false]]; - if !(_orgFleetPreview getOrDefault ["success", false]) exitWith { - _result set ["message", _orgFleetPreview getOrDefault ["message", "Organization fleet update failed."]]; - _result - }; - }; - - _result set ["lockerGranted", _lockerPreview getOrDefault ["granted", []]]; - _result set ["vehicleGranted", _vgPreview getOrDefault ["granted", []]]; - - private _paymentPreview = _self call ["applyPaymentPatch", [_uid, _player, _paymentMethod, _totalPrice, false]]; - if !(_paymentPreview getOrDefault ["success", false]) exitWith { - _result set ["message", _paymentPreview getOrDefault ["message", "Payment failed."]]; - _result - }; - - private _payment = _self call ["applyPaymentPatch", [_uid, _player, _paymentMethod, _totalPrice, true]]; - private _lockerResult = EGVAR(locker,LockerStore) call ["grantItems", [_uid, _items, true]]; - private _vaResult = EGVAR(locker,VAStore) call ["unlockItems", [_uid, _items, true]]; - private _vgResult = EGVAR(garage,VGarageStore) call ["grantVehicles", [_uid, _vehicles, true]]; - private _orgFleetResult = createHashMapFromArray [["success", true], ["message", ""], ["patch", createHashMap], ["memberUids", []]]; - if (_paymentMethod isEqualTo "org_funds" && { _vehicles isNotEqualTo [] }) then { - _orgFleetResult = EGVAR(org,OrgStore) call ["addFleetVehicles", [_uid, _vehicles, true]]; - }; - - private _lockerPatch = _lockerResult getOrDefault ["patch", createHashMap]; - private _vaPatch = _vaResult getOrDefault ["patch", createHashMap]; - private _vgPatch = _vgResult getOrDefault ["patch", createHashMap]; - if (keys _lockerPatch isNotEqualTo []) then { [CRPC(locker,responseSyncLocker), [_lockerPatch], _player] call CFUNC(targetEvent); }; - if (keys _vaPatch isNotEqualTo []) then { [CRPC(locker,responseSyncVA), [_vaPatch], _player] call CFUNC(targetEvent); }; - if (keys _vgPatch isNotEqualTo []) then { [CRPC(garage,responseSyncVG), [_vgPatch], _player] call CFUNC(targetEvent); }; - - private _bankPatch = _payment getOrDefault ["bankPatch", createHashMap]; - if (keys _bankPatch isNotEqualTo []) then { [CRPC(bank,responseSyncBank), [_bankPatch], _player] call CFUNC(targetEvent); }; - - private _orgPatch = _payment getOrDefault ["orgPatch", createHashMap]; - private _orgFleetPatch = _orgFleetResult getOrDefault ["patch", createHashMap]; - if (keys _orgFleetPatch isNotEqualTo []) then { { _orgPatch set [_x, _y]; } forEach _orgFleetPatch; }; - if (keys _orgPatch isNotEqualTo []) then { - private _orgTargetUids = _payment getOrDefault ["orgTargetUids", []]; - { - if !(_x in _orgTargetUids) then { _orgTargetUids pushBack _x; }; - } forEach (_orgFleetResult getOrDefault ["memberUids", []]); - - { - private _memberPlayer = [_x] call EFUNC(common,getPlayer); - if (_memberPlayer isNotEqualTo objNull) then { [CRPC(org,responseSyncOrg), [_orgPatch], _memberPlayer] call CFUNC(targetEvent); }; - } forEach _orgTargetUids; - }; + _self call ["syncCheckoutResult", [_player, _backendResult]]; _result set ["success", true]; - _result set ["message", format [ + _result set ["message", _backendResult getOrDefault ["message", format [ "Checkout completed. %1 charged, %2 locker grant(s), %3 vehicle unlock(s).", _self call ["formatCurrency", [_totalPrice]], - count (_lockerResult getOrDefault ["granted", []]), - count (_vgResult getOrDefault ["granted", []]) - ]]; - _result set ["lockerGranted", _lockerResult getOrDefault ["granted", []]]; - _result set ["vehicleGranted", _vgResult getOrDefault ["granted", []]]; + count (_backendResult getOrDefault ["lockerGranted", []]), + count (_backendResult getOrDefault ["vehicleGranted", []]) + ]]]; + _result set ["lockerGranted", _backendResult getOrDefault ["lockerGranted", []]]; + _result set ["vehicleGranted", _backendResult getOrDefault ["vehicleGranted", []]]; _result }] ]; 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..c3fddf6 --- /dev/null +++ b/arma/server/addons/task/XEH_preInit.sqf @@ -0,0 +1,7 @@ +#include "script_component.hpp" + +PREP_RECOMPILE_START; +#include "XEH_PREP.hpp" +PREP_RECOMPILE_END; + +call FUNC(initTaskStore); 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..e0e83bf --- /dev/null +++ b/arma/server/addons/task/functions/fnc_handleTaskRewards.sqf @@ -0,0 +1,225 @@ +#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,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 _nextFunds = (_org getOrDefault ["funds", 0]) + _funds; + _org set ["funds", _nextFunds]; + private _updatedOrg = EGVAR(org,OrgStore) call [ + "callHotOrg", + [ + "org:hot:override", + [_orgID, toJSON _org] + ] + ]; + + if (_updatedOrg isEqualTo createHashMap) then { + ["ERROR", format ["Failed to update organization %1 funds for task %2.", _orgID, _taskID]] call EFUNC(common,log); + _success = false; + } else { + private _patch = createHashMapFromArray [["funds", _nextFunds]]; + + [_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,OrgStore) call ["loadById", [_orgID]]; + 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..0349b27 --- /dev/null +++ b/arma/server/addons/task/functions/fnc_handler.sqf @@ -0,0 +1,104 @@ +#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,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..f0410f9 --- /dev/null +++ b/arma/server/addons/task/functions/fnc_initTaskStore.sqf @@ -0,0 +1,563 @@ +#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 ["taskEntityRegistries", createHashMapFromArray [ + ["cargo", createHashMap], + ["hostages", createHashMap], + ["hvts", createHashMap], + ["ieds", createHashMap], + ["entities", createHashMap], + ["shooters", createHashMap], + ["targets", createHashMap] + ]]; + + ["task:reset", []] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + if ( + !_isSuccess + || { !(_result isEqualType "") } + || { (_result find "Error:") == 0 } + ) then { + ["WARNING", "Failed to reset task backend state during task store initialization."] call EFUNC(common,log); + }; + }], + ["callTaskStateEnvelope", compileFinal { + params [["_function", "", [""]], ["_arguments", [], [[]]]]; + + private _envelope = createHashMapFromArray [ + ["success", false], + ["error", ""] + ]; + + if (_function isEqualTo "") exitWith { _envelope }; + + [_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + if !_isSuccess exitWith { + _envelope set ["error", format ["Task backend call '%1' failed.", _function]]; + _envelope + }; + if !(_result isEqualType "") exitWith { + _envelope set ["error", format ["Task backend call '%1' returned an invalid response.", _function]]; + _envelope + }; + if ((_result find "Error:") == 0) exitWith { + ["ERROR", format ["Task extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log); + _envelope set ["error", _result select [7]]; + _envelope + }; + + _envelope set ["success", true]; + if (_result isNotEqualTo "") then { + _envelope set ["data", fromJSON _result]; + }; + + _envelope + }], + ["callTaskState", compileFinal { + params [["_function", "", [""]], ["_arguments", [], [[]]], ["_fallback", nil]]; + + private _envelope = _self call ["callTaskStateEnvelope", [_function, _arguments]]; + if !(_envelope getOrDefault ["success", false]) exitWith { _fallback }; + + _envelope getOrDefault ["data", _fallback] + }], + ["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 + }; + + private _orgID = "default"; + + if (_requesterUid isNotEqualTo "") then { + 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 + }; + + _orgID = _actor getOrDefault ["organization", ""]; + if (_orgID isEqualTo "") then { _orgID = "default"; }; + }; + + private _context = createHashMapFromArray [ + ["requesterUid", _requesterUid], + ["orgId", _orgID] + ]; + private _envelope = _self call [ + "callTaskStateEnvelope", + [ + "task:ownership:bind", + [_taskID, toJSON _context] + ] + ]; + if !(_envelope getOrDefault ["success", false]) exitWith { + _result set ["message", _envelope getOrDefault ["error", "Failed to bind task ownership."]]; + _result + }; + + private _bindResult = _envelope getOrDefault ["data", createHashMap]; + _result set ["success", true]; + _result set ["message", _bindResult getOrDefault [ + "message", + ["No requester UID provided. Bound task to default organization.", "Task ownership updated."] select (_requesterUid isNotEqualTo "") + ]]; + _result set ["orgID", _bindResult getOrDefault ["orgId", _orgID]]; + _result + }], + ["releaseTaskOwnership", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + private _envelope = _self call ["callTaskStateEnvelope", ["task:ownership:release", [_taskID]]]; + _envelope getOrDefault ["success", false] + }], + ["registerTaskCatalogEntry", compileFinal { + params [["_taskID", "", [""]], ["_entry", createHashMap, [createHashMap]]]; + + if (_taskID isEqualTo "" || { _entry isEqualTo createHashMap }) exitWith { false }; + + private _envelope = _self call [ + "callTaskStateEnvelope", + [ + "task:catalog:upsert", + [_taskID, toJSON _entry] + ] + ]; + _envelope getOrDefault ["success", false] + }], + ["getActiveTaskCatalog", compileFinal { + private _entries = _self call ["callTaskState", ["task:catalog:active", [], []]]; + if !(_entries isEqualType []) exitWith { [] }; + + _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 + }; + + 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 _context = createHashMapFromArray [ + ["requesterUid", _requesterUid], + ["orgId", _orgID] + ]; + private _envelope = _self call [ + "callTaskStateEnvelope", + [ + "task:ownership:accept", + [_taskID, toJSON _context] + ] + ]; + if !(_envelope getOrDefault ["success", false]) exitWith { + _result set ["message", _envelope getOrDefault ["error", "Unable to accept task."]]; + _result + }; + + private _acceptResult = _envelope getOrDefault ["data", createHashMap]; + private _entry = _acceptResult getOrDefault ["entry", createHashMap]; + if !(_entry isEqualType createHashMap) then { + _entry = createHashMap; + }; + + _result set ["success", true]; + _result set ["message", _acceptResult getOrDefault ["message", "Task accepted."]]; + _result set ["entry", _entry]; + _result + }], + ["setTaskStatus", compileFinal { + params [["_taskID", "", [""]], ["_status", "", [""]]]; + + if (_taskID isEqualTo "" || { _status isEqualTo "" }) exitWith { false }; + + [(_self call ["callTaskState", ["task:status:set", [_taskID, _status], false]])] params [["_statusResult", false, [false]]]; + + _statusResult + }], + ["getTaskStatus", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { "" }; + + private _status = _self call ["callTaskState", ["task:status:get", [_taskID], ""]]; + if !(_status isEqualType "") exitWith { "" }; + + _status + }], + ["clearTaskStatus", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + [(_self call ["callTaskState", ["task:status:clear", [_taskID], false]])] params [["_statusResult", false, [false]]]; + + _statusResult + }], + ["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 _rewardState = _self call ["callTaskState", ["task:ownership:reward_context", [_taskID], createHashMap]]; + if (_rewardState isEqualTo createHashMap) exitWith { _result }; + + private _requesterUid = _rewardState getOrDefault ["requesterUid", ""]; + private _resolvedOrgID = _rewardState getOrDefault ["orgId", ""]; + if (_resolvedOrgID isEqualTo "") exitWith { _result }; + + private _org = EGVAR(org,OrgStore) call ["loadById", [_resolvedOrgID]]; + private _memberUids = []; + if (_org isNotEqualTo createHashMap) then { + private _members = _org getOrDefault ["members", createHashMap]; + if (_members isEqualType createHashMap) then { + _memberUids = keys _members; + }; + if (_requesterUid isNotEqualTo "" && { !(_requesterUid in _memberUids) }) then { + _memberUids pushBack _requesterUid; + }; + }; + + _result set ["requesterUid", _requesterUid]; + _result set ["orgID", _resolvedOrgID]; + _result set ["memberUids", _memberUids]; + _result + }], + ["incrementDefuseCount", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { 0 }; + + private _nextCount = _self call ["callTaskState", ["task:defuse:increment", [_taskID], 0]]; + if !(_nextCount isEqualType 0) exitWith { 0 }; + + _nextCount + }], + ["getDefuseCount", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { 0 }; + + private _defuseCount = _self call ["callTaskState", ["task:defuse:get", [_taskID], 0]]; + if !(_defuseCount isEqualType 0) exitWith { 0 }; + + _defuseCount + }], + ["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]; + _participantRegistry deleteAt _taskID; + _self set ["participantRegistry", _participantRegistry]; + _self call ["callTaskState", ["task:clear", [_taskID], false]]; + _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 _rewardContext = _self call ["resolveRewardContext", [_taskID]]; + private _participantUids = keys _participantSnapshots; + if (_participantUids isEqualTo [] && { _delta > 0 }) then { + private _requesterUid = _rewardContext getOrDefault ["requesterUid", ""]; + if (_requesterUid isNotEqualTo "") then { + private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer); + if (!isNull _requesterPlayer) then { + _participantUids pushBack _requesterUid; + _participantSnapshots set [_requesterUid, createHashMapFromArray [ + ["startRating", rating _requesterPlayer] + ]]; + _participantRegistry set [_taskID, _participantSnapshots]; + _self set ["participantRegistry", _participantRegistry]; + ["WARNING", format ["Task %1 had no tracked participants at payout time; falling back to requester %2 for personal earnings.", _taskID, _requesterUid]] call EFUNC(common,log); + }; + }; + }; + if (_participantUids isEqualTo []) exitWith { _result }; + + private _orgIds = []; + private _contributions = createHashMap; + private _totalContribution = 0; + + if (_delta > 0) then { + { + private _uid = _x; + private _player = [_uid] call EFUNC(common,getPlayer); + if (isNull _player) then { continue; }; + + _contributions set [_uid, 1]; + _totalContribution = _totalContribution + 1; + } 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,BankStore) call ["get", [_uid, ""]]; + 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", + [ + _uid, + createHashMapFromArray [["earnings", (_earnings + _earningsDelta)]], + false + ] + ]; + if !(_patch isEqualType createHashMap) then { continue; }; + if (_patch isEqualTo createHashMap) then { continue; }; + + EGVAR(bank,BankMessenger) call ["sendAccountSync", [_uid, _patch]]; + }; + }; + } forEach _participantUids; + + private _ownerOrgID = _rewardContext getOrDefault ["orgID", ""]; + if (_ownerOrgID isNotEqualTo "") then { + private _org = EGVAR(org,OrgStore) call ["loadById", [_ownerOrgID]]; + + if (_org isNotEqualTo createHashMap) then { + private _reputation = _org getOrDefault ["reputation", 0]; + private _nextReputation = round (_reputation + _delta); + _org set ["reputation", _nextReputation]; + private _updatedOrg = EGVAR(org,OrgStore) call [ + "callHotOrg", + [ + "org:hot:override", + [_ownerOrgID, toJSON _org] + ] + ]; + + if (_updatedOrg isNotEqualTo createHashMap) then { + private _patch = createHashMapFromArray [["reputation", _nextReputation]]; + 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]; + } else { + ["ERROR", format ["Failed to update organization %1 reputation for task %2.", _ownerOrgID, _taskID]] call EFUNC(common,log); + }; + }; + }; + + _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/actor.rs b/arma/server/extension/src/actor.rs index f11f103..53f3a68 100644 --- a/arma/server/extension/src/actor.rs +++ b/arma/server/extension/src/actor.rs @@ -4,11 +4,12 @@ //! Handles SQF command mapping and parameter validation. use arma_rs::{CallContext, Group}; -use forge_repositories::RedisActorRepository; -use forge_services::ActorService; +use forge_repositories::{InMemoryActorHotRepository, RedisActorRepository}; +use forge_services::{ActorHotStateService, ActorService}; use std::sync::LazyLock; use crate::adapters::ExtensionRedisClient; +use crate::enqueue_persistence_task; use crate::helpers::resolve_uid; use crate::log::log; @@ -21,6 +22,22 @@ static ACTOR_SERVICE: LazyLock, InMemoryActorHotRepository>, +> = LazyLock::new(|| { + let redis_client = ExtensionRedisClient::new(); + let repository = RedisActorRepository::new(redis_client); + let hot_repository = InMemoryActorHotRepository::new(); + ActorHotStateService::new(repository, hot_repository) +}); + +#[allow(dead_code)] +pub(crate) fn hot_service() -> &'static ActorHotStateService< + RedisActorRepository, + InMemoryActorHotRepository, +> { + &HOT_ACTOR_SERVICE +} /// Creates the Arma 3 command group for actor operations. /// @@ -32,6 +49,91 @@ pub fn group() -> Group { .command("update", update_actor) .command("exists", actor_exists) .command("delete", delete_actor) + .group( + "hot", + Group::new() + .command("init", init_hot_actor) + .command("get", get_hot_actor) + .command("override", override_hot_actor) + .command("save", save_hot_actor) + .command("remove", remove_hot_actor), + ) +} + +fn serialize_hot_actor(actor: forge_models::Actor) -> String { + match serde_json::to_string(&actor) { + Ok(json) => json, + Err(error) => format!("Error: Failed to serialize hot actor: {}", error), + } +} + +pub(crate) fn init_hot_actor(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_ACTOR_SERVICE.init_actor(resolved_uid) { + Ok(actor) => serialize_hot_actor(actor), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn get_hot_actor(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_ACTOR_SERVICE.get_actor(resolved_uid) { + Ok(actor) => serialize_hot_actor(actor), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn override_hot_actor( + call_context: CallContext, + key: String, + json_data: String, +) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_ACTOR_SERVICE.override_actor(resolved_uid, json_data) { + Ok(actor) => serialize_hot_actor(actor), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn save_hot_actor(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_ACTOR_SERVICE.get_actor(resolved_uid.clone()) { + Ok(actor) => { + enqueue_persistence_task("actor", move || { + HOT_ACTOR_SERVICE.save_actor(resolved_uid).map(|_| ()) + }); + serialize_hot_actor(actor) + } + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn remove_hot_actor(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_ACTOR_SERVICE.remove_actor(resolved_uid) { + Ok(_) => "OK".to_string(), + Err(error) => format!("Error: {}", error), + } } /// Retrieves an actor by key/UID. diff --git a/arma/server/extension/src/bank.rs b/arma/server/extension/src/bank.rs index 5e3d85e..64f6492 100644 --- a/arma/server/extension/src/bank.rs +++ b/arma/server/extension/src/bank.rs @@ -4,11 +4,16 @@ //! Handles SQF command mapping and parameter validation. use arma_rs::{CallContext, Group}; -use forge_repositories::RedisBankRepository; -use forge_services::BankService; +use forge_models::{ + BankCheckoutContext, BankMutationResult, BankOperationContext, BankPinContext, + BankTransferContext, BankTransferResult, +}; +use forge_repositories::{InMemoryBankHotRepository, RedisBankRepository}; +use forge_services::{BankHotStateService, BankService}; use std::sync::LazyLock; use crate::adapters::ExtensionRedisClient; +use crate::enqueue_persistence_task; use crate::helpers::resolve_uid; use crate::log::log; @@ -21,6 +26,21 @@ static BANK_SERVICE: LazyLock, InMemoryBankHotRepository>, +> = LazyLock::new(|| { + let redis_client = ExtensionRedisClient::new(); + let repository = RedisBankRepository::new(redis_client); + let hot_repository = InMemoryBankHotRepository::new(); + BankHotStateService::new(repository, hot_repository) +}); + +pub(crate) fn hot_service() -> &'static BankHotStateService< + RedisBankRepository, + InMemoryBankHotRepository, +> { + &HOT_BANK_SERVICE +} /// Creates the Arma 3 command group for bank operations. /// @@ -32,6 +52,305 @@ pub fn group() -> Group { .command("update", update_bank) .command("exists", bank_exists) .command("delete", delete_bank) + .group( + "hot", + Group::new() + .command("init", init_hot_bank) + .command("get", get_hot_bank) + .command("override", override_hot_bank) + .command("patch", patch_hot_bank) + .command("charge_checkout", charge_checkout_hot_bank) + .command("deposit", deposit_hot_bank) + .command("withdraw", withdraw_hot_bank) + .command("deposit_earnings", deposit_earnings_hot_bank) + .command("transfer", transfer_hot_bank) + .command("validate_pin", validate_pin_hot_bank) + .command("save", save_hot_bank) + .command("remove", remove_hot_bank), + ) +} + +fn serialize_hot_bank(bank: forge_models::Bank) -> String { + match serde_json::to_string(&bank) { + Ok(json) => json, + Err(error) => format!("Error: Failed to serialize hot bank: {}", error), + } +} + +fn serialize_hot_bank_mutation(result: BankMutationResult) -> String { + match serde_json::to_string(&result) { + Ok(json) => json, + Err(error) => format!("Error: Failed to serialize hot bank mutation: {}", error), + } +} + +fn serialize_hot_bank_transfer(result: BankTransferResult) -> String { + match serde_json::to_string(&result) { + Ok(json) => json, + Err(error) => format!("Error: Failed to serialize hot bank transfer: {}", error), + } +} + +fn parse_amount(amount: String, label: &str) -> Result { + amount + .parse::() + .map_err(|error| format!("Invalid {} amount '{}': {}", label, amount, error)) +} + +fn parse_operation_context(json_context: String) -> Result { + serde_json::from_str(&json_context) + .map_err(|error| format!("Invalid bank operation context: {}", error)) +} + +fn parse_transfer_context(json_context: String) -> Result { + serde_json::from_str(&json_context) + .map_err(|error| format!("Invalid bank transfer context: {}", error)) +} + +fn parse_checkout_context(json_context: String) -> Result { + serde_json::from_str(&json_context) + .map_err(|error| format!("Invalid bank checkout context: {}", error)) +} + +fn parse_pin_context(json_context: String) -> Result { + serde_json::from_str(&json_context) + .map_err(|error| format!("Invalid bank PIN context: {}", error)) +} + +pub(crate) fn init_hot_bank(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_BANK_SERVICE.init_bank(resolved_uid) { + Ok(bank) => serialize_hot_bank(bank), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn get_hot_bank(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_BANK_SERVICE.get_bank(resolved_uid) { + Ok(bank) => serialize_hot_bank(bank), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn override_hot_bank( + call_context: CallContext, + key: String, + json_data: String, +) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_BANK_SERVICE.override_bank(resolved_uid.clone(), json_data) { + Ok(bank) => serialize_hot_bank(bank), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn patch_hot_bank(call_context: CallContext, key: String, json_patch: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_BANK_SERVICE.patch_bank(resolved_uid, json_patch) { + Ok(result) => serialize_hot_bank_mutation(result), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn charge_checkout_hot_bank( + call_context: CallContext, + key: String, + amount: String, + json_context: String, +) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + let amount = match parse_amount(amount, "checkout") { + Ok(value) => value, + Err(error) => return format!("Error: {}", error), + }; + let context = match parse_checkout_context(json_context) { + Ok(value) => value, + Err(error) => return format!("Error: {}", error), + }; + + match HOT_BANK_SERVICE.charge_checkout(resolved_uid, amount, context) { + Ok(result) => serialize_hot_bank_mutation(result), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn deposit_hot_bank( + call_context: CallContext, + key: String, + amount: String, + json_context: String, +) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + let amount = match parse_amount(amount, "deposit") { + Ok(value) => value, + Err(error) => return format!("Error: {}", error), + }; + let context = match parse_operation_context(json_context) { + Ok(value) => value, + Err(error) => return format!("Error: {}", error), + }; + + match HOT_BANK_SERVICE.deposit(resolved_uid, amount, context) { + Ok(result) => serialize_hot_bank_mutation(result), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn withdraw_hot_bank( + call_context: CallContext, + key: String, + amount: String, + json_context: String, +) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + let amount = match parse_amount(amount, "withdraw") { + Ok(value) => value, + Err(error) => return format!("Error: {}", error), + }; + let context = match parse_operation_context(json_context) { + Ok(value) => value, + Err(error) => return format!("Error: {}", error), + }; + + match HOT_BANK_SERVICE.withdraw(resolved_uid, amount, context) { + Ok(result) => serialize_hot_bank_mutation(result), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn deposit_earnings_hot_bank( + call_context: CallContext, + key: String, + amount: String, + json_context: String, +) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + let amount = match parse_amount(amount, "deposit earnings") { + Ok(value) => value, + Err(error) => return format!("Error: {}", error), + }; + let context = match parse_operation_context(json_context) { + Ok(value) => value, + Err(error) => return format!("Error: {}", error), + }; + + match HOT_BANK_SERVICE.deposit_earnings(resolved_uid, amount, context) { + Ok(result) => serialize_hot_bank_mutation(result), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn transfer_hot_bank( + call_context: CallContext, + source_key: String, + target_key: String, + amount: String, + json_context: String, +) -> String { + let resolved_source_uid = match resolve_uid(&source_key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", source_key), + }; + let resolved_target_uid = match resolve_uid(&target_key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", target_key), + }; + let amount = match parse_amount(amount, "transfer") { + Ok(value) => value, + Err(error) => return format!("Error: {}", error), + }; + let context = match parse_transfer_context(json_context) { + Ok(value) => value, + Err(error) => return format!("Error: {}", error), + }; + + match HOT_BANK_SERVICE.transfer(resolved_source_uid, resolved_target_uid, context, amount) { + Ok(result) => serialize_hot_bank_transfer(result), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn validate_pin_hot_bank( + call_context: CallContext, + key: String, + pin: String, + json_context: String, +) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + let context = match parse_pin_context(json_context) { + Ok(value) => value, + Err(error) => return format!("Error: {}", error), + }; + + match HOT_BANK_SERVICE.validate_pin(resolved_uid, pin, context) { + Ok(_) => "{}".to_string(), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn save_hot_bank(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_BANK_SERVICE.get_bank(resolved_uid.clone()) { + Ok(bank) => { + enqueue_persistence_task("bank", move || { + HOT_BANK_SERVICE.save_bank(resolved_uid).map(|_| ()) + }); + serialize_hot_bank(bank) + } + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn remove_hot_bank(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_BANK_SERVICE.remove_bank(resolved_uid) { + Ok(_) => "OK".to_string(), + Err(error) => format!("Error: {}", error), + } } /// Retrieves an bank by key/UID. diff --git a/arma/server/extension/src/cad.rs b/arma/server/extension/src/cad.rs new file mode 100644 index 0000000..5984e99 --- /dev/null +++ b/arma/server/extension/src/cad.rs @@ -0,0 +1,183 @@ +//! CAD hot-state operations for the Arma 3 server extension. +//! +//! The extension owns the in-memory CAD state store, while the shared service +//! layer handles mutation rules and hydrate shaping. This keeps the extension +//! surface thin and aligned with the workspace architecture. + +use arma_rs::Group; +use forge_repositories::InMemoryCadRepository; +use forge_services::CadStateService; +use serde::Serialize; +use std::sync::LazyLock; + +static CAD_SERVICE: LazyLock> = + LazyLock::new(|| CadStateService::new(InMemoryCadRepository::new())); + +pub fn group() -> Group { + Group::new() + .group( + "activity", + Group::new() + .command("append", append_activity) + .command("recent", recent_activity), + ) + .group( + "assignments", + Group::new() + .command("list", list_assignments) + .command("assign", assign_assignment) + .command("acknowledge", acknowledge_assignment) + .command("decline", decline_assignment) + .command("upsert", upsert_assignment) + .command("delete", delete_assignment), + ) + .group( + "orders", + Group::new() + .command("list", list_orders) + .command("create", create_order) + .command("create_from_context", create_order_from_context) + .command("close", close_order) + .command("upsert", upsert_order) + .command("delete", delete_order), + ) + .group( + "requests", + Group::new() + .command("list", list_requests) + .command("submit", submit_request) + .command("submit_from_context", submit_request_from_context) + .command("close", close_request) + .command("upsert", upsert_request) + .command("delete", delete_request), + ) + .group( + "profiles", + Group::new() + .command("list", list_profiles) + .command("update_from_context", update_profile_from_context) + .command("upsert", upsert_profile) + .command("delete", delete_profile), + ) + .group("groups", Group::new().command("build", build_groups)) + .group("view", Group::new().command("hydrate", hydrate_view)) +} + +pub(crate) fn append_activity(json_data: String) -> String { + serialize_ok(CAD_SERVICE.append_activity(json_data)) +} + +pub(crate) fn recent_activity(limit: String) -> String { + serialize_json(CAD_SERVICE.recent_activity(limit)) +} + +pub(crate) fn list_assignments() -> String { + serialize_json(CAD_SERVICE.list_assignments()) +} + +pub(crate) fn assign_assignment(entry_id: String, json_data: String) -> String { + serialize_json(CAD_SERVICE.assign_assignment(entry_id, json_data)) +} + +pub(crate) fn acknowledge_assignment(entry_id: String, json_data: String) -> String { + serialize_json(CAD_SERVICE.acknowledge_assignment(entry_id, json_data)) +} + +pub(crate) fn decline_assignment(entry_id: String, json_data: String) -> String { + serialize_json(CAD_SERVICE.decline_assignment(entry_id, json_data)) +} + +pub(crate) fn upsert_assignment(entry_id: String, json_data: String) -> String { + serialize_ok(CAD_SERVICE.upsert_assignment(entry_id, json_data)) +} + +pub(crate) fn delete_assignment(entry_id: String) -> String { + serialize_ok(CAD_SERVICE.delete_assignment(entry_id)) +} + +pub(crate) fn list_orders() -> String { + serialize_json(CAD_SERVICE.list_orders()) +} + +pub(crate) fn create_order(json_data: String) -> String { + serialize_json(CAD_SERVICE.create_order(json_data)) +} + +pub(crate) fn create_order_from_context(json_data: String) -> String { + serialize_json(CAD_SERVICE.create_order_from_context(json_data)) +} + +pub(crate) fn close_order(entry_id: String) -> String { + serialize_json(CAD_SERVICE.close_order(entry_id)) +} + +pub(crate) fn upsert_order(entry_id: String, json_data: String) -> String { + serialize_ok(CAD_SERVICE.upsert_order(entry_id, json_data)) +} + +pub(crate) fn delete_order(entry_id: String) -> String { + serialize_ok(CAD_SERVICE.delete_order(entry_id)) +} + +pub(crate) fn list_requests() -> String { + serialize_json(CAD_SERVICE.list_requests()) +} + +pub(crate) fn submit_request(json_data: String) -> String { + serialize_json(CAD_SERVICE.submit_request(json_data)) +} + +pub(crate) fn submit_request_from_context(json_data: String) -> String { + serialize_json(CAD_SERVICE.submit_request_from_context(json_data)) +} + +pub(crate) fn close_request(entry_id: String) -> String { + serialize_json(CAD_SERVICE.close_request(entry_id)) +} + +pub(crate) fn upsert_request(entry_id: String, json_data: String) -> String { + serialize_ok(CAD_SERVICE.upsert_request(entry_id, json_data)) +} + +pub(crate) fn delete_request(entry_id: String) -> String { + serialize_ok(CAD_SERVICE.delete_request(entry_id)) +} + +pub(crate) fn list_profiles() -> String { + serialize_json(CAD_SERVICE.list_profiles()) +} + +pub(crate) fn update_profile_from_context(json_data: String) -> String { + serialize_json(CAD_SERVICE.update_profile_from_context(json_data)) +} + +pub(crate) fn upsert_profile(entry_id: String, json_data: String) -> String { + serialize_ok(CAD_SERVICE.upsert_profile(entry_id, json_data)) +} + +pub(crate) fn delete_profile(entry_id: String) -> String { + serialize_ok(CAD_SERVICE.delete_profile(entry_id)) +} + +pub(crate) fn build_groups(json_data: String) -> String { + serialize_json(CAD_SERVICE.build_groups(json_data)) +} + +pub(crate) fn hydrate_view(json_data: String) -> String { + serialize_json(CAD_SERVICE.build_hydrate_payload(json_data)) +} + +fn serialize_ok(result: Result<(), String>) -> String { + match result { + Ok(()) => "OK".to_string(), + Err(error) => format!("Error: {error}"), + } +} + +fn serialize_json(result: Result) -> String { + match result { + Ok(value) => serde_json::to_string(&value) + .unwrap_or_else(|error| format!("Error: Failed to serialize CAD state: {error}")), + Err(error) => format!("Error: {error}"), + } +} diff --git a/arma/server/extension/src/garage.rs b/arma/server/extension/src/garage.rs index c1488a2..a5e8a67 100644 --- a/arma/server/extension/src/garage.rs +++ b/arma/server/extension/src/garage.rs @@ -4,12 +4,13 @@ use arma_rs::{CallContext, Group}; use forge_models::Vehicle; -use forge_repositories::RedisGarageRepository; -use forge_services::GarageService; +use forge_repositories::{InMemoryGarageHotRepository, RedisGarageRepository}; +use forge_services::{GarageHotStateService, GarageService}; use std::collections::HashMap; use std::sync::LazyLock; use crate::adapters::ExtensionRedisClient; +use crate::enqueue_persistence_task; use crate::helpers::resolve_uid; use crate::log::log; @@ -20,6 +21,22 @@ static GARAGE_SERVICE: LazyLock, InMemoryGarageHotRepository>, +> = LazyLock::new(|| { + let redis_client = ExtensionRedisClient::new(); + let repository = RedisGarageRepository::new(redis_client); + let hot_repository = InMemoryGarageHotRepository::new(); + GarageHotStateService::new(repository, hot_repository) +}); + +#[allow(dead_code)] +pub(crate) fn hot_service() -> &'static GarageHotStateService< + RedisGarageRepository, + InMemoryGarageHotRepository, +> { + &HOT_GARAGE_SERVICE +} /// Creates the Arma 3 command group for garage operations. /// @@ -34,6 +51,153 @@ pub fn group() -> Group { .command("remove", remove_vehicle) .command("delete", delete_garage) .command("exists", garage_exists) + .group( + "hot", + Group::new() + .command("init", init_hot_garage) + .command("get", get_hot_garage) + .command("override", override_hot_garage) + .command("save", save_hot_garage) + .command("remove", remove_hot_garage) + .command("add", add_hot_vehicle) + .command("remove_vehicle", remove_hot_vehicle), + ) +} + +fn serialize_hot_vehicles(garage: forge_models::garage::Garage) -> String { + match serde_json::to_string(&garage.vehicles) { + Ok(json) => json, + Err(error) => format!("Error: Failed to serialize hot garage: {}", error), + } +} + +pub(crate) fn init_hot_garage(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_GARAGE_SERVICE.init_garage(resolved_uid) { + Ok(garage) => serialize_hot_vehicles(garage), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn get_hot_garage(call_context: CallContext, key: String) -> String { + init_hot_garage(call_context, key) +} + +pub(crate) fn override_hot_garage( + call_context: CallContext, + key: String, + json_data: String, +) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + let vehicles: HashMap = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid JSON data: {}", error), + }; + + match HOT_GARAGE_SERVICE.override_garage(resolved_uid, vehicles) { + Ok(garage) => serialize_hot_vehicles(garage), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn save_hot_garage(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_GARAGE_SERVICE.get_garage(resolved_uid.clone()) { + Ok(garage) => { + enqueue_persistence_task("garage", move || { + HOT_GARAGE_SERVICE.save_garage(resolved_uid).map(|_| ()) + }); + serialize_hot_vehicles(garage) + } + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn remove_hot_garage(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_GARAGE_SERVICE.remove_garage(resolved_uid) { + Ok(_) => "OK".to_string(), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn add_hot_vehicle(call_context: CallContext, key: String, json_data: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + let data: serde_json::Value = match serde_json::from_str(&json_data) { + Ok(d) => d, + Err(error) => return format!("Error: Invalid JSON data: {}", error), + }; + + let classname = match data.get("classname").and_then(|v| v.as_str()) { + Some(c) => c.to_string(), + None => return "Error: Missing or invalid classname".to_string(), + }; + let fuel = match data.get("fuel").and_then(|v| v.as_f64()) { + Some(f) => f, + None => return "Error: Missing or invalid fuel".to_string(), + }; + let damage = match data.get("damage").and_then(|v| v.as_f64()) { + Some(d) => d, + None => return "Error: Missing or invalid damage".to_string(), + }; + let hit_points_json = match data.get("hit_points") { + Some(hp) => match serde_json::to_string(hp) { + Ok(s) => s, + Err(error) => return format!("Error: Failed to serialize hit_points: {}", error), + }, + None => return "Error: Missing hit_points".to_string(), + }; + + match HOT_GARAGE_SERVICE.add_vehicle(resolved_uid, classname, fuel, damage, hit_points_json) { + Ok(garage) => serialize_hot_vehicles(garage), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn remove_hot_vehicle( + call_context: CallContext, + key: String, + json_data: String, +) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + let data: serde_json::Value = match serde_json::from_str(&json_data) { + Ok(d) => d, + Err(error) => return format!("Error: Invalid JSON data: {}", error), + }; + + let plate = match data.get("plate").and_then(|v| v.as_str()) { + Some(p) => p.to_string(), + None => return "Error: Missing or invalid plate".to_string(), + }; + + match HOT_GARAGE_SERVICE.remove_vehicle(resolved_uid, plate) { + Ok(garage) => serialize_hot_vehicles(garage), + Err(error) => format!("Error: {}", error), + } } /// Creates a new empty garage for a player. diff --git a/arma/server/extension/src/lib.rs b/arma/server/extension/src/lib.rs index e5665bf..c6c775c 100644 --- a/arma/server/extension/src/lib.rs +++ b/arma/server/extension/src/lib.rs @@ -14,6 +14,7 @@ use tokio::sync::RwLock as TokioRwLock; pub mod actor; pub mod adapters; pub mod bank; +pub mod cad; pub mod garage; pub mod helpers; pub mod icom; @@ -21,7 +22,10 @@ pub mod locker; mod log; pub mod org; pub mod redis; +pub mod store; +pub mod task; pub mod terrain; +pub mod transport; pub mod v_garage; pub mod v_locker; @@ -35,7 +39,7 @@ static CONTEXT: LazyLock>> = LazyLock::new(|| TokioR static REDIS_POOL: OnceLock = OnceLock::new(); /// Global multi-threaded Tokio runtime used to execute async operations from /// command handlers and startup tasks. -static RUNTIME: LazyLock = LazyLock::new(|| { +pub(crate) static RUNTIME: LazyLock = LazyLock::new(|| { Builder::new_multi_thread() .enable_all() .build() @@ -52,6 +56,21 @@ enum ConnectionState { static CONNECTION_STATE: LazyLock> = LazyLock::new(|| StdRwLock::new(ConnectionState::Initializing)); +pub(crate) fn enqueue_persistence_task(module: &'static str, job: F) +where + F: FnOnce() -> Result<(), String> + Send + 'static, +{ + RUNTIME.spawn_blocking(move || { + if let Err(error) = job() { + crate::log::log( + module, + "ERROR", + &format!("Async persistence failed: {}", error), + ); + } + }); +} + #[arma] /// Initializes the extension, registers commands/groups, and asynchronously /// creates the Redis connection pool on the global runtime. @@ -63,11 +82,15 @@ fn init() -> Extension { .group("redis", redis::group()) .group("actor", actor::group()) .group("bank", bank::group()) + .group("cad", cad::group()) .group("garage", garage::group()) .group("icom", icom::group()) .group("locker", locker::group()) .group("org", org::group()) + .group("store", store::group()) + .group("task", task::group()) .group("terrain", terrain::group()) + .group("transport", transport::group()) .group( "owned", Group::new() diff --git a/arma/server/extension/src/locker.rs b/arma/server/extension/src/locker.rs index c20b3c4..91d8e10 100644 --- a/arma/server/extension/src/locker.rs +++ b/arma/server/extension/src/locker.rs @@ -1,11 +1,12 @@ use arma_rs::{CallContext, Group}; use forge_models::locker::Item; -use forge_repositories::RedisLockerRepository; -use forge_services::LockerService; +use forge_repositories::{InMemoryLockerHotRepository, RedisLockerRepository}; +use forge_services::{LockerHotStateService, LockerService}; use std::collections::HashMap; use std::sync::LazyLock; use crate::adapters::ExtensionRedisClient; +use crate::enqueue_persistence_task; use crate::helpers::resolve_uid; use crate::log::log; @@ -15,6 +16,21 @@ static LOCKER_SERVICE: LazyLock, InMemoryLockerHotRepository>, +> = LazyLock::new(|| { + let redis_client = ExtensionRedisClient::new(); + let repository = RedisLockerRepository::new(redis_client); + let hot_repository = InMemoryLockerHotRepository::new(); + LockerHotStateService::new(repository, hot_repository) +}); + +pub(crate) fn hot_service() -> &'static LockerHotStateService< + RedisLockerRepository, + InMemoryLockerHotRepository, +> { + &HOT_LOCKER_SERVICE +} /// Creates the Arma 3 command group for locker operations. /// @@ -29,6 +45,88 @@ pub fn group() -> Group { .command("remove", remove_item) .command("delete", delete_locker) .command("exists", locker_exists) + .group( + "hot", + Group::new() + .command("init", init_hot_locker) + .command("get", get_hot_locker) + .command("override", override_hot_locker) + .command("save", save_hot_locker) + .command("remove", remove_hot_locker), + ) +} + +fn serialize_hot_items(locker: forge_models::locker::Locker) -> String { + match serde_json::to_string(&locker.items) { + Ok(json) => json, + Err(error) => format!("Error: Failed to serialize hot locker: {}", error), + } +} + +pub(crate) fn init_hot_locker(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_LOCKER_SERVICE.init_locker(resolved_uid) { + Ok(locker) => serialize_hot_items(locker), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn get_hot_locker(call_context: CallContext, key: String) -> String { + init_hot_locker(call_context, key) +} + +pub(crate) fn override_hot_locker( + call_context: CallContext, + key: String, + json_data: String, +) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + let items: std::collections::HashMap = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid JSON data: {}", error), + }; + + match HOT_LOCKER_SERVICE.override_locker(resolved_uid, items) { + Ok(locker) => serialize_hot_items(locker), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn save_hot_locker(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_LOCKER_SERVICE.get_locker(resolved_uid.clone()) { + Ok(locker) => { + enqueue_persistence_task("locker", move || { + HOT_LOCKER_SERVICE.save_locker(resolved_uid).map(|_| ()) + }); + serialize_hot_items(locker) + } + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn remove_hot_locker(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_LOCKER_SERVICE.remove_locker(resolved_uid) { + Ok(_) => "OK".to_string(), + Err(error) => format!("Error: {}", error), + } } /// Creates a new empty locker for a player. diff --git a/arma/server/extension/src/org.rs b/arma/server/extension/src/org.rs index f73ef8e..e0456ad 100644 --- a/arma/server/extension/src/org.rs +++ b/arma/server/extension/src/org.rs @@ -4,11 +4,18 @@ //! Handles SQF command mapping and parameter validation. use arma_rs::Group; -use forge_repositories::RedisOrgRepository; -use forge_services::OrgService; +use forge_models::{ + HotOrgRecord, OrgAssetGrantSeed, OrgCheckoutContext, OrgCreditLineContext, + OrgCreditLineRepaymentContext, OrgCreditLineRepaymentResult, OrgDisbandResult, + OrgEnsureMemberContext, OrgFleetGrantSeed, OrgGrantContext, OrgLeaveContext, OrgLeaveResult, + OrgRegisterContext, +}; +use forge_repositories::{InMemoryOrgHotRepository, RedisOrgRepository}; +use forge_services::{OrgHotStateService, OrgService}; use std::sync::LazyLock; use crate::adapters::ExtensionRedisClient; +use crate::enqueue_persistence_task; use crate::log::log; /// Global organization service instance. @@ -20,6 +27,19 @@ static ORG_SERVICE: LazyLock let repository = RedisOrgRepository::new(redis_client); OrgService::new(repository) }); +static HOT_ORG_SERVICE: LazyLock< + OrgHotStateService, InMemoryOrgHotRepository>, +> = LazyLock::new(|| { + let redis_client = ExtensionRedisClient::new(); + let repository = RedisOrgRepository::new(redis_client); + let hot_repository = InMemoryOrgHotRepository::new(); + OrgHotStateService::new(repository, hot_repository) +}); + +pub(crate) fn hot_service() +-> &'static OrgHotStateService, InMemoryOrgHotRepository> { + &HOT_ORG_SERVICE +} /// Creates the Arma 3 command group for organization operations. /// @@ -31,6 +51,36 @@ pub fn group() -> Group { .command("update", update_org) .command("exists", org_exists) .command("delete", delete_org) + .group( + "hot", + Group::new() + .command("init", init_hot_org) + .command("get", get_hot_org) + .command("override", override_hot_org) + .command("ensure_member", ensure_hot_org_member) + .command("register", register_hot_org) + .command("assign_credit_line", assign_credit_line_hot_org) + .command("repay_credit_line", repay_credit_line_hot_org) + .command("charge_checkout", charge_checkout_hot_org) + .command("add_assets", add_assets_hot_org) + .command("add_fleet", add_fleet_hot_org) + .command("leave", leave_hot_org) + .command("disband", disband_hot_org) + .command("save", save_hot_org) + .command("remove", remove_hot_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() @@ -40,6 +90,181 @@ pub fn group() -> Group { ) } +fn serialize_hot_org(org: HotOrgRecord) -> String { + match serde_json::to_string(&org) { + Ok(json) => json, + Err(error) => format!("Error: Failed to serialize hot org: {}", error), + } +} + +fn serialize_result(value: &T, label: &str) -> String { + match serde_json::to_string(value) { + Ok(json) => json, + Err(error) => format!("Error: Failed to serialize {}: {}", label, error), + } +} + +pub(crate) fn init_hot_org(org_id: String) -> String { + match HOT_ORG_SERVICE.init_org(org_id) { + Ok(org) => serialize_hot_org(org), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn get_hot_org(org_id: String) -> String { + match HOT_ORG_SERVICE.get_org(org_id) { + Ok(org) => serialize_hot_org(org), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn override_hot_org(org_id: String, json_data: String) -> String { + let hot_org: HotOrgRecord = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid org JSON: {}", error), + }; + + match HOT_ORG_SERVICE.override_org(org_id, hot_org) { + Ok(org) => serialize_hot_org(org), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn ensure_hot_org_member(json_data: String) -> String { + let context: OrgEnsureMemberContext = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid ensure-member JSON: {}", error), + }; + + match HOT_ORG_SERVICE.ensure_member(context) { + Ok(org) => serialize_hot_org(org), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn register_hot_org(json_data: String) -> String { + let context: OrgRegisterContext = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid register org JSON: {}", error), + }; + + match HOT_ORG_SERVICE.register_org(context) { + Ok(result) => serialize_result(&result, "org register result"), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn assign_credit_line_hot_org(json_data: String) -> String { + let context: OrgCreditLineContext = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid org credit-line JSON: {}", error), + }; + + match HOT_ORG_SERVICE.assign_credit_line(context) { + Ok(result) => serialize_result(&result, "org mutation result"), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn charge_checkout_hot_org(json_data: String) -> String { + let context: OrgCheckoutContext = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid org checkout JSON: {}", error), + }; + + match HOT_ORG_SERVICE.charge_checkout(context) { + Ok(result) => serialize_result(&result, "org mutation result"), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn repay_credit_line_hot_org(json_data: String) -> String { + let context: OrgCreditLineRepaymentContext = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid org credit repayment JSON: {}", error), + }; + + match HOT_ORG_SERVICE.repay_credit_line(context) { + Ok(result) => { + serialize_result::(&result, "org credit repayment result") + } + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn add_assets_hot_org(context_json: String, assets_json: String) -> String { + let context: OrgGrantContext = match serde_json::from_str(&context_json) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid org asset context JSON: {}", error), + }; + let assets: Vec = match serde_json::from_str(&assets_json) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid org asset seed JSON: {}", error), + }; + + match HOT_ORG_SERVICE.add_assets(context, assets) { + Ok(result) => serialize_result(&result, "org mutation result"), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn add_fleet_hot_org(context_json: String, fleet_json: String) -> String { + let context: OrgGrantContext = match serde_json::from_str(&context_json) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid org fleet context JSON: {}", error), + }; + let fleet: Vec = match serde_json::from_str(&fleet_json) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid org fleet seed JSON: {}", error), + }; + + match HOT_ORG_SERVICE.add_fleet_vehicles(context, fleet) { + Ok(result) => serialize_result(&result, "org mutation result"), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn leave_hot_org(json_data: String) -> String { + let context: OrgLeaveContext = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid org leave JSON: {}", error), + }; + + match HOT_ORG_SERVICE.leave_org(context) { + Ok(result) => serialize_result::(&result, "org leave result"), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn disband_hot_org(json_data: String) -> String { + let context: OrgLeaveContext = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid org disband JSON: {}", error), + }; + + match HOT_ORG_SERVICE.disband_org(context) { + Ok(result) => serialize_result::(&result, "org disband result"), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn save_hot_org(org_id: String) -> String { + match HOT_ORG_SERVICE.get_org(org_id.clone()) { + Ok(org) => { + enqueue_persistence_task("org", move || HOT_ORG_SERVICE.save_org(org_id).map(|_| ())); + serialize_hot_org(org) + } + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn remove_hot_org(org_id: String) -> String { + match HOT_ORG_SERVICE.remove_org(org_id) { + Ok(_) => "OK".to_string(), + Err(error) => format!("Error: {}", error), + } +} + // ============================================================================ // Organization Asset Operations // ============================================================================ @@ -162,6 +387,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/arma/server/extension/src/store.rs b/arma/server/extension/src/store.rs new file mode 100644 index 0000000..7842ca7 --- /dev/null +++ b/arma/server/extension/src/store.rs @@ -0,0 +1,37 @@ +use arma_rs::Group; +use forge_models::{StoreCheckoutContext, StoreCheckoutResult}; +use forge_services::StoreService; + +pub fn group() -> Group { + Group::new().command("checkout", checkout) +} + +fn serialize_result(result: &StoreCheckoutResult) -> String { + match serde_json::to_string(result) { + Ok(json) => json, + Err(error) => format!( + "Error: Failed to serialize store checkout result: {}", + error + ), + } +} + +pub fn checkout(json_data: String) -> String { + let context: StoreCheckoutContext = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid store checkout JSON: {}", error), + }; + + let service = StoreService::new( + crate::bank::hot_service(), + crate::org::hot_service(), + crate::locker::hot_service(), + crate::v_locker::hot_service(), + crate::v_garage::hot_service(), + ); + + match service.checkout(context) { + Ok(result) => serialize_result(&result), + Err(error) => format!("Error: {}", error), + } +} diff --git a/arma/server/extension/src/task.rs b/arma/server/extension/src/task.rs new file mode 100644 index 0000000..233d3dc --- /dev/null +++ b/arma/server/extension/src/task.rs @@ -0,0 +1,123 @@ +//! Task hot-state operations for the Arma 3 server extension. +//! +//! The extension owns portable task metadata while SQF keeps Arma-only runtime +//! state such as entity references and participant tracking. + +use arma_rs::Group; +use forge_repositories::InMemoryTaskRepository; +use forge_services::TaskStateService; +use serde::Serialize; +use std::sync::LazyLock; + +static TASK_SERVICE: LazyLock> = + LazyLock::new(|| TaskStateService::new(InMemoryTaskRepository::new())); + +pub fn group() -> Group { + Group::new() + .command("reset", reset) + .group( + "catalog", + Group::new() + .command("active", list_active_catalog) + .command("get", get_catalog_entry) + .command("upsert", upsert_catalog_entry) + .command("delete", delete_catalog_entry), + ) + .group( + "ownership", + Group::new() + .command("bind", bind_ownership) + .command("release", release_ownership) + .command("accept", accept_task) + .command("reward_context", reward_context), + ) + .group( + "status", + Group::new() + .command("set", set_status) + .command("get", get_status) + .command("clear", clear_status), + ) + .group( + "defuse", + Group::new() + .command("increment", increment_defuse_count) + .command("get", get_defuse_count), + ) + .command("clear", clear_task) +} + +pub(crate) fn list_active_catalog() -> String { + serialize_json(TASK_SERVICE.list_active_catalog()) +} + +pub(crate) fn reset() -> String { + serialize_json(TASK_SERVICE.reset()) +} + +pub(crate) fn get_catalog_entry(entry_id: String) -> String { + serialize_json(TASK_SERVICE.get_catalog_entry(entry_id)) +} + +pub(crate) fn upsert_catalog_entry(entry_id: String, json_data: String) -> String { + serialize_json(TASK_SERVICE.upsert_catalog_entry(entry_id, json_data)) +} + +pub(crate) fn delete_catalog_entry(entry_id: String) -> String { + serialize_ok(TASK_SERVICE.delete_catalog_entry(entry_id)) +} + +pub(crate) fn bind_ownership(entry_id: String, json_data: String) -> String { + serialize_json(TASK_SERVICE.bind_ownership(entry_id, json_data)) +} + +pub(crate) fn release_ownership(entry_id: String) -> String { + serialize_json(TASK_SERVICE.release_ownership(entry_id)) +} + +pub(crate) fn accept_task(entry_id: String, json_data: String) -> String { + serialize_json(TASK_SERVICE.accept_task(entry_id, json_data)) +} + +pub(crate) fn reward_context(entry_id: String) -> String { + serialize_json(TASK_SERVICE.get_reward_context(entry_id)) +} + +pub(crate) fn set_status(entry_id: String, status: String) -> String { + serialize_json(TASK_SERVICE.set_status(entry_id, status)) +} + +pub(crate) fn get_status(entry_id: String) -> String { + serialize_json(TASK_SERVICE.get_status(entry_id)) +} + +pub(crate) fn clear_status(entry_id: String) -> String { + serialize_json(TASK_SERVICE.clear_status(entry_id)) +} + +pub(crate) fn increment_defuse_count(entry_id: String) -> String { + serialize_json(TASK_SERVICE.increment_defuse_count(entry_id)) +} + +pub(crate) fn get_defuse_count(entry_id: String) -> String { + serialize_json(TASK_SERVICE.get_defuse_count(entry_id)) +} + +pub(crate) fn clear_task(entry_id: String) -> String { + serialize_json(TASK_SERVICE.clear_task(entry_id)) +} + +fn serialize_json(result: Result) -> String { + match result { + Ok(value) => serde_json::to_string(&value) + .unwrap_or_else(|error| format!("Error: Failed to serialize task state: {error}")), + Err(error) => format!("Error: {error}"), + } +} + +fn serialize_ok(result: Result<(), String>) -> String { + match result { + Ok(()) => "true".to_string(), + Err(error) => format!("Error: {error}"), + } +} diff --git a/arma/server/extension/src/transport.rs b/arma/server/extension/src/transport.rs new file mode 100644 index 0000000..0232ba4 --- /dev/null +++ b/arma/server/extension/src/transport.rs @@ -0,0 +1,998 @@ +//! Shared transport helpers for oversized extension requests and responses. +//! +//! This module provides a routed invoke path that accepts JSON-encoded string +//! arguments, supports request staging for large payloads, and stores oversized +//! responses in memory for chunked retrieval by SQF. + +use arma_rs::{CallContext, Group}; +use serde::Serialize; +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{LazyLock, Mutex as StdMutex}; + +use crate::{actor, bank, cad, garage, locker, org, v_garage, v_locker}; + +const CHUNK_PREFIX: &str = "FORGE_TRANSPORT_CHUNK:"; +const RESPONSE_CHUNK_SIZE: usize = 12_000; +const UNSUPPORTED_ROUTE_PREFIX: &str = "Unsupported transport route"; + +static REQUEST_STORE: LazyLock>> = + LazyLock::new(|| StdMutex::new(HashMap::new())); +static RESPONSE_STORE: LazyLock>>> = + LazyLock::new(|| StdMutex::new(HashMap::new())); +static TRANSFER_SEQUENCE: AtomicU64 = AtomicU64::new(1); + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct ChunkEnvelope { + transfer_id: String, + chunk_count: usize, + total_size: usize, +} + +pub fn group() -> Group { + Group::new() + .command("invoke", invoke) + .command("invoke_stored", invoke_stored) + .group( + "request", + Group::new() + .command("append", append_request_chunk) + .command("clear", clear_request_chunks), + ) + .group( + "response", + Group::new() + .command("get", get_response_chunk) + .command("clear", clear_response_chunks), + ) +} + +fn append_request_chunk(transfer_id: String, chunk: String) -> String { + let mut store = REQUEST_STORE.lock().unwrap(); + store.entry(transfer_id).or_default().push_str(&chunk); + "OK".to_string() +} + +fn clear_request_chunks(transfer_id: String) -> String { + REQUEST_STORE.lock().unwrap().remove(&transfer_id); + "OK".to_string() +} + +fn get_response_chunk(transfer_id: String, index: String) -> String { + let chunk_index = match index.parse::() { + Ok(value) => value, + Err(error) => return format!("Error: Invalid response chunk index: {error}"), + }; + + let store = RESPONSE_STORE.lock().unwrap(); + let Some(chunks) = store.get(&transfer_id) else { + return format!("Error: Response transfer '{transfer_id}' was not found"); + }; + + chunks.get(chunk_index).cloned().unwrap_or_else(|| { + format!( + "Error: Response chunk {} was not found for '{}'", + chunk_index, transfer_id + ) + }) +} + +fn clear_response_chunks(transfer_id: String) -> String { + RESPONSE_STORE.lock().unwrap().remove(&transfer_id); + "OK".to_string() +} + +fn invoke(call_context: CallContext, function_name: String, arguments_json: String) -> String { + invoke_internal(call_context, function_name, arguments_json) +} + +fn invoke_stored(call_context: CallContext, function_name: String, transfer_id: String) -> String { + let Some(arguments_json) = REQUEST_STORE.lock().unwrap().remove(&transfer_id) else { + return format!("Error: Request transfer '{transfer_id}' was not found"); + }; + + invoke_internal(call_context, function_name, arguments_json) +} + +fn invoke_internal( + call_context: CallContext, + function_name: String, + arguments_json: String, +) -> String { + let arguments: Vec = match parse_transport_arguments(&arguments_json) { + Ok(value) => value, + Err(error) => return format!("Error: Invalid transport arguments JSON: {error}"), + }; + + let result = match route_command(call_context, &function_name, arguments) { + Ok(value) => value, + Err(error) => format!("Error: {error}"), + }; + + chunk_response_if_needed(result) +} + +fn parse_transport_arguments(arguments_json: &str) -> Result, String> { + let value: serde_json::Value = + serde_json::from_str(arguments_json).map_err(|error| error.to_string())?; + parse_transport_argument_value(value) +} + +fn parse_transport_argument_value(value: serde_json::Value) -> Result, String> { + match value { + serde_json::Value::Array(values) => Ok(values + .into_iter() + .map(|entry| match entry { + serde_json::Value::String(string_value) => string_value, + other => other.to_string(), + }) + .collect()), + serde_json::Value::String(value) => { + let trimmed = value.trim(); + if trimmed.starts_with('[') || trimmed.starts_with('{') || trimmed.eq("null") { + if let Ok(nested_value) = serde_json::from_str::(trimmed) { + return parse_transport_argument_value(nested_value); + } + } + + Ok(vec![value]) + } + serde_json::Value::Null => Ok(Vec::new()), + other => Err(format!("expected string or array but received {}", other)), + } +} + +fn route_command( + call_context: CallContext, + function_name: &str, + arguments: Vec, +) -> Result { + match function_name { + "actor:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(actor::get_actor(call_context, arguments[0].clone())) + } + "actor:create" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(actor::create_actor( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "actor:update" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(actor::update_actor( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "actor:exists" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(actor::actor_exists(call_context, arguments[0].clone())) + } + "actor:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(actor::delete_actor(call_context, arguments[0].clone())) + } + "actor:hot:init" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(actor::init_hot_actor(call_context, arguments[0].clone())) + } + "actor:hot:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(actor::get_hot_actor(call_context, arguments[0].clone())) + } + "actor:hot:override" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(actor::override_hot_actor( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "actor:hot:save" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(actor::save_hot_actor(call_context, arguments[0].clone())) + } + "actor:hot:remove" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(actor::remove_hot_actor(call_context, arguments[0].clone())) + } + "bank:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(bank::get_bank(call_context, arguments[0].clone())) + } + "bank:create" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(bank::create_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "bank:update" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(bank::update_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "bank:exists" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(bank::bank_exists(call_context, arguments[0].clone())) + } + "bank:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(bank::delete_bank(call_context, arguments[0].clone())) + } + "bank:hot:init" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(bank::init_hot_bank(call_context, arguments[0].clone())) + } + "bank:hot:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(bank::get_hot_bank(call_context, arguments[0].clone())) + } + "bank:hot:override" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(bank::override_hot_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "bank:hot:patch" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(bank::patch_hot_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "bank:hot:charge_checkout" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(bank::charge_checkout_hot_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "bank:hot:deposit" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(bank::deposit_hot_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "bank:hot:withdraw" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(bank::withdraw_hot_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "bank:hot:deposit_earnings" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(bank::deposit_earnings_hot_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "bank:hot:transfer" => { + expect_arg_count(function_name, &arguments, 4)?; + Ok(bank::transfer_hot_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + arguments[3].clone(), + )) + } + "bank:hot:validate_pin" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(bank::validate_pin_hot_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "bank:hot:save" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(bank::save_hot_bank(call_context, arguments[0].clone())) + } + "bank:hot:remove" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(bank::remove_hot_bank(call_context, arguments[0].clone())) + } + "org:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::get_org(arguments[0].clone())) + } + "org:create" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::create_org(arguments[0].clone(), arguments[1].clone())) + } + "org:update" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::update_org(arguments[0].clone(), arguments[1].clone())) + } + "org:exists" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::org_exists(arguments[0].clone())) + } + "org:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::delete_org(arguments[0].clone())) + } + "org:hot:init" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::init_hot_org(arguments[0].clone())) + } + "org:hot:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::get_hot_org(arguments[0].clone())) + } + "org:hot:override" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::override_hot_org( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "org:hot:ensure_member" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::ensure_hot_org_member(arguments[0].clone())) + } + "org:hot:register" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::register_hot_org(arguments[0].clone())) + } + "org:hot:assign_credit_line" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::assign_credit_line_hot_org(arguments[0].clone())) + } + "org:hot:repay_credit_line" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::repay_credit_line_hot_org(arguments[0].clone())) + } + "org:hot:charge_checkout" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::charge_checkout_hot_org(arguments[0].clone())) + } + "org:hot:add_assets" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::add_assets_hot_org( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "org:hot:add_fleet" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::add_fleet_hot_org( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "org:hot:leave" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::leave_hot_org(arguments[0].clone())) + } + "org:hot:disband" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::disband_hot_org(arguments[0].clone())) + } + "org:hot:save" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::save_hot_org(arguments[0].clone())) + } + "org:hot:remove" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::remove_hot_org(arguments[0].clone())) + } + "org:assets:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::get_assets(arguments[0].clone())) + } + "org:assets:update" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::update_assets( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "org:fleet:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::get_fleet(arguments[0].clone())) + } + "org:fleet:update" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::update_fleet( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "org:members:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::get_members(arguments[0].clone())) + } + "org:members:add" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::add_member(arguments[0].clone(), arguments[1].clone())) + } + "org:members:remove" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::remove_member( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "store:checkout" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(crate::store::checkout(arguments[0].clone())) + } + "garage:create" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(garage::create_garage(call_context, arguments[0].clone())) + } + "garage:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(garage::get_garage(call_context, arguments[0].clone())) + } + "garage:add" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(garage::add_vehicle( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "garage:update" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(garage::update_garage( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "garage:patch" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(garage::patch_vehicle( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "garage:remove" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(garage::remove_vehicle( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "garage:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(garage::delete_garage(call_context, arguments[0].clone())) + } + "garage:exists" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(garage::garage_exists(call_context, arguments[0].clone())) + } + "garage:hot:init" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(garage::init_hot_garage(call_context, arguments[0].clone())) + } + "garage:hot:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(garage::get_hot_garage(call_context, arguments[0].clone())) + } + "garage:hot:override" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(garage::override_hot_garage( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "garage:hot:save" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(garage::save_hot_garage(call_context, arguments[0].clone())) + } + "garage:hot:remove" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(garage::remove_hot_garage( + call_context, + arguments[0].clone(), + )) + } + "garage:hot:add" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(garage::add_hot_vehicle( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "garage:hot:remove_vehicle" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(garage::remove_hot_vehicle( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "locker:create" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(locker::create_locker(call_context, arguments[0].clone())) + } + "locker:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(locker::get_locker(call_context, arguments[0].clone())) + } + "locker:add" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(locker::add_item( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "locker:update" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(locker::update_locker( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "locker:patch" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(locker::patch_item( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "locker:remove" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(locker::remove_item( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "locker:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(locker::delete_locker(call_context, arguments[0].clone())) + } + "locker:exists" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(locker::locker_exists(call_context, arguments[0].clone())) + } + "locker:hot:init" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(locker::init_hot_locker(call_context, arguments[0].clone())) + } + "locker:hot:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(locker::get_hot_locker(call_context, arguments[0].clone())) + } + "locker:hot:override" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(locker::override_hot_locker( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "locker:hot:save" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(locker::save_hot_locker(call_context, arguments[0].clone())) + } + "locker:hot:remove" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(locker::remove_hot_locker( + call_context, + arguments[0].clone(), + )) + } + "owned:garage:create" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_garage::create_vgarage(call_context, arguments[0].clone())) + } + "owned:garage:fetch" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_garage::fetch_vgarage(call_context, arguments[0].clone())) + } + "owned:garage:get" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(v_garage::get_vgarage( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "owned:garage:add" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(v_garage::add_vgarage( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "owned:garage:remove" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(v_garage::remove_vgarage( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "owned:garage:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_garage::delete_vgarage(call_context, arguments[0].clone())) + } + "owned:garage:exists" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_garage::vgarage_exists(call_context, arguments[0].clone())) + } + "owned:garage:hot:init" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_garage::init_hot_vgarage( + call_context, + arguments[0].clone(), + )) + } + "owned:garage:hot:fetch" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_garage::fetch_hot_vgarage( + call_context, + arguments[0].clone(), + )) + } + "owned:garage:hot:get" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(v_garage::get_hot_vgarage( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "owned:garage:hot:override" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(v_garage::override_hot_vgarage( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "owned:garage:hot:save" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_garage::save_hot_vgarage( + call_context, + arguments[0].clone(), + )) + } + "owned:garage:hot:remove" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_garage::remove_hot_vgarage( + call_context, + arguments[0].clone(), + )) + } + "owned:garage:hot:add" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(v_garage::add_hot_vgarage( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "owned:garage:hot:remove_item" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(v_garage::remove_hot_vgarage_item( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "owned:locker:create" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_locker::create_vlocker(call_context, arguments[0].clone())) + } + "owned:locker:fetch" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_locker::fetch_vlocker(call_context, arguments[0].clone())) + } + "owned:locker:get" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(v_locker::get_vlocker( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "owned:locker:add" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(v_locker::add_vlocker( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "owned:locker:remove" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(v_locker::remove_vlocker( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "owned:locker:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_locker::delete_vlocker(call_context, arguments[0].clone())) + } + "owned:locker:exists" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_locker::vlocker_exists(call_context, arguments[0].clone())) + } + "owned:locker:hot:init" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_locker::init_hot_vlocker( + call_context, + arguments[0].clone(), + )) + } + "owned:locker:hot:fetch" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_locker::fetch_hot_vlocker( + call_context, + arguments[0].clone(), + )) + } + "owned:locker:hot:get" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(v_locker::get_hot_vlocker( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "owned:locker:hot:override" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(v_locker::override_hot_vlocker( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "owned:locker:hot:save" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_locker::save_hot_vlocker( + call_context, + arguments[0].clone(), + )) + } + "owned:locker:hot:remove" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_locker::remove_hot_vlocker( + call_context, + arguments[0].clone(), + )) + } + "cad:activity:append" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::append_activity(arguments[0].clone())) + } + "cad:activity:recent" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::recent_activity(arguments[0].clone())) + } + "cad:assignments:list" => { + expect_arg_count(function_name, &arguments, 0)?; + Ok(cad::list_assignments()) + } + "cad:assignments:assign" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(cad::assign_assignment( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "cad:assignments:acknowledge" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(cad::acknowledge_assignment( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "cad:assignments:decline" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(cad::decline_assignment( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "cad:assignments:upsert" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(cad::upsert_assignment( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "cad:assignments:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::delete_assignment(arguments[0].clone())) + } + "cad:orders:list" => { + expect_arg_count(function_name, &arguments, 0)?; + Ok(cad::list_orders()) + } + "cad:orders:create" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::create_order(arguments[0].clone())) + } + "cad:orders:create_from_context" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::create_order_from_context(arguments[0].clone())) + } + "cad:orders:close" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::close_order(arguments[0].clone())) + } + "cad:orders:upsert" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(cad::upsert_order( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "cad:orders:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::delete_order(arguments[0].clone())) + } + "cad:requests:list" => { + expect_arg_count(function_name, &arguments, 0)?; + Ok(cad::list_requests()) + } + "cad:requests:submit" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::submit_request(arguments[0].clone())) + } + "cad:requests:submit_from_context" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::submit_request_from_context(arguments[0].clone())) + } + "cad:requests:close" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::close_request(arguments[0].clone())) + } + "cad:requests:upsert" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(cad::upsert_request( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "cad:requests:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::delete_request(arguments[0].clone())) + } + "cad:profiles:list" => { + expect_arg_count(function_name, &arguments, 0)?; + Ok(cad::list_profiles()) + } + "cad:profiles:update_from_context" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::update_profile_from_context(arguments[0].clone())) + } + "cad:profiles:upsert" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(cad::upsert_profile( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "cad:profiles:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::delete_profile(arguments[0].clone())) + } + "cad:groups:build" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::build_groups(arguments[0].clone())) + } + "cad:view:hydrate" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::hydrate_view(arguments[0].clone())) + } + _ => Err(format!( + "{UNSUPPORTED_ROUTE_PREFIX} for function '{function_name}'" + )), + } +} + +fn expect_arg_count( + function_name: &str, + arguments: &[String], + expected_count: usize, +) -> Result<(), String> { + if arguments.len() == expected_count { + return Ok(()); + } + + Err(format!( + "Transport route '{}' expected {} arguments but received {}", + function_name, + expected_count, + arguments.len() + )) +} + +fn chunk_response_if_needed(result: String) -> String { + if result.len() <= RESPONSE_CHUNK_SIZE { + return result; + } + + let transfer_id = next_transfer_id("rsp"); + let chunks = split_string_chunks(&result, RESPONSE_CHUNK_SIZE); + let envelope = ChunkEnvelope { + transfer_id: transfer_id.clone(), + chunk_count: chunks.len(), + total_size: result.len(), + }; + + RESPONSE_STORE.lock().unwrap().insert(transfer_id, chunks); + + format!( + "{CHUNK_PREFIX}{}", + serde_json::to_string(&envelope) + .unwrap_or_else(|error| format!("{{\"error\":\"{error}\"}}")) + ) +} + +fn next_transfer_id(prefix: &str) -> String { + let sequence = TRANSFER_SEQUENCE.fetch_add(1, Ordering::Relaxed); + format!("{prefix}_{sequence}") +} + +fn split_string_chunks(input: &str, max_bytes: usize) -> Vec { + if input.is_empty() { + return vec![String::new()]; + } + + let mut chunks = Vec::new(); + let mut chunk_start = 0usize; + let mut chunk_len = 0usize; + + for (index, character) in input.char_indices() { + let char_len = character.len_utf8(); + if chunk_len > 0 && chunk_len + char_len > max_bytes { + chunks.push(input[chunk_start..index].to_string()); + chunk_start = index; + chunk_len = 0; + } + + chunk_len += char_len; + } + + chunks.push(input[chunk_start..].to_string()); + chunks +} diff --git a/arma/server/extension/src/v_garage.rs b/arma/server/extension/src/v_garage.rs index 17506e9..1382d63 100644 --- a/arma/server/extension/src/v_garage.rs +++ b/arma/server/extension/src/v_garage.rs @@ -1,10 +1,11 @@ use arma_rs::{CallContext, Group}; -use forge_models::VehicleCategory; -use forge_repositories::RedisVGarageRepository; -use forge_services::VGarageService; +use forge_models::{VGarage, VehicleCategory}; +use forge_repositories::{InMemoryVGarageHotRepository, RedisVGarageRepository}; +use forge_services::{VGarageHotStateService, VGarageService}; use std::sync::LazyLock; use crate::adapters::ExtensionRedisClient; +use crate::enqueue_persistence_task; use crate::helpers::resolve_uid; use crate::log::log; @@ -14,6 +15,24 @@ static VGARAGE_SERVICE: LazyLock, + InMemoryVGarageHotRepository, + >, +> = LazyLock::new(|| { + let redis_client = ExtensionRedisClient::new(); + let repository = RedisVGarageRepository::new(redis_client); + let hot_repository = InMemoryVGarageHotRepository::new(); + VGarageHotStateService::new(repository, hot_repository) +}); + +pub(crate) fn hot_service() -> &'static VGarageHotStateService< + RedisVGarageRepository, + InMemoryVGarageHotRepository, +> { + &HOT_VGARAGE_SERVICE +} /// Creates the Arma 3 command group for virtual garage operations. /// @@ -27,6 +46,185 @@ pub fn group() -> Group { .command("remove", remove_vgarage) .command("delete", delete_vgarage) .command("exists", vgarage_exists) + .group( + "hot", + Group::new() + .command("init", init_hot_vgarage) + .command("fetch", fetch_hot_vgarage) + .command("get", get_hot_vgarage) + .command("override", override_hot_vgarage) + .command("save", save_hot_vgarage) + .command("remove", remove_hot_vgarage) + .command("add", add_hot_vgarage) + .command("remove_item", remove_hot_vgarage_item), + ) +} + +fn serialize_hot_vgarage(garage: VGarage) -> String { + match serde_json::to_string(&garage) { + Ok(json) => json, + Err(error) => format!("Error: Failed to serialize hot virtual garage: {}", error), + } +} + +pub(crate) fn init_hot_vgarage(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_VGARAGE_SERVICE.init_garage(&resolved_uid) { + Ok(garage) => serialize_hot_vgarage(garage), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn fetch_hot_vgarage(call_context: CallContext, key: String) -> String { + init_hot_vgarage(call_context, key) +} + +pub(crate) fn get_hot_vgarage(call_context: CallContext, key: String, field: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + let items = match HOT_VGARAGE_SERVICE.get_garage(&resolved_uid, &field) { + Ok(items) => items, + Err(error) => return format!("Error: {}", error), + }; + match serde_json::to_string(&items) { + Ok(json) => json, + Err(error) => format!( + "Error: Failed to serialize hot virtual garage field: {}", + error + ), + } +} + +pub(crate) fn override_hot_vgarage( + call_context: CallContext, + key: String, + json_data: String, +) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + let garage: VGarage = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid virtual garage JSON: {}", error), + }; + + match HOT_VGARAGE_SERVICE.override_garage(&resolved_uid, garage) { + Ok(garage) => serialize_hot_vgarage(garage), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn save_hot_vgarage(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_VGARAGE_SERVICE.fetch_garage(&resolved_uid) { + Ok(garage) => { + enqueue_persistence_task("owned_garage", move || { + HOT_VGARAGE_SERVICE.save_garage(&resolved_uid).map(|_| ()) + }); + serialize_hot_vgarage(garage) + } + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn remove_hot_vgarage(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_VGARAGE_SERVICE.remove_hot_garage(&resolved_uid) { + Ok(_) => "OK".to_string(), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn add_hot_vgarage( + call_context: CallContext, + key: String, + category: String, + classnames_json: String, +) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + let category_enum = match category.to_lowercase().as_str() { + "cars" => VehicleCategory::Cars, + "armor" => VehicleCategory::Armor, + "helis" => VehicleCategory::Helis, + "planes" => VehicleCategory::Planes, + "naval" => VehicleCategory::Naval, + "other" => VehicleCategory::Other, + _ => { + return format!( + "Error: Invalid category '{}'. Valid options: cars, armor, helis, planes, naval, other", + category + ); + } + }; + + let classnames: Vec = match serde_json::from_str(&classnames_json) { + Ok(names) => names, + Err(error) => return format!("Error: Invalid JSON array: {}", error), + }; + + match HOT_VGARAGE_SERVICE.add_garage(&resolved_uid, category_enum, classnames) { + Ok(garage) => match serde_json::to_string(&garage.get(category_enum)) { + Ok(json) => json, + Err(error) => format!("Error: Failed to serialize category: {}", error), + }, + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn remove_hot_vgarage_item( + call_context: CallContext, + key: String, + category: String, + classname: String, +) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + let category_enum = match category.to_lowercase().as_str() { + "cars" => VehicleCategory::Cars, + "armor" => VehicleCategory::Armor, + "heli" | "helis" => VehicleCategory::Helis, + "planes" => VehicleCategory::Planes, + "naval" => VehicleCategory::Naval, + "other" => VehicleCategory::Other, + _ => { + return format!( + "Error: Invalid category '{}'. Valid options: cars, armor, helis, planes, naval, other", + category + ); + } + }; + + match HOT_VGARAGE_SERVICE.remove_garage(&resolved_uid, category_enum, &classname) { + Ok(garage) => match serde_json::to_string(&garage.get(category_enum)) { + Ok(json) => json, + Err(error) => format!("Error: Failed to serialize category: {}", error), + }, + Err(error) => format!("Error: {}", error), + } } /// Creates a new empty virtual garage for a player. diff --git a/arma/server/extension/src/v_locker.rs b/arma/server/extension/src/v_locker.rs index 11be05e..7064e47 100644 --- a/arma/server/extension/src/v_locker.rs +++ b/arma/server/extension/src/v_locker.rs @@ -1,10 +1,11 @@ use arma_rs::{CallContext, Group}; -use forge_models::EquipmentCategory; -use forge_repositories::RedisVLockerRepository; -use forge_services::VLockerService; +use forge_models::{EquipmentCategory, VLocker}; +use forge_repositories::{InMemoryVLockerHotRepository, RedisVLockerRepository}; +use forge_services::{VLockerHotStateService, VLockerService}; use std::sync::LazyLock; use crate::adapters::ExtensionRedisClient; +use crate::enqueue_persistence_task; use crate::helpers::resolve_uid; use crate::log::log; @@ -14,6 +15,24 @@ static VLOCKER_SERVICE: LazyLock, + InMemoryVLockerHotRepository, + >, +> = LazyLock::new(|| { + let redis_client = ExtensionRedisClient::new(); + let repository = RedisVLockerRepository::new(redis_client); + let hot_repository = InMemoryVLockerHotRepository::new(); + VLockerHotStateService::new(repository, hot_repository) +}); + +pub(crate) fn hot_service() -> &'static VLockerHotStateService< + RedisVLockerRepository, + InMemoryVLockerHotRepository, +> { + &HOT_VLOCKER_SERVICE +} /// Creates the Arma 3 command group for virtual locker operations. /// @@ -27,6 +46,109 @@ pub fn group() -> Group { .command("remove", remove_vlocker) .command("delete", delete_vlocker) .command("exists", vlocker_exists) + .group( + "hot", + Group::new() + .command("init", init_hot_vlocker) + .command("fetch", fetch_hot_vlocker) + .command("get", get_hot_vlocker) + .command("override", override_hot_vlocker) + .command("save", save_hot_vlocker) + .command("remove", remove_hot_vlocker), + ) +} + +fn serialize_hot_vlocker(locker: VLocker) -> String { + match serde_json::to_string(&locker) { + Ok(json) => json, + Err(error) => format!("Error: Failed to serialize hot virtual locker: {}", error), + } +} + +pub(crate) fn init_hot_vlocker(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_VLOCKER_SERVICE.init_locker(&resolved_uid) { + Ok(locker) => serialize_hot_vlocker(locker), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn fetch_hot_vlocker(call_context: CallContext, key: String) -> String { + init_hot_vlocker(call_context, key) +} + +pub(crate) fn get_hot_vlocker(call_context: CallContext, key: String, field: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + let items = match HOT_VLOCKER_SERVICE.get_locker(&resolved_uid, &field) { + Ok(items) => items, + Err(error) => return format!("Error: {}", error), + }; + + match serde_json::to_string(&items) { + Ok(json) => json, + Err(error) => format!( + "Error: Failed to serialize hot virtual locker field: {}", + error + ), + } +} + +pub(crate) fn override_hot_vlocker( + call_context: CallContext, + key: String, + json_data: String, +) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + let locker: VLocker = match serde_json::from_str(&json_data) { + Ok(data) => data, + Err(error) => return format!("Error: Invalid virtual locker JSON: {}", error), + }; + + match HOT_VLOCKER_SERVICE.override_locker(&resolved_uid, locker) { + Ok(locker) => serialize_hot_vlocker(locker), + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn save_hot_vlocker(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_VLOCKER_SERVICE.fetch_locker(&resolved_uid) { + Ok(locker) => { + enqueue_persistence_task("owned_locker", move || { + HOT_VLOCKER_SERVICE.save_locker(&resolved_uid).map(|_| ()) + }); + serialize_hot_vlocker(locker) + } + Err(error) => format!("Error: {}", error), + } +} + +pub(crate) fn remove_hot_vlocker(call_context: CallContext, key: String) -> String { + let resolved_uid = match resolve_uid(&key, &call_context) { + Some(uid) => uid, + None => return format!("Error: Failed to resolve UID for key: {}", key), + }; + + match HOT_VLOCKER_SERVICE.remove_locker(&resolved_uid) { + Ok(_) => "OK".to_string(), + Err(error) => format!("Error: {}", error), + } } /// Creates a new empty virtual locker for a player. diff --git a/build-arma.ps1 b/build-arma.ps1 index d7b67c3..612f650 100644 --- a/build-arma.ps1 +++ b/build-arma.ps1 @@ -10,6 +10,9 @@ .PARAMETER Target Specify which target to build: 'client', 'server', or 'both' (default) +.PARAMETER BuildUI + Rebuild the web UI bundles before running the client build. + .EXAMPLE .\build-arma.ps1 Builds both client and server @@ -17,12 +20,19 @@ .EXAMPLE .\build-arma.ps1 -Target client Builds only the client + +.EXAMPLE + .\build-arma.ps1 -Target client -BuildUI + Rebuilds web UI bundles and then builds the client #> param( [Parameter(Mandatory=$false)] [ValidateSet('client', 'server', 'both')] - [string]$Target = 'both' + [string]$Target = 'both', + + [Parameter(Mandatory=$false)] + [switch]$BuildUI ) $ErrorActionPreference = "Stop" @@ -70,7 +80,9 @@ $serverPath = Join-Path $scriptDir "arma\server" try { if ($Target -eq 'client' -or $Target -eq 'both') { - Build-WebUIAssets + if ($BuildUI) { + Build-WebUIAssets + } Build-HemttProject -ProjectPath $clientPath -ProjectName "Client" } diff --git a/lib/models/Cargo.toml b/lib/models/Cargo.toml index 0e4470c..f0f9a26 100644 --- a/lib/models/Cargo.toml +++ b/lib/models/Cargo.toml @@ -12,10 +12,11 @@ serde_json = { workspace = true, optional = true } forge-shared = { path = "../shared" } [features] -default = ["actor", "bank", "member", "org"] +default = ["actor", "bank", "member", "org", "task"] actor = ["arma-rs", "serde_json"] bank = ["arma-rs", "serde_json"] member = ["arma-rs", "serde_json"] org = ["arma-rs", "serde_json"] +task = ["arma-rs", "serde_json"] arma-rs = ["arma-rs/serde_json"] diff --git a/lib/models/src/bank.rs b/lib/models/src/bank.rs index 009fc82..fcabed4 100644 --- a/lib/models/src/bank.rs +++ b/lib/models/src/bank.rs @@ -1,6 +1,7 @@ use arma_rs::{FromArma, IntoArma}; use forge_shared::BankValidationError; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Bank { @@ -13,6 +14,50 @@ pub struct Bank { pub transactions: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankMutationResult { + pub account: Bank, + pub patch: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankTransferResult { + pub source_account: Bank, + pub source_patch: HashMap, + pub target_account: Bank, + pub target_patch: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankOperationContext { + pub mode: String, + pub atm_authorized: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankTransferContext { + pub mode: String, + pub atm_authorized: bool, + pub from_field: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankCheckoutContext { + pub source_field: String, + pub commit: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BankPinContext { + pub mode: String, +} + impl Bank { pub fn new>(uid: S, name: S, pin: u64) -> Result { let bank = Self { diff --git a/lib/models/src/cad.rs b/lib/models/src/cad.rs new file mode 100644 index 0000000..ee58e7c --- /dev/null +++ b/lib/models/src/cad.rs @@ -0,0 +1,237 @@ +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +pub type CadJsonMap = Map; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(transparent)] +pub struct CadRecord { + pub fields: CadJsonMap, +} + +impl CadRecord { + pub fn into_value(self) -> Value { + Value::Object(self.fields) + } + + pub fn to_value(&self) -> Value { + Value::Object(self.fields.clone()) + } + + pub fn is_empty(&self) -> bool { + self.fields.is_empty() + } + + pub fn merge(mut self, patch: CadRecord) -> Self { + for (key, value) in patch.fields { + self.fields.insert(key, value); + } + + self + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CadDispatchOrderCreateSeed { + #[serde(default)] + pub order: CadRecord, + #[serde(default)] + pub assignment: CadRecord, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CadDispatchOrderContextSeed { + #[serde(default)] + pub assignee_group_id: String, + #[serde(default)] + pub assignee_group_callsign: String, + #[serde(default)] + pub target_group_id: String, + #[serde(default)] + pub target_group_callsign: String, + #[serde(default)] + pub target_position: Value, + #[serde(default)] + pub created_by_uid: String, + #[serde(default)] + pub created_by_name: String, + #[serde(default)] + pub request_id: String, + #[serde(default)] + pub request_type: String, + #[serde(default)] + pub request_title: String, + #[serde(default)] + pub request_summary: String, + #[serde(default)] + pub request_fields: CadRecord, + #[serde(default)] + pub note: String, + #[serde(default)] + pub priority: String, + #[serde(default)] + pub created_at: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CadDispatchOrderMutationResult { + #[serde(default)] + pub task_id: String, + #[serde(default)] + pub order: Value, + #[serde(default)] + pub assignment: Value, + #[serde(default)] + pub message: String, + #[serde(default)] + pub activity: CadActivityEntry, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CadActivityEntry { + #[serde(default)] + #[serde(rename = "type")] + pub entry_type: String, + #[serde(default)] + pub message: String, + #[serde(default)] + pub task_id: String, + #[serde(default)] + pub group_id: String, + #[serde(default)] + pub actor_uid: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CadAssignmentMutationResult { + #[serde(default)] + pub assignment: Value, + #[serde(default)] + pub message: String, + #[serde(default)] + pub activity: CadActivityEntry, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CadRequestMutationResult { + #[serde(default)] + pub request: Value, + #[serde(default)] + pub message: String, + #[serde(default)] + pub activity: CadActivityEntry, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CadGroupProfileUpdateSeed { + #[serde(default)] + pub group_id: String, + #[serde(default)] + pub group_callsign: String, + #[serde(default)] + pub requester_uid: String, + #[serde(default)] + pub current_role: String, + #[serde(default)] + pub current_status: String, + #[serde(default)] + pub role: String, + #[serde(default)] + pub status: String, + #[serde(default)] + pub mode: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CadGroupProfileMutationResult { + #[serde(default)] + pub profile: Value, + #[serde(default)] + pub message: String, + #[serde(default)] + pub activity: CadActivityEntry, + #[serde(default)] + pub changed: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CadSupportRequestSubmitSeed { + #[serde(rename = "type")] + #[serde(default)] + pub request_type: String, + #[serde(default)] + pub fields: CadRecord, + #[serde(default)] + pub group_id: String, + #[serde(default)] + pub group_callsign: String, + #[serde(default)] + pub submitted_by_uid: String, + #[serde(default)] + pub submitted_by_name: String, + #[serde(default)] + pub priority: String, + #[serde(default)] + pub position: Value, + #[serde(default)] + pub created_at: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CadGroupBuildSeed { + #[serde(default)] + pub live_groups: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CadSession { + #[serde(default)] + pub uid: String, + #[serde(default)] + pub org_id: String, + #[serde(default)] + pub is_dispatcher: bool, + #[serde(default)] + pub group_id: String, + #[serde(default)] + pub is_leader: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CadHydrateSeed { + #[serde(default)] + pub groups: Vec, + #[serde(default)] + pub active_tasks: Vec, + #[serde(default)] + pub session: CadSession, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct CadHydratePayload { + #[serde(default)] + pub groups: Vec, + #[serde(default)] + pub contracts: Vec, + #[serde(default)] + pub requests: Vec, + #[serde(default)] + pub assignments: Vec, + #[serde(default)] + pub activity: Vec, + #[serde(default)] + pub session: CadSession, +} diff --git a/lib/models/src/lib.rs b/lib/models/src/lib.rs index b710d4f..243a5a5 100644 --- a/lib/models/src/lib.rs +++ b/lib/models/src/lib.rs @@ -1,15 +1,40 @@ pub mod actor; pub mod bank; +pub mod cad; pub mod garage; pub mod locker; pub mod org; +pub mod store; +pub mod task; pub mod v_garage; pub mod v_locker; pub use actor::Actor; -pub use bank::Bank; +pub use bank::{ + Bank, BankCheckoutContext, BankMutationResult, BankOperationContext, BankPinContext, + BankTransferContext, BankTransferResult, +}; +pub use cad::{ + CadActivityEntry, CadAssignmentMutationResult, CadDispatchOrderContextSeed, + CadDispatchOrderCreateSeed, CadDispatchOrderMutationResult, CadGroupBuildSeed, + CadGroupProfileMutationResult, CadGroupProfileUpdateSeed, CadHydratePayload, CadHydrateSeed, + CadJsonMap, CadRecord, CadRequestMutationResult, CadSession, CadSupportRequestSubmitSeed, +}; pub use garage::{Garage, HitPoints, Vehicle}; pub use locker::{Item, Locker}; -pub use org::{CreditLineSummary, MemberSummary, Org}; +pub use org::{ + CreditLineSummary, DEFAULT_CREDIT_LINE_INTEREST_RATE, HotOrgRecord, MemberSummary, Org, + OrgAssetEntry, OrgAssetGrantSeed, OrgCheckoutContext, OrgCreditLineContext, + OrgCreditLineRepaymentContext, OrgCreditLineRepaymentResult, OrgDisbandMemberResult, + OrgDisbandResult, OrgEnsureMemberContext, OrgFleetEntry, OrgFleetGrantSeed, OrgGrantContext, + OrgLeaveContext, OrgLeaveResult, OrgMutationResult, OrgRegisterContext, OrgRegisterResult, +}; +pub use store::{ + StoreCheckoutContext, StoreCheckoutItemSeed, StoreCheckoutResult, StoreCheckoutVehicleSeed, + StoreGrantedItem, StoreGrantedVehicle, +}; +pub use task::{ + TaskJsonMap, TaskOwnershipContext, TaskOwnershipMutationResult, TaskRecord, TaskRewardContext, +}; 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..bda9235 100644 --- a/lib/models/src/org.rs +++ b/lib/models/src/org.rs @@ -3,13 +3,52 @@ use forge_shared::OrgValidationError; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +pub const DEFAULT_CREDIT_LINE_INTEREST_RATE: f64 = 0.10; + +fn round_currency(value: f64) -> f64 { + (value.max(0.0) * 100.0).round() / 100.0 +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreditLineSummary { pub uid: String, pub name: String, + #[serde(default)] + pub approved_amount: f64, + #[serde(default)] + pub available_amount: f64, + #[serde(default)] + pub outstanding_principal: f64, + #[serde(default = "default_credit_line_interest_rate")] + pub interest_rate: f64, + #[serde(default)] + pub amount_due: f64, + #[serde(default)] pub amount: f64, } +fn default_credit_line_interest_rate() -> f64 { + DEFAULT_CREDIT_LINE_INTEREST_RATE +} + +#[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, @@ -30,6 +69,155 @@ pub struct MemberSummary { pub name: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HotOrgRecord { + pub id: String, + pub owner: String, + pub name: String, + pub funds: f64, + pub reputation: i64, + #[serde(default)] + pub credit_lines: HashMap, + #[serde(default)] + pub assets: HashMap>, + #[serde(default)] + pub fleet: HashMap, + #[serde(default)] + pub members: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgEnsureMemberContext { + pub org_id: String, + pub member_uid: String, + pub member_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgRegisterContext { + pub requester_uid: String, + pub requester_name: String, + pub org_id: String, + pub org_name: String, + pub existing_org_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgRegisterResult { + pub org: HotOrgRecord, + pub actor_organization: String, + pub message: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgCreditLineContext { + pub requester_uid: String, + pub org_id: String, + pub requester_is_default_org_ceo: bool, + pub member_uid: String, + pub member_name: String, + pub amount: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgCheckoutContext { + pub requester_uid: String, + pub org_id: String, + pub requester_is_default_org_ceo: bool, + pub source: String, + pub amount: f64, + pub commit: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgCreditLineRepaymentContext { + pub requester_uid: String, + pub org_id: String, + pub amount: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgAssetGrantSeed { + pub classname: String, + pub category: String, + pub quantity: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgFleetGrantSeed { + pub classname: String, + pub category: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgGrantContext { + pub requester_uid: String, + pub org_id: String, + pub commit: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgMutationResult { + pub org: HotOrgRecord, + pub patch: HashMap, + pub member_uids: Vec, + pub message: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgCreditLineRepaymentResult { + pub org: HotOrgRecord, + pub patch: HashMap, + pub member_uids: Vec, + pub paid_amount: f64, + pub principal_paid: f64, + pub interest_paid: f64, + pub remaining_amount_due: f64, + pub message: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgLeaveContext { + pub requester_uid: String, + pub requester_name: String, + pub org_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgLeaveResult { + pub actor_organization: String, + pub message: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgDisbandMemberResult { + pub uid: String, + pub requester: bool, + pub actor_organization: String, + pub message: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OrgDisbandResult { + pub message: String, + pub members: Vec, +} + impl Org { pub fn new>(id: S, owner: S, name: S) -> Result { let org = Self { @@ -62,6 +250,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())); } @@ -89,7 +283,12 @@ impl Org { )); } - if credit_line.amount < 0.0 { + if credit_line.approved_amount < 0.0 + || credit_line.available_amount < 0.0 + || credit_line.outstanding_principal < 0.0 + || credit_line.amount_due < 0.0 + || credit_line.amount < 0.0 + { return Err(OrgValidationError::NegativeCreditLine( resolved_uid.to_string(), )); @@ -102,6 +301,80 @@ impl Org { pub fn id(&self) -> &str { &self.id } + + pub fn normalize_credit_lines(&mut self) { + for credit_line in self.credit_lines.values_mut() { + credit_line.normalize(); + } + } +} + +impl HotOrgRecord { + pub fn from_parts( + org: Org, + assets: HashMap>, + fleet: HashMap, + members: Vec, + ) -> Self { + Self { + id: org.id, + owner: org.owner, + name: org.name, + funds: org.funds, + reputation: org.reputation, + credit_lines: org.credit_lines, + assets, + fleet, + members: members + .into_iter() + .map(|member| (member.uid.clone(), member)) + .collect(), + } + } + + pub fn into_org(self) -> Org { + let mut org = Org { + id: self.id, + owner: self.owner, + name: self.name, + funds: self.funds, + reputation: self.reputation, + credit_lines: self.credit_lines, + }; + org.normalize_credit_lines(); + org + } +} + +impl CreditLineSummary { + pub fn normalize(&mut self) { + let legacy_amount = round_currency(self.amount); + + self.approved_amount = round_currency(self.approved_amount); + self.available_amount = round_currency(self.available_amount); + self.outstanding_principal = round_currency(self.outstanding_principal); + self.amount_due = round_currency(self.amount_due); + + if self.approved_amount <= 0.0 && self.available_amount <= 0.0 && legacy_amount > 0.0 { + self.approved_amount = legacy_amount; + self.available_amount = legacy_amount; + } else if self.approved_amount <= 0.0 && self.available_amount > 0.0 { + self.approved_amount = self.available_amount; + } else if self.available_amount <= 0.0 && self.approved_amount > 0.0 { + self.available_amount = self.approved_amount; + } + + if self.interest_rate <= 0.0 { + self.interest_rate = DEFAULT_CREDIT_LINE_INTEREST_RATE; + } + + if self.amount_due <= 0.0 && self.outstanding_principal > 0.0 { + self.amount_due = + round_currency(self.outstanding_principal * (1.0 + self.interest_rate)); + } + + self.amount = self.available_amount; + } } impl FromArma for Org { diff --git a/lib/models/src/store.rs b/lib/models/src/store.rs new file mode 100644 index 0000000..9665c5c --- /dev/null +++ b/lib/models/src/store.rs @@ -0,0 +1,72 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StoreCheckoutItemSeed { + pub classname: String, + pub category: String, + pub price_value: f64, + pub quantity: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StoreCheckoutVehicleSeed { + pub classname: String, + pub category: String, + pub price_value: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StoreCheckoutContext { + pub requester_uid: String, + pub requester_name: String, + pub org_id: String, + pub requester_is_default_org_ceo: bool, + pub payment_method: String, + #[serde(default)] + pub items: Vec, + #[serde(default)] + pub vehicles: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StoreGrantedItem { + pub classname: String, + pub category: String, + pub quantity: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StoreGrantedVehicle { + pub classname: String, + pub category: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StoreCheckoutResult { + pub charged_total: f64, + pub payment_method: String, + pub message: String, + #[serde(default)] + pub locker_granted: Vec, + #[serde(default)] + pub vehicle_granted: Vec, + #[serde(default)] + pub locker_patch: HashMap, + #[serde(default)] + pub va_patch: HashMap, + #[serde(default)] + pub vgarage_patch: HashMap, + #[serde(default)] + pub bank_patch: HashMap, + #[serde(default)] + pub org_patch: HashMap, + #[serde(default)] + pub org_target_uids: Vec, +} diff --git a/lib/models/src/task.rs b/lib/models/src/task.rs new file mode 100644 index 0000000..75237ba --- /dev/null +++ b/lib/models/src/task.rs @@ -0,0 +1,57 @@ +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +pub type TaskJsonMap = Map; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(transparent)] +pub struct TaskRecord { + pub fields: TaskJsonMap, +} + +impl TaskRecord { + pub fn into_value(self) -> Value { + Value::Object(self.fields) + } + + pub fn to_value(&self) -> Value { + Value::Object(self.fields.clone()) + } + + pub fn is_empty(&self) -> bool { + self.fields.is_empty() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct TaskOwnershipContext { + #[serde(default)] + pub requester_uid: String, + #[serde(default)] + pub org_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct TaskOwnershipMutationResult { + #[serde(default)] + pub task_id: String, + #[serde(default)] + pub requester_uid: String, + #[serde(default)] + pub org_id: String, + #[serde(default)] + pub entry: Value, + #[serde(default)] + pub message: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct TaskRewardContext { + #[serde(default)] + pub requester_uid: String, + #[serde(default)] + pub org_id: String, +} diff --git a/lib/models/src/v_locker.rs b/lib/models/src/v_locker.rs index 4ad24e4..2f68ffc 100644 --- a/lib/models/src/v_locker.rs +++ b/lib/models/src/v_locker.rs @@ -29,6 +29,7 @@ impl VLocker { "G_Combat".to_string(), "H_Cap_blk_ION".to_string(), "H_HelmetB".to_string(), + "ACE_EarPlugs".to_string(), "ItemCompass".to_string(), "ItemGPS".to_string(), "ItemMap".to_string(), diff --git a/lib/repositories/src/actor.rs b/lib/repositories/src/actor.rs index c20fcd8..cca7867 100644 --- a/lib/repositories/src/actor.rs +++ b/lib/repositories/src/actor.rs @@ -7,6 +7,8 @@ use forge_models::Actor; use forge_shared::{RedisClient, parse_json_value, parse_redis_value}; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; /// Repository trait defining the contract for actor data operations. /// @@ -30,6 +32,48 @@ pub trait ActorRepository: Send + Sync { fn exists(&self, id: &str) -> Result; } +pub trait ActorHotRepository: Send + Sync { + fn get(&self, id: &str) -> Result, String>; + fn save(&self, actor: &Actor) -> Result<(), String>; + fn delete(&self, id: &str) -> Result<(), String>; +} + +#[derive(Clone, Debug, Default)] +pub struct InMemoryActorHotRepository { + state: Arc>>, +} + +impl InMemoryActorHotRepository { + pub fn new() -> Self { + Self::default() + } +} + +impl ActorHotRepository for InMemoryActorHotRepository { + fn get(&self, id: &str) -> Result, String> { + self.state + .read() + .map(|state| state.get(id).cloned()) + .map_err(|_| "Actor hot state lock poisoned.".to_string()) + } + + fn save(&self, actor: &Actor) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Actor hot state lock poisoned.".to_string())? + .insert(actor.uid.clone(), actor.clone()); + Ok(()) + } + + fn delete(&self, id: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Actor hot state lock poisoned.".to_string())? + .remove(id); + Ok(()) + } +} + /// Redis-based implementation of the ActorRepository trait. /// /// This implementation uses Redis hash maps to store actor data, providing @@ -98,21 +142,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..a1f557d 100644 --- a/lib/repositories/src/bank.rs +++ b/lib/repositories/src/bank.rs @@ -7,6 +7,8 @@ use forge_models::Bank; use forge_shared::{RedisClient, parse_json_value, parse_redis_value}; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; /// Repository trait defining the contract for bank data operations. /// @@ -30,6 +32,48 @@ pub trait BankRepository: Send + Sync { fn exists(&self, id: &str) -> Result; } +pub trait BankHotRepository: Send + Sync { + fn get(&self, id: &str) -> Result, String>; + fn save(&self, bank: &Bank) -> Result<(), String>; + fn delete(&self, id: &str) -> Result<(), String>; +} + +#[derive(Clone, Debug, Default)] +pub struct InMemoryBankHotRepository { + state: Arc>>, +} + +impl InMemoryBankHotRepository { + pub fn new() -> Self { + Self::default() + } +} + +impl BankHotRepository for InMemoryBankHotRepository { + fn get(&self, id: &str) -> Result, String> { + self.state + .read() + .map(|state| state.get(id).cloned()) + .map_err(|_| "Bank hot state lock poisoned.".to_string()) + } + + fn save(&self, bank: &Bank) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Bank hot state lock poisoned.".to_string())? + .insert(bank.uid.clone(), bank.clone()); + Ok(()) + } + + fn delete(&self, id: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Bank hot state lock poisoned.".to_string())? + .remove(id); + Ok(()) + } +} + /// Redis-based implementation of the BankRepository trait. /// /// This implementation uses Redis hash maps to store bank data, providing @@ -98,21 +142,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/cad.rs b/lib/repositories/src/cad.rs new file mode 100644 index 0000000..d9d32e5 --- /dev/null +++ b/lib/repositories/src/cad.rs @@ -0,0 +1,236 @@ +use forge_models::CadRecord; +use serde_json::Value; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +const CAD_ACTIVITY_LIMIT: usize = 200; + +pub trait CadRepository: Send + Sync { + fn append_activity(&self, entry: Value) -> Result<(), String>; + fn recent_activity(&self, limit: usize) -> Result, String>; + fn snapshot_activity(&self) -> Result, String>; + + fn list_assignments(&self) -> Result, String>; + fn get_assignment(&self, id: &str) -> Result, String>; + fn save_assignment(&self, id: String, entry: CadRecord) -> Result<(), String>; + fn delete_assignment(&self, id: &str) -> Result<(), String>; + + fn list_orders(&self) -> Result, String>; + fn get_order(&self, id: &str) -> Result, String>; + fn save_order(&self, id: String, entry: CadRecord) -> Result<(), String>; + fn delete_order(&self, id: &str) -> Result<(), String>; + + fn list_requests(&self) -> Result, String>; + fn get_request(&self, id: &str) -> Result, String>; + fn save_request(&self, id: String, entry: CadRecord) -> Result<(), String>; + fn delete_request(&self, id: &str) -> Result<(), String>; + + fn list_profiles(&self) -> Result, String>; + fn get_profile(&self, id: &str) -> Result, String>; + fn save_profile(&self, id: String, entry: CadRecord) -> Result<(), String>; + fn delete_profile(&self, id: &str) -> Result<(), String>; + + fn next_order_id(&self) -> Result; + fn next_request_id(&self) -> Result; +} + +#[derive(Debug, Default)] +struct CadState { + activity: Vec, + assignments: HashMap, + orders: HashMap, + requests: HashMap, + profiles: HashMap, + order_sequence: u64, + request_sequence: u64, +} + +#[derive(Clone, Debug, Default)] +pub struct InMemoryCadRepository { + state: Arc>, +} + +impl InMemoryCadRepository { + pub fn new() -> Self { + Self::default() + } +} + +impl CadRepository for InMemoryCadRepository { + fn append_activity(&self, entry: Value) -> Result<(), String> { + let mut state = self + .state + .write() + .map_err(|_| "CAD activity state lock poisoned.".to_string())?; + + state.activity.push(entry); + if state.activity.len() > CAD_ACTIVITY_LIMIT { + let overflow = state.activity.len() - CAD_ACTIVITY_LIMIT; + state.activity.drain(0..overflow); + } + + Ok(()) + } + + fn recent_activity(&self, limit: usize) -> Result, String> { + let state = self + .state + .read() + .map_err(|_| "CAD activity state lock poisoned.".to_string())?; + let start = state.activity.len().saturating_sub(limit); + Ok(state.activity[start..].to_vec()) + } + + fn snapshot_activity(&self) -> Result, String> { + self.state + .read() + .map(|state| state.activity.clone()) + .map_err(|_| "CAD activity state lock poisoned.".to_string()) + } + + fn list_assignments(&self) -> Result, String> { + self.state + .read() + .map(|state| state.assignments.clone()) + .map_err(|_| "CAD assignments state lock poisoned.".to_string()) + } + + fn get_assignment(&self, id: &str) -> Result, String> { + self.state + .read() + .map(|state| state.assignments.get(id).cloned()) + .map_err(|_| "CAD assignments state lock poisoned.".to_string()) + } + + fn save_assignment(&self, id: String, entry: CadRecord) -> Result<(), String> { + self.state + .write() + .map_err(|_| "CAD assignments state lock poisoned.".to_string())? + .assignments + .insert(id, entry); + Ok(()) + } + + fn delete_assignment(&self, id: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "CAD assignments state lock poisoned.".to_string())? + .assignments + .remove(id); + Ok(()) + } + + fn list_orders(&self) -> Result, String> { + self.state + .read() + .map(|state| state.orders.clone()) + .map_err(|_| "CAD orders state lock poisoned.".to_string()) + } + + fn get_order(&self, id: &str) -> Result, String> { + self.state + .read() + .map(|state| state.orders.get(id).cloned()) + .map_err(|_| "CAD orders state lock poisoned.".to_string()) + } + + fn save_order(&self, id: String, entry: CadRecord) -> Result<(), String> { + self.state + .write() + .map_err(|_| "CAD orders state lock poisoned.".to_string())? + .orders + .insert(id, entry); + Ok(()) + } + + fn delete_order(&self, id: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "CAD orders state lock poisoned.".to_string())? + .orders + .remove(id); + Ok(()) + } + + fn list_requests(&self) -> Result, String> { + self.state + .read() + .map(|state| state.requests.clone()) + .map_err(|_| "CAD requests state lock poisoned.".to_string()) + } + + fn get_request(&self, id: &str) -> Result, String> { + self.state + .read() + .map(|state| state.requests.get(id).cloned()) + .map_err(|_| "CAD requests state lock poisoned.".to_string()) + } + + fn save_request(&self, id: String, entry: CadRecord) -> Result<(), String> { + self.state + .write() + .map_err(|_| "CAD requests state lock poisoned.".to_string())? + .requests + .insert(id, entry); + Ok(()) + } + + fn delete_request(&self, id: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "CAD requests state lock poisoned.".to_string())? + .requests + .remove(id); + Ok(()) + } + + fn list_profiles(&self) -> Result, String> { + self.state + .read() + .map(|state| state.profiles.clone()) + .map_err(|_| "CAD profiles state lock poisoned.".to_string()) + } + + fn get_profile(&self, id: &str) -> Result, String> { + self.state + .read() + .map(|state| state.profiles.get(id).cloned()) + .map_err(|_| "CAD profiles state lock poisoned.".to_string()) + } + + fn save_profile(&self, id: String, entry: CadRecord) -> Result<(), String> { + self.state + .write() + .map_err(|_| "CAD profiles state lock poisoned.".to_string())? + .profiles + .insert(id, entry); + Ok(()) + } + + fn delete_profile(&self, id: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "CAD profiles state lock poisoned.".to_string())? + .profiles + .remove(id); + Ok(()) + } + + fn next_order_id(&self) -> Result { + let mut state = self + .state + .write() + .map_err(|_| "CAD order sequence lock poisoned.".to_string())?; + state.order_sequence += 1; + Ok(format!("cad-order:{}", state.order_sequence)) + } + + fn next_request_id(&self) -> Result { + let mut state = self + .state + .write() + .map_err(|_| "CAD request sequence lock poisoned.".to_string())?; + state.request_sequence += 1; + Ok(format!("cad-request:{}", state.request_sequence)) + } +} diff --git a/lib/repositories/src/garage.rs b/lib/repositories/src/garage.rs index eadab6e..fc1e864 100644 --- a/lib/repositories/src/garage.rs +++ b/lib/repositories/src/garage.rs @@ -6,6 +6,7 @@ use forge_models::{Garage, Vehicle}; use forge_shared::RedisClient; use std::collections::HashMap; +use std::sync::{Arc, RwLock}; /// Repository trait defining the contract for garage data operations. pub trait GarageRepository: Send + Sync { @@ -25,6 +26,48 @@ pub trait GarageRepository: Send + Sync { fn exists(&self, uid: &str) -> Result; } +pub trait GarageHotRepository: Send + Sync { + fn get(&self, uid: &str) -> Result, String>; + fn save(&self, garage: &Garage, uid: &str) -> Result<(), String>; + fn delete(&self, uid: &str) -> Result<(), String>; +} + +#[derive(Clone, Debug, Default)] +pub struct InMemoryGarageHotRepository { + state: Arc>>, +} + +impl InMemoryGarageHotRepository { + pub fn new() -> Self { + Self::default() + } +} + +impl GarageHotRepository for InMemoryGarageHotRepository { + fn get(&self, uid: &str) -> Result, String> { + self.state + .read() + .map(|state| state.get(uid).cloned()) + .map_err(|_| "Garage hot state lock poisoned.".to_string()) + } + + fn save(&self, garage: &Garage, uid: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Garage hot state lock poisoned.".to_string())? + .insert(uid.to_string(), garage.clone()); + Ok(()) + } + + fn delete(&self, uid: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Garage hot state lock poisoned.".to_string())? + .remove(uid); + Ok(()) + } +} + /// Redis-based implementation of the GarageRepository trait. /// /// Stores each player's garage as a single JSON string array with the key format `garage:{uid}`. diff --git a/lib/repositories/src/lib.rs b/lib/repositories/src/lib.rs index aca474b..2481c2a 100644 --- a/lib/repositories/src/lib.rs +++ b/lib/repositories/src/lib.rs @@ -1,18 +1,32 @@ pub mod actor; pub mod bank; +pub mod cad; pub mod garage; pub mod locker; pub mod org; +pub mod task; pub mod v_garage; pub mod v_locker; -pub use actor::{ActorRepository, RedisActorRepository}; -pub use bank::{BankRepository, RedisBankRepository}; -pub use garage::{GarageRepository, RedisGarageRepository}; -pub use locker::{LockerRepository, RedisLockerRepository}; -pub use org::{OrgRepository, RedisOrgRepository}; -pub use v_garage::{RedisVGarageRepository, VGarageRepository}; -pub use v_locker::{RedisVLockerRepository, VLockerRepository}; +pub use actor::{ + ActorHotRepository, ActorRepository, InMemoryActorHotRepository, RedisActorRepository, +}; +pub use bank::{BankHotRepository, BankRepository, InMemoryBankHotRepository, RedisBankRepository}; +pub use cad::{CadRepository, InMemoryCadRepository}; +pub use garage::{ + GarageHotRepository, GarageRepository, InMemoryGarageHotRepository, RedisGarageRepository, +}; +pub use locker::{ + InMemoryLockerHotRepository, LockerHotRepository, LockerRepository, RedisLockerRepository, +}; +pub use org::{InMemoryOrgHotRepository, OrgHotRepository, OrgRepository, RedisOrgRepository}; +pub use task::{InMemoryTaskRepository, TaskRepository}; +pub use v_garage::{ + InMemoryVGarageHotRepository, RedisVGarageRepository, VGarageHotRepository, VGarageRepository, +}; +pub use v_locker::{ + InMemoryVLockerHotRepository, RedisVLockerRepository, VLockerHotRepository, VLockerRepository, +}; // Re-export RedisClient from shared library for convenience pub use forge_shared::RedisClient; diff --git a/lib/repositories/src/locker.rs b/lib/repositories/src/locker.rs index 7559a74..73724f8 100644 --- a/lib/repositories/src/locker.rs +++ b/lib/repositories/src/locker.rs @@ -6,6 +6,7 @@ use forge_models::{Item, Locker}; use forge_shared::RedisClient; use std::collections::HashMap; +use std::sync::{Arc, RwLock}; /// Repository trait defining the contract for locker data operations. pub trait LockerRepository: Send + Sync { @@ -25,6 +26,48 @@ pub trait LockerRepository: Send + Sync { fn exists(&self, uid: &str) -> Result; } +pub trait LockerHotRepository: Send + Sync { + fn get(&self, uid: &str) -> Result, String>; + fn save(&self, locker: &Locker, uid: &str) -> Result<(), String>; + fn delete(&self, uid: &str) -> Result<(), String>; +} + +#[derive(Clone, Debug, Default)] +pub struct InMemoryLockerHotRepository { + state: Arc>>, +} + +impl InMemoryLockerHotRepository { + pub fn new() -> Self { + Self::default() + } +} + +impl LockerHotRepository for InMemoryLockerHotRepository { + fn get(&self, uid: &str) -> Result, String> { + self.state + .read() + .map(|state| state.get(uid).cloned()) + .map_err(|_| "Locker hot state lock poisoned.".to_string()) + } + + fn save(&self, locker: &Locker, uid: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Locker hot state lock poisoned.".to_string())? + .insert(uid.to_string(), locker.clone()); + Ok(()) + } + + fn delete(&self, uid: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Locker hot state lock poisoned.".to_string())? + .remove(uid); + Ok(()) + } +} + /// Redis-based implementation of the LockerRepository trait. /// /// Stores each player's locker as a single JSON string array with the key format `locker:{uid}`. diff --git a/lib/repositories/src/org.rs b/lib/repositories/src/org.rs index 60340cd..eef8441 100644 --- a/lib/repositories/src/org.rs +++ b/lib/repositories/src/org.rs @@ -5,8 +5,10 @@ //! //! For full documentation and examples, see the [crate README](../README.md). -use forge_models::{MemberSummary, Org}; +use forge_models::{HotOrgRecord, MemberSummary, Org, OrgAssetEntry, OrgFleetEntry}; use forge_shared::{RedisClient, parse_json_value, parse_redis_value}; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; /// Repository trait defining the contract for organization data operations. /// @@ -37,6 +39,71 @@ 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>; +} + +pub trait OrgHotRepository: Send + Sync { + fn get(&self, id: &str) -> Result, String>; + fn save(&self, org: &HotOrgRecord) -> Result<(), String>; + fn delete(&self, id: &str) -> Result<(), String>; +} + +#[derive(Clone, Debug, Default)] +pub struct InMemoryOrgHotRepository { + state: Arc>>, +} + +impl InMemoryOrgHotRepository { + pub fn new() -> Self { + Self::default() + } +} + +impl OrgHotRepository for InMemoryOrgHotRepository { + fn get(&self, id: &str) -> Result, String> { + self.state + .read() + .map(|state| state.get(id).cloned()) + .map_err(|_| "Org hot state lock poisoned.".to_string()) + } + + fn save(&self, org: &HotOrgRecord) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Org hot state lock poisoned.".to_string())? + .insert(org.id.clone(), org.clone()); + Ok(()) + } + + fn delete(&self, id: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Org hot state lock poisoned.".to_string())? + .remove(id); + Ok(()) + } } /// Redis-based implementation of the OrgRepository trait. @@ -109,21 +176,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 +321,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/task.rs b/lib/repositories/src/task.rs new file mode 100644 index 0000000..cdd09b3 --- /dev/null +++ b/lib/repositories/src/task.rs @@ -0,0 +1,204 @@ +use forge_models::{TaskOwnershipContext, TaskRecord}; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +pub trait TaskRepository: Send + Sync { + fn reset(&self) -> Result<(), String>; + + fn list_catalog(&self) -> Result, String>; + fn get_catalog_entry(&self, id: &str) -> Result, String>; + fn save_catalog_entry(&self, id: String, entry: TaskRecord) -> Result<(), String>; + fn delete_catalog_entry(&self, id: &str) -> Result<(), String>; + + fn get_ownership(&self, id: &str) -> Result, String>; + fn save_ownership(&self, id: String, ownership: TaskOwnershipContext) -> Result<(), String>; + fn delete_ownership(&self, id: &str) -> Result<(), String>; + + fn list_active_statuses(&self) -> Result, String>; + fn get_active_status(&self, id: &str) -> Result, String>; + fn set_active_status(&self, id: String, status: String) -> Result<(), String>; + fn delete_active_status(&self, id: &str) -> Result<(), String>; + + fn get_completed_status(&self, id: &str) -> Result, String>; + fn set_completed_status(&self, id: String, status: String) -> Result<(), String>; + fn delete_completed_status(&self, id: &str) -> Result<(), String>; + + fn increment_defuse_count(&self, id: &str) -> Result; + fn get_defuse_count(&self, id: &str) -> Result; + fn clear_defuse_count(&self, id: &str) -> Result<(), String>; +} + +#[derive(Debug, Default)] +struct TaskState { + catalog: HashMap, + ownership: HashMap, + active_statuses: HashMap, + completed_statuses: HashMap, + defuse_counts: HashMap, +} + +#[derive(Clone, Debug, Default)] +pub struct InMemoryTaskRepository { + state: Arc>, +} + +impl InMemoryTaskRepository { + pub fn new() -> Self { + Self::default() + } +} + +impl TaskRepository for InMemoryTaskRepository { + fn reset(&self) -> Result<(), String> { + let mut state = self + .state + .write() + .map_err(|_| "Task state lock poisoned.".to_string())?; + state.catalog.clear(); + state.ownership.clear(); + state.active_statuses.clear(); + state.completed_statuses.clear(); + state.defuse_counts.clear(); + Ok(()) + } + + fn list_catalog(&self) -> Result, String> { + self.state + .read() + .map(|state| state.catalog.clone()) + .map_err(|_| "Task catalog state lock poisoned.".to_string()) + } + + fn get_catalog_entry(&self, id: &str) -> Result, String> { + self.state + .read() + .map(|state| state.catalog.get(id).cloned()) + .map_err(|_| "Task catalog state lock poisoned.".to_string()) + } + + fn save_catalog_entry(&self, id: String, entry: TaskRecord) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Task catalog state lock poisoned.".to_string())? + .catalog + .insert(id, entry); + Ok(()) + } + + fn delete_catalog_entry(&self, id: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Task catalog state lock poisoned.".to_string())? + .catalog + .remove(id); + Ok(()) + } + + fn get_ownership(&self, id: &str) -> Result, String> { + self.state + .read() + .map(|state| state.ownership.get(id).cloned()) + .map_err(|_| "Task ownership state lock poisoned.".to_string()) + } + + fn save_ownership(&self, id: String, ownership: TaskOwnershipContext) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Task ownership state lock poisoned.".to_string())? + .ownership + .insert(id, ownership); + Ok(()) + } + + fn delete_ownership(&self, id: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Task ownership state lock poisoned.".to_string())? + .ownership + .remove(id); + Ok(()) + } + + fn list_active_statuses(&self) -> Result, String> { + self.state + .read() + .map(|state| state.active_statuses.clone()) + .map_err(|_| "Task status state lock poisoned.".to_string()) + } + + fn get_active_status(&self, id: &str) -> Result, String> { + self.state + .read() + .map(|state| state.active_statuses.get(id).cloned()) + .map_err(|_| "Task status state lock poisoned.".to_string()) + } + + fn set_active_status(&self, id: String, status: String) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Task status state lock poisoned.".to_string())? + .active_statuses + .insert(id, status); + Ok(()) + } + + fn delete_active_status(&self, id: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Task status state lock poisoned.".to_string())? + .active_statuses + .remove(id); + Ok(()) + } + + fn get_completed_status(&self, id: &str) -> Result, String> { + self.state + .read() + .map(|state| state.completed_statuses.get(id).cloned()) + .map_err(|_| "Task completed status state lock poisoned.".to_string()) + } + + fn set_completed_status(&self, id: String, status: String) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Task completed status state lock poisoned.".to_string())? + .completed_statuses + .insert(id, status); + Ok(()) + } + + fn delete_completed_status(&self, id: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Task completed status state lock poisoned.".to_string())? + .completed_statuses + .remove(id); + Ok(()) + } + + fn increment_defuse_count(&self, id: &str) -> Result { + let mut state = self + .state + .write() + .map_err(|_| "Task defuse state lock poisoned.".to_string())?; + let next_count = 1 + state.defuse_counts.get(id).copied().unwrap_or_default(); + state.defuse_counts.insert(id.to_string(), next_count); + Ok(next_count) + } + + fn get_defuse_count(&self, id: &str) -> Result { + self.state + .read() + .map(|state| state.defuse_counts.get(id).copied().unwrap_or_default()) + .map_err(|_| "Task defuse state lock poisoned.".to_string()) + } + + fn clear_defuse_count(&self, id: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Task defuse state lock poisoned.".to_string())? + .defuse_counts + .remove(id); + Ok(()) + } +} diff --git a/lib/repositories/src/v_garage.rs b/lib/repositories/src/v_garage.rs index e9e0e00..71bba6f 100644 --- a/lib/repositories/src/v_garage.rs +++ b/lib/repositories/src/v_garage.rs @@ -11,6 +11,8 @@ use forge_models::VGarage; use forge_shared::{RedisClient, parse_json_value, parse_redis_value}; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; /// Repository trait defining the contract for virtual garage data operations. pub trait VGarageRepository: Send + Sync { @@ -34,6 +36,48 @@ pub trait VGarageRepository: Send + Sync { fn exists(&self, uid: &str) -> Result; } +pub trait VGarageHotRepository: Send + Sync { + fn get(&self, uid: &str) -> Result, String>; + fn save(&self, garage: &VGarage, uid: &str) -> Result<(), String>; + fn delete(&self, uid: &str) -> Result<(), String>; +} + +#[derive(Clone, Debug, Default)] +pub struct InMemoryVGarageHotRepository { + state: Arc>>, +} + +impl InMemoryVGarageHotRepository { + pub fn new() -> Self { + Self::default() + } +} + +impl VGarageHotRepository for InMemoryVGarageHotRepository { + fn get(&self, uid: &str) -> Result, String> { + self.state + .read() + .map(|state| state.get(uid).cloned()) + .map_err(|_| "Virtual garage hot state lock poisoned.".to_string()) + } + + fn save(&self, garage: &VGarage, uid: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Virtual garage hot state lock poisoned.".to_string())? + .insert(uid.to_string(), garage.clone()); + Ok(()) + } + + fn delete(&self, uid: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Virtual garage hot state lock poisoned.".to_string())? + .remove(uid); + Ok(()) + } +} + /// Redis-based implementation of the VGarageRepository trait. /// /// Stores each player's virtual garage as a Redis hash with six fields: @@ -98,21 +142,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..83c50a9 100644 --- a/lib/repositories/src/v_locker.rs +++ b/lib/repositories/src/v_locker.rs @@ -9,6 +9,8 @@ use forge_models::VLocker; use forge_shared::{RedisClient, parse_json_value, parse_redis_value}; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; /// Repository trait defining the contract for virtual locker data operations. pub trait VLockerRepository: Send + Sync { @@ -32,6 +34,48 @@ pub trait VLockerRepository: Send + Sync { fn exists(&self, uid: &str) -> Result; } +pub trait VLockerHotRepository: Send + Sync { + fn get(&self, uid: &str) -> Result, String>; + fn save(&self, locker: &VLocker, uid: &str) -> Result<(), String>; + fn delete(&self, uid: &str) -> Result<(), String>; +} + +#[derive(Clone, Debug, Default)] +pub struct InMemoryVLockerHotRepository { + state: Arc>>, +} + +impl InMemoryVLockerHotRepository { + pub fn new() -> Self { + Self::default() + } +} + +impl VLockerHotRepository for InMemoryVLockerHotRepository { + fn get(&self, uid: &str) -> Result, String> { + self.state + .read() + .map(|state| state.get(uid).cloned()) + .map_err(|_| "Virtual locker hot state lock poisoned.".to_string()) + } + + fn save(&self, locker: &VLocker, uid: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Virtual locker hot state lock poisoned.".to_string())? + .insert(uid.to_string(), locker.clone()); + Ok(()) + } + + fn delete(&self, uid: &str) -> Result<(), String> { + self.state + .write() + .map_err(|_| "Virtual locker hot state lock poisoned.".to_string())? + .remove(uid); + Ok(()) + } +} + /// Redis-based implementation of the VLockerRepository trait. /// /// Stores each player's virtual locker as a Redis hash with four fields: @@ -94,21 +138,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/actor.rs b/lib/services/src/actor.rs index 16a780f..6f693b4 100644 --- a/lib/services/src/actor.rs +++ b/lib/services/src/actor.rs @@ -6,7 +6,7 @@ //! For full documentation, architecture, and examples, see the [crate README](../README.md). use forge_models::Actor; -use forge_repositories::ActorRepository; +use forge_repositories::{ActorHotRepository, ActorRepository}; use forge_shared::{generate_email, generate_phone_number}; /// Service layer implementation for actor business logic and operations. @@ -24,6 +24,64 @@ pub struct ActorService { repository: R, } +pub struct ActorHotStateService { + service: ActorService, + repository: H, +} + +impl ActorHotStateService { + pub fn new(repository: R, hot_repository: H) -> Self { + Self { + service: ActorService::new(repository), + repository: hot_repository, + } + } + + pub fn init_actor(&self, key: String) -> Result { + if let Some(actor) = self.repository.get(&key)? { + return Ok(actor); + } + + let actor = self.service.get_actor(key)?; + self.repository.save(&actor)?; + Ok(actor) + } + + pub fn get_actor(&self, key: String) -> Result { + self.init_actor(key) + } + + pub fn override_actor(&self, key: String, json_data: String) -> Result { + let mut actor: Actor = + serde_json::from_str(&json_data).map_err(|e| format!("Invalid Actor JSON: {}", e))?; + + actor.uid = key; + actor + .validate() + .map_err(|e| format!("Validation failed: {}", e))?; + + self.repository.save(&actor)?; + Ok(actor) + } + + pub fn save_actor(&self, key: String) -> Result { + let actor = self + .repository + .get(&key)? + .ok_or_else(|| format!("Actor with UID '{}' not found in hot state", key))?; + let actor_json = serde_json::to_string(&actor) + .map_err(|e| format!("Failed to serialize actor: {}", e))?; + + let saved_actor = self.service.update_actor(key, actor_json)?; + self.repository.save(&saved_actor)?; + Ok(saved_actor) + } + + pub fn remove_actor(&self, key: String) -> Result<(), String> { + self.repository.delete(&key) + } +} + impl ActorService { /// Creates a new actor service with the provided repository. /// diff --git a/lib/services/src/bank.rs b/lib/services/src/bank.rs index 452c817..9d1f00e 100644 --- a/lib/services/src/bank.rs +++ b/lib/services/src/bank.rs @@ -5,8 +5,13 @@ //! //! For full documentation, architecture, and examples, see the [crate README](../README.md). -use forge_models::Bank; -use forge_repositories::BankRepository; +use forge_models::{ + Bank, BankCheckoutContext, BankMutationResult, BankOperationContext, BankPinContext, + BankTransferContext, BankTransferResult, +}; +use forge_repositories::{BankHotRepository, BankRepository}; +use serde_json::{Value, json}; +use std::collections::HashMap; /// Service layer implementation for bank business logic and operations. /// @@ -23,6 +28,399 @@ pub struct BankService { repository: R, } +pub struct BankHotStateService { + service: BankService, + repository: H, +} + +impl BankHotStateService { + pub fn new(repository: R, hot_repository: H) -> Self { + Self { + service: BankService::new(repository), + repository: hot_repository, + } + } + + pub fn init_bank(&self, key: String) -> Result { + if let Some(bank) = self.repository.get(&key)? { + return Ok(bank); + } + + let bank = self.service.get_bank(key)?; + self.repository.save(&bank)?; + Ok(bank) + } + + pub fn get_bank(&self, key: String) -> Result { + self.init_bank(key) + } + + pub fn override_bank(&self, key: String, json_data: String) -> Result { + let mut bank: Bank = + serde_json::from_str(&json_data).map_err(|e| format!("Invalid Bank JSON: {}", e))?; + + bank.uid = key; + bank.validate() + .map_err(|e| format!("Validation failed: {}", e))?; + + self.repository.save(&bank)?; + Ok(bank) + } + + pub fn patch_bank( + &self, + key: String, + json_patch: String, + ) -> Result { + let patch_value: Value = + serde_json::from_str(&json_patch).map_err(|e| format!("Invalid patch JSON: {}", e))?; + let patch_object = patch_value + .as_object() + .ok_or_else(|| "Patch data must be a JSON object".to_string())?; + + let mut bank = self.get_bank(key.clone())?; + let mut patch = HashMap::new(); + + for (field, value) in patch_object { + apply_bank_field(&mut bank, field, value)?; + patch.insert(field.clone(), current_bank_field_value(&bank, field)?); + } + + bank.validate() + .map_err(|e| format!("Validation failed: {}", e))?; + self.repository.save(&bank)?; + + Ok(BankMutationResult { + account: bank, + patch, + }) + } + + pub fn charge_checkout( + &self, + key: String, + amount: f64, + context: BankCheckoutContext, + ) -> Result { + if amount <= 0.0 { + return Err("Checkout amount must be greater than zero".to_string()); + } + + let mut bank = self.get_bank(key)?; + let source_field = match context.source_field.trim().to_ascii_lowercase().as_str() { + "cash" => "cash", + "bank" => "bank", + _ => return Err("Selected bank payment source is unsupported.".to_string()), + }; + + let source_balance = match source_field { + "cash" => bank.cash, + _ => bank.bank, + }; + if source_balance < amount { + return Err(match source_field { + "cash" => "Cash on hand cannot cover this checkout.".to_string(), + _ => "Bank balance cannot cover this checkout.".to_string(), + }); + } + + match source_field { + "cash" => bank.cash -= amount, + _ => bank.bank -= amount, + } + + bank.validate() + .map_err(|e| format!("Validation failed: {}", e))?; + if context.commit { + self.repository.save(&bank)?; + } + + Ok(BankMutationResult { + account: bank.clone(), + patch: build_patch(&bank, &[source_field])?, + }) + } + + pub fn deposit( + &self, + key: String, + amount: f64, + context: BankOperationContext, + ) -> Result { + if amount <= 0.0 { + return Err("Deposit amount must be greater than zero".to_string()); + } + validate_atm_access(&context, "deposit")?; + + let mut bank = self.get_bank(key)?; + if bank.cash < amount { + return Err("Cash on hand cannot cover that deposit.".to_string()); + } + + bank.cash -= amount; + bank.bank += amount; + bank.validate() + .map_err(|e| format!("Validation failed: {}", e))?; + self.repository.save(&bank)?; + + Ok(BankMutationResult { + account: bank.clone(), + patch: build_patch(&bank, &["bank", "cash"])?, + }) + } + + pub fn withdraw( + &self, + key: String, + amount: f64, + context: BankOperationContext, + ) -> Result { + if amount <= 0.0 { + return Err("Withdrawal amount must be greater than zero".to_string()); + } + validate_atm_access(&context, "withdrawal")?; + + let mut bank = self.get_bank(key)?; + if bank.bank < amount { + return Err("Bank balance cannot cover that withdrawal.".to_string()); + } + + bank.bank -= amount; + bank.cash += amount; + bank.validate() + .map_err(|e| format!("Validation failed: {}", e))?; + self.repository.save(&bank)?; + + Ok(BankMutationResult { + account: bank.clone(), + patch: build_patch(&bank, &["bank", "cash"])?, + }) + } + + pub fn deposit_earnings( + &self, + key: String, + amount: f64, + context: BankOperationContext, + ) -> Result { + if amount <= 0.0 { + return Err("Deposit earnings amount must be greater than zero".to_string()); + } + validate_bank_mode(&context, "Earnings deposits")?; + + let mut bank = self.get_bank(key)?; + if bank.earnings < amount { + return Err("Pending earnings cannot cover that deposit request.".to_string()); + } + + bank.bank += amount; + bank.earnings -= amount; + bank.validate() + .map_err(|e| format!("Validation failed: {}", e))?; + self.repository.save(&bank)?; + + Ok(BankMutationResult { + account: bank.clone(), + patch: build_patch(&bank, &["bank", "earnings"])?, + }) + } + + pub fn transfer( + &self, + source_key: String, + target_key: String, + context: BankTransferContext, + amount: f64, + ) -> Result { + if amount <= 0.0 { + return Err("Transfer amount must be greater than zero".to_string()); + } + validate_bank_mode( + &BankOperationContext { + mode: context.mode.clone(), + atm_authorized: context.atm_authorized, + }, + "Transfers", + )?; + if source_key == target_key { + return Err("You cannot transfer funds to yourself.".to_string()); + } + + let mut source_account = self.get_bank(source_key)?; + let mut target_account = self.get_bank(target_key)?; + let source_field = match context.from_field.trim().to_ascii_lowercase().as_str() { + "cash" => "cash", + _ => "bank", + }; + + let source_balance = match source_field { + "cash" => source_account.cash, + _ => source_account.bank, + }; + if source_balance < amount { + return Err(match source_field { + "cash" => "Cash on hand cannot cover that transfer.".to_string(), + _ => "Bank balance cannot cover that transfer.".to_string(), + }); + } + + match source_field { + "cash" => source_account.cash -= amount, + _ => source_account.bank -= amount, + } + target_account.bank += amount; + + source_account + .validate() + .map_err(|e| format!("Validation failed: {}", e))?; + target_account + .validate() + .map_err(|e| format!("Validation failed: {}", e))?; + + self.repository.save(&source_account)?; + self.repository.save(&target_account)?; + + Ok(BankTransferResult { + source_patch: build_patch(&source_account, &[source_field])?, + source_account, + target_patch: build_patch(&target_account, &["bank"])?, + target_account, + }) + } + + pub fn validate_pin( + &self, + key: String, + pin: String, + context: BankPinContext, + ) -> Result<(), String> { + if !context.mode.eq_ignore_ascii_case("atm") { + return Err("PIN entry is only available from an ATM session.".to_string()); + } + + if pin.len() != 4 || !pin.chars().all(|character| character.is_ascii_digit()) { + return Err("Enter your four-digit access PIN.".to_string()); + } + + let bank = self.get_bank(key)?; + if pin != bank.pin.to_string() { + return Err("Incorrect PIN.".to_string()); + } + + Ok(()) + } + + pub fn save_bank(&self, key: String) -> Result { + let bank = self + .repository + .get(&key)? + .ok_or_else(|| format!("Bank with UID '{}' not found in hot state", key))?; + let bank_json = + serde_json::to_string(&bank).map_err(|e| format!("Failed to serialize bank: {}", e))?; + + let saved_bank = self.service.update_bank(key, bank_json)?; + self.repository.save(&saved_bank)?; + Ok(saved_bank) + } + + pub fn remove_bank(&self, key: String) -> Result<(), String> { + self.repository.delete(&key) + } +} + +fn apply_bank_field(bank: &mut Bank, field: &str, value: &Value) -> Result<(), String> { + match field { + "uid" => Ok(()), + "name" => { + bank.name = value + .as_str() + .ok_or_else(|| "Name must be a string".to_string())? + .to_string(); + Ok(()) + } + "bank" => { + bank.bank = value + .as_f64() + .ok_or_else(|| "Bank balance must be a number".to_string())?; + Ok(()) + } + "cash" => { + bank.cash = value + .as_f64() + .ok_or_else(|| "Cash must be a number".to_string())?; + Ok(()) + } + "earnings" => { + bank.earnings = value + .as_f64() + .ok_or_else(|| "Earnings must be a number".to_string())?; + Ok(()) + } + "pin" => { + bank.pin = value + .as_u64() + .ok_or_else(|| "PIN must be a number".to_string())?; + Ok(()) + } + "transactions" => { + let values = value + .as_array() + .ok_or_else(|| "Transactions must be an array".to_string())?; + bank.transactions = values + .iter() + .map(|entry| { + entry + .as_str() + .map(|item| item.to_string()) + .ok_or_else(|| "Transactions must contain strings".to_string()) + }) + .collect::, _>>()?; + Ok(()) + } + _ => Err(format!("Unknown field: {}", field)), + } +} + +fn current_bank_field_value(bank: &Bank, field: &str) -> Result { + match field { + "uid" => Ok(json!(bank.uid)), + "name" => Ok(json!(bank.name)), + "bank" => Ok(json!(bank.bank)), + "cash" => Ok(json!(bank.cash)), + "earnings" => Ok(json!(bank.earnings)), + "pin" => Ok(json!(bank.pin)), + "transactions" => Ok(json!(bank.transactions)), + _ => Err(format!("Unknown field: {}", field)), + } +} + +fn build_patch(bank: &Bank, fields: &[&str]) -> Result, String> { + let mut patch = HashMap::new(); + for field in fields { + patch.insert((*field).to_string(), current_bank_field_value(bank, field)?); + } + Ok(patch) +} + +fn validate_atm_access(context: &BankOperationContext, action: &str) -> Result<(), String> { + if context.mode.eq_ignore_ascii_case("atm") && !context.atm_authorized { + return Err(format!("ATM authorization is required before {}.", action)); + } + + Ok(()) +} + +fn validate_bank_mode(context: &BankOperationContext, action: &str) -> Result<(), String> { + if !context.mode.eq_ignore_ascii_case("bank") { + return Err(format!( + "{} are only available from the full bank interface.", + action + )); + } + + Ok(()) +} + impl BankService { /// Creates a new bank service with the provided repository. /// diff --git a/lib/services/src/cad.rs b/lib/services/src/cad.rs new file mode 100644 index 0000000..a165af0 --- /dev/null +++ b/lib/services/src/cad.rs @@ -0,0 +1,1170 @@ +use forge_models::{ + CadActivityEntry, CadAssignmentMutationResult, CadDispatchOrderContextSeed, + CadDispatchOrderCreateSeed, CadDispatchOrderMutationResult, CadGroupBuildSeed, + CadGroupProfileMutationResult, CadGroupProfileUpdateSeed, CadHydratePayload, CadHydrateSeed, + CadRecord, CadRequestMutationResult, CadSession, CadSupportRequestSubmitSeed, +}; +use forge_repositories::CadRepository; +use serde_json::{Map, Value}; +use std::collections::HashMap; + +const CAD_ACTIVITY_LIMIT: usize = 200; +const CAD_RECENT_ACTIVITY_LIMIT: usize = 50; + +pub struct CadStateService { + repository: R, +} + +impl CadStateService { + pub fn new(repository: R) -> Self { + Self { repository } + } + + pub fn append_activity(&self, json_data: String) -> Result<(), String> { + let entry = Self::parse_value(&json_data)?; + self.repository.append_activity(entry) + } + + pub fn recent_activity(&self, limit: String) -> Result, String> { + let parsed_limit = limit + .trim() + .parse::() + .ok() + .filter(|value| *value > 0) + .unwrap_or(CAD_RECENT_ACTIVITY_LIMIT) + .min(CAD_ACTIVITY_LIMIT); + + self.repository.recent_activity(parsed_limit) + } + + pub fn list_assignments(&self) -> Result, String> { + Ok(Self::records_to_values(self.repository.list_assignments()?)) + } + + pub fn assign_assignment( + &self, + entry_id: String, + json_data: String, + ) -> Result { + let entry_id = Self::validate_entry_id(entry_id)?; + let mut assignment = Self::parse_record(&json_data)?; + Self::set_task_id(&mut assignment, &entry_id); + self.repository + .save_assignment(entry_id.clone(), assignment.clone())?; + + let assignee = Self::display_group_name(&assignment.fields); + let assigned_by = Self::string_field(&assignment.fields, "assignedByName") + .unwrap_or_else(|| "Dispatcher".to_string()); + let group_id = Self::string_field(&assignment.fields, "groupId").unwrap_or_default(); + let actor_uid = Self::string_field(&assignment.fields, "assignedByUid").unwrap_or_default(); + Ok(CadAssignmentMutationResult { + assignment: assignment.into_value(), + message: "Task assigned.".to_string(), + activity: Self::build_activity( + "task_assigned", + format!("{assigned_by} assigned {entry_id} to {assignee}."), + entry_id, + group_id, + actor_uid, + ), + }) + } + + pub fn acknowledge_assignment( + &self, + entry_id: String, + json_data: String, + ) -> Result { + let entry_id = Self::validate_entry_id(entry_id)?; + let patch = Self::parse_record(&json_data)?; + let existing = self + .repository + .get_assignment(&entry_id)? + .ok_or_else(|| "CAD assignment could not be resolved.".to_string())?; + let merged = existing.merge(patch); + self.repository.save_assignment(entry_id, merged.clone())?; + Ok(CadAssignmentMutationResult { + assignment: merged.to_value(), + message: "Task acknowledged.".to_string(), + activity: Self::build_activity( + "task_acknowledged", + format!( + "{} acknowledged {}.", + Self::string_field(&merged.fields, "acknowledgedByUid").unwrap_or_default(), + Self::string_field(&merged.fields, "taskId").unwrap_or_default() + ), + Self::string_field(&merged.fields, "taskId").unwrap_or_default(), + Self::string_field(&merged.fields, "groupId").unwrap_or_default(), + Self::string_field(&merged.fields, "acknowledgedByUid").unwrap_or_default(), + ), + }) + } + + pub fn decline_assignment( + &self, + entry_id: String, + json_data: String, + ) -> Result { + let entry_id = Self::validate_entry_id(entry_id)?; + let patch = Self::parse_record(&json_data)?; + let existing = self + .repository + .get_assignment(&entry_id)? + .ok_or_else(|| "CAD assignment could not be resolved.".to_string())?; + let merged = existing.merge(patch); + self.repository.delete_assignment(&entry_id)?; + Ok(CadAssignmentMutationResult { + assignment: merged.to_value(), + message: "Task declined and returned to the contract board.".to_string(), + activity: Self::build_activity( + "task_declined", + format!( + "{} declined {}.", + Self::string_field(&merged.fields, "declinedByUid").unwrap_or_default(), + Self::string_field(&merged.fields, "taskId").unwrap_or_default() + ), + Self::string_field(&merged.fields, "taskId").unwrap_or_default(), + Self::string_field(&merged.fields, "groupId").unwrap_or_default(), + Self::string_field(&merged.fields, "declinedByUid").unwrap_or_default(), + ), + }) + } + + pub fn upsert_assignment(&self, entry_id: String, json_data: String) -> Result<(), String> { + let entry_id = Self::validate_entry_id(entry_id)?; + let entry = Self::parse_record(&json_data)?; + self.repository.save_assignment(entry_id, entry) + } + + pub fn delete_assignment(&self, entry_id: String) -> Result<(), String> { + let entry_id = Self::validate_entry_id(entry_id)?; + self.repository.delete_assignment(&entry_id) + } + + pub fn list_orders(&self) -> Result, String> { + Ok(Self::records_to_values(self.repository.list_orders()?)) + } + + pub fn create_order( + &self, + json_data: String, + ) -> Result { + let payload = serde_json::from_str::(&json_data) + .map_err(|error| format!("Invalid CAD order payload: {error}"))?; + + if payload.order.is_empty() { + return Err("Order payload is required.".to_string()); + } + if payload.assignment.is_empty() { + return Err("Assignment payload is required.".to_string()); + } + + let task_id = self.repository.next_order_id()?; + let mut order = payload.order; + let mut assignment = payload.assignment; + + Self::set_task_id(&mut order, &task_id); + order + .fields + .insert("isDispatchOrder".to_string(), Value::Bool(true)); + + Self::set_task_id(&mut assignment, &task_id); + + self.repository.save_order(task_id.clone(), order.clone())?; + self.repository + .save_assignment(task_id.clone(), assignment.clone())?; + + Ok(CadDispatchOrderMutationResult { + task_id: task_id.clone(), + order: order.to_value(), + assignment: assignment.to_value(), + message: "Dispatch order created.".to_string(), + activity: Self::build_activity( + "dispatch_order_created", + format!( + "{} created backup order {task_id} for {} to support {}.", + Self::string_field(&order.fields, "createdByName") + .unwrap_or_else(|| "Dispatcher".to_string()), + Self::display_group_name(&assignment.fields), + Self::string_field(&order.fields, "targetGroupCallsign") + .unwrap_or_else(|| Self::string_field(&order.fields, "targetGroupId") + .unwrap_or_else(|| "target group".to_string())) + ), + task_id, + Self::string_field(&assignment.fields, "groupId").unwrap_or_default(), + Self::string_field(&order.fields, "createdByUid").unwrap_or_default(), + ), + }) + } + + pub fn create_order_from_context( + &self, + json_data: String, + ) -> Result { + let seed = serde_json::from_str::(&json_data) + .map_err(|error| format!("Invalid CAD order context: {error}"))?; + + if seed.assignee_group_id.trim().is_empty() || seed.target_group_id.trim().is_empty() { + return Err("Assignee and target groups are required.".to_string()); + } + + let final_priority = Self::normalize_priority(&seed.priority); + let target_callsign = + Self::fallback_string(&seed.target_group_callsign, &seed.target_group_id); + let created_by_name = Self::fallback_string(&seed.created_by_name, "Dispatcher"); + let assignee_callsign = + Self::fallback_string(&seed.assignee_group_callsign, &seed.assignee_group_id); + + let order = CadRecord { + fields: Map::from_iter([ + ( + "title".to_string(), + Value::String(format!("Backup {target_callsign}")), + ), + ( + "description".to_string(), + Value::String(if seed.note.trim().is_empty() { + format!( + "Dispatch order to back up {target_callsign} at its current position." + ) + } else { + seed.note.clone() + }), + ), + ( + "type".to_string(), + Value::String("dispatch_order".to_string()), + ), + ("priority".to_string(), Value::String(final_priority)), + ("position".to_string(), seed.target_position.clone()), + ( + "targetGroupId".to_string(), + Value::String(seed.target_group_id.clone()), + ), + ( + "targetGroupCallsign".to_string(), + Value::String(target_callsign.clone()), + ), + ( + "createdByUid".to_string(), + Value::String(seed.created_by_uid.clone()), + ), + ( + "createdByName".to_string(), + Value::String(created_by_name.clone()), + ), + ( + "sourceRequestId".to_string(), + Value::String(seed.request_id.clone()), + ), + ( + "sourceRequestType".to_string(), + Value::String(seed.request_type.clone()), + ), + ( + "sourceRequestTitle".to_string(), + Value::String(seed.request_title.clone()), + ), + ( + "sourceRequestSummary".to_string(), + Value::String(seed.request_summary.clone()), + ), + ( + "sourceRequestFields".to_string(), + seed.request_fields.to_value(), + ), + ("createdAt".to_string(), Value::from(seed.created_at)), + ("note".to_string(), Value::String(seed.note.clone())), + ("isDispatchOrder".to_string(), Value::Bool(true)), + ]), + }; + + let assignment = CadRecord { + fields: Map::from_iter([ + ( + "groupId".to_string(), + Value::String(seed.assignee_group_id.clone()), + ), + ( + "assigneeGroupCallsign".to_string(), + Value::String(assignee_callsign.clone()), + ), + ( + "assignedByUid".to_string(), + Value::String(seed.created_by_uid.clone()), + ), + ( + "assignedByName".to_string(), + Value::String(created_by_name.clone()), + ), + ("assignedAt".to_string(), Value::from(seed.created_at)), + ("state".to_string(), Value::String("assigned".to_string())), + ("note".to_string(), Value::String(seed.note)), + ]), + }; + + let payload = CadDispatchOrderCreateSeed { order, assignment }; + self.create_order( + serde_json::to_string(&payload) + .map_err(|error| format!("Failed to serialize CAD order payload: {error}"))?, + ) + } + + pub fn close_order(&self, entry_id: String) -> Result { + let entry_id = Self::validate_entry_id(entry_id)?; + let order = self + .repository + .get_order(&entry_id)? + .ok_or_else(|| "CAD order could not be resolved.".to_string())?; + let assignment = self.repository.get_assignment(&entry_id)?; + + self.repository.delete_order(&entry_id)?; + self.repository.delete_assignment(&entry_id)?; + + Ok(CadDispatchOrderMutationResult { + task_id: entry_id.clone(), + order: order.to_value(), + assignment: assignment.map_or(Value::Null, CadRecord::into_value), + message: "Dispatch order closed.".to_string(), + activity: Self::build_activity( + "dispatch_order_closed", + format!("{entry_id} was closed."), + entry_id, + Self::string_field(&order.fields, "groupId").unwrap_or_default(), + String::new(), + ), + }) + } + + pub fn upsert_order(&self, entry_id: String, json_data: String) -> Result<(), String> { + let entry_id = Self::validate_entry_id(entry_id)?; + let entry = Self::parse_record(&json_data)?; + self.repository.save_order(entry_id, entry) + } + + pub fn delete_order(&self, entry_id: String) -> Result<(), String> { + let entry_id = Self::validate_entry_id(entry_id)?; + self.repository.delete_order(&entry_id) + } + + pub fn list_requests(&self) -> Result, String> { + Ok(Self::records_to_values(self.repository.list_requests()?)) + } + + pub fn submit_request(&self, json_data: String) -> Result { + let mut request = Self::parse_record(&json_data)?; + let request_id = self.repository.next_request_id()?; + request + .fields + .insert("requestId".to_string(), Value::String(request_id.clone())); + self.repository.save_request(request_id, request.clone())?; + Ok(CadRequestMutationResult { + request: request.to_value(), + message: "Support request submitted.".to_string(), + activity: Self::build_activity( + "support_request_submitted", + format!( + "{} submitted {}.", + Self::string_field(&request.fields, "groupCallsign") + .unwrap_or_else(|| "Unknown Group".to_string()), + Self::string_field(&request.fields, "title") + .unwrap_or_else(|| "support request".to_string()) + ), + Self::string_field(&request.fields, "requestId").unwrap_or_default(), + Self::string_field(&request.fields, "groupId").unwrap_or_default(), + Self::string_field(&request.fields, "submittedByUid").unwrap_or_default(), + ), + }) + } + + pub fn submit_request_from_context( + &self, + json_data: String, + ) -> Result { + let seed = serde_json::from_str::(&json_data) + .map_err(|error| format!("Invalid CAD support request context: {error}"))?; + + if seed.request_type.trim().is_empty() { + return Err("Support request type is required.".to_string()); + } + if seed.group_id.trim().is_empty() { + return Err("Group ID is required.".to_string()); + } + + let request_type = seed.request_type.to_lowercase(); + let group_callsign = Self::fallback_string(&seed.group_callsign, &seed.group_id); + let request = CadRecord { + fields: Map::from_iter([ + ("type".to_string(), Value::String(request_type.clone())), + ( + "title".to_string(), + Value::String(Self::build_request_title(&request_type, &group_callsign)), + ), + ( + "summary".to_string(), + Value::String(Self::build_request_summary( + &request_type, + &seed.fields.fields, + &group_callsign, + )), + ), + ("groupId".to_string(), Value::String(seed.group_id)), + ( + "groupCallsign".to_string(), + Value::String(group_callsign.clone()), + ), + ( + "submittedByUid".to_string(), + Value::String(seed.submitted_by_uid), + ), + ( + "submittedByName".to_string(), + Value::String(Self::fallback_string( + &seed.submitted_by_name, + &group_callsign, + )), + ), + ("fields".to_string(), seed.fields.into_value()), + ( + "priority".to_string(), + Value::String(Self::normalize_priority(&seed.priority)), + ), + ("position".to_string(), seed.position), + ("createdAt".to_string(), Value::from(seed.created_at)), + ]), + }; + + self.submit_request( + serde_json::to_string(&request) + .map_err(|error| format!("Failed to serialize CAD request payload: {error}"))?, + ) + } + + pub fn close_request(&self, entry_id: String) -> Result { + let entry_id = Self::validate_entry_id(entry_id)?; + let request = self + .repository + .get_request(&entry_id)? + .ok_or_else(|| "CAD request could not be resolved.".to_string())?; + self.repository.delete_request(&entry_id)?; + Ok(CadRequestMutationResult { + request: request.to_value(), + message: "Support request closed.".to_string(), + activity: Self::build_activity( + "support_request_closed", + format!( + "{} was closed.", + Self::string_field(&request.fields, "title").unwrap_or(entry_id.clone()) + ), + entry_id, + Self::string_field(&request.fields, "groupId").unwrap_or_default(), + String::new(), + ), + }) + } + + pub fn upsert_request(&self, entry_id: String, json_data: String) -> Result<(), String> { + let entry_id = Self::validate_entry_id(entry_id)?; + let entry = Self::parse_record(&json_data)?; + self.repository.save_request(entry_id, entry) + } + + pub fn delete_request(&self, entry_id: String) -> Result<(), String> { + let entry_id = Self::validate_entry_id(entry_id)?; + self.repository.delete_request(&entry_id) + } + + pub fn list_profiles(&self) -> Result, String> { + Ok(Self::records_to_values(self.repository.list_profiles()?)) + } + + pub fn update_profile_from_context( + &self, + json_data: String, + ) -> Result { + let seed = serde_json::from_str::(&json_data) + .map_err(|error| format!("Invalid CAD group profile context: {error}"))?; + + let group_id = Self::validate_entry_id(seed.group_id)?; + let mode = if seed.mode.trim().is_empty() { + "profile".to_string() + } else { + seed.mode.to_lowercase() + }; + + let current_role = Self::fallback_string(&seed.current_role, "infantry"); + let current_status = Self::fallback_string(&seed.current_status, "available"); + let final_role = if seed.role.trim().is_empty() { + current_role.clone() + } else { + seed.role.to_lowercase() + }; + let final_status = if seed.status.trim().is_empty() { + current_status.clone() + } else { + seed.status.to_lowercase() + }; + + let changed = current_role != final_role || current_status != final_status; + let callsign = Self::fallback_string(&seed.group_callsign, &group_id); + + let profile = if changed { + let patch = CadRecord { + fields: Map::from_iter([ + ("groupId".to_string(), Value::String(group_id.clone())), + ("role".to_string(), Value::String(final_role.clone())), + ("status".to_string(), Value::String(final_status.clone())), + ]), + }; + let existing = self.repository.get_profile(&group_id)?.unwrap_or_default(); + let merged = existing.merge(patch); + self.repository + .save_profile(group_id.clone(), merged.clone())?; + merged + } else { + CadRecord { + fields: Map::from_iter([ + ("groupId".to_string(), Value::String(group_id.clone())), + ("role".to_string(), Value::String(current_role.clone())), + ("status".to_string(), Value::String(current_status.clone())), + ]), + } + }; + + let message = if changed { + match mode.as_str() { + "status" => "Group status updated.".to_string(), + "role" => "Group role updated.".to_string(), + _ => "Group profile updated.".to_string(), + } + } else { + match mode.as_str() { + "status" => "Group status already up to date.".to_string(), + "role" => "Group role already up to date.".to_string(), + _ => "Group profile already up to date.".to_string(), + } + }; + + let activity = if changed { + match mode.as_str() { + "status" => Self::build_activity( + "group_status", + format!( + "{} updated {} to {}.", + seed.requester_uid, callsign, final_status + ), + String::new(), + group_id.clone(), + seed.requester_uid.clone(), + ), + "role" => Self::build_activity( + "group_role", + format!( + "{} updated {} role to {}.", + seed.requester_uid, callsign, final_role + ), + String::new(), + group_id.clone(), + seed.requester_uid.clone(), + ), + _ => { + let mut parts = Vec::new(); + if current_role != final_role { + parts.push(format!("role to {}", final_role)); + } + if current_status != final_status { + parts.push(format!("status to {}", final_status)); + } + Self::build_activity( + "group_profile", + format!( + "{} updated {} {}.", + seed.requester_uid, + callsign, + parts.join(" and ") + ), + String::new(), + group_id.clone(), + seed.requester_uid.clone(), + ) + } + } + } else { + CadActivityEntry::default() + }; + + Ok(CadGroupProfileMutationResult { + profile: profile.into_value(), + message, + activity, + changed, + }) + } + + pub fn build_groups(&self, json_data: String) -> Result, String> { + let seed: CadGroupBuildSeed = serde_json::from_str(&json_data) + .map_err(|error| format!("Invalid CAD group seed: {error}"))?; + let profiles = self.repository.list_profiles()?; + + let mut groups = Vec::with_capacity(seed.live_groups.len()); + for group in seed.live_groups { + let Some(mut entry) = Self::as_object_clone(&group) else { + continue; + }; + + let group_id = Self::string_field(&entry, "groupId").unwrap_or_default(); + if group_id.is_empty() { + continue; + } + + if let Some(profile) = profiles.get(&group_id) { + if let Some(role) = Self::string_field(&profile.fields, "role") { + entry.insert("role".to_string(), Value::String(role)); + } + if let Some(status) = Self::string_field(&profile.fields, "status") { + entry.insert("status".to_string(), Value::String(status)); + } + } + + groups.push(Value::Object(entry)); + } + + Ok(groups) + } + + pub fn upsert_profile(&self, entry_id: String, json_data: String) -> Result<(), String> { + let entry_id = Self::validate_entry_id(entry_id)?; + let entry = Self::parse_record(&json_data)?; + self.repository.save_profile(entry_id, entry) + } + + pub fn delete_profile(&self, entry_id: String) -> Result<(), String> { + let entry_id = Self::validate_entry_id(entry_id)?; + self.repository.delete_profile(&entry_id) + } + + pub fn build_hydrate_payload(&self, json_data: String) -> Result { + let seed: CadHydrateSeed = serde_json::from_str(&json_data) + .map_err(|error| format!("Invalid CAD hydrate seed: {error}"))?; + + let assignments = self.repository.list_assignments()?; + let dispatch_orders = self.repository.list_orders()?; + let requests = self.repository.list_requests()?; + let activity = self.repository.snapshot_activity()?; + + Ok(CadViewService::build_hydrate_payload( + seed, + assignments, + dispatch_orders, + requests, + activity, + )) + } + + fn validate_entry_id(entry_id: String) -> Result { + if entry_id.trim().is_empty() { + return Err("Entry ID is required.".to_string()); + } + + Ok(entry_id) + } + + fn parse_value(json_data: &str) -> Result { + serde_json::from_str::(json_data).map_err(|error| format!("Invalid JSON: {error}")) + } + + fn parse_record(json_data: &str) -> Result { + serde_json::from_str::(json_data) + .map_err(|error| format!("Invalid CAD JSON: {error}")) + } + + fn records_to_values(records: HashMap) -> Vec { + records.into_values().map(CadRecord::into_value).collect() + } + + fn set_task_id(record: &mut CadRecord, task_id: &str) { + let task_id_value = Value::String(task_id.to_string()); + record + .fields + .insert("taskId".to_string(), task_id_value.clone()); + record.fields.insert("taskID".to_string(), task_id_value); + } + + fn build_activity( + entry_type: &str, + message: String, + task_id: String, + group_id: String, + actor_uid: String, + ) -> CadActivityEntry { + CadActivityEntry { + entry_type: entry_type.to_string(), + message, + task_id, + group_id, + actor_uid, + } + } + + fn display_group_name(record: &Map) -> String { + Self::string_field(record, "groupCallsign") + .or_else(|| Self::string_field(record, "assigneeGroupCallsign")) + .or_else(|| Self::string_field(record, "groupId")) + .unwrap_or_else(|| "assigned group".to_string()) + } + + fn normalize_priority(priority: &str) -> String { + let normalized = priority.to_lowercase(); + if ["routine", "priority", "emergency"].contains(&normalized.as_str()) { + normalized + } else { + "priority".to_string() + } + } + + fn fallback_string(value: &str, fallback: &str) -> String { + if value.trim().is_empty() { + fallback.to_string() + } else { + value.to_string() + } + } + + fn build_request_title(request_type: &str, group_callsign: &str) -> String { + format!( + "{} | {}", + Self::format_request_type(request_type), + group_callsign + ) + } + + fn build_request_summary( + request_type: &str, + fields: &Map, + group_callsign: &str, + ) -> String { + match request_type { + "medevac_9line" => format!( + "Pickup {} | Precedence {} | Security {}", + Self::string_field(fields, "pickup_location") + .unwrap_or_else(|| "Unknown".to_string()), + Self::string_field(fields, "precedence").unwrap_or_else(|| "unknown".to_string()), + Self::string_field(fields, "security").unwrap_or_else(|| "unknown".to_string()) + ), + "ace_lace" => format!( + "Ammo {} | Casualties {} | Equipment {}", + Self::string_field(fields, "ammo").unwrap_or_else(|| "unknown".to_string()), + Self::string_field(fields, "casualties").unwrap_or_else(|| "unknown".to_string()), + Self::string_field(fields, "equipment").unwrap_or_else(|| "unknown".to_string()) + ), + "fire_support" => format!( + "Target {} | Effect {} | Danger Close {}", + Self::string_field(fields, "target_location") + .unwrap_or_else(|| "Unknown".to_string()), + Self::string_field(fields, "requested_effect") + .unwrap_or_else(|| "unknown".to_string()), + Self::string_field(fields, "danger_close").unwrap_or_else(|| "no".to_string()) + ), + "air_support" => format!( + "Target {} | Marking {} | Effect {}", + Self::string_field(fields, "target_location") + .unwrap_or_else(|| "Unknown".to_string()), + Self::string_field(fields, "target_marking") + .unwrap_or_else(|| "unknown".to_string()), + Self::string_field(fields, "requested_effect") + .unwrap_or_else(|| "unknown".to_string()) + ), + "logreq" => format!( + "Category {} | Requested {} | Quantity {} | Delivery {} | Location {}", + Self::string_field(fields, "category").unwrap_or_else(|| "mixed".to_string()), + Self::string_field(fields, "requested_items") + .unwrap_or_else(|| "unspecified".to_string()), + Self::string_field(fields, "quantity").unwrap_or_else(|| "unspecified".to_string()), + Self::string_field(fields, "delivery_method") + .unwrap_or_else(|| "dispatch discretion".to_string()), + Self::string_field(fields, "delivery_location") + .unwrap_or_else(|| "Unknown".to_string()) + ), + _ => format!( + "{} request from {}.", + Self::format_request_type(request_type), + group_callsign + ), + } + } + + fn format_request_type(request_type: &str) -> String { + match request_type { + "medevac_9line" => "9-Line MEDEVAC".to_string(), + "ace_lace" => "ACE/LACE".to_string(), + "fire_support" => "Fire Support".to_string(), + "air_support" => "Air Support".to_string(), + "logreq" => "LOGREQ".to_string(), + _ => request_type.to_string(), + } + } + + fn as_object_clone(value: &Value) -> Option> { + value.as_object().cloned() + } + + fn string_field(object: &Map, key: &str) -> Option { + object.get(key)?.as_str().map(ToString::to_string) + } +} + +pub struct CadViewService; + +impl CadViewService { + pub fn build_hydrate_payload( + seed: CadHydrateSeed, + assignments: HashMap, + dispatch_orders: HashMap, + requests: HashMap, + activity: Vec, + ) -> CadHydratePayload { + let groups = seed.groups.clone(); + let contracts = Self::build_contracts( + &seed.active_tasks, + &groups, + &seed.session, + &assignments, + &dispatch_orders, + ); + let requests = Self::build_requests(&seed.session, &requests); + let assignments = assignments + .into_values() + .map(CadRecord::into_value) + .collect(); + let activity = Self::build_activity(activity); + + CadHydratePayload { + groups, + contracts, + requests, + assignments, + activity, + session: seed.session, + } + } + + fn build_contracts( + active_tasks: &[Value], + groups: &[Value], + session: &CadSession, + assignments: &HashMap, + dispatch_orders: &HashMap, + ) -> Vec { + let mut contracts = Vec::new(); + + for task in active_tasks { + let Some(mut entry) = Self::as_object_clone(task) else { + continue; + }; + + let task_id = Self::string_field(&entry, "taskID") + .or_else(|| Self::string_field(&entry, "taskId")) + .unwrap_or_default(); + if task_id.is_empty() { + continue; + } + + let assignment = assignments.get(&task_id).map(|value| &value.fields); + let assigned_group_id = assignment + .and_then(|value| Self::string_field(value, "groupId")) + .unwrap_or_default(); + let assignment_state = assignment + .and_then(|value| Self::string_field(value, "state")) + .unwrap_or_else(|| "unassigned".to_string()); + + if !session.is_dispatcher + && (assigned_group_id.is_empty() || assigned_group_id != session.group_id) + { + continue; + } + + entry.insert("taskId".to_string(), Value::String(task_id)); + entry.insert( + "assignedGroupId".to_string(), + Value::String(assigned_group_id), + ); + entry.insert( + "assignmentState".to_string(), + Value::String(assignment_state), + ); + contracts.push(Value::Object(entry)); + } + + for (task_id, order) in dispatch_orders { + let assignment = assignments.get(task_id).map(|value| &value.fields); + let assigned_group_id = assignment + .and_then(|value| Self::string_field(value, "groupId")) + .unwrap_or_default(); + let assignment_state = assignment + .and_then(|value| Self::string_field(value, "state")) + .unwrap_or_else(|| "unassigned".to_string()); + + if !session.is_dispatcher + && (assigned_group_id.is_empty() || assigned_group_id != session.group_id) + { + continue; + } + + let mut entry = order.fields.clone(); + if let Some(target_group_id) = Self::string_field(&entry, "targetGroupId") { + if let Some(target_group) = groups.iter().find_map(|group| { + let object = Self::as_object_ref(group)?; + (Self::string_field(object, "groupId").unwrap_or_default() == target_group_id) + .then_some(object) + }) { + if let Some(callsign) = Self::string_field(target_group, "callsign") { + entry.insert( + "targetGroupCallsign".to_string(), + Value::String(callsign.clone()), + ); + entry.insert( + "title".to_string(), + Value::String(format!("Backup {callsign}")), + ); + } + + if let Some(position) = target_group.get("position") { + entry.insert("position".to_string(), position.clone()); + } + + if Self::string_field(&entry, "note") + .unwrap_or_default() + .is_empty() + { + if let Some(callsign) = Self::string_field(&entry, "targetGroupCallsign") { + entry.insert( + "description".to_string(), + Value::String(format!( + "Dispatch order to back up {callsign} at its current position." + )), + ); + } + } + } + } + + entry.insert("taskId".to_string(), Value::String(task_id.clone())); + entry.insert("taskID".to_string(), Value::String(task_id.clone())); + entry.insert("isDispatchOrder".to_string(), Value::Bool(true)); + entry.insert( + "assignedGroupId".to_string(), + Value::String(assigned_group_id), + ); + entry.insert( + "assignmentState".to_string(), + Value::String(assignment_state), + ); + contracts.push(Value::Object(entry)); + } + + contracts + } + + fn build_requests(session: &CadSession, requests: &HashMap) -> Vec { + let mut filtered: Vec<(f64, Value)> = requests + .values() + .filter_map(|request| { + let object = &request.fields; + let group_id = Self::string_field(object, "groupId").unwrap_or_default(); + if !session.is_dispatcher && group_id != session.group_id { + return None; + } + + let created_at = Self::number_field(object, "createdAt").unwrap_or_default(); + Some((created_at, request.to_value())) + }) + .collect(); + + filtered.sort_by(|(left, _), (right, _)| { + right.partial_cmp(left).unwrap_or(std::cmp::Ordering::Equal) + }); + filtered.into_iter().map(|(_, value)| value).collect() + } + + fn build_activity(mut activity: Vec) -> Vec { + if activity.len() > CAD_RECENT_ACTIVITY_LIMIT { + let drain_count = activity.len() - CAD_RECENT_ACTIVITY_LIMIT; + activity.drain(0..drain_count); + } + + activity + } + + fn as_object_ref(value: &Value) -> Option<&Map> { + value.as_object() + } + + fn as_object_clone(value: &Value) -> Option> { + value.as_object().cloned() + } + + fn string_field(object: &Map, key: &str) -> Option { + object.get(key)?.as_str().map(ToString::to_string) + } + + fn number_field(object: &Map, key: &str) -> Option { + object.get(key)?.as_f64() + } +} + +#[cfg(test)] +mod tests { + use super::CadStateService; + use forge_repositories::{CadRepository, InMemoryCadRepository}; + use serde_json::Value; + + #[test] + fn create_order_assigns_shared_task_id() { + let repository = InMemoryCadRepository::new(); + let service = CadStateService::new(repository.clone()); + + let result = service + .create_order( + r#"{ + "order": {"type":"dispatch_order","targetGroupId":"alpha"}, + "assignment": {"groupId":"bravo","state":"assigned"} + }"# + .to_string(), + ) + .expect("create order should succeed"); + + assert_eq!(result.task_id, "cad-order:1"); + + let stored_order = repository + .get_order(&result.task_id) + .expect("get order should succeed") + .expect("order should exist"); + let stored_assignment = repository + .get_assignment(&result.task_id) + .expect("get assignment should succeed") + .expect("assignment should exist"); + + assert_eq!( + stored_order.fields.get("taskId"), + Some(&Value::String(result.task_id.clone())) + ); + assert_eq!( + stored_assignment.fields.get("taskId"), + Some(&Value::String(result.task_id)) + ); + } + + #[test] + fn create_order_from_context_persists_source_request_metadata() { + let repository = InMemoryCadRepository::new(); + let service = CadStateService::new(repository.clone()); + + let result = service + .create_order_from_context( + r#"{ + "assigneeGroupId": "bravo", + "assigneeGroupCallsign": "Bravo 1-1", + "targetGroupId": "alpha", + "targetGroupCallsign": "Alpha 1-1", + "targetPosition": [1000, 2000, 0], + "createdByUid": "dispatcher-1", + "createdByName": "Dispatch", + "requestId": "cad-request:7", + "requestType": "logreq", + "requestTitle": "LOGREQ | Alpha 1-1", + "requestSummary": "Category ammo | Requested MX rifle ammo", + "requestFields": { + "category": "ammo", + "requested_items": "MX rifle ammo", + "quantity": "4 crates" + }, + "note": "LOGREQ requested by Alpha 1-1. Requested Items MX rifle ammo | Quantity 4 crates", + "priority": "priority", + "createdAt": 123.45 + }"# + .to_string(), + ) + .expect("create order from context should succeed"); + + let stored_order = repository + .get_order(&result.task_id) + .expect("get order should succeed") + .expect("order should exist"); + + assert_eq!( + stored_order.fields.get("sourceRequestId"), + Some(&Value::String("cad-request:7".to_string())) + ); + assert_eq!( + stored_order.fields.get("sourceRequestType"), + Some(&Value::String("logreq".to_string())) + ); + assert_eq!( + stored_order.fields.get("sourceRequestFields"), + Some(&serde_json::json!({ + "category": "ammo", + "requested_items": "MX rifle ammo", + "quantity": "4 crates" + })) + ); + } + + #[test] + fn decline_assignment_returns_record_and_removes_state() { + let repository = InMemoryCadRepository::new(); + let service = CadStateService::new(repository.clone()); + + service + .assign_assignment( + "task-1".to_string(), + r#"{"groupId":"alpha","state":"assigned"}"#.to_string(), + ) + .expect("assign should succeed"); + + let declined = service + .decline_assignment( + "task-1".to_string(), + r#"{"state":"declined","declinedAt":123}"#.to_string(), + ) + .expect("decline should succeed"); + + assert_eq!( + declined.assignment.get("state").and_then(Value::as_str), + Some("declined") + ); + assert!( + repository + .get_assignment("task-1") + .expect("get assignment should succeed") + .is_none() + ); + } + + #[test] + fn submit_request_from_context_accepts_scalar_created_at() { + let repository = InMemoryCadRepository::new(); + let service = CadStateService::new(repository); + + let result = service + .submit_request_from_context( + r#"{ + "type": "medevac_9line", + "fields": {"pickup_location":"1000 2000"}, + "groupId": "alpha", + "groupCallsign": "Alpha 1-1", + "submittedByUid": "uid-1", + "submittedByName": "Leader", + "priority": "emergency", + "position": [1000, 2000, 0], + "createdAt": 123.45 + }"# + .to_string(), + ) + .expect("submit request should accept scalar createdAt"); + + assert_eq!( + result.request.get("createdAt").and_then(Value::as_f64), + Some(123.45) + ); + } +} diff --git a/lib/services/src/garage.rs b/lib/services/src/garage.rs index 3b55f68..3e14f90 100644 --- a/lib/services/src/garage.rs +++ b/lib/services/src/garage.rs @@ -3,7 +3,7 @@ //! Handles validation, storage, and retrieval of player vehicle garages. use forge_models::garage::{Garage, HitPoints, Vehicle}; -use forge_repositories::GarageRepository; +use forge_repositories::{GarageHotRepository, GarageRepository}; use std::collections::HashMap; use uuid::Uuid; @@ -12,6 +12,11 @@ pub struct GarageService { repository: R, } +pub struct GarageHotStateService { + service: GarageService, + repository: H, +} + impl GarageService { /// Creates a new garage service with the provided repository. pub fn new(repository: R) -> Self { @@ -170,3 +175,86 @@ impl GarageService { self.repository.exists(&key) } } + +impl GarageHotStateService { + pub fn new(repository: R, hot_repository: H) -> Self { + Self { + service: GarageService::new(repository), + repository: hot_repository, + } + } + + pub fn init_garage(&self, uid: String) -> Result { + if let Some(garage) = self.repository.get(&uid)? { + return Ok(garage); + } + + let garage = match self.service.get_garage(uid.clone()) { + Ok(garage) => garage, + Err(_) => self.service.create_garage(uid.clone())?, + }; + self.repository.save(&garage, &uid)?; + Ok(garage) + } + + pub fn get_garage(&self, uid: String) -> Result { + self.init_garage(uid) + } + + pub fn override_garage( + &self, + uid: String, + vehicles: HashMap, + ) -> Result { + for vehicle in vehicles.values() { + vehicle + .validate() + .map_err(|e| format!("Validation failed for vehicle {}: {}", vehicle.plate, e))?; + } + + let garage = Garage { vehicles }; + if garage.vehicles.len() > 5 { + return Err("Garage exceeds maximum capacity of 5 vehicles.".to_string()); + } + + self.repository.save(&garage, &uid)?; + Ok(garage) + } + + pub fn save_garage(&self, uid: String) -> Result { + let garage = self + .repository + .get(&uid)? + .ok_or_else(|| format!("No garage found for player '{}'", uid))?; + let saved = self + .service + .update_garage(uid.clone(), garage.vehicles.clone())?; + self.repository.save(&saved, &uid)?; + Ok(saved) + } + + pub fn add_vehicle( + &self, + uid: String, + classname: String, + fuel: f64, + damage: f64, + hit_points_json: String, + ) -> Result { + let garage = + self.service + .add_vehicle(uid.clone(), classname, fuel, damage, hit_points_json)?; + self.repository.save(&garage, &uid)?; + Ok(garage) + } + + pub fn remove_vehicle(&self, uid: String, plate: String) -> Result { + let garage = self.service.remove_vehicle(uid.clone(), plate)?; + self.repository.save(&garage, &uid)?; + Ok(garage) + } + + pub fn remove_garage(&self, uid: String) -> Result<(), String> { + self.repository.delete(&uid) + } +} diff --git a/lib/services/src/lib.rs b/lib/services/src/lib.rs index e6c8162..070143d 100644 --- a/lib/services/src/lib.rs +++ b/lib/services/src/lib.rs @@ -1,15 +1,21 @@ pub mod actor; pub mod bank; +pub mod cad; pub mod garage; pub mod locker; pub mod org; +pub mod store; +pub mod task; pub mod v_garage; pub mod v_locker; -pub use actor::ActorService; -pub use bank::BankService; -pub use garage::GarageService; -pub use locker::LockerService; -pub use org::OrgService; -pub use v_garage::VGarageService; -pub use v_locker::VLockerService; +pub use actor::{ActorHotStateService, ActorService}; +pub use bank::{BankHotStateService, BankService}; +pub use cad::{CadStateService, CadViewService}; +pub use garage::{GarageHotStateService, GarageService}; +pub use locker::{LockerHotStateService, LockerService}; +pub use org::{OrgHotStateService, OrgService}; +pub use store::StoreService; +pub use task::TaskStateService; +pub use v_garage::{VGarageHotStateService, VGarageService}; +pub use v_locker::{VLockerHotStateService, VLockerService}; diff --git a/lib/services/src/locker.rs b/lib/services/src/locker.rs index af401b0..fded255 100644 --- a/lib/services/src/locker.rs +++ b/lib/services/src/locker.rs @@ -3,7 +3,7 @@ //! Handles validation, storage, and retrieval of player item lockers. use forge_models::locker::{Item, Locker}; -use forge_repositories::LockerRepository; +use forge_repositories::{LockerHotRepository, LockerRepository}; use std::collections::HashMap; /// Service layer implementation for locker business logic and operations. @@ -11,6 +11,11 @@ pub struct LockerService { repository: R, } +pub struct LockerHotStateService { + service: LockerService, + repository: H, +} + impl LockerService { /// Creates a new locker service with the provided repository. pub fn new(repository: R) -> Self { @@ -141,3 +146,59 @@ impl LockerService { self.repository.exists(&uid) } } + +impl LockerHotStateService { + pub fn new(repository: R, hot_repository: H) -> Self { + Self { + service: LockerService::new(repository), + repository: hot_repository, + } + } + + pub fn init_locker(&self, uid: String) -> Result { + if let Some(locker) = self.repository.get(&uid)? { + return Ok(locker); + } + + let locker = match self.service.get_locker(uid.clone()) { + Ok(locker) => locker, + Err(_) => self.service.create_locker(uid.clone())?, + }; + self.repository.save(&locker, &uid)?; + Ok(locker) + } + + pub fn get_locker(&self, uid: String) -> Result { + self.init_locker(uid) + } + + pub fn override_locker( + &self, + uid: String, + items: HashMap, + ) -> Result { + let locker = Locker { items }; + if locker.items.len() > 25 { + return Err("Locker exceeds maximum capacity of 25 items.".to_string()); + } + + self.repository.save(&locker, &uid)?; + Ok(locker) + } + + pub fn save_locker(&self, uid: String) -> Result { + let locker = self + .repository + .get(&uid)? + .ok_or_else(|| format!("No locker found for player '{}'", uid))?; + let saved = self + .service + .update_locker(uid.clone(), locker.items.clone())?; + self.repository.save(&saved, &uid)?; + Ok(saved) + } + + pub fn remove_locker(&self, uid: String) -> Result<(), String> { + self.repository.delete(&uid) + } +} diff --git a/lib/services/src/org.rs b/lib/services/src/org.rs index 05bddee..96e00e1 100644 --- a/lib/services/src/org.rs +++ b/lib/services/src/org.rs @@ -5,9 +5,16 @@ //! //! For full documentation, architecture, and examples, see the [crate README](../README.md). -use forge_models::{CreditLineSummary, MemberSummary, Org}; -use forge_repositories::OrgRepository; -use std::collections::HashMap; +use forge_models::{ + CreditLineSummary, DEFAULT_CREDIT_LINE_INTEREST_RATE, HotOrgRecord, MemberSummary, Org, + OrgAssetEntry, OrgAssetGrantSeed, OrgCheckoutContext, OrgCreditLineContext, + OrgCreditLineRepaymentContext, OrgCreditLineRepaymentResult, OrgDisbandMemberResult, + OrgDisbandResult, OrgEnsureMemberContext, OrgFleetEntry, OrgFleetGrantSeed, OrgGrantContext, + OrgLeaveContext, OrgLeaveResult, OrgMutationResult, OrgRegisterContext, OrgRegisterResult, +}; +use forge_repositories::{OrgHotRepository, OrgRepository}; +use serde_json::{Value, json}; +use std::collections::{HashMap, HashSet}; /// Service layer implementation for organization business logic and operations. /// @@ -24,6 +31,11 @@ pub struct OrgService { repository: R, } +pub struct OrgHotStateService { + service: OrgService, + repository: H, +} + impl OrgService { fn normalize_org_value( mut org_value: serde_json::Value, @@ -47,7 +59,10 @@ impl OrgService { ); } - serde_json::from_value::(org_value).map_err(|e| format!("Invalid Org JSON: {}", e)) + let mut org = serde_json::from_value::(org_value) + .map_err(|e| format!("Invalid Org JSON: {}", e))?; + org.normalize_credit_lines(); + Ok(org) } /// Creates a new organization service with the provided repository. @@ -83,9 +98,12 @@ impl OrgService { } pub fn get_org(&self, key: String) -> Result { - self.repository + let mut org = self + .repository .get_by_id(&key)? - .ok_or_else(|| format!("Organization with ID '{}' not found", key)) + .ok_or_else(|| format!("Organization with ID '{}' not found", key))?; + org.normalize_credit_lines(); + Ok(org) } /// Updates an existing organization with new data from JSON. @@ -180,6 +198,7 @@ impl OrgService { } // Validate the updated organization before committing changes + updated_org.normalize_credit_lines(); updated_org .validate() .map_err(|e| format!("Validation failed: {}", e))?; @@ -237,4 +256,776 @@ 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) + } +} + +impl OrgHotStateService { + pub fn new(repository: R, hot_repository: H) -> Self { + Self { + service: OrgService::new(repository), + repository: hot_repository, + } + } + + pub fn init_org(&self, id: String) -> Result { + if let Some(org) = self.repository.get(&id)? { + if !org.members.is_empty() || !org.assets.is_empty() || !org.fleet.is_empty() { + return Ok(org); + } + + let hydrated_org = self.hydrate_org(&id)?; + if !hydrated_org.members.is_empty() + || !hydrated_org.assets.is_empty() + || !hydrated_org.fleet.is_empty() + { + self.repository.save(&hydrated_org)?; + return Ok(hydrated_org); + } + + return Ok(org); + } + + let hot_org = self.hydrate_org(&id)?; + self.repository.save(&hot_org)?; + Ok(hot_org) + } + + pub fn get_org(&self, id: String) -> Result { + self.init_org(id) + } + + pub fn override_org( + &self, + id: String, + mut hot_org: HotOrgRecord, + ) -> Result { + hot_org.id = id; + self.repository.save(&hot_org)?; + Ok(hot_org) + } + + pub fn save_org(&self, id: String) -> Result { + let hot_org = self + .repository + .get(&id)? + .ok_or_else(|| format!("Organization with ID '{}' not found", id))?; + + let core_org = hot_org.clone().into_org(); + let current_members = self + .service + .get_members(id.clone())? + .into_iter() + .map(|member| member.uid) + .collect::>(); + let target_members = hot_org.members.keys().cloned().collect::>(); + + if self.service.org_exists(id.clone())? { + self.service.repository.update(&core_org)?; + } else { + self.service.repository.create(&core_org)?; + } + + self.service + .repository + .update_assets(&id, &hot_org.assets)?; + self.service.repository.update_fleet(&id, &hot_org.fleet)?; + + for member_uid in target_members.difference(¤t_members) { + self.service.repository.add_member(&id, member_uid)?; + } + + for member_uid in current_members.difference(&target_members) { + self.service.repository.remove_member(&id, member_uid)?; + } + + self.repository.save(&hot_org)?; + Ok(hot_org) + } + + pub fn remove_org(&self, id: String) -> Result<(), String> { + self.repository.delete(&id) + } + + pub fn ensure_member(&self, context: OrgEnsureMemberContext) -> Result { + if context.org_id.trim().is_empty() || context.member_uid.trim().is_empty() { + return Err("A valid organization and member UID are required.".to_string()); + } + + let mut org = self.get_org(context.org_id)?; + if !org.members.contains_key(&context.member_uid) { + let member_name = if context.member_name.trim().is_empty() { + "Unknown".to_string() + } else { + context.member_name + }; + org.members.insert( + context.member_uid.clone(), + MemberSummary { + uid: context.member_uid, + name: member_name, + }, + ); + self.repository.save(&org)?; + } + + Ok(org) + } + + pub fn register_org(&self, context: OrgRegisterContext) -> Result { + if context.requester_uid.trim().is_empty() || context.org_id.trim().is_empty() { + return Err("A valid requester and organization ID are required.".to_string()); + } + if context.org_name.trim().is_empty() { + return Err("Organization name cannot be empty.".to_string()); + } + if !context.existing_org_id.trim().is_empty() + && !context.existing_org_id.eq_ignore_ascii_case("default") + { + return Err("Player already belongs to an organization.".to_string()); + } + if self.service.org_exists(context.org_id.clone())? + || self.repository.get(&context.org_id)?.is_some() + { + return Err("An organization already exists for this phone number.".to_string()); + } + + let org = Org { + id: context.org_id.clone(), + owner: context.requester_uid.clone(), + name: context.org_name, + funds: 0.0, + reputation: 0, + credit_lines: HashMap::new(), + }; + org.validate() + .map_err(|error| format!("Validation failed: {}", error))?; + + let json_data = serde_json::to_string(&org) + .map_err(|error| format!("Failed to serialize org: {}", error))?; + let persisted_org = self.service.create_org(context.org_id.clone(), json_data)?; + let mut hot_org = + HotOrgRecord::from_parts(persisted_org, HashMap::new(), HashMap::new(), Vec::new()); + hot_org.members.insert( + context.requester_uid.clone(), + MemberSummary { + uid: context.requester_uid.clone(), + name: if context.requester_name.trim().is_empty() { + "Unknown".to_string() + } else { + context.requester_name + }, + }, + ); + self.repository.save(&hot_org)?; + + if context.existing_org_id.eq_ignore_ascii_case("default") { + let mut default_org = self.init_org("default".to_string())?; + default_org.members.remove(&context.requester_uid); + self.repository.save(&default_org)?; + } + + Ok(OrgRegisterResult { + org: hot_org, + actor_organization: context.org_id, + message: String::new(), + }) + } + + pub fn assign_credit_line( + &self, + context: OrgCreditLineContext, + ) -> Result { + if context.requester_uid.trim().is_empty() + || context.member_uid.trim().is_empty() + || context.org_id.trim().is_empty() + { + return Err("A valid requester, member, and organization are required.".to_string()); + } + if context.amount <= 0.0 { + return Err("A valid credit amount is required.".to_string()); + } + + let mut org = self.get_org(context.org_id)?; + if !can_manage_treasury( + &org, + &context.requester_uid, + context.requester_is_default_org_ceo, + ) { + return Err( + "Only the organization leader or CEO can manage treasury actions.".to_string(), + ); + } + + let member_record = org + .members + .get(&context.member_uid) + .cloned() + .ok_or_else(|| { + "Selected member was not found in the organization roster.".to_string() + })?; + let member_name = if context.member_name.trim().is_empty() { + member_record.name + } else { + context.member_name + }; + + let mut credit_line = org + .credit_lines + .get(&context.member_uid) + .cloned() + .unwrap_or_else(|| CreditLineSummary { + uid: context.member_uid.clone(), + name: member_name.clone(), + approved_amount: 0.0, + available_amount: 0.0, + outstanding_principal: 0.0, + interest_rate: DEFAULT_CREDIT_LINE_INTEREST_RATE, + amount_due: 0.0, + amount: 0.0, + }); + credit_line.normalize(); + + let next_reserved_amount = round_currency(context.amount); + let previous_reserved_amount = round_currency(credit_line.available_amount); + let treasury_delta = round_currency(next_reserved_amount - previous_reserved_amount); + if treasury_delta > 0.0 && org.funds < treasury_delta { + return Err("Organization funds cannot cover that credit assignment.".to_string()); + } + + org.funds = round_currency(org.funds - treasury_delta); + credit_line.uid = context.member_uid.clone(); + credit_line.name = member_name.clone(); + credit_line.approved_amount = next_reserved_amount; + credit_line.available_amount = next_reserved_amount; + credit_line.amount = next_reserved_amount; + if credit_line.interest_rate <= 0.0 { + credit_line.interest_rate = DEFAULT_CREDIT_LINE_INTEREST_RATE; + } + + org.credit_lines + .insert(context.member_uid.clone(), credit_line); + self.repository.save(&org)?; + + Ok(OrgMutationResult { + patch: build_org_patch(&org, &["funds", "credit_lines"])?, + member_uids: resolve_member_uids(&org, Some(&context.requester_uid)), + message: format!( + "Credit line for {} set to ${}.", + member_name, + format_currency(next_reserved_amount) + ), + org, + }) + } + + pub fn charge_checkout( + &self, + context: OrgCheckoutContext, + ) -> Result { + if context.requester_uid.trim().is_empty() || context.org_id.trim().is_empty() { + return Err("A valid requester and organization are required.".to_string()); + } + if context.amount <= 0.0 { + return Err("Checkout amount must be greater than zero.".to_string()); + } + + let mut org = self.get_org(context.org_id)?; + let member_uids = resolve_member_uids(&org, Some(&context.requester_uid)); + + match context.source.trim().to_ascii_lowercase().as_str() { + "org_funds" => { + if !can_manage_treasury( + &org, + &context.requester_uid, + context.requester_is_default_org_ceo, + ) { + return Err( + "Only the organization leader or CEO can charge org funds.".to_string() + ); + } + if org.funds < context.amount { + return Err("Organization funds cannot cover this checkout.".to_string()); + } + + org.funds -= context.amount; + self.repository.save(&org)?; + + Ok(OrgMutationResult { + patch: build_org_patch(&org, &["funds"])?, + member_uids, + message: String::new(), + org, + }) + } + "credit_line" => { + let mut credit_line = org + .credit_lines + .get(&context.requester_uid) + .cloned() + .ok_or_else(|| { + "Assigned credit line cannot cover this checkout.".to_string() + })?; + + credit_line.normalize(); + + if credit_line.available_amount < context.amount { + return Err("Assigned credit line cannot cover this checkout.".to_string()); + } + + let charged_amount = round_currency(context.amount); + credit_line.available_amount = + round_currency(credit_line.available_amount - charged_amount); + credit_line.approved_amount = credit_line.available_amount; + credit_line.outstanding_principal = + round_currency(credit_line.outstanding_principal + charged_amount); + credit_line.amount_due = round_currency( + credit_line.amount_due + (charged_amount * (1.0 + credit_line.interest_rate)), + ); + credit_line.amount = credit_line.available_amount; + org.credit_lines + .insert(context.requester_uid.clone(), credit_line); + self.repository.save(&org)?; + + Ok(OrgMutationResult { + patch: build_org_patch(&org, &["credit_lines"])?, + member_uids, + message: String::new(), + org, + }) + } + _ => Err("Selected organization payment source is unsupported.".to_string()), + } + } + + pub fn repay_credit_line( + &self, + context: OrgCreditLineRepaymentContext, + ) -> Result { + if context.requester_uid.trim().is_empty() || context.org_id.trim().is_empty() { + return Err("A valid requester and organization are required.".to_string()); + } + if context.amount <= 0.0 { + return Err("Repayment amount must be greater than zero.".to_string()); + } + + let mut org = self.get_org(context.org_id)?; + let member_uids = resolve_member_uids(&org, Some(&context.requester_uid)); + let mut credit_line = org + .credit_lines + .get(&context.requester_uid) + .cloned() + .ok_or_else(|| "No active credit line is assigned to this member.".to_string())?; + credit_line.normalize(); + + if credit_line.amount_due <= 0.0 { + return Err("This credit line has no outstanding balance.".to_string()); + } + + let paid_amount = round_currency(context.amount.min(credit_line.amount_due)); + let principal_paid = if paid_amount >= credit_line.amount_due { + credit_line.outstanding_principal + } else { + round_currency( + paid_amount * (credit_line.outstanding_principal / credit_line.amount_due), + ) + .min(credit_line.outstanding_principal) + .min(paid_amount) + }; + let interest_paid = round_currency(paid_amount - principal_paid); + + credit_line.outstanding_principal = + round_currency(credit_line.outstanding_principal - principal_paid); + credit_line.amount_due = round_currency(credit_line.amount_due - paid_amount); + if credit_line.outstanding_principal <= 0.0 { + credit_line.outstanding_principal = 0.0; + } + if credit_line.amount_due <= 0.0 { + credit_line.amount_due = 0.0; + } + credit_line.amount = credit_line.available_amount; + + org.funds = round_currency(org.funds + paid_amount); + org.credit_lines + .insert(context.requester_uid.clone(), credit_line.clone()); + self.repository.save(&org)?; + + Ok(OrgCreditLineRepaymentResult { + patch: build_org_patch(&org, &["funds", "credit_lines"])?, + member_uids, + paid_amount, + principal_paid, + interest_paid, + remaining_amount_due: credit_line.amount_due, + message: if credit_line.amount_due > 0.0 { + format!( + "Credit repayment posted. ${} paid with ${} still due.", + format_currency(paid_amount), + format_currency(credit_line.amount_due) + ) + } else { + format!( + "Credit repayment posted. ${} cleared the outstanding balance.", + format_currency(paid_amount) + ) + }, + org, + }) + } + + pub fn add_assets( + &self, + context: OrgGrantContext, + assets: Vec, + ) -> Result { + if context.org_id.trim().is_empty() { + return Err("A valid organization is required for asset updates.".to_string()); + } + if assets.is_empty() { + let org = self.get_org(context.org_id)?; + return Ok(OrgMutationResult { + org, + patch: HashMap::new(), + member_uids: Vec::new(), + message: String::new(), + }); + } + + let mut org = self.get_org(context.org_id)?; + for asset in assets { + if asset.classname.trim().is_empty() || asset.quantity <= 0 { + continue; + } + let category = asset.category.trim().to_ascii_lowercase(); + let category_assets = org.assets.entry(category.clone()).or_default(); + let entry = category_assets + .entry(asset.classname.clone()) + .or_insert_with(|| OrgAssetEntry { + classname: asset.classname.clone(), + asset_type: category.clone(), + quantity: 0, + }); + entry.quantity += asset.quantity; + } + + self.repository.save(&org)?; + + Ok(OrgMutationResult { + patch: build_org_patch(&org, &["assets"])?, + member_uids: resolve_member_uids(&org, Some(&context.requester_uid)), + message: String::new(), + org, + }) + } + + pub fn add_fleet_vehicles( + &self, + context: OrgGrantContext, + vehicles: Vec, + ) -> Result { + if context.org_id.trim().is_empty() { + return Err("A valid organization is required for fleet updates.".to_string()); + } + if vehicles.is_empty() { + let org = self.get_org(context.org_id)?; + return Ok(OrgMutationResult { + org, + patch: HashMap::new(), + member_uids: Vec::new(), + message: String::new(), + }); + } + + let mut org = self.get_org(context.org_id)?; + let mut fleet_index = org.fleet.len(); + for vehicle in vehicles { + if vehicle.classname.trim().is_empty() { + continue; + } + let fleet_type = vehicle.category.trim().to_ascii_lowercase(); + let mut fleet_key = format!("{}_{}", vehicle.classname, fleet_index); + while org.fleet.contains_key(&fleet_key) { + fleet_index += 1; + fleet_key = format!("{}_{}", vehicle.classname, fleet_index); + } + + org.fleet.insert( + fleet_key, + OrgFleetEntry { + classname: vehicle.classname.clone(), + name: vehicle.classname, + fleet_type, + status: "Ready".to_string(), + damage: "0%".to_string(), + }, + ); + fleet_index += 1; + } + + self.repository.save(&org)?; + + Ok(OrgMutationResult { + patch: build_org_patch(&org, &["fleet"])?, + member_uids: resolve_member_uids(&org, Some(&context.requester_uid)), + message: String::new(), + org, + }) + } + + pub fn leave_org(&self, context: OrgLeaveContext) -> Result { + if context.requester_uid.trim().is_empty() { + return Err("A valid player UID is required.".to_string()); + } + if context.org_id.trim().is_empty() || context.org_id.eq_ignore_ascii_case("default") { + return Err("You are already assigned to the default organization.".to_string()); + } + + let mut org = self.get_org(context.org_id)?; + if org.owner == context.requester_uid { + return Err( + "Organization owners must disband the organization instead of leaving it." + .to_string(), + ); + } + + let org_name = org.name.clone(); + org.members.remove(&context.requester_uid); + self.repository.save(&org)?; + + let mut default_org = self.init_org("default".to_string())?; + let requester_uid = context.requester_uid.clone(); + default_org.members.insert( + requester_uid.clone(), + MemberSummary { + uid: requester_uid, + name: if context.requester_name.trim().is_empty() { + "Unknown".to_string() + } else { + context.requester_name + }, + }, + ); + self.repository.save(&default_org)?; + + Ok(OrgLeaveResult { + actor_organization: "default".to_string(), + message: format!( + "You left {} and returned to the default organization.", + org_name + ), + }) + } + + pub fn disband_org(&self, context: OrgLeaveContext) -> Result { + if context.requester_uid.trim().is_empty() { + return Err("A valid player UID is required.".to_string()); + } + if context.org_id.trim().is_empty() || context.org_id.eq_ignore_ascii_case("default") { + return Err("Only active player organizations can be disbanded.".to_string()); + } + + let org = self.get_org(context.org_id.clone())?; + if org.owner != context.requester_uid { + return Err("Only the organization owner can disband this organization.".to_string()); + } + + let org_name = org.name.clone(); + let mut default_org = self.init_org("default".to_string())?; + let mut member_results = Vec::new(); + let mut seen = HashSet::new(); + + for (member_uid, member) in &org.members { + if seen.insert(member_uid.clone()) { + default_org + .members + .insert(member_uid.clone(), member.clone()); + member_results.push(OrgDisbandMemberResult { + uid: member_uid.clone(), + requester: member_uid == &context.requester_uid, + actor_organization: "default".to_string(), + message: if member_uid == &context.requester_uid { + format!("Your organization, {}, has been disbanded.", org_name) + } else { + format!("{} has been disbanded.", org_name) + }, + }); + } + } + + if seen.insert(context.requester_uid.clone()) { + default_org.members.insert( + context.requester_uid.clone(), + MemberSummary { + uid: context.requester_uid.clone(), + name: if context.requester_name.trim().is_empty() { + "Unknown".to_string() + } else { + context.requester_name + }, + }, + ); + member_results.push(OrgDisbandMemberResult { + uid: context.requester_uid, + requester: true, + actor_organization: "default".to_string(), + message: format!("Your organization, {}, has been disbanded.", org_name), + }); + } + + self.repository.save(&default_org)?; + self.service.delete_org(context.org_id.clone())?; + self.repository.delete(&context.org_id)?; + + Ok(OrgDisbandResult { + message: format!("{} has been disbanded.", org_name), + members: member_results, + }) + } + + fn hydrate_org(&self, id: &str) -> Result { + let org = self + .service + .get_org(id.to_string()) + .map_err(|error| format!("Organization with ID '{}' not found: {}", id, error))?; + let assets = self.service.get_assets(id.to_string())?; + let fleet = self.service.get_fleet(id.to_string())?; + let members = self.service.get_members(id.to_string())?; + Ok(HotOrgRecord::from_parts(org, assets, fleet, members)) + } +} + +fn can_manage_treasury( + org: &HotOrgRecord, + requester_uid: &str, + requester_is_default_org_ceo: bool, +) -> bool { + org.owner == requester_uid + || ((org.id.eq_ignore_ascii_case("default") || org.owner.eq_ignore_ascii_case("server")) + && requester_is_default_org_ceo) +} + +fn resolve_member_uids(org: &HotOrgRecord, requester_uid: Option<&str>) -> Vec { + let mut member_uids = org.members.keys().cloned().collect::>(); + if let Some(uid) = requester_uid { + if !uid.is_empty() && !member_uids.iter().any(|member_uid| member_uid == uid) { + member_uids.push(uid.to_string()); + } + } + member_uids +} + +fn build_org_patch(org: &HotOrgRecord, fields: &[&str]) -> Result, String> { + let mut patch = HashMap::new(); + for field in fields { + patch.insert((*field).to_string(), current_org_field_value(org, field)?); + } + Ok(patch) +} + +fn current_org_field_value(org: &HotOrgRecord, field: &str) -> Result { + match field { + "id" => Ok(json!(org.id)), + "owner" => Ok(json!(org.owner)), + "name" => Ok(json!(org.name)), + "funds" => Ok(json!(org.funds)), + "reputation" => Ok(json!(org.reputation)), + "credit_lines" => serde_json::to_value(&org.credit_lines) + .map_err(|error| format!("Failed to serialize org credit lines: {}", error)), + "assets" => serde_json::to_value(&org.assets) + .map_err(|error| format!("Failed to serialize org assets: {}", error)), + "fleet" => serde_json::to_value(&org.fleet) + .map_err(|error| format!("Failed to serialize org fleet: {}", error)), + "members" => serde_json::to_value(&org.members) + .map_err(|error| format!("Failed to serialize org members: {}", error)), + _ => Err(format!("Unknown field: {}", field)), + } +} + +fn format_currency(amount: f64) -> String { + let rounded = round_currency(amount).round() as i64; + let digits = rounded.to_string(); + let mut formatted = String::new(); + + for (index, character) in digits.chars().rev().enumerate() { + if index > 0 && index % 3 == 0 { + formatted.push(','); + } + formatted.push(character); + } + + formatted.chars().rev().collect() +} + +fn round_currency(amount: f64) -> f64 { + (amount.max(0.0) * 100.0).round() / 100.0 } diff --git a/lib/services/src/store.rs b/lib/services/src/store.rs new file mode 100644 index 0000000..b0e06e1 --- /dev/null +++ b/lib/services/src/store.rs @@ -0,0 +1,699 @@ +use forge_models::{ + Bank, BankCheckoutContext, BankMutationResult, EquipmentCategory, HotOrgRecord, Item, Locker, + OrgFleetEntry, StoreCheckoutContext, StoreCheckoutResult, StoreGrantedItem, + StoreGrantedVehicle, VGarage, VLocker, VehicleCategory, +}; +use forge_repositories::{ + BankHotRepository, BankRepository, LockerHotRepository, LockerRepository, OrgHotRepository, + OrgRepository, VGarageHotRepository, VGarageRepository, VLockerHotRepository, + VLockerRepository, +}; +use serde_json::json; +use std::collections::HashMap; + +use crate::{ + BankHotStateService, LockerHotStateService, OrgHotStateService, VGarageHotStateService, + VLockerHotStateService, +}; + +pub trait StoreBankBackend { + fn get_bank(&self, uid: &str) -> Result; + fn preview_checkout( + &self, + uid: &str, + amount: f64, + source: &str, + ) -> Result; + fn override_bank(&self, uid: &str, bank: &Bank) -> Result; +} + +pub trait StoreOrgBackend { + fn get_org(&self, org_id: &str) -> Result; + fn override_org(&self, org_id: &str, org: HotOrgRecord) -> Result; +} + +pub trait StoreLockerBackend { + fn get_locker(&self, uid: &str) -> Result; + fn override_locker(&self, uid: &str, items: HashMap) -> Result; +} + +pub trait StoreVLockerBackend { + fn fetch_locker(&self, uid: &str) -> Result; + fn override_locker(&self, uid: &str, locker: VLocker) -> Result; +} + +pub trait StoreVGarageBackend { + fn fetch_garage(&self, uid: &str) -> Result; + fn override_garage(&self, uid: &str, garage: VGarage) -> Result; +} + +impl StoreBankBackend for BankHotStateService { + fn get_bank(&self, uid: &str) -> Result { + BankHotStateService::get_bank(self, uid.to_string()) + } + + fn preview_checkout( + &self, + uid: &str, + amount: f64, + source: &str, + ) -> Result { + BankHotStateService::charge_checkout( + self, + uid.to_string(), + amount, + BankCheckoutContext { + source_field: source.to_string(), + commit: false, + }, + ) + } + + fn override_bank(&self, uid: &str, bank: &Bank) -> Result { + let json = serde_json::to_string(bank) + .map_err(|error| format!("Invalid bank override JSON: {}", error))?; + BankHotStateService::override_bank(self, uid.to_string(), json) + } +} + +impl StoreBankBackend for &BankHotStateService { + fn get_bank(&self, uid: &str) -> Result { + BankHotStateService::get_bank(self, uid.to_string()) + } + + fn preview_checkout( + &self, + uid: &str, + amount: f64, + source: &str, + ) -> Result { + BankHotStateService::charge_checkout( + self, + uid.to_string(), + amount, + BankCheckoutContext { + source_field: source.to_string(), + commit: false, + }, + ) + } + + fn override_bank(&self, uid: &str, bank: &Bank) -> Result { + let json = serde_json::to_string(bank) + .map_err(|error| format!("Invalid bank override JSON: {}", error))?; + BankHotStateService::override_bank(self, uid.to_string(), json) + } +} + +impl StoreOrgBackend for OrgHotStateService { + fn get_org(&self, org_id: &str) -> Result { + OrgHotStateService::get_org(self, org_id.to_string()) + } + + fn override_org(&self, org_id: &str, org: HotOrgRecord) -> Result { + OrgHotStateService::override_org(self, org_id.to_string(), org) + } +} + +impl StoreOrgBackend for &OrgHotStateService { + fn get_org(&self, org_id: &str) -> Result { + OrgHotStateService::get_org(self, org_id.to_string()) + } + + fn override_org(&self, org_id: &str, org: HotOrgRecord) -> Result { + OrgHotStateService::override_org(self, org_id.to_string(), org) + } +} + +impl StoreLockerBackend + for LockerHotStateService +{ + fn get_locker(&self, uid: &str) -> Result { + LockerHotStateService::get_locker(self, uid.to_string()) + } + + fn override_locker(&self, uid: &str, items: HashMap) -> Result { + LockerHotStateService::override_locker(self, uid.to_string(), items) + } +} + +impl StoreLockerBackend + for &LockerHotStateService +{ + fn get_locker(&self, uid: &str) -> Result { + LockerHotStateService::get_locker(self, uid.to_string()) + } + + fn override_locker(&self, uid: &str, items: HashMap) -> Result { + LockerHotStateService::override_locker(self, uid.to_string(), items) + } +} + +impl StoreVLockerBackend + for VLockerHotStateService +{ + fn fetch_locker(&self, uid: &str) -> Result { + VLockerHotStateService::fetch_locker(self, uid) + } + + fn override_locker(&self, uid: &str, locker: VLocker) -> Result { + VLockerHotStateService::override_locker(self, uid, locker) + } +} + +impl StoreVLockerBackend + for &VLockerHotStateService +{ + fn fetch_locker(&self, uid: &str) -> Result { + VLockerHotStateService::fetch_locker(self, uid) + } + + fn override_locker(&self, uid: &str, locker: VLocker) -> Result { + VLockerHotStateService::override_locker(self, uid, locker) + } +} + +impl StoreVGarageBackend + for VGarageHotStateService +{ + fn fetch_garage(&self, uid: &str) -> Result { + VGarageHotStateService::fetch_garage(self, uid) + } + + fn override_garage(&self, uid: &str, garage: VGarage) -> Result { + VGarageHotStateService::override_garage(self, uid, garage) + } +} + +impl StoreVGarageBackend + for &VGarageHotStateService +{ + fn fetch_garage(&self, uid: &str) -> Result { + VGarageHotStateService::fetch_garage(self, uid) + } + + fn override_garage(&self, uid: &str, garage: VGarage) -> Result { + VGarageHotStateService::override_garage(self, uid, garage) + } +} + +pub struct StoreService { + bank: B, + org: O, + locker: L, + vlocker: VL, + vgarage: VG, +} + +impl StoreService { + pub fn new(bank: B, org: O, locker: L, vlocker: VL, vgarage: VG) -> Self { + Self { + bank, + org, + locker, + vlocker, + vgarage, + } + } +} + +impl StoreService +where + B: StoreBankBackend, + O: StoreOrgBackend, + L: StoreLockerBackend, + VL: StoreVLockerBackend, + VG: StoreVGarageBackend, +{ + pub fn checkout(&self, context: StoreCheckoutContext) -> Result { + if context.requester_uid.trim().is_empty() { + return Err("A valid requester UID is required.".to_string()); + } + if context.items.is_empty() && context.vehicles.is_empty() { + return Err("Add at least one item before checkout.".to_string()); + } + + let charged_total = checkout_total(&context); + if charged_total <= 0.0 { + return Err("Checkout total must be greater than zero.".to_string()); + } + + let requester_uid = context.requester_uid.trim(); + let payment_method = context.payment_method.trim().to_ascii_lowercase(); + + let original_locker = self.locker.get_locker(requester_uid)?; + let original_vlocker = self.vlocker.fetch_locker(requester_uid)?; + let original_vgarage = self.vgarage.fetch_garage(requester_uid)?; + + let mut next_locker = original_locker.clone(); + let mut next_vlocker = original_vlocker.clone(); + let mut next_vgarage = original_vgarage.clone(); + + let mut locker_patch = HashMap::new(); + let mut va_patch = HashMap::new(); + let mut vgarage_patch = HashMap::new(); + let mut locker_granted = Vec::new(); + let mut vehicle_granted = Vec::new(); + let mut va_categories_changed: Vec<&str> = Vec::new(); + let mut vgarage_categories_changed: Vec<&str> = Vec::new(); + + for item_seed in &context.items { + if item_seed.classname.trim().is_empty() || item_seed.quantity == 0 { + return Err("Checkout contains an invalid item entry.".to_string()); + } + + let locker_category = resolve_locker_category(&item_seed.category)?; + let arsenal_category = resolve_arsenal_category(&item_seed.category)?; + + let existing_amount = next_locker + .items + .get(&item_seed.classname) + .map(|entry| entry.amount) + .unwrap_or(0); + let updated_item = Item { + category: locker_category.to_string(), + classname: item_seed.classname.clone(), + amount: existing_amount.saturating_add(item_seed.quantity), + }; + + next_locker + .items + .insert(item_seed.classname.clone(), updated_item.clone()); + locker_patch.insert( + item_seed.classname.clone(), + serde_json::to_value(&updated_item) + .map_err(|error| format!("Failed to serialize locker patch: {}", error))?, + ); + locker_granted.push(StoreGrantedItem { + classname: item_seed.classname.clone(), + category: locker_category.to_string(), + quantity: item_seed.quantity, + }); + + match arsenal_category { + EquipmentCategory::Items => { + push_unique(&mut next_vlocker.items, &item_seed.classname); + push_unique_str(&mut va_categories_changed, "items"); + } + EquipmentCategory::Weapons => { + push_unique(&mut next_vlocker.weapons, &item_seed.classname); + push_unique_str(&mut va_categories_changed, "weapons"); + } + EquipmentCategory::Magazines => { + push_unique(&mut next_vlocker.magazines, &item_seed.classname); + push_unique_str(&mut va_categories_changed, "magazines"); + } + EquipmentCategory::Backpacks => { + push_unique(&mut next_vlocker.backpacks, &item_seed.classname); + push_unique_str(&mut va_categories_changed, "backpacks"); + } + } + } + + if next_locker.items.len() > 25 { + return Err( + "Locker capacity would exceed 25 unique items. Clear space before checkout." + .to_string(), + ); + } + + for category in va_categories_changed { + match category { + "items" => { + va_patch.insert(category.to_string(), json!(next_vlocker.items)); + } + "weapons" => { + va_patch.insert(category.to_string(), json!(next_vlocker.weapons)); + } + "magazines" => { + va_patch.insert(category.to_string(), json!(next_vlocker.magazines)); + } + "backpacks" => { + va_patch.insert(category.to_string(), json!(next_vlocker.backpacks)); + } + _ => {} + } + } + + for vehicle_seed in &context.vehicles { + if vehicle_seed.classname.trim().is_empty() { + return Err("Vehicle checkout entry was missing a classname.".to_string()); + } + + let vehicle_category = resolve_vehicle_category(&vehicle_seed.category)?; + match vehicle_category { + VehicleCategory::Cars => { + push_unique(&mut next_vgarage.cars, &vehicle_seed.classname); + push_unique_str(&mut vgarage_categories_changed, "cars"); + } + VehicleCategory::Armor => { + push_unique(&mut next_vgarage.armor, &vehicle_seed.classname); + push_unique_str(&mut vgarage_categories_changed, "armor"); + } + VehicleCategory::Helis => { + push_unique(&mut next_vgarage.helis, &vehicle_seed.classname); + push_unique_str(&mut vgarage_categories_changed, "helis"); + } + VehicleCategory::Planes => { + push_unique(&mut next_vgarage.planes, &vehicle_seed.classname); + push_unique_str(&mut vgarage_categories_changed, "planes"); + } + VehicleCategory::Naval => { + push_unique(&mut next_vgarage.naval, &vehicle_seed.classname); + push_unique_str(&mut vgarage_categories_changed, "naval"); + } + VehicleCategory::Other => { + push_unique(&mut next_vgarage.other, &vehicle_seed.classname); + push_unique_str(&mut vgarage_categories_changed, "other"); + } + } + + vehicle_granted.push(StoreGrantedVehicle { + classname: vehicle_seed.classname.clone(), + category: vehicle_seed.category.clone(), + }); + } + + for category in vgarage_categories_changed { + match category { + "cars" => { + vgarage_patch.insert(category.to_string(), json!(next_vgarage.cars)); + } + "armor" => { + vgarage_patch.insert(category.to_string(), json!(next_vgarage.armor)); + } + "helis" => { + vgarage_patch.insert(category.to_string(), json!(next_vgarage.helis)); + } + "planes" => { + vgarage_patch.insert(category.to_string(), json!(next_vgarage.planes)); + } + "naval" => { + vgarage_patch.insert(category.to_string(), json!(next_vgarage.naval)); + } + "other" => { + vgarage_patch.insert(category.to_string(), json!(next_vgarage.other)); + } + _ => {} + } + } + + let mut bank_patch = HashMap::new(); + let mut final_bank = None; + let mut original_bank = None; + + let mut org_patch = HashMap::new(); + let mut org_target_uids = Vec::new(); + let mut final_org = None; + let mut original_org = None; + + match payment_method.as_str() { + "cash" | "bank" => { + original_bank = Some(self.bank.get_bank(requester_uid)?); + let preview = self.bank.preview_checkout( + requester_uid, + charged_total, + payment_method.as_str(), + )?; + bank_patch = preview.patch.clone(); + final_bank = Some(preview.account); + } + "org_funds" | "credit_line" => { + if context.org_id.trim().is_empty() { + return Err("A valid organization is required for this checkout.".to_string()); + } + + let mut org = self.org.get_org(&context.org_id)?; + original_org = Some(org.clone()); + + match payment_method.as_str() { + "org_funds" => { + if !can_manage_treasury( + &org, + requester_uid, + context.requester_is_default_org_ceo, + ) { + return Err( + "Only the organization leader or CEO can charge org funds." + .to_string(), + ); + } + if org.funds < charged_total { + return Err( + "Organization funds cannot cover this checkout.".to_string() + ); + } + org.funds -= charged_total; + org_patch.insert("funds".to_string(), json!(org.funds)); + } + "credit_line" => { + let credit_line = + org.credit_lines.get_mut(requester_uid).ok_or_else(|| { + "Assigned credit line cannot cover this checkout.".to_string() + })?; + credit_line.normalize(); + if credit_line.available_amount < charged_total { + return Err( + "Assigned credit line cannot cover this checkout.".to_string() + ); + } + + credit_line.available_amount = + round_currency(credit_line.available_amount - charged_total); + credit_line.approved_amount = credit_line.available_amount; + credit_line.outstanding_principal = + round_currency(credit_line.outstanding_principal + charged_total); + credit_line.amount_due = round_currency( + credit_line.amount_due + + (charged_total * (1.0 + credit_line.interest_rate)), + ); + credit_line.amount = credit_line.available_amount; + org_patch.insert("credit_lines".to_string(), json!(org.credit_lines)); + } + _ => unreachable!(), + } + + if payment_method == "org_funds" && !context.vehicles.is_empty() { + add_org_fleet_vehicles(&mut org, &context.vehicles); + org_patch.insert("fleet".to_string(), json!(org.fleet)); + } + + org_target_uids = resolve_member_uids(&org, Some(requester_uid)); + final_org = Some(org); + } + _ => return Err("Selected payment source is unsupported.".to_string()), + } + + let mut locker_saved = false; + let mut vlocker_saved = false; + let mut vgarage_saved = false; + let mut org_saved = false; + + let commit_result = (|| -> Result<(), String> { + if !locker_patch.is_empty() { + self.locker + .override_locker(requester_uid, next_locker.items.clone())?; + locker_saved = true; + } + + if !va_patch.is_empty() { + self.vlocker + .override_locker(requester_uid, next_vlocker.clone())?; + vlocker_saved = true; + } + + if !vgarage_patch.is_empty() { + self.vgarage + .override_garage(requester_uid, next_vgarage.clone())?; + vgarage_saved = true; + } + + if let Some(org) = final_org.clone() { + self.org.override_org(&context.org_id, org)?; + org_saved = true; + } + + if let Some(bank) = final_bank.as_ref() { + self.bank.override_bank(requester_uid, bank)?; + } + + Ok(()) + })(); + + if let Err(error) = commit_result { + if org_saved { + if let Some(org) = original_org { + let org_id = org.id.clone(); + let _ = self.org.override_org(&org_id, org); + } + } + if vgarage_saved { + let _ = self + .vgarage + .override_garage(requester_uid, original_vgarage); + } + if vlocker_saved { + let _ = self + .vlocker + .override_locker(requester_uid, original_vlocker); + } + if locker_saved { + let _ = self + .locker + .override_locker(requester_uid, original_locker.items); + } + if let Some(bank) = original_bank { + let _ = self.bank.override_bank(requester_uid, &bank); + } + return Err(error); + } + + Ok(StoreCheckoutResult { + charged_total, + payment_method, + message: format!( + "Checkout completed. {} charged, {} locker grant(s), {} vehicle unlock(s).", + format_currency(charged_total), + locker_granted.len(), + vehicle_granted.len() + ), + locker_granted, + vehicle_granted, + locker_patch, + va_patch, + vgarage_patch, + bank_patch, + org_patch, + org_target_uids, + }) + } +} + +fn checkout_total(context: &StoreCheckoutContext) -> f64 { + let item_total = context + .items + .iter() + .map(|entry| entry.price_value.max(0.0) * f64::from(entry.quantity)) + .sum::(); + let vehicle_total = context + .vehicles + .iter() + .map(|entry| entry.price_value.max(0.0)) + .sum::(); + + (item_total + vehicle_total).floor() +} + +fn resolve_locker_category(category: &str) -> Result<&'static str, String> { + match category.trim().to_ascii_lowercase().as_str() { + "item" | "attachment" => Ok("item"), + "weapon" => Ok("weapon"), + "magazine" => Ok("magazine"), + "backpack" => Ok("backpack"), + other => Err(format!("Store item category '{}' is unsupported.", other)), + } +} + +fn resolve_arsenal_category(category: &str) -> Result { + match category.trim().to_ascii_lowercase().as_str() { + "item" | "attachment" => Ok(EquipmentCategory::Items), + "weapon" => Ok(EquipmentCategory::Weapons), + "magazine" => Ok(EquipmentCategory::Magazines), + "backpack" => Ok(EquipmentCategory::Backpacks), + other => Err(format!("Store item category '{}' is unsupported.", other)), + } +} + +fn resolve_vehicle_category(category: &str) -> Result { + match category.trim().to_ascii_lowercase().as_str() { + "cars" => Ok(VehicleCategory::Cars), + "armor" => Ok(VehicleCategory::Armor), + "helis" | "heli" => Ok(VehicleCategory::Helis), + "planes" => Ok(VehicleCategory::Planes), + "naval" => Ok(VehicleCategory::Naval), + "other" => Ok(VehicleCategory::Other), + other => Err(format!("Vehicle category '{}' is unsupported.", other)), + } +} + +fn push_unique(values: &mut Vec, value: &str) { + if !values.iter().any(|entry| entry == value) { + values.push(value.to_string()); + } +} + +fn push_unique_str<'a>(values: &mut Vec<&'a str>, value: &'a str) { + if !values.contains(&value) { + values.push(value); + } +} + +fn can_manage_treasury( + org: &HotOrgRecord, + requester_uid: &str, + requester_is_default_org_ceo: bool, +) -> bool { + org.owner == requester_uid + || ((org.id.eq_ignore_ascii_case("default") || org.owner.eq_ignore_ascii_case("server")) + && requester_is_default_org_ceo) +} + +fn resolve_member_uids(org: &HotOrgRecord, requester_uid: Option<&str>) -> Vec { + let mut member_uids = org.members.keys().cloned().collect::>(); + if let Some(uid) = requester_uid { + if !uid.is_empty() && !member_uids.iter().any(|member_uid| member_uid == uid) { + member_uids.push(uid.to_string()); + } + } + member_uids +} + +fn add_org_fleet_vehicles( + org: &mut HotOrgRecord, + vehicles: &[forge_models::StoreCheckoutVehicleSeed], +) { + let mut fleet_index = org.fleet.len(); + for vehicle in vehicles { + if vehicle.classname.trim().is_empty() { + continue; + } + + let fleet_type = vehicle.category.trim().to_ascii_lowercase(); + let mut fleet_key = format!("{}_{}", vehicle.classname, fleet_index); + while org.fleet.contains_key(&fleet_key) { + fleet_index += 1; + fleet_key = format!("{}_{}", vehicle.classname, fleet_index); + } + + org.fleet.insert( + fleet_key, + OrgFleetEntry { + classname: vehicle.classname.clone(), + name: vehicle.classname.clone(), + fleet_type, + status: "Ready".to_string(), + damage: "0%".to_string(), + }, + ); + fleet_index += 1; + } +} + +fn format_currency(amount: f64) -> String { + let rounded = amount.max(0.0).round() as i64; + let digits = rounded.to_string(); + let mut formatted = String::new(); + + for (index, character) in digits.chars().rev().enumerate() { + if index > 0 && index % 3 == 0 { + formatted.push(','); + } + formatted.push(character); + } + + format!("${}", formatted.chars().rev().collect::()) +} + +fn round_currency(amount: f64) -> f64 { + (amount.max(0.0) * 100.0).round() / 100.0 +} diff --git a/lib/services/src/task.rs b/lib/services/src/task.rs new file mode 100644 index 0000000..292367f --- /dev/null +++ b/lib/services/src/task.rs @@ -0,0 +1,379 @@ +use forge_models::{ + TaskOwnershipContext, TaskOwnershipMutationResult, TaskRecord, TaskRewardContext, +}; +use forge_repositories::TaskRepository; +use serde_json::Value; + +pub struct TaskStateService { + repository: R, +} + +impl TaskStateService { + pub fn new(repository: R) -> Self { + Self { repository } + } + + pub fn reset(&self) -> Result { + self.repository.reset()?; + Ok(true) + } + + pub fn upsert_catalog_entry( + &self, + entry_id: String, + json_data: String, + ) -> Result { + let entry_id = Self::validate_entry_id(entry_id)?; + let mut entry = Self::parse_record(&json_data)?; + Self::normalize_catalog_entry(&mut entry, &entry_id); + self.repository + .save_catalog_entry(entry_id, entry.clone())?; + Ok(entry) + } + + pub fn get_catalog_entry(&self, entry_id: String) -> Result, String> { + let entry_id = Self::validate_entry_id(entry_id)?; + self.repository + .get_catalog_entry(&entry_id) + .map(|entry| entry.map(TaskRecord::into_value)) + } + + pub fn delete_catalog_entry(&self, entry_id: String) -> Result<(), String> { + let entry_id = Self::validate_entry_id(entry_id)?; + self.repository.delete_catalog_entry(&entry_id) + } + + pub fn list_active_catalog(&self) -> Result, String> { + let catalog = self.repository.list_catalog()?; + let active_statuses = self.repository.list_active_statuses()?; + let mut active_entries = Vec::new(); + + for (task_id, status) in active_statuses { + if status != "active" { + continue; + } + + let Some(entry) = catalog.get(&task_id) else { + continue; + }; + + let mut entry = entry.fields.clone(); + entry.insert("taskId".to_string(), Value::String(task_id.clone())); + entry.insert("taskID".to_string(), Value::String(task_id)); + entry.insert("status".to_string(), Value::String(status)); + active_entries.push(Value::Object(entry)); + } + + Ok(active_entries) + } + + pub fn bind_ownership( + &self, + entry_id: String, + json_data: String, + ) -> Result { + let entry_id = Self::validate_entry_id(entry_id)?; + let mut ownership = Self::parse_ownership_context(&json_data)?; + if ownership.org_id.trim().is_empty() { + ownership.org_id = "default".to_string(); + } + + self.repository + .save_ownership(entry_id.clone(), ownership.clone())?; + let entry = self.patch_catalog_ownership( + &entry_id, + true, + &ownership.requester_uid, + &ownership.org_id, + )?; + + Ok(TaskOwnershipMutationResult { + task_id: entry_id, + requester_uid: ownership.requester_uid, + org_id: ownership.org_id, + entry, + message: "Task ownership updated.".to_string(), + }) + } + + pub fn release_ownership( + &self, + entry_id: String, + ) -> Result { + let entry_id = Self::validate_entry_id(entry_id)?; + let ownership = self + .repository + .get_ownership(&entry_id)? + .unwrap_or_default(); + self.repository.delete_ownership(&entry_id)?; + let entry = self.patch_catalog_ownership(&entry_id, false, "", "default")?; + + Ok(TaskOwnershipMutationResult { + task_id: entry_id, + requester_uid: ownership.requester_uid, + org_id: ownership.org_id, + entry, + message: "Task ownership released.".to_string(), + }) + } + + pub fn accept_task( + &self, + entry_id: String, + json_data: String, + ) -> Result { + let entry_id = Self::validate_entry_id(entry_id)?; + let ownership = Self::parse_ownership_context(&json_data)?; + if ownership.requester_uid.trim().is_empty() { + return Err("Missing task ID or requester UID.".to_string()); + } + + if self.get_status(entry_id.clone())? != "active" { + return Err("Task is no longer active.".to_string()); + } + + if let Some(existing) = self.repository.get_ownership(&entry_id)? + && !existing.requester_uid.trim().is_empty() + && existing.requester_uid != ownership.requester_uid + { + return Err("Task has already been accepted.".to_string()); + } + + let mut result = self.bind_ownership( + entry_id, + serde_json::to_string(&ownership) + .map_err(|error| format!("Failed to serialize task ownership: {error}"))?, + )?; + result.message = "Task accepted.".to_string(); + Ok(result) + } + + pub fn set_status(&self, entry_id: String, status: String) -> Result { + let entry_id = Self::validate_entry_id(entry_id)?; + let final_status = Self::validate_status(status)?; + self.repository + .set_active_status(entry_id.clone(), final_status.clone())?; + if matches!(final_status.as_str(), "succeeded" | "failed") { + self.repository + .set_completed_status(entry_id, final_status)?; + } else { + self.repository.delete_completed_status(&entry_id)?; + } + + Ok(true) + } + + pub fn get_status(&self, entry_id: String) -> Result { + let entry_id = Self::validate_entry_id(entry_id)?; + if let Some(status) = self.repository.get_active_status(&entry_id)? { + return Ok(status); + } + + Ok(self + .repository + .get_completed_status(&entry_id)? + .unwrap_or_default()) + } + + pub fn clear_status(&self, entry_id: String) -> Result { + let entry_id = Self::validate_entry_id(entry_id)?; + self.repository.delete_active_status(&entry_id)?; + self.repository.delete_completed_status(&entry_id)?; + Ok(true) + } + + pub fn get_reward_context(&self, entry_id: String) -> Result { + let entry_id = Self::validate_entry_id(entry_id)?; + let ownership = self + .repository + .get_ownership(&entry_id)? + .unwrap_or_default(); + Ok(TaskRewardContext { + requester_uid: ownership.requester_uid, + org_id: ownership.org_id, + }) + } + + pub fn increment_defuse_count(&self, entry_id: String) -> Result { + let entry_id = Self::validate_entry_id(entry_id)?; + self.repository.increment_defuse_count(&entry_id) + } + + pub fn get_defuse_count(&self, entry_id: String) -> Result { + let entry_id = Self::validate_entry_id(entry_id)?; + self.repository.get_defuse_count(&entry_id) + } + + pub fn clear_task(&self, entry_id: String) -> Result { + let entry_id = Self::validate_entry_id(entry_id)?; + self.repository.delete_catalog_entry(&entry_id)?; + self.repository.delete_ownership(&entry_id)?; + self.repository.delete_active_status(&entry_id)?; + self.repository.delete_completed_status(&entry_id)?; + self.repository.clear_defuse_count(&entry_id)?; + Ok(true) + } + + fn patch_catalog_ownership( + &self, + entry_id: &str, + accepted: bool, + requester_uid: &str, + org_id: &str, + ) -> Result { + let Some(mut entry) = self.repository.get_catalog_entry(entry_id)? else { + return Ok(Value::Null); + }; + + entry + .fields + .insert("accepted".to_string(), Value::Bool(accepted)); + entry.fields.insert( + "requesterUid".to_string(), + Value::String(requester_uid.to_string()), + ); + entry + .fields + .insert("orgID".to_string(), Value::String(org_id.to_string())); + Self::normalize_catalog_entry(&mut entry, entry_id); + self.repository + .save_catalog_entry(entry_id.to_string(), entry.clone())?; + Ok(entry.into_value()) + } + + fn normalize_catalog_entry(entry: &mut TaskRecord, entry_id: &str) { + let fields = &mut entry.fields; + fields + .entry("accepted".to_string()) + .or_insert(Value::Bool(false)); + fields + .entry("requesterUid".to_string()) + .or_insert(Value::String(String::new())); + fields + .entry("orgID".to_string()) + .or_insert(Value::String("default".to_string())); + fields + .entry("taskId".to_string()) + .or_insert(Value::String(entry_id.to_string())); + fields + .entry("taskID".to_string()) + .or_insert(Value::String(entry_id.to_string())); + } + + fn validate_entry_id(entry_id: String) -> Result { + if entry_id.trim().is_empty() { + return Err("Task ID is required.".to_string()); + } + + Ok(entry_id) + } + + fn validate_status(status: String) -> Result { + if status.trim().is_empty() { + return Err("Task status is required.".to_string()); + } + + Ok(status) + } + + fn parse_record(json_data: &str) -> Result { + serde_json::from_str::(json_data) + .map_err(|error| format!("Invalid task JSON: {error}")) + } + + fn parse_ownership_context(json_data: &str) -> Result { + serde_json::from_str::(json_data) + .map_err(|error| format!("Invalid task ownership JSON: {error}")) + } +} + +#[cfg(test)] +mod tests { + use super::TaskStateService; + use forge_repositories::{InMemoryTaskRepository, TaskRepository}; + use serde_json::Value; + + #[test] + fn bind_ownership_updates_catalog_entry() { + let repository = InMemoryTaskRepository::new(); + let service = TaskStateService::new(repository.clone()); + + service + .upsert_catalog_entry("task-1".to_string(), r#"{"title":"Attack"}"#.to_string()) + .expect("catalog upsert should succeed"); + + let result = service + .bind_ownership( + "task-1".to_string(), + r#"{"requesterUid":"uid-1","orgId":"org-1"}"#.to_string(), + ) + .expect("bind should succeed"); + + assert_eq!(result.requester_uid, "uid-1"); + assert_eq!(result.org_id, "org-1"); + assert_eq!( + result.entry.get("accepted").and_then(Value::as_bool), + Some(true) + ); + + let stored = repository + .get_catalog_entry("task-1") + .expect("catalog lookup should succeed") + .expect("catalog entry should exist"); + assert_eq!( + stored.fields.get("requesterUid").and_then(Value::as_str), + Some("uid-1") + ); + } + + #[test] + fn get_status_falls_back_to_completed_status() { + let repository = InMemoryTaskRepository::new(); + let service = TaskStateService::new(repository.clone()); + + service + .set_status("task-1".to_string(), "failed".to_string()) + .expect("status update should succeed"); + repository + .delete_active_status("task-1") + .expect("active status delete should succeed"); + + assert_eq!( + service + .get_status("task-1".to_string()) + .expect("status lookup should succeed"), + "failed" + ); + } + + #[test] + fn list_active_catalog_only_returns_active_entries() { + let service = TaskStateService::new(InMemoryTaskRepository::new()); + + service + .upsert_catalog_entry( + "task-active".to_string(), + r#"{"title":"Active"}"#.to_string(), + ) + .expect("active catalog upsert should succeed"); + service + .upsert_catalog_entry("task-done".to_string(), r#"{"title":"Done"}"#.to_string()) + .expect("done catalog upsert should succeed"); + service + .set_status("task-active".to_string(), "active".to_string()) + .expect("active status update should succeed"); + service + .set_status("task-done".to_string(), "succeeded".to_string()) + .expect("done status update should succeed"); + + let active_catalog = service + .list_active_catalog() + .expect("active catalog should build"); + + assert_eq!(active_catalog.len(), 1); + assert_eq!( + active_catalog[0].get("taskId").and_then(Value::as_str), + Some("task-active") + ); + } +} diff --git a/lib/services/src/v_garage.rs b/lib/services/src/v_garage.rs index 7d8f468..c8563d7 100644 --- a/lib/services/src/v_garage.rs +++ b/lib/services/src/v_garage.rs @@ -4,7 +4,7 @@ //! validation, and orchestration. use forge_models::{VGarage, VehicleCategory}; -use forge_repositories::VGarageRepository; +use forge_repositories::{VGarageHotRepository, VGarageRepository}; /// Service layer implementation for virtual garage business logic and operations. /// @@ -22,6 +22,11 @@ pub struct VGarageService { repository: R, } +pub struct VGarageHotStateService { + service: VGarageService, + repository: H, +} + impl VGarageService { /// Creates a new garage service with the provided repository. /// @@ -54,6 +59,11 @@ impl VGarageService { } } + pub fn update_garage(&self, uid: &str, garage: &VGarage) -> Result { + self.repository.update(uid, garage)?; + Ok(garage.clone()) + } + /// Retrieves a specific field from a player's virtual garage. /// /// Fields: "cars", "armor", "heli", "planes", "naval", "other" @@ -122,3 +132,87 @@ impl VGarageService { self.repository.exists(uid) } } + +impl VGarageHotStateService { + pub fn new(repository: R, hot_repository: H) -> Self { + Self { + service: VGarageService::new(repository), + repository: hot_repository, + } + } + + pub fn init_garage(&self, uid: &str) -> Result { + if let Some(garage) = self.repository.get(uid)? { + return Ok(garage); + } + + let garage = match self.service.fetch_garage(uid) { + Ok(garage) => garage, + Err(_) => self.service.create_garage(uid)?, + }; + self.repository.save(&garage, uid)?; + Ok(garage) + } + + pub fn fetch_garage(&self, uid: &str) -> Result { + self.init_garage(uid) + } + + pub fn get_garage(&self, uid: &str, field: &str) -> Result, String> { + let garage = self.init_garage(uid)?; + Ok(match field.to_lowercase().as_str() { + "cars" => garage.cars, + "armor" => garage.armor, + "helis" | "heli" => garage.helis, + "planes" => garage.planes, + "naval" => garage.naval, + "other" => garage.other, + _ => Vec::new(), + }) + } + + pub fn override_garage(&self, uid: &str, garage: VGarage) -> Result { + self.repository.save(&garage, uid)?; + Ok(garage) + } + + pub fn save_garage(&self, uid: &str) -> Result { + let garage = self + .repository + .get(uid)? + .ok_or_else(|| format!("No garage found for player '{}'", uid))?; + let saved = if self.service.garage_exists(uid)? { + self.service.update_garage(uid, &garage)? + } else { + self.service.create_garage(uid)? + }; + self.repository.save(&saved, uid)?; + Ok(saved) + } + + pub fn add_garage( + &self, + uid: &str, + category: VehicleCategory, + classnames: Vec, + ) -> Result { + let garage = self.service.add_garage(uid, category, classnames)?; + self.repository.save(&garage, uid)?; + Ok(garage) + } + + pub fn remove_garage( + &self, + uid: &str, + category: VehicleCategory, + classname: &str, + ) -> Result { + let garage = self.service.remove_garage(uid, category, classname)?; + self.repository.save(&garage, uid)?; + Ok(garage) + } + + pub fn remove_hot_garage(&self, uid: &str) -> Result<(), String> { + self.repository.delete(uid) + } +} diff --git a/lib/services/src/v_locker.rs b/lib/services/src/v_locker.rs index b6cf193..59ac643 100644 --- a/lib/services/src/v_locker.rs +++ b/lib/services/src/v_locker.rs @@ -4,7 +4,7 @@ //! validation, and orchestration. use forge_models::{EquipmentCategory, VLocker}; -use forge_repositories::VLockerRepository; +use forge_repositories::{VLockerHotRepository, VLockerRepository}; /// Service layer implementation for virtual locker business logic and operations. /// @@ -22,6 +22,11 @@ pub struct VLockerService { repository: R, } +pub struct VLockerHotStateService { + service: VLockerService, + repository: H, +} + impl VLockerService { /// Creates a new locker service with the provided repository. /// @@ -54,6 +59,11 @@ impl VLockerService { } } + pub fn update_locker(&self, uid: &str, locker: &VLocker) -> Result { + self.repository.update(uid, locker)?; + Ok(locker.clone()) + } + /// Retrieves a specific field from a player's virtual locker. /// /// Fields: "items", "weapons", "magazines", "backpacks" @@ -122,3 +132,63 @@ impl VLockerService { self.repository.exists(uid) } } + +impl VLockerHotStateService { + pub fn new(repository: R, hot_repository: H) -> Self { + Self { + service: VLockerService::new(repository), + repository: hot_repository, + } + } + + pub fn init_locker(&self, uid: &str) -> Result { + if let Some(locker) = self.repository.get(uid)? { + return Ok(locker); + } + + let locker = match self.service.fetch_locker(uid) { + Ok(locker) => locker, + Err(_) => self.service.create_locker(uid)?, + }; + self.repository.save(&locker, uid)?; + Ok(locker) + } + + pub fn fetch_locker(&self, uid: &str) -> Result { + self.init_locker(uid) + } + + pub fn get_locker(&self, uid: &str, field: &str) -> Result, String> { + let locker = self.init_locker(uid)?; + Ok(match field.to_lowercase().as_str() { + "items" => locker.items, + "weapons" => locker.weapons, + "magazines" => locker.magazines, + "backpacks" => locker.backpacks, + _ => Vec::new(), + }) + } + + pub fn override_locker(&self, uid: &str, locker: VLocker) -> Result { + self.repository.save(&locker, uid)?; + Ok(locker) + } + + pub fn save_locker(&self, uid: &str) -> Result { + let locker = self + .repository + .get(uid)? + .ok_or_else(|| format!("No locker found for player '{}'", uid))?; + let saved = if self.service.locker_exists(uid)? { + self.service.update_locker(uid, &locker)? + } else { + self.service.create_locker(uid)? + }; + self.repository.save(&saved, uid)?; + Ok(saved) + } + + pub fn remove_locker(&self, uid: &str) -> Result<(), String> { + self.repository.delete(uid) + } +} 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) => {