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
This commit is contained in:
Jacob Schmidt 2026-05-14 22:11:23 -05:00
parent e6eceac4ec
commit 732433f848
13 changed files with 558 additions and 135 deletions

View File

@ -5,3 +5,4 @@ PREP(initGroupRepository);
PREP(initPermissionService); PREP(initPermissionService);
PREP(initPersistenceService); PREP(initPersistenceService);
PREP(initRequestRepository); PREP(initRequestRepository);
PREP(registerTaskEventListeners);

View File

@ -5,6 +5,7 @@ PREP_RECOMPILE_START;
PREP_RECOMPILE_END; PREP_RECOMPILE_END;
call FUNC(initCadStore); call FUNC(initCadStore);
call FUNC(registerTaskEventListeners);
[QGVAR(requestHydrateCad), { [QGVAR(requestHydrateCad), {
params [["_uid", "", [""]]]; params [["_uid", "", [""]]];

View File

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

View File

@ -11,6 +11,8 @@ the specific domain addons or the Rust extension.
## Main Components ## Main Components
- `fnc_baseStore.sqf` provides shared hash-map object behavior such as JSON - `fnc_baseStore.sqf` provides shared hash-map object behavior such as JSON
conversion. conversion.
- `fnc_eventBus.sqf` provides a framework-wide in-process event bus for
cross-addon notifications.
- `fnc_log.sqf` standardizes server log messages. - `fnc_log.sqf` standardizes server log messages.
- `fnc_getPlayer.sqf` resolves online players by UID. - `fnc_getPlayer.sqf` resolves online players by UID.
- `fnc_formatNumber.sqf` formats numeric values for notifications and UI text. - `fnc_formatNumber.sqf` formats numeric values for notifications and UI text.
@ -21,3 +23,27 @@ the specific domain addons or the Rust extension.
## Notes ## Notes
Keep this addon free of domain-specific behavior. If a helper needs actor, 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. 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]];
```

View File

@ -1,4 +1,5 @@
PREP(baseStore); PREP(baseStore);
PREP(eventBus);
PREP(formatNumber); PREP(formatNumber);
PREP(getPlayer); PREP(getPlayer);
PREP(generateHash); PREP(generateHash);

View File

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

View File

@ -18,6 +18,7 @@
// Base // Base
if (isNil QEGVAR(common,BaseStore)) then { call EFUNC(common,baseStore); }; if (isNil QEGVAR(common,BaseStore)) then { call EFUNC(common,baseStore); };
if (isNil QEGVAR(common,EventBus)) then { call EFUNC(common,eventBus); };
// Actor // Actor
if (isNil QEGVAR(actor,ActorStore)) then { call EFUNC(actor,initActorStore); }; if (isNil QEGVAR(actor,ActorStore)) then { call EFUNC(actor,initActorStore); };

View File

@ -1,5 +1,29 @@
#include "script_component.hpp" #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", { ["ace_explosives_defuse", {
private _taskID = ""; private _taskID = "";
private _explosive = objNull; private _explosive = objNull;

View File

@ -8,4 +8,22 @@ private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
#include "initSettings.inc.sqf" #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); call FUNC(initTaskStore);

View File

@ -2,152 +2,77 @@
/* /*
* Author: IDSolutions * 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: * Arguments:
* 0: ID of the task <STRING> * 0: ID of the task <STRING>
* 1: Amount of targets escaped to fail the task <NUMBER> * 1: Amount of targets escaped to fail the task <NUMBER>
* 2: Amount of targets eliminated to complete the task <NUMBER> * 2: Amount of targets eliminated to complete the task <NUMBER>
* 3: Amount of funds the company recieves if the task is successful <NUMBER> (default: 0) * 3: Amount of funds the company receives if the task is successful <NUMBER>
* 4: Amount of rating the company and player lose if the task is failed <NUMBER> (default: 0) * 4: Amount of rating the company and player lose if the task is failed <NUMBER>
* 5: Amount of rating the company and player recieve if the task is successful <NUMBER> (default: 0) * 5: Amount of rating the company and player receive if the task is successful <NUMBER>
* 6: Should the mission end (MissionSuccess) if the task is successful <BOOL> (default: false) * 6: Should the mission end if the task is successful <BOOL>
* 7: Should the mission end (MissionFailed) if the task is failed <BOOL> (default: false) * 7: Should the mission end if the task is failed <BOOL>
* 8: Amount of time before target(s) escape <NUMBER> (default: 0, 0 = no limit) * 8: Amount of time before target(s) escape <NUMBER>
* 9: Equipment rewards <ARRAY> (default: []) * 9: Equipment rewards <ARRAY>
* 10: Supply rewards <ARRAY> (default: []) * 10: Supply rewards <ARRAY>
* 11: Weapon rewards <ARRAY> (default: []) * 11: Weapon rewards <ARRAY>
* 12: Vehicle rewards <ARRAY> (default: []) * 12: Vehicle rewards <ARRAY>
* 13: Special rewards <ARRAY> (default: []) * 13: Special rewards <ARRAY>
* *
* Return Value: * Return Value:
* None * 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 * Public: Yes
*/ */
params [ params [
["_taskID", "", [""]], ["_taskID", "", [""]],
["_limitFail", -1, [0]], ["_limitFail", -1, [0]],
["_limitSuccess", -1, [0]], ["_limitSuccess", -1, [0]],
["_companyFunds", 0, [0]], ["_companyFunds", 0, [0]],
["_ratingFail", 0, [0]], ["_ratingFail", 0, [0]],
["_ratingSuccess", 0, [0]], ["_ratingSuccess", 0, [0]],
["_endSuccess", false, [false]], ["_endSuccess", false, [false]],
["_endFail", false, [false]], ["_endFail", false, [false]],
["_timeLimit", 0, [0]], ["_timeLimit", 0, [0]],
["_equipmentRewards", [], [[]]], ["_equipmentRewards", [], [[]]],
["_supplyRewards", [], [[]]], ["_supplyRewards", [], [[]]],
["_weaponRewards", [], [[]]], ["_weaponRewards", [], [[]]],
["_vehicleRewards", [], [[]]], ["_vehicleRewards", [], [[]]],
["_specialRewards", [], [[]]] ["_specialRewards", [], [[]]]
]; ];
private _result = 0; private _taskParams = createHashMapFromArray [
private _targets = []; ["limitFail", _limitFail],
["limitSuccess", _limitSuccess],
["funds", _companyFunds],
["ratingFail", _ratingFail],
["ratingSuccess", _ratingSuccess],
["endSuccess", _endSuccess],
["endFail", _endFail],
["timeLimit", _timeLimit],
["useTaskStore", true]
];
waitUntil { if (_equipmentRewards isNotEqualTo []) then { _taskParams set ["equipment", _equipmentRewards]; };
sleep 1; if (_supplyRewards isNotEqualTo []) then { _taskParams set ["supplies", _supplyRewards]; };
_targets = GVAR(TaskStore) call ["getTaskEntities", ["targets", _taskID]]; if (_weaponRewards isNotEqualTo []) then { _taskParams set ["weapons", _weaponRewards]; };
GVAR(TaskStore) call ["trackParticipants", [_taskID, _targets, "", 300]]; if (_vehicleRewards isNotEqualTo []) then { _taskParams set ["vehicles", _vehicleRewards]; };
count _targets > 0 if (_specialRewards isNotEqualTo []) then { _taskParams set ["special", _specialRewards]; };
};
_targets = GVAR(TaskStore) call ["getTaskEntities", ["targets", _taskID]]; private _task = createHashMapObject [
GVAR(AttackTaskBaseClass),
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",
_taskID, _taskID,
_catalogEntry getOrDefault ["accepted", false], createHashMapFromArray [["targets", GVAR(TaskStore) call ["getTaskEntities", ["targets", _taskID]]]],
_catalogEntry getOrDefault ["requesterUid", ""], _taskParams
_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); _task call ["runLoop", []];
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); };
};

View File

@ -26,6 +26,7 @@ GVAR(TaskStore) = createHashMapObject [[
["#type", "TaskStore"], ["#type", "TaskStore"],
["#create", compileFinal { ["#create", compileFinal {
_self set ["participantRegistry", createHashMap]; _self set ["participantRegistry", createHashMap];
_self set ["taskLifecycleRegistry", createHashMap];
_self set ["taskEntityRegistries", createHashMapFromArray [ _self set ["taskEntityRegistries", createHashMapFromArray [
["cargo", createHashMap], ["cargo", createHashMap],
["hostages", createHashMap], ["hostages", createHashMap],
@ -148,6 +149,51 @@ GVAR(TaskStore) = createHashMapObject [[
private _envelope = _self call ["callTaskStateEnvelope", ["task:ownership:release", [_taskID]]]; private _envelope = _self call ["callTaskStateEnvelope", ["task:ownership:release", [_taskID]]];
_envelope getOrDefault ["success", false] _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 { ["registerTaskCatalogEntry", compileFinal {
params [["_taskID", "", [""]], ["_entry", createHashMap, [createHashMap]]]; params [["_taskID", "", [""]], ["_entry", createHashMap, [createHashMap]]];
@ -160,7 +206,19 @@ GVAR(TaskStore) = createHashMapObject [[
[_taskID, toJSON _entry] [_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 { ["getActiveTaskCatalog", compileFinal {
private _entries = _self call ["callTaskState", ["task:catalog:active", [], []]]; private _entries = _self call ["callTaskState", ["task:catalog:active", [], []]];
@ -250,7 +308,37 @@ GVAR(TaskStore) = createHashMapObject [[
if (_taskID isEqualTo "" || { _status isEqualTo "" }) exitWith { false }; 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 _statusResult
}], }],
@ -391,6 +479,21 @@ GVAR(TaskStore) = createHashMapObject [[
_participantSnapshots _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 { ["resolveRewardContext", compileFinal {
params [["_taskID", "", [""]]]; params [["_taskID", "", [""]]];
@ -483,14 +586,20 @@ GVAR(TaskStore) = createHashMapObject [[
params [["_taskID", "", [""]]]; params [["_taskID", "", [""]]];
if (_taskID isEqualTo "") exitWith { false }; if (_taskID isEqualTo "") exitWith { false };
if !(isNil QGVAR(MissionManager)) then { if !(isNil QGVAR(MissionManager)) then {
GVAR(MissionManager) call ["completeMission", [_taskID]]; GVAR(MissionManager) call ["completeMission", [_taskID]];
}; };
_self call ["emitTaskLifecycleEvent", ["task.cleared", _taskID, "cleared", createHashMap]];
private _participantRegistry = _self getOrDefault ["participantRegistry", createHashMap]; private _participantRegistry = _self getOrDefault ["participantRegistry", createHashMap];
_participantRegistry deleteAt _taskID; _participantRegistry deleteAt _taskID;
_self set ["participantRegistry", _participantRegistry]; _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 ["callTaskState", ["task:clear", [_taskID], false]];
_self call ["clearTaskEntities", [_taskID]]; _self call ["clearTaskEntities", [_taskID]];
true true

View File

@ -67,6 +67,24 @@ GVAR(AttackTaskBaseClass) = createHashMapFromArray [
["#delete", compileFinal { ["#delete", compileFinal {
_self call ["unregisterInstance", []]; _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 { ["countKilledTargets", compileFinal {
private _targets = _self getOrDefault ["targets", []]; private _targets = _self getOrDefault ["targets", []];
{ !alive _x } count _targets { !alive _x } count _targets
@ -94,7 +112,6 @@ GVAR(AttackTaskBaseClass) = createHashMapFromArray [
}], }],
["runLoop", compileFinal { ["runLoop", compileFinal {
private _taskID = _self getOrDefault ["taskID", ""]; private _taskID = _self getOrDefault ["taskID", ""];
private _targets = _self getOrDefault ["targets", []];
private _timeLimit = _self getOrDefault ["timeLimit", 0]; private _timeLimit = _self getOrDefault ["timeLimit", 0];
private _rewardData = _self getOrDefault ["rewardData", createHashMap]; private _rewardData = _self getOrDefault ["rewardData", createHashMap];
private _ratingFail = _rewardData getOrDefault ["ratingFail", 0]; private _ratingFail = _rewardData getOrDefault ["ratingFail", 0];
@ -107,37 +124,69 @@ GVAR(AttackTaskBaseClass) = createHashMapFromArray [
if (_useTaskStore) then { if (_useTaskStore) then {
waitUntil { waitUntil {
sleep 1; sleep 1;
_self call ["refreshTargetsFromStore", []];
private _targets = _self getOrDefault ["targets", []];
GVAR(TaskStore) call ["trackParticipants", [_taskID, _targets, "", 300]]; GVAR(TaskStore) call ["trackParticipants", [_taskID, _targets, "", 300]];
count _targets > 0 count _targets > 0
}; };
} else { } else {
waitUntil { waitUntil {
sleep 1; sleep 1;
count _targets > 0 count (_self getOrDefault ["targets", []]) > 0
}; };
}; };
if (_timeLimit isNotEqualTo 0 && { _useTaskStore }) then { 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 { waitUntil {
sleep 1; sleep 1;
GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] GVAR(TaskStore) call ["isTaskAccepted", [_taskID]]
}; };
["INFO", format ["Attack task %1 accepted. Starting %2s time limit.", _taskID, _timeLimit]] call EFUNC(common,log);
}; };
_self call ["markActive", []]; _self call ["markActive", []];
while { (_self call ["getStatus", []]) isEqualTo "active" } do { while { (_self call ["getStatus", []]) isEqualTo "active" } do {
private _targets = _self getOrDefault ["targets", []];
if (_useTaskStore) then { if (_useTaskStore) then {
_self call ["refreshTargetsFromStore", []];
_targets = _self getOrDefault ["targets", []];
GVAR(TaskStore) call ["trackParticipants", [_taskID, _targets, "", 300]]; GVAR(TaskStore) call ["trackParticipants", [_taskID, _targets, "", 300]];
}; };
private _snapshot = _self call ["tick", []]; private _snapshot = _self call ["tick", []];
if (_snapshot getOrDefault ["shouldFail", false]) exitWith { 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]]; _self call ["markFailed", ["Attack fail conditions met.", _snapshot]];
}; };
if (_snapshot getOrDefault ["shouldSucceed", false]) exitWith { 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]]; _self call ["markSucceeded", [_snapshot]];
}; };
@ -145,6 +194,7 @@ GVAR(AttackTaskBaseClass) = createHashMapFromArray [
}; };
if ((_self call ["getStatus", []]) isEqualTo "failed") then { if ((_self call ["getStatus", []]) isEqualTo "failed") then {
private _targets = _self getOrDefault ["targets", []];
{ deleteVehicle _x } forEach _targets; { deleteVehicle _x } forEach _targets;
if (_useTaskStore) then { if (_useTaskStore) then {
@ -160,6 +210,7 @@ GVAR(AttackTaskBaseClass) = createHashMapFromArray [
if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); }; if (_endFail) then { "EveryoneLost" call BFUNC(endMissionServer); };
} else { } else {
private _targets = _self getOrDefault ["targets", []];
{ deleteVehicle _x } forEach _targets; { deleteVehicle _x } forEach _targets;
if (_useTaskStore) then { if (_useTaskStore) then {

View File

@ -108,9 +108,55 @@ GVAR(TaskInstanceBaseClass) = createHashMapFromArray [
missionNamespace setVariable [_registryKey, nil]; missionNamespace setVariable [_registryKey, nil];
true 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 { ["markActive", compileFinal {
_self set ["status", "active"]; _self set ["status", "active"];
_self set ["startedAt", serverTime]; _self set ["startedAt", serverTime];
if !(_self getOrDefault ["useTaskStore", false]) then {
_self call ["emitLifecycleEvent", ["task.started"]];
};
true true
}], }],
["markSucceeded", compileFinal { ["markSucceeded", compileFinal {
@ -119,6 +165,9 @@ GVAR(TaskInstanceBaseClass) = createHashMapFromArray [
_self set ["status", "succeeded"]; _self set ["status", "succeeded"];
_self set ["finishedAt", serverTime]; _self set ["finishedAt", serverTime];
_self set ["resultSnapshot", _resultSnapshot]; _self set ["resultSnapshot", _resultSnapshot];
if !(_self getOrDefault ["useTaskStore", false]) then {
_self call ["emitLifecycleEvent", ["task.completed"]];
};
true true
}], }],
["markFailed", compileFinal { ["markFailed", compileFinal {
@ -128,6 +177,9 @@ GVAR(TaskInstanceBaseClass) = createHashMapFromArray [
_self set ["finishedAt", serverTime]; _self set ["finishedAt", serverTime];
_self set ["failureReason", _reason]; _self set ["failureReason", _reason];
_self set ["resultSnapshot", _resultSnapshot]; _self set ["resultSnapshot", _resultSnapshot];
if !(_self getOrDefault ["useTaskStore", false]) then {
_self call ["emitLifecycleEvent", ["task.failed"]];
};
true true
}], }],
["cleanup", compileFinal { ["cleanup", compileFinal {