From 732433f84852af7a51bfdd1a0883e88517a86264 Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Thu, 14 May 2026 22:11:23 -0500 Subject: [PATCH] Add task lifecycle event bus integration - Add a common in-process event bus - Emit task lifecycle events from task store and instances - Register CAD listeners to invalidate task state --- arma/server/addons/cad/XEH_PREP.hpp | 1 + arma/server/addons/cad/XEH_preInit.sqf | 1 + .../fnc_registerTaskEventListeners.sqf | 47 +++++ arma/server/addons/common/README.md | 26 +++ arma/server/addons/common/XEH_PREP.hpp | 1 + .../addons/common/functions/fnc_eventBus.sqf | 167 ++++++++++++++++ .../addons/main/functions/fnc_initStores.sqf | 1 + arma/server/addons/task/XEH_postInit.sqf | 24 +++ arma/server/addons/task/XEH_preInit.sqf | 18 ++ .../addons/task/functions/fnc_attack.sqf | 185 ++++++------------ .../task/functions/fnc_initTaskStore.sqf | 115 ++++++++++- .../prototypes/fnc_AttackTaskBaseClass.sqf | 55 +++++- .../prototypes/fnc_TaskInstanceBaseClass.sqf | 52 +++++ 13 files changed, 558 insertions(+), 135 deletions(-) create mode 100644 arma/server/addons/cad/functions/fnc_registerTaskEventListeners.sqf create mode 100644 arma/server/addons/common/functions/fnc_eventBus.sqf diff --git a/arma/server/addons/cad/XEH_PREP.hpp b/arma/server/addons/cad/XEH_PREP.hpp index 3f556c5..1e9c4a9 100644 --- a/arma/server/addons/cad/XEH_PREP.hpp +++ b/arma/server/addons/cad/XEH_PREP.hpp @@ -5,3 +5,4 @@ PREP(initGroupRepository); PREP(initPermissionService); PREP(initPersistenceService); PREP(initRequestRepository); +PREP(registerTaskEventListeners); diff --git a/arma/server/addons/cad/XEH_preInit.sqf b/arma/server/addons/cad/XEH_preInit.sqf index 187b250..eb8b31b 100644 --- a/arma/server/addons/cad/XEH_preInit.sqf +++ b/arma/server/addons/cad/XEH_preInit.sqf @@ -5,6 +5,7 @@ PREP_RECOMPILE_START; PREP_RECOMPILE_END; call FUNC(initCadStore); +call FUNC(registerTaskEventListeners); [QGVAR(requestHydrateCad), { params [["_uid", "", [""]]]; diff --git a/arma/server/addons/cad/functions/fnc_registerTaskEventListeners.sqf b/arma/server/addons/cad/functions/fnc_registerTaskEventListeners.sqf new file mode 100644 index 0000000..add0587 --- /dev/null +++ b/arma/server/addons/cad/functions/fnc_registerTaskEventListeners.sqf @@ -0,0 +1,47 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_registerTaskEventListeners.sqf + * Author: IDSolutions + * Date: 2026-05-14 + * Public: No + * + * Description: + * Registers CAD listeners for framework task lifecycle events. + * + * Arguments: + * None + * + * Return Value: + * Listener tokens [ARRAY] + * + * Example: + * call forge_server_cad_fnc_registerTaskEventListeners + */ + +if (isNil QEGVAR(common,EventBus)) then { call EFUNC(common,eventBus); }; +if !(isNil QGVAR(TaskEventListenerTokens)) exitWith { GVAR(TaskEventListenerTokens) }; + +private _invalidateCadState = { + params ["_event"]; + + ["INFO", format [ + "CAD task event received: %1 taskID=%2 taskType=%3 status=%4", + _event getOrDefault ["event", ""], + _event getOrDefault ["taskID", ""], + _event getOrDefault ["taskType", ""], + _event getOrDefault ["status", ""] + ]] call EFUNC(common,log); + + [CRPC(cad,invalidateCadState), []] call CFUNC(globalEvent); +}; + +GVAR(TaskEventListenerTokens) = [ + EGVAR(common,EventBus) call ["on", ["task.created", _invalidateCadState, "cad.task.invalidate"]], + EGVAR(common,EventBus) call ["on", ["task.started", _invalidateCadState, "cad.task.invalidate"]], + EGVAR(common,EventBus) call ["on", ["task.completed", _invalidateCadState, "cad.task.invalidate"]], + EGVAR(common,EventBus) call ["on", ["task.failed", _invalidateCadState, "cad.task.invalidate"]], + EGVAR(common,EventBus) call ["on", ["task.cleared", _invalidateCadState, "cad.task.invalidate"]] +]; + +GVAR(TaskEventListenerTokens) diff --git a/arma/server/addons/common/README.md b/arma/server/addons/common/README.md index a097c6b..a5d3b94 100644 --- a/arma/server/addons/common/README.md +++ b/arma/server/addons/common/README.md @@ -11,6 +11,8 @@ the specific domain addons or the Rust extension. ## Main Components - `fnc_baseStore.sqf` provides shared hash-map object behavior such as JSON conversion. +- `fnc_eventBus.sqf` provides a framework-wide in-process event bus for + cross-addon notifications. - `fnc_log.sqf` standardizes server log messages. - `fnc_getPlayer.sqf` resolves online players by UID. - `fnc_formatNumber.sqf` formats numeric values for notifications and UI text. @@ -21,3 +23,27 @@ the specific domain addons or the Rust extension. ## Notes Keep this addon free of domain-specific behavior. If a helper needs actor, bank, org, task, store, or CAD state, it belongs in that addon instead. + +## Event Bus +The event bus is initialized as `forge_server_common_EventBus` during store +bootstrap. It is synchronous and in-process: listeners run immediately when an +event is emitted. + +```sqf +private _token = EGVAR(common,EventBus) call ["on", [ + "task.completed", + { + params ["_event"]; + ["INFO", format ["Task completed: %1", _event getOrDefault ["taskID", ""]]] call EFUNC(common,log); + }, + "example" +]]; + +EGVAR(common,EventBus) call ["emit", [ + "task.completed", + createHashMapFromArray [["taskID", "task_001"]], + createHashMapFromArray [["source", "task"]] +]]; + +EGVAR(common,EventBus) call ["off", [_token]]; +``` diff --git a/arma/server/addons/common/XEH_PREP.hpp b/arma/server/addons/common/XEH_PREP.hpp index a5a2c37..4b810bc 100644 --- a/arma/server/addons/common/XEH_PREP.hpp +++ b/arma/server/addons/common/XEH_PREP.hpp @@ -1,4 +1,5 @@ PREP(baseStore); +PREP(eventBus); PREP(formatNumber); PREP(getPlayer); PREP(generateHash); diff --git a/arma/server/addons/common/functions/fnc_eventBus.sqf b/arma/server/addons/common/functions/fnc_eventBus.sqf new file mode 100644 index 0000000..e66fbcb --- /dev/null +++ b/arma/server/addons/common/functions/fnc_eventBus.sqf @@ -0,0 +1,167 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_eventBus.sqf + * Author: IDSolutions + * Date: 2026-05-14 + * Public: No + * + * Description: + * Initializes the framework-wide in-process event bus. + * + * Arguments: + * None + * + * Return Value: + * Event bus object [HASHMAP OBJECT] + * + * Example: + * call forge_server_common_fnc_eventBus + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(EventBusBase) = compileFinal createHashMapFromArray [ + ["#type", "EventBus"], + ["#create", compileFinal { + _self set ["handlers", createHashMap]; + _self set ["nextToken", 0]; + + ["INFO", "Common EventBus Initialized!"] call EFUNC(common,log); + }], + ["on", compileFinal { + params [["_eventName", "", [""]], ["_handler", {}, [{}]], ["_owner", "", [""]]]; + + if (_eventName isEqualTo "") exitWith { "" }; + + private _handlers = _self getOrDefault ["handlers", createHashMap]; + private _eventHandlers = +(_handlers getOrDefault [_eventName, []]); + private _nextToken = (_self getOrDefault ["nextToken", 0]) + 1; + private _token = format ["%1:%2", _eventName, _nextToken]; + + _eventHandlers pushBack createHashMapFromArray [ + ["token", _token], + ["owner", _owner], + ["handler", _handler] + ]; + + _handlers set [_eventName, _eventHandlers]; + _self set ["handlers", _handlers]; + _self set ["nextToken", _nextToken]; + + _token + }], + ["off", compileFinal { + params [["_token", "", [""]]]; + + if (_token isEqualTo "") exitWith { false }; + + private _handlers = _self getOrDefault ["handlers", createHashMap]; + private _removed = false; + + { + private _eventHandlers = +(_handlers getOrDefault [_x, []]); + private _remainingHandlers = _eventHandlers select { + (_x getOrDefault ["token", ""]) isNotEqualTo _token + }; + + if ((count _remainingHandlers) isNotEqualTo (count _eventHandlers)) then { + _removed = true; + if (_remainingHandlers isEqualTo []) then { + _handlers deleteAt _x; + } else { + _handlers set [_x, _remainingHandlers]; + }; + }; + } forEach (keys _handlers); + + _self set ["handlers", _handlers]; + _removed + }], + ["emit", compileFinal { + params [["_eventName", "", [""]], ["_payload", createHashMap], ["_options", createHashMap]]; + + private _result = createHashMapFromArray [ + ["event", _eventName], + ["listenerCount", 0], + ["invoked", 0], + ["failed", 0] + ]; + + if (_eventName isEqualTo "") exitWith { _result }; + + if !(_payload isEqualType createHashMap) then { + _payload = createHashMapFromArray [["value", _payload]]; + }; + if !(_options isEqualType createHashMap) then { + _options = createHashMap; + }; + + private _eventPayload = +_payload; + _eventPayload set ["event", _eventName]; + _eventPayload set ["source", _eventPayload getOrDefault ["source", _options getOrDefault ["source", "unknown"]]]; + _eventPayload set ["timestamp", _eventPayload getOrDefault ["timestamp", serverTime]]; + + private _handlers = _self getOrDefault ["handlers", createHashMap]; + private _eventHandlers = +(_handlers getOrDefault [_eventName, []]); + _result set ["listenerCount", count _eventHandlers]; + + { + private _handler = _x getOrDefault ["handler", {}]; + private _token = _x getOrDefault ["token", ""]; + private _owner = _x getOrDefault ["owner", ""]; + + try { + [_eventPayload] call _handler; + _result set ["invoked", (_result getOrDefault ["invoked", 0]) + 1]; + } catch { + _result set ["failed", (_result getOrDefault ["failed", 0]) + 1]; + ["ERROR", format ["EventBus handler failed. Event=%1 Token=%2 Owner=%3 Error=%4", _eventName, _token, _owner, _exception]] call EFUNC(common,log); + }; + } forEach _eventHandlers; + + _result + }], + ["clear", compileFinal { + params [["_eventName", "", [""]]]; + + private _handlers = _self getOrDefault ["handlers", createHashMap]; + + if (_eventName isEqualTo "") then { + _self set ["handlers", createHashMap]; + } else { + _handlers deleteAt _eventName; + _self set ["handlers", _handlers]; + }; + + true + }], + ["listenerCount", compileFinal { + params [["_eventName", "", [""]]]; + + private _handlers = _self getOrDefault ["handlers", createHashMap]; + + if (_eventName isEqualTo "") exitWith { + private _total = 0; + { _total = _total + (count _y); } forEach _handlers; + _total + }; + + count (_handlers getOrDefault [_eventName, []]) + }], + ["listeners", compileFinal { + params [["_eventName", "", [""]]]; + + private _handlers = _self getOrDefault ["handlers", createHashMap]; + + if (_eventName isNotEqualTo "") exitWith { +(_handlers getOrDefault [_eventName, []]) }; + + private _counts = createHashMap; + { _counts set [_x, count _y]; } forEach _handlers; + + _counts + }] +]; + +GVAR(EventBus) = createHashMapObject [GVAR(EventBusBase)]; + +GVAR(EventBus) diff --git a/arma/server/addons/main/functions/fnc_initStores.sqf b/arma/server/addons/main/functions/fnc_initStores.sqf index e3486f9..06f7766 100644 --- a/arma/server/addons/main/functions/fnc_initStores.sqf +++ b/arma/server/addons/main/functions/fnc_initStores.sqf @@ -18,6 +18,7 @@ // Base if (isNil QEGVAR(common,BaseStore)) then { call EFUNC(common,baseStore); }; +if (isNil QEGVAR(common,EventBus)) then { call EFUNC(common,eventBus); }; // Actor if (isNil QEGVAR(actor,ActorStore)) then { call EFUNC(actor,initActorStore); }; diff --git a/arma/server/addons/task/XEH_postInit.sqf b/arma/server/addons/task/XEH_postInit.sqf index 464334d..0ea1d62 100644 --- a/arma/server/addons/task/XEH_postInit.sqf +++ b/arma/server/addons/task/XEH_postInit.sqf @@ -1,5 +1,29 @@ #include "script_component.hpp" +if (isNil QEGVAR(common,EventBus)) then { call EFUNC(common,eventBus); }; +if (isNil QGVAR(TaskLifecycleEventLogTokens)) then { + private _logTaskLifecycleEvent = { + params ["_event"]; + + ["INFO", format [ + "Task lifecycle event: %1 taskID=%2 taskType=%3 status=%4 participants=%5", + _event getOrDefault ["event", ""], + _event getOrDefault ["taskID", ""], + _event getOrDefault ["taskType", ""], + _event getOrDefault ["status", ""], + _event getOrDefault ["participants", []] + ]] call EFUNC(common,log); + }; + + GVAR(TaskLifecycleEventLogTokens) = [ + EGVAR(common,EventBus) call ["on", ["task.created", _logTaskLifecycleEvent, "task.lifecycle.log"]], + EGVAR(common,EventBus) call ["on", ["task.started", _logTaskLifecycleEvent, "task.lifecycle.log"]], + EGVAR(common,EventBus) call ["on", ["task.completed", _logTaskLifecycleEvent, "task.lifecycle.log"]], + EGVAR(common,EventBus) call ["on", ["task.failed", _logTaskLifecycleEvent, "task.lifecycle.log"]], + EGVAR(common,EventBus) call ["on", ["task.cleared", _logTaskLifecycleEvent, "task.lifecycle.log"]] + ]; +}; + ["ace_explosives_defuse", { private _taskID = ""; private _explosive = objNull; diff --git a/arma/server/addons/task/XEH_preInit.sqf b/arma/server/addons/task/XEH_preInit.sqf index 9602e35..95eb2ef 100644 --- a/arma/server/addons/task/XEH_preInit.sqf +++ b/arma/server/addons/task/XEH_preInit.sqf @@ -8,4 +8,22 @@ private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)]; #include "initSettings.inc.sqf" +[] call FUNC(TaskInstanceBaseClass); +[] call FUNC(EntityControllerBaseClass); +[] call FUNC(AttackTaskBaseClass); +[] call FUNC(HostageTaskBaseClass); +[] call FUNC(HostageEntityController); +[] call FUNC(TargetEntityController); +[] call FUNC(ShooterEntityController); +[] call FUNC(HVTEntityController); +[] call FUNC(CargoEntityController); +[] call FUNC(ProtectedEntityController); +[] call FUNC(IEDEntityController); +[] call FUNC(DefenseEnemyController); +[] call FUNC(DefuseTaskBaseClass); +[] call FUNC(DestroyTaskBaseClass); +[] call FUNC(DeliveryTaskBaseClass); +[] call FUNC(HVTTaskBaseClass); +[] call FUNC(DefendTaskBaseClass); + call FUNC(initTaskStore); diff --git a/arma/server/addons/task/functions/fnc_attack.sqf b/arma/server/addons/task/functions/fnc_attack.sqf index 4867f02..c1564f29 100644 --- a/arma/server/addons/task/functions/fnc_attack.sqf +++ b/arma/server/addons/task/functions/fnc_attack.sqf @@ -2,152 +2,77 @@ /* * Author: IDSolutions - * Registers an attack task + * Registers an attack task. + * + * This public function is now a compatibility adapter around + * AttackTaskBaseClass. Keep the argument list stable for Eden modules, + * startTask, and external scripts while the object-style task prototypes + * become the live implementation. * * 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: 0, 0 = no limit) - * 9: Equipment rewards (default: []) - * 10: Supply rewards (default: []) - * 11: Weapon rewards (default: []) - * 12: Vehicle rewards (default: []) - * 13: Special rewards (default: []) + * 3: Amount of funds the company receives if the task is successful + * 4: Amount of rating the company and player lose if the task is failed + * 5: Amount of rating the company and player receive if the task is successful + * 6: Should the mission end if the task is successful + * 7: Should the mission end if the task is failed + * 8: Amount of time before target(s) escape + * 9: Equipment rewards + * 10: Supply rewards + * 11: Weapon rewards + * 12: Vehicle rewards + * 13: Special rewards * * 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]], - ["_timeLimit", 0, [0]], - ["_equipmentRewards", [], [[]]], - ["_supplyRewards", [], [[]]], - ["_weaponRewards", [], [[]]], - ["_vehicleRewards", [], [[]]], - ["_specialRewards", [], [[]]] + ["_taskID", "", [""]], + ["_limitFail", -1, [0]], + ["_limitSuccess", -1, [0]], + ["_companyFunds", 0, [0]], + ["_ratingFail", 0, [0]], + ["_ratingSuccess", 0, [0]], + ["_endSuccess", false, [false]], + ["_endFail", false, [false]], + ["_timeLimit", 0, [0]], + ["_equipmentRewards", [], [[]]], + ["_supplyRewards", [], [[]]], + ["_weaponRewards", [], [[]]], + ["_vehicleRewards", [], [[]]], + ["_specialRewards", [], [[]]] ]; -private _result = 0; -private _targets = []; +private _taskParams = createHashMapFromArray [ + ["limitFail", _limitFail], + ["limitSuccess", _limitSuccess], + ["funds", _companyFunds], + ["ratingFail", _ratingFail], + ["ratingSuccess", _ratingSuccess], + ["endSuccess", _endSuccess], + ["endFail", _endFail], + ["timeLimit", _timeLimit], + ["useTaskStore", true] +]; -waitUntil { - sleep 1; - _targets = GVAR(TaskStore) call ["getTaskEntities", ["targets", _taskID]]; - GVAR(TaskStore) call ["trackParticipants", [_taskID, _targets, "", 300]]; - count _targets > 0 -}; +if (_equipmentRewards isNotEqualTo []) then { _taskParams set ["equipment", _equipmentRewards]; }; +if (_supplyRewards isNotEqualTo []) then { _taskParams set ["supplies", _supplyRewards]; }; +if (_weaponRewards isNotEqualTo []) then { _taskParams set ["weapons", _weaponRewards]; }; +if (_vehicleRewards isNotEqualTo []) then { _taskParams set ["vehicles", _vehicleRewards]; }; +if (_specialRewards isNotEqualTo []) then { _taskParams set ["special", _specialRewards]; }; -_targets = GVAR(TaskStore) call ["getTaskEntities", ["targets", _taskID]]; - -if (_timeLimit isNotEqualTo 0) then { - private _catalogEntry = GVAR(TaskStore) call ["getTaskCatalogEntry", [_taskID]]; - ["INFO", format [ - "Attack task %1 initial state before acceptance wait. Accepted=%2, RequesterUid='%3', Source='%4', TimeLimit=%5s", +private _task = createHashMapObject [ + GVAR(AttackTaskBaseClass), + [ _taskID, - _catalogEntry getOrDefault ["accepted", false], - _catalogEntry getOrDefault ["requesterUid", ""], - _catalogEntry getOrDefault ["source", ""], - _timeLimit - ]] call EFUNC(common,log); + createHashMapFromArray [["targets", GVAR(TaskStore) call ["getTaskEntities", ["targets", _taskID]]]], + _taskParams + ] +]; - ["INFO", format ["Attack task %1 waiting for acceptance before starting %2s time limit.", _taskID, _timeLimit]] call EFUNC(common,log); - waitUntil { - sleep 1; - GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] - }; - - ["INFO", format ["Attack task %1 accepted. Starting %2s time limit.", _taskID, _timeLimit]] call EFUNC(common,log); -}; - -private _startTime = if (_timeLimit isNotEqualTo 0) then { floor(time) } else { nil }; - -waitUntil { - sleep 1; - GVAR(TaskStore) call ["trackParticipants", [_taskID, _targets, "", 300]]; - - private _targetsKilled = ({ !alive _x } count _targets); - - if (_timeLimit isNotEqualTo 0) then { - private _timeExpired = (floor time - _startTime >= _timeLimit); - - if (_targetsKilled < _limitSuccess && _timeExpired) then { - ["WARNING", format [ - "Attack task %1 failed by timeout. TargetsKilled=%2, Required=%3, TimeLimit=%4s", - _taskID, - _targetsKilled, - _limitSuccess, - _timeLimit - ]] call EFUNC(common,log); - _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 { "EveryoneLost" call BFUNC(endMissionServer); }; -} else { - ["INFO", format [ - "Attack task %1 succeeded. TargetsRequired=%2, TargetsKilled=%3", - _taskID, - _limitSuccess, - { !alive _x } count _targets - ]] call EFUNC(common,log); - - { 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 { "EveryoneWon" call BFUNC(endMissionServer); }; -}; +_task call ["runLoop", []]; diff --git a/arma/server/addons/task/functions/fnc_initTaskStore.sqf b/arma/server/addons/task/functions/fnc_initTaskStore.sqf index 369000b..1c831ae 100644 --- a/arma/server/addons/task/functions/fnc_initTaskStore.sqf +++ b/arma/server/addons/task/functions/fnc_initTaskStore.sqf @@ -26,6 +26,7 @@ GVAR(TaskStore) = createHashMapObject [[ ["#type", "TaskStore"], ["#create", compileFinal { _self set ["participantRegistry", createHashMap]; + _self set ["taskLifecycleRegistry", createHashMap]; _self set ["taskEntityRegistries", createHashMapFromArray [ ["cargo", createHashMap], ["hostages", createHashMap], @@ -148,6 +149,51 @@ GVAR(TaskStore) = createHashMapObject [[ private _envelope = _self call ["callTaskStateEnvelope", ["task:ownership:release", [_taskID]]]; _envelope getOrDefault ["success", false] }], + ["buildTaskLifecycleEventPayload", compileFinal { + params [["_taskID", "", [""]], ["_status", "", [""]], ["_extra", createHashMap]]; + + if !(_extra isEqualType createHashMap) then { + _extra = createHashMap; + }; + + private _catalogEntry = _self call ["getTaskCatalogEntry", [_taskID]]; + private _lifecycleRegistry = _self getOrDefault ["taskLifecycleRegistry", createHashMap]; + private _lifecycle = +(_lifecycleRegistry getOrDefault [_taskID, createHashMap]); + private _startedAt = _lifecycle getOrDefault ["startedAt", -1]; + private _finishedAt = _lifecycle getOrDefault ["finishedAt", -1]; + + createHashMapFromArray [ + ["taskID", _taskID], + ["taskType", _catalogEntry getOrDefault ["type", ""]], + ["title", _catalogEntry getOrDefault ["title", _taskID]], + ["description", _catalogEntry getOrDefault ["description", ""]], + ["position", +(_catalogEntry getOrDefault ["position", []])], + ["status", _status], + ["source", _catalogEntry getOrDefault ["source", "task"]], + ["requesterUid", _catalogEntry getOrDefault ["requesterUid", ""]], + ["orgID", _catalogEntry getOrDefault ["orgID", "default"]], + ["startedAt", _startedAt], + ["finishedAt", _finishedAt], + ["duration", if (_startedAt >= 0 && { _finishedAt >= 0 }) then { _finishedAt - _startedAt } else { -1 }], + ["failureReason", _extra getOrDefault ["failureReason", ""]], + ["participants", _self call ["getTaskParticipantUids", [_taskID]]], + ["rewardData", +(_extra getOrDefault ["rewardData", createHashMap])], + ["resultSnapshot", +(_extra getOrDefault ["resultSnapshot", createHashMap])], + ["catalogEntry", +_catalogEntry] + ] + }], + ["emitTaskLifecycleEvent", compileFinal { + params [["_eventName", "", [""]], ["_taskID", "", [""]], ["_status", "", [""]], ["_extra", createHashMap]]; + + if (_eventName isEqualTo "" || { _taskID isEqualTo "" }) exitWith { createHashMap }; + if (isNil QEGVAR(common,EventBus)) exitWith { createHashMap }; + + EGVAR(common,EventBus) call ["emit", [ + _eventName, + _self call ["buildTaskLifecycleEventPayload", [_taskID, _status, _extra]], + createHashMapFromArray [["source", "task"]] + ]] + }], ["registerTaskCatalogEntry", compileFinal { params [["_taskID", "", [""]], ["_entry", createHashMap, [createHashMap]]]; @@ -160,7 +206,19 @@ GVAR(TaskStore) = createHashMapObject [[ [_taskID, toJSON _entry] ] ]; - _envelope getOrDefault ["success", false] + private _registered = _envelope getOrDefault ["success", false]; + + if (_registered) then { + private _lifecycleRegistry = _self getOrDefault ["taskLifecycleRegistry", createHashMap]; + private _lifecycle = +(_lifecycleRegistry getOrDefault [_taskID, createHashMap]); + _lifecycle set ["createdAt", serverTime]; + _lifecycleRegistry set [_taskID, _lifecycle]; + _self set ["taskLifecycleRegistry", _lifecycleRegistry]; + + _self call ["emitTaskLifecycleEvent", ["task.created", _taskID, "created", createHashMap]]; + }; + + _registered }], ["getActiveTaskCatalog", compileFinal { private _entries = _self call ["callTaskState", ["task:catalog:active", [], []]]; @@ -250,7 +308,37 @@ GVAR(TaskStore) = createHashMapObject [[ if (_taskID isEqualTo "" || { _status isEqualTo "" }) exitWith { false }; - [(_self call ["callTaskState", ["task:status:set", [_taskID, _status], false]])] params [["_statusResult", false, [false]]]; + private _envelope = _self call ["callTaskStateEnvelope", ["task:status:set", [_taskID, _status]]]; + private _statusResult = _envelope getOrDefault ["success", false]; + + if (_statusResult) then { + private _normalizedStatus = toLowerANSI _status; + private _lifecycleRegistry = _self getOrDefault ["taskLifecycleRegistry", createHashMap]; + private _lifecycle = +(_lifecycleRegistry getOrDefault [_taskID, createHashMap]); + private _eventName = ""; + + switch (_normalizedStatus) do { + case "active": { + _lifecycle set ["startedAt", serverTime]; + _eventName = "task.started"; + }; + case "succeeded": { + _lifecycle set ["finishedAt", serverTime]; + _eventName = "task.completed"; + }; + case "failed": { + _lifecycle set ["finishedAt", serverTime]; + _eventName = "task.failed"; + }; + }; + + _lifecycleRegistry set [_taskID, _lifecycle]; + _self set ["taskLifecycleRegistry", _lifecycleRegistry]; + + if (_eventName isNotEqualTo "") then { + _self call ["emitTaskLifecycleEvent", [_eventName, _taskID, _normalizedStatus, createHashMap]]; + }; + }; _statusResult }], @@ -391,6 +479,21 @@ GVAR(TaskStore) = createHashMapObject [[ _participantSnapshots }], + ["getTaskParticipants", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { createHashMap }; + + private _participantRegistry = _self getOrDefault ["participantRegistry", createHashMap]; + +(_participantRegistry getOrDefault [_taskID, createHashMap]) + }], + ["getTaskParticipantUids", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { [] }; + + keys (_self call ["getTaskParticipants", [_taskID]]) + }], ["resolveRewardContext", compileFinal { params [["_taskID", "", [""]]]; @@ -483,14 +586,20 @@ GVAR(TaskStore) = createHashMapObject [[ params [["_taskID", "", [""]]]; if (_taskID isEqualTo "") exitWith { false }; - if !(isNil QGVAR(MissionManager)) then { GVAR(MissionManager) call ["completeMission", [_taskID]]; }; + _self call ["emitTaskLifecycleEvent", ["task.cleared", _taskID, "cleared", createHashMap]]; + private _participantRegistry = _self getOrDefault ["participantRegistry", createHashMap]; _participantRegistry deleteAt _taskID; _self set ["participantRegistry", _participantRegistry]; + + private _lifecycleRegistry = _self getOrDefault ["taskLifecycleRegistry", createHashMap]; + _lifecycleRegistry deleteAt _taskID; + _self set ["taskLifecycleRegistry", _lifecycleRegistry]; + _self call ["callTaskState", ["task:clear", [_taskID], false]]; _self call ["clearTaskEntities", [_taskID]]; true diff --git a/arma/server/addons/task/functions/prototypes/fnc_AttackTaskBaseClass.sqf b/arma/server/addons/task/functions/prototypes/fnc_AttackTaskBaseClass.sqf index c2eef10..f50bcc5 100644 --- a/arma/server/addons/task/functions/prototypes/fnc_AttackTaskBaseClass.sqf +++ b/arma/server/addons/task/functions/prototypes/fnc_AttackTaskBaseClass.sqf @@ -67,6 +67,24 @@ GVAR(AttackTaskBaseClass) = createHashMapFromArray [ ["#delete", compileFinal { _self call ["unregisterInstance", []]; }], + ["refreshTargetsFromStore", compileFinal { + private _taskID = _self getOrDefault ["taskID", ""]; + if (_taskID isEqualTo "" || { !(_self getOrDefault ["useTaskStore", false]) }) exitWith { false }; + + private _targets = GVAR(TaskStore) call ["getTaskEntities", ["targets", _taskID]]; + _self set ["targets", _targets]; + + private _taskParams = _self getOrDefault ["taskParams", createHashMap]; + private _requiredKills = _taskParams getOrDefault ["limitSuccess", -1]; + if (_requiredKills < 0) then { _requiredKills = count _targets; }; + + private _maxTargetLosses = _taskParams getOrDefault ["limitFail", -1]; + if (_maxTargetLosses < 0) then { _maxTargetLosses = count _targets; }; + + _self set ["requiredKills", _requiredKills]; + _self set ["maxTargetLosses", _maxTargetLosses]; + true + }], ["countKilledTargets", compileFinal { private _targets = _self getOrDefault ["targets", []]; { !alive _x } count _targets @@ -94,7 +112,6 @@ GVAR(AttackTaskBaseClass) = createHashMapFromArray [ }], ["runLoop", compileFinal { private _taskID = _self getOrDefault ["taskID", ""]; - private _targets = _self getOrDefault ["targets", []]; private _timeLimit = _self getOrDefault ["timeLimit", 0]; private _rewardData = _self getOrDefault ["rewardData", createHashMap]; private _ratingFail = _rewardData getOrDefault ["ratingFail", 0]; @@ -107,37 +124,69 @@ GVAR(AttackTaskBaseClass) = createHashMapFromArray [ if (_useTaskStore) then { waitUntil { sleep 1; + _self call ["refreshTargetsFromStore", []]; + private _targets = _self getOrDefault ["targets", []]; GVAR(TaskStore) call ["trackParticipants", [_taskID, _targets, "", 300]]; count _targets > 0 }; } else { waitUntil { sleep 1; - count _targets > 0 + count (_self getOrDefault ["targets", []]) > 0 }; }; if (_timeLimit isNotEqualTo 0 && { _useTaskStore }) then { + private _catalogEntry = GVAR(TaskStore) call ["getTaskCatalogEntry", [_taskID]]; + ["INFO", format [ + "Attack task %1 initial state before acceptance wait. Accepted=%2, RequesterUid='%3', Source='%4', TimeLimit=%5s", + _taskID, + _catalogEntry getOrDefault ["accepted", false], + _catalogEntry getOrDefault ["requesterUid", ""], + _catalogEntry getOrDefault ["source", ""], + _timeLimit + ]] call EFUNC(common,log); + + ["INFO", format ["Attack task %1 waiting for acceptance before starting %2s time limit.", _taskID, _timeLimit]] call EFUNC(common,log); waitUntil { sleep 1; GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] }; + + ["INFO", format ["Attack task %1 accepted. Starting %2s time limit.", _taskID, _timeLimit]] call EFUNC(common,log); }; _self call ["markActive", []]; while { (_self call ["getStatus", []]) isEqualTo "active" } do { + private _targets = _self getOrDefault ["targets", []]; + if (_useTaskStore) then { + _self call ["refreshTargetsFromStore", []]; + _targets = _self getOrDefault ["targets", []]; GVAR(TaskStore) call ["trackParticipants", [_taskID, _targets, "", 300]]; }; private _snapshot = _self call ["tick", []]; if (_snapshot getOrDefault ["shouldFail", false]) exitWith { + ["WARNING", format [ + "Attack task %1 failed by timeout. TargetsKilled=%2, Required=%3, TimeLimit=%4s", + _taskID, + _snapshot getOrDefault ["targetsKilled", 0], + _snapshot getOrDefault ["requiredKills", 0], + _timeLimit + ]] call EFUNC(common,log); _self call ["markFailed", ["Attack fail conditions met.", _snapshot]]; }; if (_snapshot getOrDefault ["shouldSucceed", false]) exitWith { + ["INFO", format [ + "Attack task %1 succeeded. TargetsRequired=%2, TargetsKilled=%3", + _taskID, + _snapshot getOrDefault ["requiredKills", 0], + _snapshot getOrDefault ["targetsKilled", 0] + ]] call EFUNC(common,log); _self call ["markSucceeded", [_snapshot]]; }; @@ -145,6 +194,7 @@ GVAR(AttackTaskBaseClass) = createHashMapFromArray [ }; if ((_self call ["getStatus", []]) isEqualTo "failed") then { + private _targets = _self getOrDefault ["targets", []]; { deleteVehicle _x } forEach _targets; if (_useTaskStore) then { @@ -160,6 +210,7 @@ GVAR(AttackTaskBaseClass) = createHashMapFromArray [ if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); }; } else { + private _targets = _self getOrDefault ["targets", []]; { deleteVehicle _x } forEach _targets; if (_useTaskStore) then { diff --git a/arma/server/addons/task/functions/prototypes/fnc_TaskInstanceBaseClass.sqf b/arma/server/addons/task/functions/prototypes/fnc_TaskInstanceBaseClass.sqf index a92b769..a6d7f7d 100644 --- a/arma/server/addons/task/functions/prototypes/fnc_TaskInstanceBaseClass.sqf +++ b/arma/server/addons/task/functions/prototypes/fnc_TaskInstanceBaseClass.sqf @@ -108,9 +108,55 @@ GVAR(TaskInstanceBaseClass) = createHashMapFromArray [ missionNamespace setVariable [_registryKey, nil]; true }], + ["buildLifecycleEventPayload", compileFinal { + private _taskID = _self getOrDefault ["taskID", ""]; + private _taskType = _self getOrDefault ["taskType", "custom"]; + private _status = _self getOrDefault ["status", "created"]; + private _startedAt = _self getOrDefault ["startedAt", -1]; + private _finishedAt = _self getOrDefault ["finishedAt", -1]; + private _participantUids = []; + + if ( + _taskID isNotEqualTo "" + && { _self getOrDefault ["useTaskStore", false] } + && { !(isNil QGVAR(TaskStore)) } + ) then { + _participantUids = GVAR(TaskStore) call ["getTaskParticipantUids", [_taskID]]; + }; + + private _payload = createHashMapFromArray [ + ["taskID", _taskID], + ["taskType", _taskType], + ["status", _status], + ["startedAt", _startedAt], + ["finishedAt", _finishedAt], + ["duration", if (_startedAt >= 0 && { _finishedAt >= 0 }) then { _finishedAt - _startedAt } else { -1 }], + ["failureReason", _self getOrDefault ["failureReason", ""]], + ["participants", _participantUids], + ["rewardData", +(_self getOrDefault ["rewardData", createHashMap])], + ["resultSnapshot", +(_self getOrDefault ["resultSnapshot", createHashMap])] + ]; + + _payload + }], + ["emitLifecycleEvent", compileFinal { + params [["_eventName", "", [""]]]; + + if (_eventName isEqualTo "") exitWith { createHashMap }; + if (isNil QEGVAR(common,EventBus)) exitWith { createHashMap }; + + EGVAR(common,EventBus) call ["emit", [ + _eventName, + _self call ["buildLifecycleEventPayload", []], + createHashMapFromArray [["source", "task"]] + ]] + }], ["markActive", compileFinal { _self set ["status", "active"]; _self set ["startedAt", serverTime]; + if !(_self getOrDefault ["useTaskStore", false]) then { + _self call ["emitLifecycleEvent", ["task.started"]]; + }; true }], ["markSucceeded", compileFinal { @@ -119,6 +165,9 @@ GVAR(TaskInstanceBaseClass) = createHashMapFromArray [ _self set ["status", "succeeded"]; _self set ["finishedAt", serverTime]; _self set ["resultSnapshot", _resultSnapshot]; + if !(_self getOrDefault ["useTaskStore", false]) then { + _self call ["emitLifecycleEvent", ["task.completed"]]; + }; true }], ["markFailed", compileFinal { @@ -128,6 +177,9 @@ GVAR(TaskInstanceBaseClass) = createHashMapFromArray [ _self set ["finishedAt", serverTime]; _self set ["failureReason", _reason]; _self set ["resultSnapshot", _resultSnapshot]; + if !(_self getOrDefault ["useTaskStore", false]) then { + _self call ["emitLifecycleEvent", ["task.failed"]]; + }; true }], ["cleanup", compileFinal {