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(initPersistenceService);
PREP(initRequestRepository);
PREP(registerTaskEventListeners);

View File

@ -5,6 +5,7 @@ PREP_RECOMPILE_START;
PREP_RECOMPILE_END;
call FUNC(initCadStore);
call FUNC(registerTaskEventListeners);
[QGVAR(requestHydrateCad), {
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
- `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]];
```

View File

@ -1,4 +1,5 @@
PREP(baseStore);
PREP(eventBus);
PREP(formatNumber);
PREP(getPlayer);
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
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); };

View File

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

View File

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

View File

@ -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 <STRING>
* 1: Amount of targets escaped to fail 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)
* 4: Amount of rating the company and player lose if the task is failed <NUMBER> (default: 0)
* 5: Amount of rating the company and player recieve if the task is successful <NUMBER> (default: 0)
* 6: Should the mission end (MissionSuccess) if the task is successful <BOOL> (default: false)
* 7: Should the mission end (MissionFailed) if the task is failed <BOOL> (default: false)
* 8: Amount of time before target(s) escape <NUMBER> (default: 0, 0 = no limit)
* 9: Equipment rewards <ARRAY> (default: [])
* 10: Supply rewards <ARRAY> (default: [])
* 11: Weapon rewards <ARRAY> (default: [])
* 12: Vehicle rewards <ARRAY> (default: [])
* 13: Special rewards <ARRAY> (default: [])
* 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>
* 5: Amount of rating the company and player receive if the task is successful <NUMBER>
* 6: Should the mission end if the task is successful <BOOL>
* 7: Should the mission end if the task is failed <BOOL>
* 8: Amount of time before target(s) escape <NUMBER>
* 9: Equipment rewards <ARRAY>
* 10: Supply rewards <ARRAY>
* 11: Weapon rewards <ARRAY>
* 12: Vehicle rewards <ARRAY>
* 13: Special rewards <ARRAY>
*
* 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", []];

View File

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

View File

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

View File

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