diff --git a/arma/server/addons/task/CfgVehicles.hpp b/arma/server/addons/task/CfgVehicles.hpp index 96001bb..8870c7f 100644 --- a/arma/server/addons/task/CfgVehicles.hpp +++ b/arma/server/addons/task/CfgVehicles.hpp @@ -34,6 +34,7 @@ class CfgVehicles { typeName = "STRING"; // defaultValue = """"; }; + TASK_CHAIN_ATTRIBUTES(FORGE_Module_Attack) class LimitFail: Edit { property = "FORGE_Module_Attack_LimitFail"; displayName = "Fail Limit"; @@ -292,6 +293,7 @@ class CfgVehicles { tooltip = "Unique identifier for this task"; typeName = "STRING"; }; + TASK_CHAIN_ATTRIBUTES(FORGE_Module_Defend) class DefenseZone: Edit { property = "FORGE_Module_Defend_DefenseZone"; displayName = "Defense Zone Marker"; @@ -417,6 +419,7 @@ class CfgVehicles { typeName = "STRING"; // defaultValue = """"; }; + TASK_CHAIN_ATTRIBUTES(FORGE_Module_Defuse) class LimitFail: Edit { property = "FORGE_Module_Defuse_LimitFail"; displayName = "Fail Limit"; @@ -529,6 +532,7 @@ class CfgVehicles { typeName = "STRING"; // defaultValue = """"; }; + TASK_CHAIN_ATTRIBUTES(FORGE_Module_Destroy) class LimitFail: Edit { property = "FORGE_Module_Destroy_LimitFail"; displayName = "Fail Limit"; @@ -641,6 +645,7 @@ class CfgVehicles { typeName = "STRING"; // defaultValue = """"; }; + TASK_CHAIN_ATTRIBUTES(FORGE_Module_Hostage) class LimitFail: Edit { property = "FORGE_Module_Hostage_LimitFail"; displayName = "Fail Limit"; @@ -789,6 +794,7 @@ class CfgVehicles { tooltip = "Unique identifier for this task"; typeName = "STRING"; }; + TASK_CHAIN_ATTRIBUTES(FORGE_Module_Delivery) class DeliveryZone: Edit { property = "FORGE_Module_Delivery_DeliveryZone"; displayName = "Delivery Zone Marker"; @@ -942,6 +948,7 @@ class CfgVehicles { typeName = "STRING"; // defaultValue = """"; }; + TASK_CHAIN_ATTRIBUTES(FORGE_Module_HVT) class LimitFail: Edit { property = "FORGE_Module_HVT_LimitFail"; displayName = "Fail Limit"; diff --git a/arma/server/addons/task/XEH_PREP.hpp b/arma/server/addons/task/XEH_PREP.hpp index d417e5e..426b5e0 100644 --- a/arma/server/addons/task/XEH_PREP.hpp +++ b/arma/server/addons/task/XEH_PREP.hpp @@ -19,6 +19,7 @@ PREP(initTaskStore); PREP_SUBDIR(generators,attackMissionGenerator); PREP_SUBDIR(helpers,handleTaskRewards); +PREP_SUBDIR(helpers,parseTaskChainAttributes); PREP_SUBDIR(helpers,parseRewards); PREP_SUBDIR(helpers,spawnEnemyWave); PREP_SUBDIR(helpers,startTask); @@ -37,6 +38,12 @@ PREP_SUBDIR(modules,protectedModule); PREP_SUBDIR(modules,shootersModule); PREP_SUBDIR(objects,TaskInstanceBaseClass); +PREP_SUBDIR(objects,TaskStateGateway); +PREP_SUBDIR(objects,TaskLifecycleReporter); +PREP_SUBDIR(objects,TaskCatalogStore); +PREP_SUBDIR(objects,TaskEntityRegistry); +PREP_SUBDIR(objects,TaskParticipantTracker); +PREP_SUBDIR(objects,TaskRewardService); PREP_SUBDIR(objects,EntityControllerBaseClass); PREP_SUBDIR(objects,AttackTaskBaseClass); PREP_SUBDIR(objects,HostageTaskBaseClass); diff --git a/arma/server/addons/task/XEH_preInit.sqf b/arma/server/addons/task/XEH_preInit.sqf index b447afd..9aea1c0 100644 --- a/arma/server/addons/task/XEH_preInit.sqf +++ b/arma/server/addons/task/XEH_preInit.sqf @@ -8,6 +8,12 @@ private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)]; #include "initSettings.inc.sqf" +[] call FUNC(TaskStateGateway); +[] call FUNC(TaskLifecycleReporter); +[] call FUNC(TaskCatalogStore); +[] call FUNC(TaskEntityRegistry); +[] call FUNC(TaskParticipantTracker); +[] call FUNC(TaskRewardService); [] call FUNC(TaskInstanceBaseClass); [] call FUNC(EntityControllerBaseClass); [] call FUNC(AttackTaskBaseClass); diff --git a/arma/server/addons/task/functions/fnc_handler.sqf b/arma/server/addons/task/functions/fnc_handler.sqf index 8644cf2..a26640d 100644 --- a/arma/server/addons/task/functions/fnc_handler.sqf +++ b/arma/server/addons/task/functions/fnc_handler.sqf @@ -22,6 +22,7 @@ params [["_taskType", "", [""]], ["_args", [], [[]]], ["_minRating", 0, [0]], ["_requesterUid", "", [""]]]; private _taskID = ""; +private _shouldStartTaskLogic = true; if (_minRating > 0) then { if (_requesterUid isEqualTo "") then { @@ -71,9 +72,24 @@ if (_taskID isNotEqualTo "") then { ]] call EFUNC(common,log); }; - GVAR(TaskStore) call ["setTaskStatus", [_taskID, "available"]]; + private _initialStatus = GVAR(TaskStore) call ["resolveInitialTaskStatus", [_taskID, _catalogEntry]]; + GVAR(TaskStore) call ["setTaskStatus", [_taskID, _initialStatus]]; + if (_initialStatus isEqualTo "locked") then { + ["INFO", format ["Task %1 is waiting for chained prerequisites before task logic starts.", _taskID]] call EFUNC(common,log); + waitUntil { + sleep 2; + private _status = GVAR(TaskStore) call ["getTaskStatus", [_taskID]]; + _status isNotEqualTo "locked" + }; + if ((GVAR(TaskStore) call ["getTaskStatus", [_taskID]]) isEqualTo "") then { + _shouldStartTaskLogic = false; + ["WARNING", format ["Task %1 was cleared before its chained prerequisites unlocked.", _taskID]] call EFUNC(common,log); + }; + }; }; +if !(_shouldStartTaskLogic) exitWith {}; + switch (_taskType) do { case "attack": { private _thread = _args spawn FUNC(attack); diff --git a/arma/server/addons/task/functions/fnc_initTaskStore.sqf b/arma/server/addons/task/functions/fnc_initTaskStore.sqf index 2ffab29..5579ccd 100644 --- a/arma/server/addons/task/functions/fnc_initTaskStore.sqf +++ b/arma/server/addons/task/functions/fnc_initTaskStore.sqf @@ -24,600 +24,119 @@ #pragma hemtt ignore_variables ["_self"] GVAR(TaskStore) = createHashMapObject [[ ["#type", "TaskStore"], - ["#create", compileFinal { - _self set ["participantRegistry", createHashMap]; - _self set ["taskLifecycleRegistry", createHashMap]; - _self set ["taskEntityRegistries", createHashMapFromArray [ - ["cargo", createHashMap], - ["hostages", createHashMap], - ["hvts", createHashMap], - ["ieds", createHashMap], - ["entities", createHashMap], - ["shooters", createHashMap], - ["targets", createHashMap] - ]]; - - }], + ["#create", compileFinal {}], ["resetMissionState", compileFinal { - _self set ["participantRegistry", createHashMap]; - _self set ["taskLifecycleRegistry", createHashMap]; - _self set ["taskEntityRegistries", createHashMapFromArray [ - ["cargo", createHashMap], - ["hostages", createHashMap], - ["hvts", createHashMap], - ["ieds", createHashMap], - ["entities", createHashMap], - ["shooters", createHashMap], - ["targets", createHashMap] - ]]; + GVAR(TaskLifecycleReporter) call ["resetRuntimeState", []]; + GVAR(TaskCatalogStore) call ["resetRuntimeState", []]; + GVAR(TaskEntityRegistry) call ["resetRuntimeState", []]; + GVAR(TaskParticipantTracker) call ["resetRuntimeState", []]; - ["task:reset", []] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - if ( - !_isSuccess - || { !(_result isEqualType "") } - || { (_result find "Error:") == 0 } - ) exitWith { - ["WARNING", "Failed to reset task backend state during task store initialization."] call EFUNC(common,log); - false - }; - - ["INFO", "Task backend state reset for mission lifecycle."] call EFUNC(common,log); - true - }], - ["callTaskStateEnvelope", compileFinal { - params [["_function", "", [""]], ["_arguments", [], [[]]]]; - - private _envelope = createHashMapFromArray [ - ["success", false], - ["error", ""] - ]; - - if (_function isEqualTo "") exitWith { _envelope }; - - [_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - if !_isSuccess exitWith { - _envelope set ["error", format ["Task backend call '%1' failed.", _function]]; - _envelope - }; - if !(_result isEqualType "") exitWith { - _envelope set ["error", format ["Task backend call '%1' returned an invalid response.", _function]]; - _envelope - }; - if ((_result find "Error:") == 0) exitWith { - ["ERROR", format ["Task extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log); - _envelope set ["error", _result select [7]]; - _envelope - }; - - _envelope set ["success", true]; - if (_result isNotEqualTo "") then { - _envelope set ["data", fromJSON _result]; - }; - - _envelope - }], - ["callTaskState", compileFinal { - params [["_function", "", [""]], ["_arguments", [], [[]]], ["_fallback", nil]]; - - private _envelope = _self call ["callTaskStateEnvelope", [_function, _arguments]]; - if !(_envelope getOrDefault ["success", false]) exitWith { _fallback }; - - _envelope getOrDefault ["data", _fallback] + GVAR(TaskStateGateway) call ["reset", []] }], ["bindTaskOwnership", compileFinal { params [["_taskID", "", [""]], ["_requesterUid", "", [""]]]; - - private _result = createHashMapFromArray [ - ["success", false], - ["requesterUid", _requesterUid], - ["orgID", "default"], - ["message", ""] - ]; - - if (_taskID isEqualTo "") exitWith { - _result set ["message", "Missing task ID."]; - _result - }; - - private _orgID = "default"; - - if (_requesterUid isNotEqualTo "") then { - private _actor = EGVAR(actor,ActorStore) call ["load", [_requesterUid]]; - - if (_actor isEqualTo createHashMap) exitWith { - _result set ["message", format ["Failed to load actor for %1.", _requesterUid]]; - _result - }; - - _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]]; - }; - - private _context = createHashMapFromArray [ - ["requesterUid", _requesterUid], - ["orgId", _orgID] - ]; - private _envelope = _self call [ - "callTaskStateEnvelope", - [ - "task:ownership:bind", - [_taskID, toJSON _context] - ] - ]; - if !(_envelope getOrDefault ["success", false]) exitWith { - _result set ["message", _envelope getOrDefault ["error", "Failed to bind task ownership."]]; - _result - }; - - private _bindResult = _envelope getOrDefault ["data", createHashMap]; - _result set ["success", true]; - _result set ["message", _bindResult getOrDefault [ - "message", - ["No requester UID provided. Bound task to default organization.", "Task ownership updated."] select (_requesterUid isNotEqualTo "") - ]]; - _result set ["orgID", _bindResult getOrDefault ["orgId", _orgID]]; - _result + GVAR(TaskCatalogStore) call ["bindTaskOwnership", [_taskID, _requesterUid]] }], ["releaseTaskOwnership", compileFinal { params [["_taskID", "", [""]]]; - - if (_taskID isEqualTo "") exitWith { false }; - - private _envelope = _self call ["callTaskStateEnvelope", ["task:ownership:release", [_taskID]]]; - _envelope getOrDefault ["success", false] + GVAR(TaskCatalogStore) call ["releaseTaskOwnership", [_taskID]] }], ["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] - ] + GVAR(TaskLifecycleReporter) call ["buildTaskLifecycleEventPayload", _this] }], ["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"]] - ]] + GVAR(TaskLifecycleReporter) call ["emitTaskLifecycleEvent", _this] + }], + ["normalizePrerequisiteTaskIds", compileFinal { + GVAR(TaskCatalogStore) call ["normalizePrerequisiteTaskIds", _this] + }], + ["getTaskPrerequisites", compileFinal { + GVAR(TaskCatalogStore) call ["getTaskPrerequisites", _this] + }], + ["isTaskCompleted", compileFinal { + GVAR(TaskCatalogStore) call ["isTaskCompleted", _this] + }], + ["areTaskPrerequisitesSatisfied", compileFinal { + GVAR(TaskCatalogStore) call ["areTaskPrerequisitesSatisfied", _this] + }], + ["resolveInitialTaskStatus", compileFinal { + GVAR(TaskCatalogStore) call ["resolveInitialTaskStatus", _this] + }], + ["markTaskCompleted", compileFinal { + GVAR(TaskCatalogStore) call ["markTaskCompleted", _this] + }], + ["unlockDependentTasks", compileFinal { + GVAR(TaskCatalogStore) call ["unlockDependentTasks", _this] }], ["registerTaskCatalogEntry", compileFinal { - params [["_taskID", "", [""]], ["_entry", createHashMap, [createHashMap]]]; - - if (_taskID isEqualTo "" || { _entry isEqualTo createHashMap }) exitWith { false }; - - private _envelope = _self call [ - "callTaskStateEnvelope", - [ - "task:catalog:upsert", - [_taskID, toJSON _entry] - ] - ]; - 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 + GVAR(TaskCatalogStore) call ["registerTaskCatalogEntry", _this] }], ["getActiveTaskCatalog", compileFinal { - private _entries = _self call ["callTaskState", ["task:catalog:active", [], []]]; - if !(_entries isEqualType []) exitWith { [] }; - - _entries + GVAR(TaskCatalogStore) call ["getActiveTaskCatalog", _this] }], ["hasTaskCatalogEntry", compileFinal { - params [["_taskID", "", [""]]]; - - if (_taskID isEqualTo "") exitWith { false }; - - private _entry = _self call ["callTaskState", ["task:catalog:get", [_taskID], objNull]]; - _entry isEqualType createHashMap + GVAR(TaskCatalogStore) call ["hasTaskCatalogEntry", _this] }], ["getTaskCatalogEntry", compileFinal { - params [["_taskID", "", [""]]]; - - if (_taskID isEqualTo "") exitWith { createHashMap }; - - [(_self call ["callTaskState", ["task:catalog:get", [_taskID], createHashMap]])] params [["_entry", createHashMap, [createHashMap]]]; - if !(_entry isEqualType createHashMap) exitWith { createHashMap }; - - _entry + GVAR(TaskCatalogStore) call ["getTaskCatalogEntry", _this] }], ["isTaskAccepted", compileFinal { - params [["_taskID", "", [""]]]; - - if (_taskID isEqualTo "") exitWith { false }; - - [(_self call ["getTaskCatalogEntry", [_taskID]])] params [["_entry", createHashMap, [createHashMap]]]; - if (_entry isEqualTo createHashMap) exitWith { false }; - - [(_entry getOrDefault ["accepted", false])] params [["_accepted", false, [false]]]; - [(_entry getOrDefault ["requesterUid", ""])] params [["_requesterUid", "", [""]]]; - - _accepted || { _requesterUid isNotEqualTo "" } + GVAR(TaskCatalogStore) call ["isTaskAccepted", _this] }], ["acceptTask", compileFinal { - params [["_taskID", "", [""]], ["_requesterUid", "", [""]]]; - - private _result = createHashMapFromArray [ - ["success", false], - ["message", "Unable to accept task."], - ["entry", createHashMap] - ]; - - if (_taskID isEqualTo "" || { _requesterUid isEqualTo "" }) exitWith { - _result set ["message", "Missing task ID or requester UID."]; - _result - }; - - private _actor = EGVAR(actor,ActorStore) call ["load", [_requesterUid]]; - if (_actor isEqualTo createHashMap) exitWith { - _result set ["message", format ["Failed to load actor for %1.", _requesterUid]]; - _result - }; - - private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]]; - - private _context = createHashMapFromArray [ - ["requesterUid", _requesterUid], - ["orgId", _orgID] - ]; - private _envelope = _self call [ - "callTaskStateEnvelope", - [ - "task:ownership:accept", - [_taskID, toJSON _context] - ] - ]; - if !(_envelope getOrDefault ["success", false]) exitWith { - _result set ["message", _envelope getOrDefault ["error", "Unable to accept task."]]; - _result - }; - - private _acceptResult = _envelope getOrDefault ["data", createHashMap]; - private _entry = _acceptResult getOrDefault ["entry", createHashMap]; - if !(_entry isEqualType createHashMap) then { - _entry = createHashMap; - }; - - _result set ["success", true]; - _result set ["message", _acceptResult getOrDefault ["message", "Task accepted."]]; - _result set ["entry", _entry]; - _result + GVAR(TaskCatalogStore) call ["acceptTask", _this] }], ["setTaskStatus", compileFinal { - params [["_taskID", "", [""]], ["_status", "", [""]]]; - - if (_taskID isEqualTo "" || { _status isEqualTo "" }) exitWith { 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 + GVAR(TaskCatalogStore) call ["setTaskStatus", _this] }], ["getTaskStatus", compileFinal { - params [["_taskID", "", [""]]]; - - if (_taskID isEqualTo "") exitWith { "" }; - - private _status = _self call ["callTaskState", ["task:status:get", [_taskID], ""]]; - if !(_status isEqualType "") exitWith { "" }; - - _status + GVAR(TaskCatalogStore) call ["getTaskStatus", _this] }], ["clearTaskStatus", compileFinal { - params [["_taskID", "", [""]]]; - - if (_taskID isEqualTo "") exitWith { false }; - - [(_self call ["callTaskState", ["task:status:clear", [_taskID], false]])] params [["_statusResult", false, [false]]]; - - _statusResult + GVAR(TaskCatalogStore) call ["clearTaskStatus", _this] }], ["registerTaskEntity", compileFinal { - params [["_registryKey", "", [""]], ["_taskID", "", [""]], ["_entity", objNull, [objNull]]]; - - if (_registryKey isEqualTo "" || { _taskID isEqualTo "" } || { isNull _entity }) exitWith { false }; - - private _taskEntityRegistries = _self getOrDefault ["taskEntityRegistries", createHashMap]; - private _registry = +(_taskEntityRegistries getOrDefault [_registryKey, createHashMap]); - private _entities = +(_registry getOrDefault [_taskID, []]); - _entities pushBackUnique _entity; - _registry set [_taskID, _entities]; - _taskEntityRegistries set [_registryKey, _registry]; - _self set ["taskEntityRegistries", _taskEntityRegistries]; - - true + GVAR(TaskEntityRegistry) call ["registerTaskEntity", _this] }], ["getTaskEntities", compileFinal { - params [["_registryKey", "", [""]], ["_taskID", "", [""]]]; - - if (_registryKey isEqualTo "" || { _taskID isEqualTo "" }) exitWith { [] }; - - private _taskEntityRegistries = _self getOrDefault ["taskEntityRegistries", createHashMap]; - private _registry = _taskEntityRegistries getOrDefault [_registryKey, createHashMap]; - - +(_registry getOrDefault [_taskID, []]) + GVAR(TaskEntityRegistry) call ["getTaskEntities", _this] }], ["findTaskEntityOwner", compileFinal { - params [["_registryKey", "", [""]], ["_entity", objNull, [objNull]]]; - - if (_registryKey isEqualTo "" || { isNull _entity }) exitWith { "" }; - - private _taskEntityRegistries = _self getOrDefault ["taskEntityRegistries", createHashMap]; - private _registry = _taskEntityRegistries getOrDefault [_registryKey, createHashMap]; - private _resolvedTaskID = ""; - - { - private _taskID = _x; - private _entities = _y; - - if (_entity in _entities) exitWith { - _resolvedTaskID = _taskID; - }; - - private _matchingEntity = _entities select { - !isNull _x - && { (typeOf _x) isEqualTo (typeOf _entity) } - && { _x distance _entity < 1 } - }; - if (_matchingEntity isNotEqualTo []) exitWith { - _resolvedTaskID = _taskID; - }; - } forEach _registry; - - _resolvedTaskID + GVAR(TaskEntityRegistry) call ["findTaskEntityOwner", _this] }], ["clearTaskEntities", compileFinal { - params [["_taskID", "", [""]]]; - - if (_taskID isEqualTo "") exitWith { false }; - - private _taskEntityRegistries = _self getOrDefault ["taskEntityRegistries", createHashMap]; - - { - private _registry = +_y; - _registry deleteAt _taskID; - _taskEntityRegistries set [_x, _registry]; - } forEach _taskEntityRegistries; - - _self set ["taskEntityRegistries", _taskEntityRegistries]; - true + GVAR(TaskEntityRegistry) call ["clearTaskEntities", _this] }], ["trackParticipants", compileFinal { - params [["_taskID", "", [""]], ["_entities", [], [[]]], ["_marker", "", [""]], ["_radius", 300, [0]]]; - - if (_taskID isEqualTo "") exitWith { createHashMap }; - - private _participantRegistry = _self getOrDefault ["participantRegistry", createHashMap]; - private _participantSnapshots = +(_participantRegistry getOrDefault [_taskID, createHashMap]); - private _activePlayers = allPlayers select { - alive _x - && { side group _x isEqualTo west } - }; - - if (_marker isNotEqualTo "" && { markerShape _marker in ["RECTANGLE", "ELLIPSE"] }) then { - { - private _uid = getPlayerUID _x; - if (_uid isNotEqualTo "" && { _x inArea _marker }) then { - if !(_uid in _participantSnapshots) then { - _participantSnapshots set [_uid, createHashMapFromArray [ - ["startRating", rating _x] - ]]; - }; - }; - } forEach _activePlayers; - }; - - if (_radius > 0 && { _entities isNotEqualTo [] }) then { - { - private _entity = _x; - if (isNull _entity) then { continue; }; - - { - private _uid = getPlayerUID _x; - if (_uid isNotEqualTo "" && { (_x distance2D _entity) <= _radius }) then { - if !(_uid in _participantSnapshots) then { - _participantSnapshots set [_uid, createHashMapFromArray [ - ["startRating", rating _x] - ]]; - }; - }; - } forEach _activePlayers; - } forEach _entities; - }; - - _participantRegistry set [_taskID, _participantSnapshots]; - _self set ["participantRegistry", _participantRegistry]; - - _participantSnapshots + GVAR(TaskParticipantTracker) call ["trackParticipants", _this] }], ["getTaskParticipants", compileFinal { - params [["_taskID", "", [""]]]; - - if (_taskID isEqualTo "") exitWith { createHashMap }; - - private _participantRegistry = _self getOrDefault ["participantRegistry", createHashMap]; - +(_participantRegistry getOrDefault [_taskID, createHashMap]) + GVAR(TaskParticipantTracker) call ["getTaskParticipants", _this] }], ["getTaskParticipantUids", compileFinal { - params [["_taskID", "", [""]]]; - - if (_taskID isEqualTo "") exitWith { [] }; - - keys (_self call ["getTaskParticipants", [_taskID]]) + GVAR(TaskParticipantTracker) call ["getTaskParticipantUids", _this] }], ["resolveRewardContext", compileFinal { - params [["_taskID", "", [""]]]; - - private _result = createHashMapFromArray [ - ["requesterUid", ""], - ["orgID", ""], - ["memberUids", []] - ]; - - if (_taskID isEqualTo "") exitWith { _result }; - - private _rewardState = _self call ["callTaskState", ["task:ownership:reward_context", [_taskID], createHashMap]]; - if (_rewardState isEqualTo createHashMap) exitWith { _result }; - - private _requesterUid = _rewardState getOrDefault ["requesterUid", ""]; - private _resolvedOrgID = _rewardState getOrDefault ["orgId", ""]; - if (_resolvedOrgID isEqualTo "") exitWith { _result }; - - private _org = EGVAR(org,OrgStore) call ["loadById", [_resolvedOrgID]]; - private _memberUids = []; - if (_org isNotEqualTo createHashMap) then { - private _members = _org getOrDefault ["members", createHashMap]; - if (_members isEqualType createHashMap) then { - _memberUids = keys _members; - }; - if (_requesterUid isNotEqualTo "" && { !(_requesterUid in _memberUids) }) then { - _memberUids pushBack _requesterUid; - }; - }; - - _result set ["requesterUid", _requesterUid]; - _result set ["orgID", _resolvedOrgID]; - _result set ["memberUids", _memberUids]; - _result + GVAR(TaskRewardService) call ["resolveRewardContext", _this] }], ["incrementDefuseCount", compileFinal { params [["_taskID", "", [""]]]; if (_taskID isEqualTo "") exitWith { 0 }; - ["task:defuse:increment", [_taskID]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - - if !_isSuccess exitWith { 0 }; - if !(_result isEqualType "") exitWith { 0 }; - if ((_result find "Error:") == 0) exitWith { - ["ERROR", format ["Task extension call 'task:defuse:increment' failed: %1", _result]] call EFUNC(common,log); - 0 - }; - - parseNumber _result + [GVAR(TaskStateGateway) call ["callTaskState", ["task:defuse:increment", [_taskID], 0]]] params [["_count", 0, [0]]]; + _count }], ["getDefuseCount", compileFinal { params [["_taskID", "", [""]]]; if (_taskID isEqualTo "") exitWith { 0 }; - ["task:defuse:get", [_taskID]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - if !_isSuccess exitWith { 0 }; - if !(_result isEqualType "") exitWith { 0 }; - if ((_result find "Error:") == 0) exitWith { - ["ERROR", format ["Task extension call 'task:defuse:get' failed: %1", _result]] call EFUNC(common,log); - 0 - }; - - parseNumber _result + [GVAR(TaskStateGateway) call ["callTaskState", ["task:defuse:get", [_taskID], 0]]] params [["_count", 0, [0]]]; + _count }], ["notifyParticipants", compileFinal { - params [ - ["_taskID", "", [""]], - ["_type", "info", [""]], - ["_title", "Tasks", [""]], - ["_message", "", [""]] - ]; - - if (_taskID isEqualTo "" || { _message isEqualTo "" }) exitWith { false }; - - private _participantRegistry = _self getOrDefault ["participantRegistry", createHashMap]; - private _participantSnapshots = +(_participantRegistry getOrDefault [_taskID, createHashMap]); - if (_participantSnapshots isEqualTo createHashMap) exitWith { false }; - - private _participantUids = keys _participantSnapshots; - if (_participantUids isEqualTo []) exitWith { false }; - - if (isNil QEGVAR(common,EventBus)) exitWith { - { - private _player = [_x] call EFUNC(common,getPlayer); - if (isNull _player) then { continue; }; - [CRPC(notifications,recieveNotification), [_type, _title, _message], _player] call CFUNC(targetEvent); - } forEach _participantUids; - true - }; - - EGVAR(common,EventBus) call ["emit", [ - "task.notification.requested", - createHashMapFromArray [ - ["taskID", _taskID], - ["notificationType", _type], - ["title", _title], - ["message", _message], - ["participantUids", _participantUids] - ], - createHashMapFromArray [["source", "task"]] - ]]; - - true + GVAR(TaskParticipantTracker) call ["notifyParticipants", _this] }], ["clearTask", compileFinal { params [["_taskID", "", [""]]]; @@ -626,246 +145,14 @@ GVAR(TaskStore) = createHashMapObject [[ _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]]; + GVAR(TaskLifecycleReporter) call ["clearTaskLifecycle", [_taskID]]; + GVAR(TaskParticipantTracker) call ["clearTaskParticipants", [_taskID]]; + GVAR(TaskStateGateway) call ["callTaskState", ["task:clear", [_taskID], false]]; _self call ["clearTaskEntities", [_taskID]]; true }], ["applyRatingOutcome", compileFinal { - params [["_taskID", "", [""]], ["_delta", 0, [0]]]; - - private _emitRatingEvent = { - params [["_eventName", "", [""]], ["_payload", createHashMap, [createHashMap]]]; - - if (_eventName isEqualTo "" || { isNil QEGVAR(common,EventBus) }) exitWith { createHashMap }; - - private _eventPayload = +_payload; - _eventPayload set ["taskID", _taskID]; - _eventPayload set ["ratingDelta", _delta]; - - EGVAR(common,EventBus) call ["emit", [ - _eventName, - _eventPayload, - createHashMapFromArray [["source", "task"]] - ]] - }; - - private _result = createHashMapFromArray [ - ["participantUids", []], - ["orgIds", []], - ["contributions", createHashMap], - ["success", true], - ["mutationFailures", []], - ["persistenceFailures", []], - ["message", ""] - ]; - - if (_taskID isEqualTo "" || { _delta isEqualTo 0 }) exitWith { _result }; - - private _participantRegistry = _self getOrDefault ["participantRegistry", createHashMap]; - private _participantSnapshots = +(_participantRegistry getOrDefault [_taskID, createHashMap]); - if (_participantSnapshots isEqualTo createHashMap) exitWith { _result }; - - private _rewardContext = _self call ["resolveRewardContext", [_taskID]]; - private _participantUids = keys _participantSnapshots; - if (_participantUids isEqualTo [] && { _delta > 0 }) then { - private _requesterUid = _rewardContext getOrDefault ["requesterUid", ""]; - if (_requesterUid isNotEqualTo "") then { - private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer); - if (!isNull _requesterPlayer) then { - _participantUids pushBack _requesterUid; - _participantSnapshots set [_requesterUid, createHashMapFromArray [ - ["startRating", rating _requesterPlayer] - ]]; - _participantRegistry set [_taskID, _participantSnapshots]; - _self set ["participantRegistry", _participantRegistry]; - ["WARNING", format ["Task %1 had no tracked participants at payout time; falling back to requester %2 for personal earnings.", _taskID, _requesterUid]] call EFUNC(common,log); - }; - }; - }; - if (_participantUids isEqualTo []) exitWith { - _result set ["success", false]; - _result set ["message", "No task participants were available for rating outcome."]; - ["task.rating.failed", createHashMapFromArray [ - ["participantUids", []], - ["orgIds", []], - ["contributions", createHashMap], - ["mutationFailures", []], - ["persistenceFailures", []], - ["message", _result get "message"] - ]] call _emitRatingEvent; - _result - }; - - private _orgIds = []; - private _contributions = createHashMap; - private _totalContribution = 0; - private _mutationFailures = []; - private _persistenceFailures = []; - - if (_delta > 0) then { - { - private _uid = _x; - private _player = [_uid] call EFUNC(common,getPlayer); - if (isNull _player) then { continue; }; - - _contributions set [_uid, 1]; - _totalContribution = _totalContribution + 1; - } forEach _participantUids; - }; - - if (_totalContribution <= 0) exitWith { - _result set ["success", false]; - _result set ["message", "No eligible participant contribution was available for rating outcome."]; - ["task.rating.failed", createHashMapFromArray [ - ["participantUids", +_participantUids], - ["orgIds", +_orgIds], - ["contributions", +_contributions], - ["mutationFailures", []], - ["persistenceFailures", []], - ["message", _result get "message"] - ]] call _emitRatingEvent; - _self call ["clearTask", [_taskID]]; - _result - }; - - { - private _uid = _x; - private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_uid, ""]]; - if (_orgID isNotEqualTo "") then { - _orgIds pushBackUnique _orgID; - }; - - if (_delta > 0) then { - private _contribution = _contributions getOrDefault [_uid, 0]; - if (_contribution <= 0) then { continue; }; - - private _account = EGVAR(bank,BankStore) call ["get", [_uid, ""]]; - if (_account isEqualTo createHashMap) then { - _account = EGVAR(bank,BankStore) call ["init", [_uid]]; - }; - - if (_account isNotEqualTo createHashMap) then { - private _earnings = _account getOrDefault ["earnings", 0]; - private _earningsDelta = round ((_delta * _contribution) / _totalContribution); - if (_earningsDelta <= 0) then { continue; }; - - private _patch = EGVAR(bank,BankStore) call [ - "mset", - [ - _uid, - createHashMapFromArray [["earnings", (_earnings + _earningsDelta)]], - false - ] - ]; - if !(_patch isEqualType createHashMap) then { continue; }; - if (_patch isEqualTo createHashMap) then { continue; }; - - if (isNil QEGVAR(common,EventBus)) then { - EGVAR(bank,BankMessenger) call ["sendAccountSync", [_uid, _patch]]; - } else { - EGVAR(common,EventBus) call ["emit", [ - "bank.account.sync.requested", - createHashMapFromArray [ - ["uid", _uid], - ["account", +_patch] - ], - createHashMapFromArray [["source", "task"]] - ]]; - }; - - if ((EGVAR(bank,BankStore) call ["save", [_uid]]) isEqualTo createHashMap) then { - _persistenceFailures pushBackUnique format ["bank:%1", _uid]; - ["ERROR", format ["Task %1 updated bank earnings for %2, but durable save failed.", _taskID, _uid]] call EFUNC(common,log); - }; - }; - }; - } forEach _participantUids; - - private _ownerOrgID = _rewardContext getOrDefault ["orgID", ""]; - if (_ownerOrgID isNotEqualTo "") then { - private _org = EGVAR(org,OrgStore) call ["loadById", [_ownerOrgID]]; - - if (_org isNotEqualTo createHashMap) then { - private _reputation = _org getOrDefault ["reputation", 0]; - private _nextReputation = round (_reputation + _delta); - _org set ["reputation", _nextReputation]; - private _updatedOrg = EGVAR(org,OrgStore) call [ - "callHotOrg", - [ - "org:hot:override", - [_ownerOrgID, toJSON _org] - ] - ]; - - if (_updatedOrg isNotEqualTo createHashMap) then { - private _patch = createHashMapFromArray [["reputation", _nextReputation]]; - private _memberUids = _rewardContext getOrDefault ["memberUids", []]; - if (isNil QEGVAR(common,EventBus)) then { - { - private _player = [_x] call EFUNC(common,getPlayer); - if (isNull _player) then { continue; }; - [CRPC(org,responseSyncOrg), [_patch], _player] call CFUNC(targetEvent); - } forEach _memberUids; - } else { - EGVAR(common,EventBus) call ["emit", [ - "org.sync.requested", - createHashMapFromArray [ - ["orgID", _ownerOrgID], - ["memberUids", +_memberUids], - ["patch", +_patch] - ], - createHashMapFromArray [["source", "task"]] - ]]; - }; - - _orgIds = [_ownerOrgID]; - if ((EGVAR(org,OrgStore) call ["saveById", [_ownerOrgID]]) isEqualTo createHashMap) then { - _persistenceFailures pushBackUnique format ["organization:%1", _ownerOrgID]; - ["ERROR", format ["Task %1 updated reputation for organization %2, but durable save failed.", _taskID, _ownerOrgID]] call EFUNC(common,log); - }; - } else { - ["ERROR", format ["Failed to update organization %1 reputation for task %2.", _ownerOrgID, _taskID]] call EFUNC(common,log); - _mutationFailures pushBackUnique format ["organization:%1", _ownerOrgID]; - }; - }; - }; - - _result set ["participantUids", _participantUids]; - _result set ["orgIds", _orgIds]; - _result set ["contributions", _contributions]; - _result set ["success", (_mutationFailures isEqualTo []) && { _persistenceFailures isEqualTo [] }]; - _result set ["mutationFailures", _mutationFailures]; - _result set ["persistenceFailures", _persistenceFailures]; - if (_mutationFailures isNotEqualTo [] || { _persistenceFailures isNotEqualTo [] }) then { - private _messageParts = []; - if (_mutationFailures isNotEqualTo []) then { - _messageParts pushBack format ["mutation failures: %1", _mutationFailures joinString ", "]; - }; - if (_persistenceFailures isNotEqualTo []) then { - _messageParts pushBack format ["persistence failures: %1", _persistenceFailures joinString ", "]; - }; - _result set ["message", _messageParts joinString "; "]; - }; - - private _eventName = ["task.rating.failed", "task.rating.applied"] select (_result getOrDefault ["success", false]); - [_eventName, createHashMapFromArray [ - ["participantUids", +(_result getOrDefault ["participantUids", []])], - ["orgIds", +(_result getOrDefault ["orgIds", []])], - ["contributions", +(_result getOrDefault ["contributions", createHashMap])], - ["mutationFailures", +(_result getOrDefault ["mutationFailures", []])], - ["persistenceFailures", +(_result getOrDefault ["persistenceFailures", []])], - ["message", _result getOrDefault ["message", ""]] - ]] call _emitRatingEvent; - - _result + GVAR(TaskRewardService) call ["applyRatingOutcome", _this] }] ]]; diff --git a/arma/server/addons/task/functions/helpers/fnc_parseRewards.sqf b/arma/server/addons/task/functions/helpers/fnc_parseRewards.sqf index 7650cec..c87c9fe 100644 --- a/arma/server/addons/task/functions/helpers/fnc_parseRewards.sqf +++ b/arma/server/addons/task/functions/helpers/fnc_parseRewards.sqf @@ -2,7 +2,12 @@ /* * Author: OpenAI - * Parses an Eden module reward-array string into a SQF array. + * Parses an Eden module reward string into a SQF array. + * + * Supports both the preferred comma-separated format: + * ItemGPS, FirstAidKit + * and the legacy SQF array string format: + * ["ItemGPS","FirstAidKit"] * * Arguments: * 0: Raw value @@ -13,28 +18,35 @@ * Parsed reward array * * Example: - * [_logic getVariable ["EquipmentRewards", "[]"], "attack_01", "equipment"] call forge_server_task_fnc_parseRewards; + * [_logic getVariable ["EquipmentRewards", ""], "attack_01", "equipment"] call forge_server_task_fnc_parseRewards; * * Public: No */ -params [ - ["_rawValue", "[]", [""]], - ["_taskLabel", "", [""]], - ["_rewardKey", "", [""]] -]; +params [["_rawValue", "", [""]], ["_taskLabel", "", [""]], ["_rewardKey", "", [""]]]; private _trimmed = trim _rawValue; if (_trimmed isEqualTo "") exitWith { [] }; +if ((_trimmed select [0, 1]) isEqualTo "[") then { + private _parsed = parseSimpleArray _trimmed; + if (_parsed isEqualType []) exitWith { _parsed }; -private _parsed = parseSimpleArray _trimmed; -if (_parsed isEqualType []) exitWith { _parsed }; + ["WARNING", format [ + "Task module '%1' reward input '%2' is invalid: %3. Expected comma-separated class names like ItemGPS, FirstAidKit or SQF array syntax like [""ItemGPS"",""FirstAidKit""].", + _taskLabel, + _rewardKey, + _rawValue + ]] call EFUNC(common,log); -["WARNING", format [ - "Task module '%1' reward input '%2' is invalid: %3. Expected SQF array syntax like [""ItemGPS"",""FirstAidKit""].", - _taskLabel, - _rewardKey, - _rawValue -]] call EFUNC(common,log); + [] +}; -[] +private _parsedRewards = []; +{ + private _reward = trim _x; + if (_reward isEqualTo "") then { continue; }; + + _parsedRewards pushBackUnique _reward; +} forEach (_trimmed splitString ","); + +_parsedRewards diff --git a/arma/server/addons/task/functions/helpers/fnc_parseTaskChainAttributes.sqf b/arma/server/addons/task/functions/helpers/fnc_parseTaskChainAttributes.sqf new file mode 100644 index 0000000..f15f732 --- /dev/null +++ b/arma/server/addons/task/functions/helpers/fnc_parseTaskChainAttributes.sqf @@ -0,0 +1,38 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Reads shared Eden task chain attributes and returns startTask parameter pairs. + * + * Arguments: + * 0: Logic + * + * Return Value: + * Task parameter pairs + * + * Public: No + */ + +params [["_logic", objNull, [objNull]]]; + +private _prerequisiteRaw = _logic getVariable ["PrerequisiteTaskIds", ""]; +private _prerequisiteTaskIds = []; + +if (_prerequisiteRaw isEqualType []) then { + { + if !(_x isEqualType "") then { continue; }; + if (_x isEqualTo "") then { continue; }; + _prerequisiteTaskIds pushBackUnique _x; + } forEach _prerequisiteRaw; +} else { + if (_prerequisiteRaw isEqualType "") then { + { + if (_x isEqualTo "") then { continue; }; + _prerequisiteTaskIds pushBackUnique _x; + } forEach (_prerequisiteRaw splitString ", "); + }; +}; + +[ + ["prerequisiteTaskIds", _prerequisiteTaskIds] +] diff --git a/arma/server/addons/task/functions/helpers/fnc_startTask.sqf b/arma/server/addons/task/functions/helpers/fnc_startTask.sqf index cdd9825..9640383 100644 --- a/arma/server/addons/task/functions/helpers/fnc_startTask.sqf +++ b/arma/server/addons/task/functions/helpers/fnc_startTask.sqf @@ -24,6 +24,7 @@ * Common keys: * "limitFail" (default: -1) * "limitSuccess" (default: -1) + * "prerequisiteTaskIds" (default: []) -- task IDs that must succeed before this task is available * "funds" (default: 0) * "ratingFail" (default: 0) * "ratingSuccess" (default: 0) @@ -123,7 +124,17 @@ private _iedTimer = _taskParams getOrDefault ["iedTimer", 0]; // --- 3. Register catalog entry --- +private _prerequisiteTaskIds = _taskParams getOrDefault [ + "prerequisiteTaskIds", + _taskParams getOrDefault [ + "prerequisiteTaskIDs", + _taskParams getOrDefault ["requiresTaskIds", []] + ] +]; + GVAR(TaskStore) call ["registerTaskCatalogEntry", [_taskID, createHashMapFromArray [ + ["taskID", _taskID], + ["taskId", _taskID], ["type", _taskType], ["title", _title], ["description", _description], @@ -131,7 +142,8 @@ GVAR(TaskStore) call ["registerTaskCatalogEntry", [_taskID, createHashMapFromArr ["accepted", false], ["requesterUid", _requesterUid], ["orgID", "default"], - ["source", _source] + ["source", _source], + ["prerequisiteTaskIds", _prerequisiteTaskIds] ]]]; // --- 4. Assemble type-specific handler args --- diff --git a/arma/server/addons/task/functions/modules/fnc_attackModule.sqf b/arma/server/addons/task/functions/modules/fnc_attackModule.sqf index bd2a0e1..aa57225 100644 --- a/arma/server/addons/task/functions/modules/fnc_attackModule.sqf +++ b/arma/server/addons/task/functions/modules/fnc_attackModule.sqf @@ -41,6 +41,7 @@ private _supplyRewards = [_logic getVariable ["SupplyRewards", "[]"], _taskID, " private _weaponRewards = [_logic getVariable ["WeaponRewards", "[]"], _taskID, "weapons"] call FUNC(parseRewards); private _vehicleRewards = [_logic getVariable ["VehicleRewards", "[]"], _taskID, "vehicles"] call FUNC(parseRewards); private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, "special"] call FUNC(parseRewards); +private _taskChainParams = [_logic] call FUNC(parseTaskChainAttributes); [ "attack", @@ -51,7 +52,7 @@ private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, createHashMapFromArray [ ["targets", _syncedEntities] ], - createHashMapFromArray [ + createHashMapFromArray ([ ["limitFail", _logic getVariable ["LimitFail", -1]], ["limitSuccess", _logic getVariable ["LimitSuccess", -1]], ["funds", _logic getVariable ["CompanyFunds", 0]], @@ -65,7 +66,7 @@ private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, ["weapons", _weaponRewards], ["vehicles", _vehicleRewards], ["special", _specialRewards] - ] + ] + _taskChainParams) ] call FUNC(startTask); deleteVehicle _logic; diff --git a/arma/server/addons/task/functions/modules/fnc_defendModule.sqf b/arma/server/addons/task/functions/modules/fnc_defendModule.sqf index bdd074a..a61eee9 100644 --- a/arma/server/addons/task/functions/modules/fnc_defendModule.sqf +++ b/arma/server/addons/task/functions/modules/fnc_defendModule.sqf @@ -84,6 +84,7 @@ private _supplyRewards = [_logic getVariable ["SupplyRewards", "[]"], _taskID, " private _weaponRewards = [_logic getVariable ["WeaponRewards", "[]"], _taskID, "weapons"] call FUNC(parseRewards); private _vehicleRewards = [_logic getVariable ["VehicleRewards", "[]"], _taskID, "vehicles"] call FUNC(parseRewards); private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, "special"] call FUNC(parseRewards); +private _taskChainParams = [_logic] call FUNC(parseTaskChainAttributes); [ "defend", @@ -92,7 +93,7 @@ private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, format ["Defend: %1", _taskID], "Hold the defense zone against incoming enemy forces.", createHashMap, - createHashMapFromArray [ + createHashMapFromArray ([ ["funds", _logic getVariable ["CompanyFunds", 0]], ["ratingFail", _logic getVariable ["RatingFail", 0]], ["ratingSuccess", _logic getVariable ["RatingSuccess", 0]], @@ -109,7 +110,7 @@ private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, ["weapons", _weaponRewards], ["vehicles", _vehicleRewards], ["special", _specialRewards] - ] + ] + _taskChainParams) ] call FUNC(startTask); deleteVehicle _logic; diff --git a/arma/server/addons/task/functions/modules/fnc_defuseModule.sqf b/arma/server/addons/task/functions/modules/fnc_defuseModule.sqf index 25359bb..b1f1f1c 100644 --- a/arma/server/addons/task/functions/modules/fnc_defuseModule.sqf +++ b/arma/server/addons/task/functions/modules/fnc_defuseModule.sqf @@ -58,6 +58,7 @@ private _supplyRewards = [_logic getVariable ["SupplyRewards", "[]"], _taskID, " private _weaponRewards = [_logic getVariable ["WeaponRewards", "[]"], _taskID, "weapons"] call FUNC(parseRewards); private _vehicleRewards = [_logic getVariable ["VehicleRewards", "[]"], _taskID, "vehicles"] call FUNC(parseRewards); private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, "special"] call FUNC(parseRewards); +private _taskChainParams = [_logic] call FUNC(parseTaskChainAttributes); [ "defuse", @@ -69,7 +70,7 @@ private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, ["ieds", _iedEntities], ["protected", _protectedEntities] ], - createHashMapFromArray [ + createHashMapFromArray ([ ["limitFail", _logic getVariable ["LimitFail", -1]], ["limitSuccess", _logic getVariable ["LimitSuccess", -1]], ["funds", _logic getVariable ["CompanyFunds", 0]], @@ -83,7 +84,7 @@ private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, ["weapons", _weaponRewards], ["vehicles", _vehicleRewards], ["special", _specialRewards] - ] + ] + _taskChainParams) ] call FUNC(startTask); deleteVehicle _logic; diff --git a/arma/server/addons/task/functions/modules/fnc_deliveryModule.sqf b/arma/server/addons/task/functions/modules/fnc_deliveryModule.sqf index 1a7fb0a..3528638 100644 --- a/arma/server/addons/task/functions/modules/fnc_deliveryModule.sqf +++ b/arma/server/addons/task/functions/modules/fnc_deliveryModule.sqf @@ -51,6 +51,7 @@ private _supplyRewards = [_logic getVariable ["SupplyRewards", "[]"], _taskID, " private _weaponRewards = [_logic getVariable ["WeaponRewards", "[]"], _taskID, "weapons"] call FUNC(parseRewards); private _vehicleRewards = [_logic getVariable ["VehicleRewards", "[]"], _taskID, "vehicles"] call FUNC(parseRewards); private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, "special"] call FUNC(parseRewards); +private _taskChainParams = [_logic] call FUNC(parseTaskChainAttributes); [ "delivery", @@ -61,7 +62,7 @@ private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, createHashMapFromArray [ ["cargo", _cargoEntities] ], - createHashMapFromArray [ + createHashMapFromArray ([ ["limitFail", _logic getVariable ["LimitFail", -1]], ["limitSuccess", _logic getVariable ["LimitSuccess", -1]], ["funds", _logic getVariable ["CompanyFunds", 0]], @@ -76,7 +77,7 @@ private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, ["weapons", _weaponRewards], ["vehicles", _vehicleRewards], ["special", _specialRewards] - ] + ] + _taskChainParams) ] call FUNC(startTask); deleteVehicle _logic; diff --git a/arma/server/addons/task/functions/modules/fnc_destroyModule.sqf b/arma/server/addons/task/functions/modules/fnc_destroyModule.sqf index 82b6add..46f6c88 100644 --- a/arma/server/addons/task/functions/modules/fnc_destroyModule.sqf +++ b/arma/server/addons/task/functions/modules/fnc_destroyModule.sqf @@ -41,6 +41,7 @@ private _supplyRewards = [_logic getVariable ["SupplyRewards", "[]"], _taskID, " private _weaponRewards = [_logic getVariable ["WeaponRewards", "[]"], _taskID, "weapons"] call FUNC(parseRewards); private _vehicleRewards = [_logic getVariable ["VehicleRewards", "[]"], _taskID, "vehicles"] call FUNC(parseRewards); private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, "special"] call FUNC(parseRewards); +private _taskChainParams = [_logic] call FUNC(parseTaskChainAttributes); [ "destroy", @@ -51,7 +52,7 @@ private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, createHashMapFromArray [ ["targets", _syncedEntities] ], - createHashMapFromArray [ + createHashMapFromArray ([ ["limitFail", _logic getVariable ["LimitFail", -1]], ["limitSuccess", _logic getVariable ["LimitSuccess", -1]], ["funds", _logic getVariable ["CompanyFunds", 0]], @@ -65,7 +66,7 @@ private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, ["weapons", _weaponRewards], ["vehicles", _vehicleRewards], ["special", _specialRewards] - ] + ] + _taskChainParams) ] call FUNC(startTask); deleteVehicle _logic; diff --git a/arma/server/addons/task/functions/modules/fnc_hostageModule.sqf b/arma/server/addons/task/functions/modules/fnc_hostageModule.sqf index 03e3170..46389fc 100644 --- a/arma/server/addons/task/functions/modules/fnc_hostageModule.sqf +++ b/arma/server/addons/task/functions/modules/fnc_hostageModule.sqf @@ -66,6 +66,7 @@ private _supplyRewards = [_logic getVariable ["SupplyRewards", "[]"], _taskID, " private _weaponRewards = [_logic getVariable ["WeaponRewards", "[]"], _taskID, "weapons"] call FUNC(parseRewards); private _vehicleRewards = [_logic getVariable ["VehicleRewards", "[]"], _taskID, "vehicles"] call FUNC(parseRewards); private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, "special"] call FUNC(parseRewards); +private _taskChainParams = [_logic] call FUNC(parseTaskChainAttributes); [ "hostage", @@ -77,7 +78,7 @@ private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, ["hostages", _hostageEntities], ["shooters", _shooterEntities] ], - createHashMapFromArray [ + createHashMapFromArray ([ ["limitFail", _logic getVariable ["LimitFail", -1]], ["limitSuccess", _logic getVariable ["LimitSuccess", -1]], ["funds", _logic getVariable ["CompanyFunds", 0]], @@ -95,7 +96,7 @@ private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, ["weapons", _weaponRewards], ["vehicles", _vehicleRewards], ["special", _specialRewards] - ] + ] + _taskChainParams) ] call FUNC(startTask); deleteVehicle _logic; diff --git a/arma/server/addons/task/functions/modules/fnc_hvtModule.sqf b/arma/server/addons/task/functions/modules/fnc_hvtModule.sqf index 2a378fb..77d220e 100644 --- a/arma/server/addons/task/functions/modules/fnc_hvtModule.sqf +++ b/arma/server/addons/task/functions/modules/fnc_hvtModule.sqf @@ -47,6 +47,7 @@ private _supplyRewards = [_logic getVariable ["SupplyRewards", "[]"], _taskID, " private _weaponRewards = [_logic getVariable ["WeaponRewards", "[]"], _taskID, "weapons"] call FUNC(parseRewards); private _vehicleRewards = [_logic getVariable ["VehicleRewards", "[]"], _taskID, "vehicles"] call FUNC(parseRewards); private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, "special"] call FUNC(parseRewards); +private _taskChainParams = [_logic] call FUNC(parseTaskChainAttributes); [ "hvt", @@ -57,7 +58,7 @@ private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, createHashMapFromArray [ ["hvts", _syncedEntities] ], - createHashMapFromArray [ + createHashMapFromArray ([ ["limitFail", _logic getVariable ["LimitFail", -1]], ["limitSuccess", _logic getVariable ["LimitSuccess", -1]], ["funds", _logic getVariable ["CompanyFunds", 0]], @@ -73,7 +74,7 @@ private _specialRewards = [_logic getVariable ["SpecialRewards", "[]"], _taskID, ["weapons", _weaponRewards], ["vehicles", _vehicleRewards], ["special", _specialRewards] - ] + ] + _taskChainParams) ] call FUNC(startTask); deleteVehicle _logic; diff --git a/arma/server/addons/task/functions/objects/fnc_TaskCatalogStore.sqf b/arma/server/addons/task/functions/objects/fnc_TaskCatalogStore.sqf new file mode 100644 index 0000000..fb9eefb --- /dev/null +++ b/arma/server/addons/task/functions/objects/fnc_TaskCatalogStore.sqf @@ -0,0 +1,356 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Catalog/status/chaining store for task metadata. + * + * TaskStore keeps the public facade used by the rest of the task system. This + * object owns catalog persistence calls, active status, acceptance, and chained + * task availability. + * + * Arguments: + * None + * + * Return Value: + * Task catalog store object + * + * Public: No + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(TaskCatalogStore) = createHashMapObject [[ + ["#type", "TaskCatalogStore"], + ["#create", compileFinal { + _self call ["resetRuntimeState", []]; + }], + ["resetRuntimeState", compileFinal { + _self set ["completedTaskRegistry", createHashMap]; + _self set ["taskDependencyRegistry", createHashMap]; + true + }], + ["bindTaskOwnership", compileFinal { + params [["_taskID", "", [""]], ["_requesterUid", "", [""]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["requesterUid", _requesterUid], + ["orgID", "default"], + ["message", ""] + ]; + + if (_taskID isEqualTo "") exitWith { + _result set ["message", "Missing task ID."]; + _result + }; + + private _orgID = "default"; + + if (_requesterUid isNotEqualTo "") then { + private _actor = EGVAR(actor,ActorStore) call ["load", [_requesterUid]]; + + if (_actor isEqualTo createHashMap) exitWith { + _result set ["message", format ["Failed to load actor for %1.", _requesterUid]]; + _result + }; + + _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]]; + }; + + private _context = createHashMapFromArray [ + ["requesterUid", _requesterUid], + ["orgId", _orgID] + ]; + private _envelope = GVAR(TaskStateGateway) call [ + "callTaskStateEnvelope", + [ + "task:ownership:bind", + [_taskID, toJSON _context] + ] + ]; + if !(_envelope getOrDefault ["success", false]) exitWith { + _result set ["message", _envelope getOrDefault ["error", "Failed to bind task ownership."]]; + _result + }; + + private _bindResult = _envelope getOrDefault ["data", createHashMap]; + _result set ["success", true]; + _result set ["message", _bindResult getOrDefault [ + "message", + ["No requester UID provided. Bound task to default organization.", "Task ownership updated."] select (_requesterUid isNotEqualTo "") + ]]; + _result set ["orgID", _bindResult getOrDefault ["orgId", _orgID]]; + _result + }], + ["releaseTaskOwnership", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + private _envelope = GVAR(TaskStateGateway) call ["callTaskStateEnvelope", ["task:ownership:release", [_taskID]]]; + _envelope getOrDefault ["success", false] + }], + ["normalizePrerequisiteTaskIds", compileFinal { + params [["_value", [], [[], ""]]]; + + if (_value isEqualType "") then { _value = [_value]; }; + if !(_value isEqualType []) exitWith { [] }; + + private _taskIDs = []; + { + if !(_x isEqualType "") then { continue; }; + if (_x isEqualTo "") then { continue; }; + _taskIDs pushBackUnique _x; + } forEach _value; + + _taskIDs + }], + ["getTaskPrerequisites", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { [] }; + + private _dependencyRegistry = _self getOrDefault ["taskDependencyRegistry", createHashMap]; + +(_dependencyRegistry getOrDefault [_taskID, []]) + }], + ["isTaskCompleted", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + private _completedRegistry = _self getOrDefault ["completedTaskRegistry", createHashMap]; + if (_completedRegistry getOrDefault [_taskID, false]) exitWith { true }; + + (_self call ["getTaskStatus", [_taskID]]) isEqualTo "succeeded" + }], + ["areTaskPrerequisitesSatisfied", compileFinal { + params [["_taskID", "", [""]], ["_entry", createHashMap, [createHashMap]]]; + + private _prerequisites = _self call ["getTaskPrerequisites", [_taskID]]; + if (_prerequisites isEqualTo [] && { _entry isNotEqualTo createHashMap }) then { + _prerequisites = _self call ["normalizePrerequisiteTaskIds", [_entry getOrDefault ["prerequisiteTaskIds", []]]]; + }; + if (_prerequisites isEqualTo []) exitWith { true }; + + private _satisfied = true; + { + if !(_self call ["isTaskCompleted", [_x]]) exitWith { _satisfied = false; }; + } forEach _prerequisites; + + _satisfied + }], + ["resolveInitialTaskStatus", compileFinal { + params [["_taskID", "", [""]], ["_entry", createHashMap, [createHashMap]]]; + + if (_self call ["areTaskPrerequisitesSatisfied", [_taskID, _entry]]) exitWith { "available" }; + + "locked" + }], + ["markTaskCompleted", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + private _completedRegistry = _self getOrDefault ["completedTaskRegistry", createHashMap]; + _completedRegistry set [_taskID, true]; + _self set ["completedTaskRegistry", _completedRegistry]; + true + }], + ["unlockDependentTasks", compileFinal { + params [["_completedTaskID", "", [""]]]; + + private _dependencyRegistry = _self getOrDefault ["taskDependencyRegistry", createHashMap]; + { + private _dependentTaskID = _x; + private _prerequisites = _y; + + if !(_completedTaskID in _prerequisites) then { continue; }; + if ((_self call ["getTaskStatus", [_dependentTaskID]]) isNotEqualTo "locked") then { continue; }; + if !(_self call ["areTaskPrerequisitesSatisfied", [_dependentTaskID]]) then { continue; }; + + _self call ["setTaskStatus", [_dependentTaskID, "available"]]; + ["INFO", format ["Unlocked chained task '%1' after prerequisite '%2' completed.", _dependentTaskID, _completedTaskID]] call EFUNC(common,log); + } forEach _dependencyRegistry; + + true + }], + ["registerTaskCatalogEntry", compileFinal { + params [["_taskID", "", [""]], ["_entry", createHashMap, [createHashMap]]]; + + if (_taskID isEqualTo "" || { _entry isEqualTo createHashMap }) exitWith { false }; + + _entry set ["taskID", _taskID]; + _entry set ["taskId", _taskID]; + + private _prerequisiteTaskIds = _self call ["normalizePrerequisiteTaskIds", [_entry getOrDefault ["prerequisiteTaskIds", []]]]; + _entry set ["prerequisiteTaskIds", _prerequisiteTaskIds]; + + private _dependencyRegistry = _self getOrDefault ["taskDependencyRegistry", createHashMap]; + if (_prerequisiteTaskIds isEqualTo []) then { + _dependencyRegistry deleteAt _taskID; + } else { + _dependencyRegistry set [_taskID, _prerequisiteTaskIds]; + }; + _self set ["taskDependencyRegistry", _dependencyRegistry]; + + private _initialStatus = ["available", "locked"] select !(_self call ["areTaskPrerequisitesSatisfied", [_taskID, _entry]]); + _entry set ["locked", _initialStatus isEqualTo "locked"]; + + private _envelope = GVAR(TaskStateGateway) call [ + "callTaskStateEnvelope", + [ + "task:catalog:upsert", + [_taskID, toJSON _entry] + ] + ]; + private _registered = _envelope getOrDefault ["success", false]; + + if (_registered) then { + GVAR(TaskLifecycleReporter) call ["recordTaskCreated", [_taskID]]; + GVAR(TaskLifecycleReporter) call ["emitTaskLifecycleEvent", ["task.created", _taskID, "created", createHashMap]]; + _self call ["setTaskStatus", [_taskID, _initialStatus]]; + }; + + _registered + }], + ["getActiveTaskCatalog", compileFinal { + private _entries = GVAR(TaskStateGateway) call ["callTaskState", ["task:catalog:active", [], []]]; + if !(_entries isEqualType []) exitWith { [] }; + + private _visibleEntries = []; + { + if !(_x isEqualType createHashMap) then { continue; }; + + private _taskID = _x getOrDefault ["taskID", _x getOrDefault ["taskId", ""]]; + if (_taskID isEqualTo "") then { continue; }; + + private _status = _self call ["getTaskStatus", [_taskID]]; + if !(_status in ["available", "assigned", "active"]) then { continue; }; + if !(_self call ["areTaskPrerequisitesSatisfied", [_taskID, _x]]) then { continue; }; + + _visibleEntries pushBack _x; + } forEach _entries; + + _visibleEntries + }], + ["hasTaskCatalogEntry", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + private _entry = GVAR(TaskStateGateway) call ["callTaskState", ["task:catalog:get", [_taskID], objNull]]; + _entry isEqualType createHashMap + }], + ["getTaskCatalogEntry", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { createHashMap }; + + [(GVAR(TaskStateGateway) call ["callTaskState", ["task:catalog:get", [_taskID], createHashMap]])] params [["_entry", createHashMap, [createHashMap]]]; + if !(_entry isEqualType createHashMap) exitWith { createHashMap }; + + _entry + }], + ["isTaskAccepted", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + [(_self call ["getTaskCatalogEntry", [_taskID]])] params [["_entry", createHashMap, [createHashMap]]]; + if (_entry isEqualTo createHashMap) exitWith { false }; + + [(_entry getOrDefault ["accepted", false])] params [["_accepted", false, [false]]]; + [(_entry getOrDefault ["requesterUid", ""])] params [["_requesterUid", "", [""]]]; + + _accepted || { _requesterUid isNotEqualTo "" } + }], + ["acceptTask", compileFinal { + params [["_taskID", "", [""]], ["_requesterUid", "", [""]]]; + + private _result = createHashMapFromArray [ + ["success", false], + ["message", "Unable to accept task."], + ["entry", createHashMap] + ]; + + if (_taskID isEqualTo "" || { _requesterUid isEqualTo "" }) exitWith { + _result set ["message", "Missing task ID or requester UID."]; + _result + }; + + private _actor = EGVAR(actor,ActorStore) call ["load", [_requesterUid]]; + if (_actor isEqualTo createHashMap) exitWith { + _result set ["message", format ["Failed to load actor for %1.", _requesterUid]]; + _result + }; + + private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]]; + + private _context = createHashMapFromArray [ + ["requesterUid", _requesterUid], + ["orgId", _orgID] + ]; + private _envelope = GVAR(TaskStateGateway) call [ + "callTaskStateEnvelope", + [ + "task:ownership:accept", + [_taskID, toJSON _context] + ] + ]; + if !(_envelope getOrDefault ["success", false]) exitWith { + _result set ["message", _envelope getOrDefault ["error", "Unable to accept task."]]; + _result + }; + + private _acceptResult = _envelope getOrDefault ["data", createHashMap]; + private _entry = _acceptResult getOrDefault ["entry", createHashMap]; + if !(_entry isEqualType createHashMap) then { _entry = createHashMap; }; + + _result set ["success", true]; + _result set ["message", _acceptResult getOrDefault ["message", "Task accepted."]]; + _result set ["entry", _entry]; + _result + }], + ["setTaskStatus", compileFinal { + params [["_taskID", "", [""]], ["_status", "", [""]]]; + + if (_taskID isEqualTo "" || { _status isEqualTo "" }) exitWith { false }; + + private _envelope = GVAR(TaskStateGateway) call ["callTaskStateEnvelope", ["task:status:set", [_taskID, _status]]]; + private _statusResult = _envelope getOrDefault ["success", false]; + + if (_statusResult) then { + private _normalizedStatus = toLowerANSI _status; + private _eventName = GVAR(TaskLifecycleReporter) call ["recordTaskStatus", [_taskID, _normalizedStatus]]; + + if (_eventName isNotEqualTo "") then { + GVAR(TaskLifecycleReporter) call ["emitTaskLifecycleEvent", [_eventName, _taskID, _normalizedStatus, createHashMap]]; + }; + + if (_normalizedStatus isEqualTo "succeeded") then { + _self call ["markTaskCompleted", [_taskID]]; + _self call ["unlockDependentTasks", [_taskID]]; + }; + }; + + _statusResult + }], + ["getTaskStatus", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { "" }; + + [(GVAR(TaskStateGateway) call ["callTaskState", ["task:status:get", [_taskID], ""]])] params [["_status", "", [""]]]; + _status + }], + ["clearTaskStatus", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + [(GVAR(TaskStateGateway) call ["callTaskState", ["task:status:clear", [_taskID], false]])] params [["_statusResult", false, [false]]]; + + _statusResult + }] +]]; + +GVAR(TaskCatalogStore) diff --git a/arma/server/addons/task/functions/objects/fnc_TaskEntityRegistry.sqf b/arma/server/addons/task/functions/objects/fnc_TaskEntityRegistry.sqf new file mode 100644 index 0000000..0653a11 --- /dev/null +++ b/arma/server/addons/task/functions/objects/fnc_TaskEntityRegistry.sqf @@ -0,0 +1,106 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Runtime entity registry for task-owned Arma objects. + * + * Stores object references by registry key and task ID. TaskStore remains the + * public facade, while this object owns entity storage and lookup behavior. + * + * Arguments: + * None + * + * Return Value: + * Task entity registry object + * + * Public: No + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(TaskEntityRegistry) = createHashMapObject [[ + ["#type", "TaskEntityRegistry"], + ["#create", compileFinal { + _self call ["resetRuntimeState", []]; + }], + ["resetRuntimeState", compileFinal { + _self set ["taskEntityRegistries", createHashMapFromArray [ + ["cargo", createHashMap], + ["hostages", createHashMap], + ["hvts", createHashMap], + ["ieds", createHashMap], + ["entities", createHashMap], + ["shooters", createHashMap], + ["targets", createHashMap] + ]]; + true + }], + ["registerTaskEntity", compileFinal { + params [["_registryKey", "", [""]], ["_taskID", "", [""]], ["_entity", objNull, [objNull]]]; + + if (_registryKey isEqualTo "" || { _taskID isEqualTo "" } || { isNull _entity }) exitWith { false }; + + private _taskEntityRegistries = _self getOrDefault ["taskEntityRegistries", createHashMap]; + private _registry = +(_taskEntityRegistries getOrDefault [_registryKey, createHashMap]); + private _entities = +(_registry getOrDefault [_taskID, []]); + _entities pushBackUnique _entity; + _registry set [_taskID, _entities]; + _taskEntityRegistries set [_registryKey, _registry]; + _self set ["taskEntityRegistries", _taskEntityRegistries]; + + true + }], + ["getTaskEntities", compileFinal { + params [["_registryKey", "", [""]], ["_taskID", "", [""]]]; + + if (_registryKey isEqualTo "" || { _taskID isEqualTo "" }) exitWith { [] }; + + private _taskEntityRegistries = _self getOrDefault ["taskEntityRegistries", createHashMap]; + private _registry = _taskEntityRegistries getOrDefault [_registryKey, createHashMap]; + + +(_registry getOrDefault [_taskID, []]) + }], + ["findTaskEntityOwner", compileFinal { + params [["_registryKey", "", [""]], ["_entity", objNull, [objNull]]]; + + if (_registryKey isEqualTo "" || { isNull _entity }) exitWith { "" }; + + private _taskEntityRegistries = _self getOrDefault ["taskEntityRegistries", createHashMap]; + private _registry = _taskEntityRegistries getOrDefault [_registryKey, createHashMap]; + private _resolvedTaskID = ""; + + { + private _taskID = _x; + private _entities = _y; + + if (_entity in _entities) exitWith { _resolvedTaskID = _taskID; }; + + private _matchingEntity = _entities select { + !isNull _x + && { (typeOf _x) isEqualTo (typeOf _entity) } + && { _x distance _entity < 1 } + }; + + if (_matchingEntity isNotEqualTo []) exitWith { _resolvedTaskID = _taskID; }; + } forEach _registry; + + _resolvedTaskID + }], + ["clearTaskEntities", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + private _taskEntityRegistries = _self getOrDefault ["taskEntityRegistries", createHashMap]; + + { + private _registry = +_y; + _registry deleteAt _taskID; + _taskEntityRegistries set [_x, _registry]; + } forEach _taskEntityRegistries; + + _self set ["taskEntityRegistries", _taskEntityRegistries]; + true + }] +]]; + +GVAR(TaskEntityRegistry) diff --git a/arma/server/addons/task/functions/objects/fnc_TaskLifecycleReporter.sqf b/arma/server/addons/task/functions/objects/fnc_TaskLifecycleReporter.sqf new file mode 100644 index 0000000..e11a795 --- /dev/null +++ b/arma/server/addons/task/functions/objects/fnc_TaskLifecycleReporter.sqf @@ -0,0 +1,128 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Task lifecycle timestamp tracking and event reporting. + * + * Owns task lifecycle timestamps and emits task lifecycle events through the + * common event bus. TaskStore remains the public facade. + * + * Arguments: + * None + * + * Return Value: + * Task lifecycle reporter object + * + * Public: No + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(TaskLifecycleReporter) = createHashMapObject [[ + ["#type", "TaskLifecycleReporter"], + ["#create", compileFinal { + _self call ["resetRuntimeState", []]; + }], + ["resetRuntimeState", compileFinal { + _self set ["taskLifecycleRegistry", createHashMap]; + true + }], + ["recordTaskCreated", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + private _lifecycleRegistry = _self getOrDefault ["taskLifecycleRegistry", createHashMap]; + private _lifecycle = +(_lifecycleRegistry getOrDefault [_taskID, createHashMap]); + _lifecycle set ["createdAt", serverTime]; + _lifecycleRegistry set [_taskID, _lifecycle]; + _self set ["taskLifecycleRegistry", _lifecycleRegistry]; + true + }], + ["recordTaskStatus", compileFinal { + params [["_taskID", "", [""]], ["_status", "", [""]]]; + + if (_taskID isEqualTo "" || { _status isEqualTo "" }) exitWith { "" }; + + 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]; + + _eventName + }], + ["clearTaskLifecycle", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + private _lifecycleRegistry = _self getOrDefault ["taskLifecycleRegistry", createHashMap]; + _lifecycleRegistry deleteAt _taskID; + _self set ["taskLifecycleRegistry", _lifecycleRegistry]; + true + }], + ["buildTaskLifecycleEventPayload", compileFinal { + params [["_taskID", "", [""]], ["_status", "", [""]], ["_extra", createHashMap]]; + + if !(_extra isEqualType createHashMap) then { + _extra = createHashMap; + }; + + private _catalogEntry = GVAR(TaskCatalogStore) 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", GVAR(TaskParticipantTracker) 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"]] + ]] + }] +]]; + +GVAR(TaskLifecycleReporter) diff --git a/arma/server/addons/task/functions/objects/fnc_TaskParticipantTracker.sqf b/arma/server/addons/task/functions/objects/fnc_TaskParticipantTracker.sqf new file mode 100644 index 0000000..e149596 --- /dev/null +++ b/arma/server/addons/task/functions/objects/fnc_TaskParticipantTracker.sqf @@ -0,0 +1,155 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Runtime participant tracking and task notification fanout. + * + * TaskStore remains the public facade, while this object owns participant + * snapshots keyed by task ID. + * + * Arguments: + * None + * + * Return Value: + * Task participant tracker object + * + * Public: No + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(TaskParticipantTracker) = createHashMapObject [[ + ["#type", "TaskParticipantTracker"], + ["#create", compileFinal { + _self call ["resetRuntimeState", []]; + }], + ["resetRuntimeState", compileFinal { + _self set ["participantRegistry", createHashMap]; + true + }], + ["trackParticipants", compileFinal { + params [["_taskID", "", [""]], ["_entities", [], [[]]], ["_marker", "", [""]], ["_radius", 300, [0]]]; + + if (_taskID isEqualTo "") exitWith { createHashMap }; + + private _participantRegistry = _self getOrDefault ["participantRegistry", createHashMap]; + private _participantSnapshots = +(_participantRegistry getOrDefault [_taskID, createHashMap]); + private _activePlayers = allPlayers select { + alive _x + && { side group _x isEqualTo west } + }; + + if (_marker isNotEqualTo "" && { markerShape _marker in ["RECTANGLE", "ELLIPSE"] }) then { + { + private _uid = getPlayerUID _x; + if (_uid isNotEqualTo "" && { _x inArea _marker }) then { + if !(_uid in _participantSnapshots) then { + _participantSnapshots set [_uid, createHashMapFromArray [ + ["startRating", rating _x] + ]]; + }; + }; + } forEach _activePlayers; + }; + + if (_radius > 0 && { _entities isNotEqualTo [] }) then { + { + private _entity = _x; + if (isNull _entity) then { continue; }; + + { + private _uid = getPlayerUID _x; + if (_uid isNotEqualTo "" && { (_x distance2D _entity) <= _radius }) then { + if !(_uid in _participantSnapshots) then { + _participantSnapshots set [_uid, createHashMapFromArray [ + ["startRating", rating _x] + ]]; + }; + }; + } forEach _activePlayers; + } forEach _entities; + }; + + _participantRegistry set [_taskID, _participantSnapshots]; + _self set ["participantRegistry", _participantRegistry]; + + _participantSnapshots + }], + ["recordParticipant", compileFinal { + params [["_taskID", "", [""]], ["_uid", "", [""]], ["_snapshot", createHashMap, [createHashMap]]]; + + if (_taskID isEqualTo "" || { _uid isEqualTo "" }) exitWith { createHashMap }; + + private _participantRegistry = _self getOrDefault ["participantRegistry", createHashMap]; + private _participantSnapshots = +(_participantRegistry getOrDefault [_taskID, createHashMap]); + _participantSnapshots set [_uid, +_snapshot]; + _participantRegistry set [_taskID, _participantSnapshots]; + _self set ["participantRegistry", _participantRegistry]; + + _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]]) + }], + ["clearTaskParticipants", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + private _participantRegistry = _self getOrDefault ["participantRegistry", createHashMap]; + _participantRegistry deleteAt _taskID; + _self set ["participantRegistry", _participantRegistry]; + true + }], + ["notifyParticipants", compileFinal { + params [ + ["_taskID", "", [""]], + ["_type", "info", [""]], + ["_title", "Tasks", [""]], + ["_message", "", [""]] + ]; + + if (_taskID isEqualTo "" || { _message isEqualTo "" }) exitWith { false }; + + private _participantSnapshots = _self call ["getTaskParticipants", [_taskID]]; + if (_participantSnapshots isEqualTo createHashMap) exitWith { false }; + + private _participantUids = keys _participantSnapshots; + if (_participantUids isEqualTo []) exitWith { false }; + if (isNil QEGVAR(common,EventBus)) exitWith { + { + private _player = [_x] call EFUNC(common,getPlayer); + if (isNull _player) then { continue; }; + [CRPC(notifications,recieveNotification), [_type, _title, _message], _player] call CFUNC(targetEvent); + } forEach _participantUids; + true + }; + + EGVAR(common,EventBus) call ["emit", [ + "task.notification.requested", + createHashMapFromArray [ + ["taskID", _taskID], + ["notificationType", _type], + ["title", _title], + ["message", _message], + ["participantUids", _participantUids] + ], + createHashMapFromArray [["source", "task"]] + ]]; + + true + }] +]]; + +GVAR(TaskParticipantTracker) diff --git a/arma/server/addons/task/functions/objects/fnc_TaskRewardService.sqf b/arma/server/addons/task/functions/objects/fnc_TaskRewardService.sqf new file mode 100644 index 0000000..aaf6bf1 --- /dev/null +++ b/arma/server/addons/task/functions/objects/fnc_TaskRewardService.sqf @@ -0,0 +1,276 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Task reward and rating outcome service. + * + * Resolves task ownership reward context and applies player earnings plus + * organization reputation outcomes. TaskStore remains the public facade. + * + * Arguments: + * None + * + * Return Value: + * Task reward service object + * + * Public: No + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(TaskRewardService) = createHashMapObject [[ + ["#type", "TaskRewardService"], + ["resolveRewardContext", compileFinal { + params [["_taskID", "", [""]]]; + + private _result = createHashMapFromArray [ + ["requesterUid", ""], + ["orgID", ""], + ["memberUids", []] + ]; + + if (_taskID isEqualTo "") exitWith { _result }; + + private _rewardState = GVAR(TaskStateGateway) call ["callTaskState", ["task:ownership:reward_context", [_taskID], createHashMap]]; + if (_rewardState isEqualTo createHashMap) exitWith { _result }; + + private _requesterUid = _rewardState getOrDefault ["requesterUid", ""]; + private _resolvedOrgID = _rewardState getOrDefault ["orgId", ""]; + if (_resolvedOrgID isEqualTo "") exitWith { _result }; + + private _org = EGVAR(org,OrgStore) call ["loadById", [_resolvedOrgID]]; + private _memberUids = []; + if (_org isNotEqualTo createHashMap) then { + private _members = _org getOrDefault ["members", createHashMap]; + + if (_members isEqualType createHashMap) then { _memberUids = keys _members; }; + if (_requesterUid isNotEqualTo "" && { !(_requesterUid in _memberUids) }) then { _memberUids pushBack _requesterUid; }; + }; + + _result set ["requesterUid", _requesterUid]; + _result set ["orgID", _resolvedOrgID]; + _result set ["memberUids", _memberUids]; + _result + }], + ["applyRatingOutcome", compileFinal { + params [["_taskID", "", [""]], ["_delta", 0, [0]]]; + + private _emitRatingEvent = { + params [["_eventName", "", [""]], ["_payload", createHashMap, [createHashMap]]]; + + if (_eventName isEqualTo "" || { isNil QEGVAR(common,EventBus) }) exitWith { createHashMap }; + + private _eventPayload = +_payload; + _eventPayload set ["taskID", _taskID]; + _eventPayload set ["ratingDelta", _delta]; + + EGVAR(common,EventBus) call ["emit", [ + _eventName, + _eventPayload, + createHashMapFromArray [["source", "task"]] + ]] + }; + + private _result = createHashMapFromArray [ + ["participantUids", []], + ["orgIds", []], + ["contributions", createHashMap], + ["success", true], + ["mutationFailures", []], + ["persistenceFailures", []], + ["message", ""] + ]; + + if (_taskID isEqualTo "" || { _delta isEqualTo 0 }) exitWith { _result }; + + private _participantSnapshots = GVAR(TaskParticipantTracker) call ["getTaskParticipants", [_taskID]]; + if (_participantSnapshots isEqualTo createHashMap) exitWith { _result }; + + private _rewardContext = _self call ["resolveRewardContext", [_taskID]]; + private _participantUids = keys _participantSnapshots; + if (_participantUids isEqualTo [] && { _delta > 0 }) then { + private _requesterUid = _rewardContext getOrDefault ["requesterUid", ""]; + if (_requesterUid isNotEqualTo "") then { + private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer); + if (!isNull _requesterPlayer) then { + _participantUids pushBack _requesterUid; + _participantSnapshots = GVAR(TaskParticipantTracker) call ["recordParticipant", [_taskID, _requesterUid, createHashMapFromArray [ + ["startRating", rating _requesterPlayer] + ]]]; + ["WARNING", format ["Task %1 had no tracked participants at payout time; falling back to requester %2 for personal earnings.", _taskID, _requesterUid]] call EFUNC(common,log); + }; + }; + }; + if (_participantUids isEqualTo []) exitWith { + _result set ["success", false]; + _result set ["message", "No task participants were available for rating outcome."]; + ["task.rating.failed", createHashMapFromArray [ + ["participantUids", []], + ["orgIds", []], + ["contributions", createHashMap], + ["mutationFailures", []], + ["persistenceFailures", []], + ["message", _result get "message"] + ]] call _emitRatingEvent; + _result + }; + + private _orgIds = []; + private _contributions = createHashMap; + private _totalContribution = 0; + private _mutationFailures = []; + private _persistenceFailures = []; + + if (_delta > 0) then { + { + private _uid = _x; + private _player = [_uid] call EFUNC(common,getPlayer); + if (isNull _player) then { continue; }; + + _contributions set [_uid, 1]; + _totalContribution = _totalContribution + 1; + } forEach _participantUids; + }; + + if (_totalContribution <= 0) exitWith { + _result set ["success", false]; + _result set ["message", "No eligible participant contribution was available for rating outcome."]; + ["task.rating.failed", createHashMapFromArray [ + ["participantUids", +_participantUids], + ["orgIds", +_orgIds], + ["contributions", +_contributions], + ["mutationFailures", []], + ["persistenceFailures", []], + ["message", _result get "message"] + ]] call _emitRatingEvent; + GVAR(TaskStore) call ["clearTask", [_taskID]]; + _result + }; + + { + private _uid = _x; + private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_uid, ""]]; + if (_orgID isNotEqualTo "") then { _orgIds pushBackUnique _orgID; }; + if (_delta > 0) then { + private _contribution = _contributions getOrDefault [_uid, 0]; + if (_contribution <= 0) then { continue; }; + + private _account = EGVAR(bank,BankStore) call ["get", [_uid, ""]]; + if (_account isEqualTo createHashMap) then { _account = EGVAR(bank,BankStore) call ["init", [_uid]]; }; + if (_account isNotEqualTo createHashMap) then { + private _earnings = _account getOrDefault ["earnings", 0]; + private _earningsDelta = round ((_delta * _contribution) / _totalContribution); + if (_earningsDelta <= 0) then { continue; }; + + private _patch = EGVAR(bank,BankStore) call [ + "mset", + [ + _uid, + createHashMapFromArray [["earnings", (_earnings + _earningsDelta)]], + false + ] + ]; + + if !(_patch isEqualType createHashMap) then { continue; }; + if (_patch isEqualTo createHashMap) then { continue; }; + if (isNil QEGVAR(common,EventBus)) then { + EGVAR(bank,BankMessenger) call ["sendAccountSync", [_uid, _patch]]; + } else { + EGVAR(common,EventBus) call ["emit", [ + "bank.account.sync.requested", + createHashMapFromArray [ + ["uid", _uid], + ["account", +_patch] + ], + createHashMapFromArray [["source", "task"]] + ]]; + }; + + if ((EGVAR(bank,BankStore) call ["save", [_uid]]) isEqualTo createHashMap) then { + _persistenceFailures pushBackUnique format ["bank:%1", _uid]; + ["ERROR", format ["Task %1 updated bank earnings for %2, but durable save failed.", _taskID, _uid]] call EFUNC(common,log); + }; + }; + }; + } forEach _participantUids; + + private _ownerOrgID = _rewardContext getOrDefault ["orgID", ""]; + if (_ownerOrgID isNotEqualTo "") then { + private _org = EGVAR(org,OrgStore) call ["loadById", [_ownerOrgID]]; + + if (_org isNotEqualTo createHashMap) then { + private _reputation = _org getOrDefault ["reputation", 0]; + private _nextReputation = round (_reputation + _delta); + _org set ["reputation", _nextReputation]; + private _updatedOrg = EGVAR(org,OrgStore) call [ + "callHotOrg", + [ + "org:hot:override", + [_ownerOrgID, toJSON _org] + ] + ]; + + if (_updatedOrg isNotEqualTo createHashMap) then { + private _patch = createHashMapFromArray [["reputation", _nextReputation]]; + private _memberUids = _rewardContext getOrDefault ["memberUids", []]; + if (isNil QEGVAR(common,EventBus)) then { + { + private _player = [_x] call EFUNC(common,getPlayer); + if (isNull _player) then { continue; }; + [CRPC(org,responseSyncOrg), [_patch], _player] call CFUNC(targetEvent); + } forEach _memberUids; + } else { + EGVAR(common,EventBus) call ["emit", [ + "org.sync.requested", + createHashMapFromArray [ + ["orgID", _ownerOrgID], + ["memberUids", +_memberUids], + ["patch", +_patch] + ], + createHashMapFromArray [["source", "task"]] + ]]; + }; + + _orgIds = [_ownerOrgID]; + if ((EGVAR(org,OrgStore) call ["saveById", [_ownerOrgID]]) isEqualTo createHashMap) then { + _persistenceFailures pushBackUnique format ["organization:%1", _ownerOrgID]; + ["ERROR", format ["Task %1 updated reputation for organization %2, but durable save failed.", _taskID, _ownerOrgID]] call EFUNC(common,log); + }; + } else { + ["ERROR", format ["Failed to update organization %1 reputation for task %2.", _ownerOrgID, _taskID]] call EFUNC(common,log); + _mutationFailures pushBackUnique format ["organization:%1", _ownerOrgID]; + }; + }; + }; + + _result set ["participantUids", _participantUids]; + _result set ["orgIds", _orgIds]; + _result set ["contributions", _contributions]; + _result set ["success", (_mutationFailures isEqualTo []) && { _persistenceFailures isEqualTo [] }]; + _result set ["mutationFailures", _mutationFailures]; + _result set ["persistenceFailures", _persistenceFailures]; + if (_mutationFailures isNotEqualTo [] || { _persistenceFailures isNotEqualTo [] }) then { + private _messageParts = []; + if (_mutationFailures isNotEqualTo []) then { + _messageParts pushBack format ["mutation failures: %1", _mutationFailures joinString ", "]; + }; + if (_persistenceFailures isNotEqualTo []) then { + _messageParts pushBack format ["persistence failures: %1", _persistenceFailures joinString ", "]; + }; + _result set ["message", _messageParts joinString "; "]; + }; + + private _eventName = ["task.rating.failed", "task.rating.applied"] select (_result getOrDefault ["success", false]); + [_eventName, createHashMapFromArray [ + ["participantUids", +(_result getOrDefault ["participantUids", []])], + ["orgIds", +(_result getOrDefault ["orgIds", []])], + ["contributions", +(_result getOrDefault ["contributions", createHashMap])], + ["mutationFailures", +(_result getOrDefault ["mutationFailures", []])], + ["persistenceFailures", +(_result getOrDefault ["persistenceFailures", []])], + ["message", _result getOrDefault ["message", ""]] + ]] call _emitRatingEvent; + + _result + }] +]]; + +GVAR(TaskRewardService) diff --git a/arma/server/addons/task/functions/objects/fnc_TaskStateGateway.sqf b/arma/server/addons/task/functions/objects/fnc_TaskStateGateway.sqf new file mode 100644 index 0000000..f76c273 --- /dev/null +++ b/arma/server/addons/task/functions/objects/fnc_TaskStateGateway.sqf @@ -0,0 +1,77 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Gateway for task hot-state extension calls. + * + * TaskStore owns gameplay/runtime behavior. This gateway owns the transport + * boundary to the extension-backed task state service. + * + * Arguments: + * None + * + * Return Value: + * Task state gateway object + * + * Public: No + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(TaskStateGateway) = createHashMapObject [[ + ["#type", "TaskStateGateway"], + ["reset", compileFinal { + ["task:reset", []] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + if ( + !_isSuccess + || { !(_result isEqualType "") } + || { (_result find "Error:") == 0 } + ) exitWith { + ["WARNING", "Failed to reset task backend state during task store initialization."] call EFUNC(common,log); + false + }; + + ["INFO", "Task backend state reset for mission lifecycle."] call EFUNC(common,log); + true + }], + ["callTaskStateEnvelope", compileFinal { + params [["_function", "", [""]], ["_arguments", [], [[]]]]; + + private _envelope = createHashMapFromArray [ + ["success", false], + ["error", ""] + ]; + + if (_function isEqualTo "") exitWith { _envelope }; + + [_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + if !_isSuccess exitWith { + _envelope set ["error", format ["Task backend call '%1' failed.", _function]]; + _envelope + }; + if !(_result isEqualType "") exitWith { + _envelope set ["error", format ["Task backend call '%1' returned an invalid response.", _function]]; + _envelope + }; + if ((_result find "Error:") == 0) exitWith { + ["ERROR", format ["Task extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log); + _envelope set ["error", _result select [7]]; + _envelope + }; + + _envelope set ["success", true]; + if (_result isNotEqualTo "") then { _envelope set ["data", fromJSON _result]; }; + + _envelope + }], + ["callTaskState", compileFinal { + params [["_function", "", [""]], ["_arguments", [], [[]]], ["_fallback", nil]]; + + private _envelope = _self call ["callTaskStateEnvelope", [_function, _arguments]]; + if !(_envelope getOrDefault ["success", false]) exitWith { _fallback }; + if (isNil { _envelope get "data" }) exitWith { _fallback }; + + _envelope get "data" + }] +]]; + +GVAR(TaskStateGateway) diff --git a/arma/server/addons/task/script_component.hpp b/arma/server/addons/task/script_component.hpp index e56f4f2..31b5f85 100644 --- a/arma/server/addons/task/script_component.hpp +++ b/arma/server/addons/task/script_component.hpp @@ -12,30 +12,38 @@ class EquipmentRewards: Edit { \ property = QUOTE(DOUBLES(PREFIX,EquipmentRewards)); \ displayName = "Equipment Rewards"; \ - tooltip = "SQF array string for equipment rewards, e.g. [""ItemGPS"",""ItemCompass""]"; \ + tooltip = "Comma-separated equipment class names, e.g. ItemGPS, ItemCompass. Legacy SQF arrays still work."; \ typeName = "STRING"; \ }; \ class SupplyRewards: Edit { \ property = QUOTE(DOUBLES(PREFIX,SupplyRewards)); \ displayName = "Supply Rewards"; \ - tooltip = "SQF array string for supply rewards, e.g. [""FirstAidKit"",""Medikit""]"; \ + tooltip = "Comma-separated supply class names, e.g. FirstAidKit, Medikit. Legacy SQF arrays still work."; \ typeName = "STRING"; \ }; \ class WeaponRewards: Edit { \ property = QUOTE(DOUBLES(PREFIX,WeaponRewards)); \ displayName = "Weapon Rewards"; \ - tooltip = "SQF array string for weapon rewards, e.g. [""arifle_MX_F""]"; \ + tooltip = "Comma-separated weapon class names, e.g. arifle_MX_F, arifle_Katiba_F. Legacy SQF arrays still work."; \ typeName = "STRING"; \ }; \ class VehicleRewards: Edit { \ property = QUOTE(DOUBLES(PREFIX,VehicleRewards)); \ displayName = "Vehicle Rewards"; \ - tooltip = "SQF array string for vehicle rewards, e.g. [""B_MRAP_01_F""]"; \ + tooltip = "Comma-separated vehicle class names, e.g. B_MRAP_01_F, B_Quadbike_01_F. Legacy SQF arrays still work."; \ typeName = "STRING"; \ }; \ class SpecialRewards: Edit { \ property = QUOTE(DOUBLES(PREFIX,SpecialRewards)); \ displayName = "Special Rewards"; \ - tooltip = "SQF array string for special rewards, e.g. [""B_UAV_01_F""]"; \ + tooltip = "Comma-separated special reward class names, e.g. B_UAV_01_F, B_Heli_Light_01_F. Legacy SQF arrays still work."; \ + typeName = "STRING"; \ + }; + +#define TASK_CHAIN_ATTRIBUTES(PREFIX) \ + class PrerequisiteTaskIds: Edit { \ + property = QUOTE(DOUBLES(PREFIX,PrerequisiteTaskIds)); \ + displayName = "Prerequisite Task IDs"; \ + tooltip = "Comma-separated task IDs that must succeed before this task appears in CAD or can be assigned"; \ typeName = "STRING"; \ }; diff --git a/docs/MISSION_DESIGNER_GUIDE.md b/docs/MISSION_DESIGNER_GUIDE.md index 34267eb..f659210 100644 --- a/docs/MISSION_DESIGNER_GUIDE.md +++ b/docs/MISSION_DESIGNER_GUIDE.md @@ -295,7 +295,19 @@ General task rules: 4. Use Forge grouping modules where required. 5. Sync task modules to real world objects, units, vehicles, or grouping modules. -6. Test that the task appears in CAD before relying on dispatch assignment. +6. To chain tasks, set `Prerequisite Task IDs` on the dependent task module to + a comma-separated list of task IDs that must succeed first. +7. Reward class fields use comma-separated class names without brackets, such + as `ItemGPS, FirstAidKit`. Existing SQF array strings such as + `["ItemGPS","FirstAidKit"]` still work for older missions. +8. Test that unchained tasks appear in CAD immediately and chained tasks appear + only after their prerequisite tasks succeed. + +Task chaining uses only task IDs. The dependent task is still registered during +mission setup, but it stays hidden from CAD, cannot be assigned, and does not +start its task logic until every prerequisite task has completed successfully. +If any prerequisite task fails or never completes, the dependent task remains +locked. Zone fields that must reference area markers: @@ -333,7 +345,9 @@ Setup: 5. Set `LimitFail` if the mission should fail after too many losses. 6. Set reward funds, rating gain/loss, end-state behavior, and optional `TimeLimit`. -7. Sync the attack module directly to the target units or vehicles. +7. Set `Prerequisite Task IDs` only if this attack task should unlock after + other tasks succeed. +8. Sync the attack module directly to the target units or vehicles. Validation: @@ -362,7 +376,9 @@ Setup: or failed conditions. 6. Set reward funds, rating gain/loss, end-state behavior, and optional `TimeLimit`. -7. Sync the destroy module directly to the targets. +7. Set `Prerequisite Task IDs` only if this destroy task should unlock after + other tasks succeed. +8. Sync the destroy module directly to the targets. Validation: @@ -408,8 +424,10 @@ Setup: failure. 11. Set `TimeLimit` to the IED countdown in seconds. 12. Set reward funds, rating gain/loss, and end-state behavior. -13. Sync `FORGE_Module_Defuse` to `FORGE_Module_Explosives`. -14. Sync `FORGE_Module_Defuse` to `FORGE_Module_Protected` if used. +13. Set `Prerequisite Task IDs` only if this defuse task should unlock after + other tasks succeed. +14. Sync `FORGE_Module_Defuse` to `FORGE_Module_Explosives`. +15. Sync `FORGE_Module_Defuse` to `FORGE_Module_Protected` if used. Validation: @@ -455,7 +473,9 @@ Setup: fail threshold. 10. Set reward funds, rating gain/loss, end-state behavior, and optional `TimeLimit`. -11. Sync `FORGE_Module_Delivery` to `FORGE_Module_Cargo`. +11. Set `Prerequisite Task IDs` only if this delivery task should unlock after + other tasks succeed. +12. Sync `FORGE_Module_Delivery` to `FORGE_Module_Cargo`. Validation: @@ -510,8 +530,10 @@ Setup: 15. If `CBRN Attack` is enabled, set `CBRNZone`. 16. Set reward funds, rating gain/loss, end-state behavior, and optional `TimeLimit`. -17. Sync `FORGE_Module_Hostage` to `FORGE_Module_Hostages`. -18. Sync `FORGE_Module_Hostage` to `FORGE_Module_Shooters`. +17. Set `Prerequisite Task IDs` only if this hostage task should unlock after + other tasks succeed. +18. Sync `FORGE_Module_Hostage` to `FORGE_Module_Hostages`. +19. Sync `FORGE_Module_Hostage` to `FORGE_Module_Shooters`. Validation: @@ -562,7 +584,9 @@ Setup: capture mode. 9. Set reward funds, rating gain/loss, end-state behavior, and optional `TimeLimit`. -10. Sync the HVT module directly to the HVT unit or units. +10. Set `Prerequisite Task IDs` only if this HVT task should unlock after other + tasks succeed. +11. Sync the HVT module directly to the HVT unit or units. Validation: @@ -600,6 +624,8 @@ Setup: 9. Place one or more enemy groups or units to use as wave templates. 10. Sync any unit from each enemy group to the defend module. 11. Set reward funds, rating gain/loss, and end-state behavior. +12. Set `Prerequisite Task IDs` only if this defend task should unlock after + other tasks succeed. Validation: @@ -652,7 +678,8 @@ Before publishing a mission, verify: - Grouping modules are synced in the correct direction. - Success and fail limits match the number of required entities. - Reward funds and rating changes are intentional. -- The task appears in CAD when created. +- Unchained tasks appear in CAD when created. +- Chained tasks remain hidden until all prerequisite task IDs succeed. - Assigned CAD tasks can be acknowledged, declined, and completed. ## Mission Validation Checklist diff --git a/docs/TASK_USAGE_GUIDE.md b/docs/TASK_USAGE_GUIDE.md index bd898b7..e971204 100644 --- a/docs/TASK_USAGE_GUIDE.md +++ b/docs/TASK_USAGE_GUIDE.md @@ -28,6 +28,7 @@ when a catalog entry is inserted or ownership changes: - `accepted` - `requesterUid` - `orgID` +- `prerequisiteTaskIds` Ownership context: @@ -209,6 +210,10 @@ Available task modules: These modules delegate to `forge_server_task_fnc_startTask`. +Each task module also includes an optional chain field: + +- `Prerequisite Task IDs`: comma-separated task IDs that must succeed first. + ## Mission Designer Guide This section is the practical Eden setup guide for mission designers. @@ -230,6 +235,13 @@ Use these rules for every Forge task: 6. Grouping modules such as `Explosive Entities`, `Protected Entities`, `Cargo`, `Hostages`, and `Shooters` should be synced to real world objects, not other logic modules. +7. To chain tasks, set `Prerequisite Task IDs` on the dependent task module. + Use comma-separated IDs such as `attack_01, delivery_02`. The dependent + task stays hidden from CAD and cannot be assigned until every listed task + succeeds. +8. Reward class fields accept comma-separated class names without brackets, + such as `ItemGPS, FirstAidKit`. Legacy SQF array strings such as + `["ItemGPS","FirstAidKit"]` are still supported. ### Attack Task @@ -473,6 +485,7 @@ through `forge_server_task_fnc_handler`. createHashMapFromArray [ ["limitFail", 0], ["limitSuccess", 3], + ["prerequisiteTaskIds", ["recon_01"]], ["funds", 50000], ["ratingFail", -10], ["ratingSuccess", 20], @@ -484,6 +497,37 @@ through `forge_server_task_fnc_handler`. ] call forge_server_task_fnc_startTask; ``` +## Chained Tasks + +Use `prerequisiteTaskIds` when a task should stay hidden until one or more +other tasks succeed. The task is still registered during mission setup, but it +is stored with `locked` status, filtered out of CAD, blocked from assignment, +and its task logic does not start until every prerequisite task has completed +with `succeeded`. + +```sqf +[ + "delivery", + "supply_delivery_02", + getMarkerPos "delivery_zone_02", + "Deliver Medical Supplies", + "Move the cargo into the marked delivery area.", + createHashMapFromArray [["cargo", [cargoBox1, cargoBox2]]], + createHashMapFromArray [ + ["deliveryZone", "delivery_zone_02"], + ["limitSuccess", 2], + ["prerequisiteTaskIds", ["compound_attack_01"]], + ["funds", 30000] + ] +] call forge_server_task_fnc_startTask; +``` + +Notes: + +- `prerequisiteTaskIds` accepts either a string or an array of task ID strings. +- All prerequisite tasks must succeed before the chained task unlocks. +- If a prerequisite fails or never completes, the chained task remains locked. + ## Handler Calls Use `forge_server_task_fnc_handler` directly when the task entities are already diff --git a/docus/content/1.getting-started/4.mission-designer.md b/docus/content/1.getting-started/4.mission-designer.md index c22b883..2db17c5 100644 --- a/docus/content/1.getting-started/4.mission-designer.md +++ b/docus/content/1.getting-started/4.mission-designer.md @@ -295,7 +295,19 @@ General task rules: 4. Use Forge grouping modules where required. 5. Sync task modules to real world objects, units, vehicles, or grouping modules. -6. Test that the task appears in CAD before relying on dispatch assignment. +6. To chain tasks, set `Prerequisite Task IDs` on the dependent task module to + a comma-separated list of task IDs that must succeed first. +7. Reward class fields use comma-separated class names without brackets, such + as `ItemGPS, FirstAidKit`. Existing SQF array strings such as + `["ItemGPS","FirstAidKit"]` still work for older missions. +8. Test that unchained tasks appear in CAD immediately and chained tasks appear + only after their prerequisite tasks succeed. + +Task chaining uses only task IDs. The dependent task is still registered during +mission setup, but it stays hidden from CAD, cannot be assigned, and does not +start its task logic until every prerequisite task has completed successfully. +If any prerequisite task fails or never completes, the dependent task remains +locked. Zone fields that must reference area markers: @@ -333,7 +345,9 @@ Setup: 5. Set `LimitFail` if the mission should fail after too many losses. 6. Set reward funds, rating gain/loss, end-state behavior, and optional `TimeLimit`. -7. Sync the attack module directly to the target units or vehicles. +7. Set `Prerequisite Task IDs` only if this attack task should unlock after + other tasks succeed. +8. Sync the attack module directly to the target units or vehicles. Validation: @@ -362,7 +376,9 @@ Setup: or failed conditions. 6. Set reward funds, rating gain/loss, end-state behavior, and optional `TimeLimit`. -7. Sync the destroy module directly to the targets. +7. Set `Prerequisite Task IDs` only if this destroy task should unlock after + other tasks succeed. +8. Sync the destroy module directly to the targets. Validation: @@ -408,8 +424,10 @@ Setup: failure. 11. Set `TimeLimit` to the IED countdown in seconds. 12. Set reward funds, rating gain/loss, and end-state behavior. -13. Sync `FORGE_Module_Defuse` to `FORGE_Module_Explosives`. -14. Sync `FORGE_Module_Defuse` to `FORGE_Module_Protected` if used. +13. Set `Prerequisite Task IDs` only if this defuse task should unlock after + other tasks succeed. +14. Sync `FORGE_Module_Defuse` to `FORGE_Module_Explosives`. +15. Sync `FORGE_Module_Defuse` to `FORGE_Module_Protected` if used. Validation: @@ -455,7 +473,9 @@ Setup: fail threshold. 10. Set reward funds, rating gain/loss, end-state behavior, and optional `TimeLimit`. -11. Sync `FORGE_Module_Delivery` to `FORGE_Module_Cargo`. +11. Set `Prerequisite Task IDs` only if this delivery task should unlock after + other tasks succeed. +12. Sync `FORGE_Module_Delivery` to `FORGE_Module_Cargo`. Validation: @@ -510,8 +530,10 @@ Setup: 15. If `CBRN Attack` is enabled, set `CBRNZone`. 16. Set reward funds, rating gain/loss, end-state behavior, and optional `TimeLimit`. -17. Sync `FORGE_Module_Hostage` to `FORGE_Module_Hostages`. -18. Sync `FORGE_Module_Hostage` to `FORGE_Module_Shooters`. +17. Set `Prerequisite Task IDs` only if this hostage task should unlock after + other tasks succeed. +18. Sync `FORGE_Module_Hostage` to `FORGE_Module_Hostages`. +19. Sync `FORGE_Module_Hostage` to `FORGE_Module_Shooters`. Validation: @@ -562,7 +584,9 @@ Setup: capture mode. 9. Set reward funds, rating gain/loss, end-state behavior, and optional `TimeLimit`. -10. Sync the HVT module directly to the HVT unit or units. +10. Set `Prerequisite Task IDs` only if this HVT task should unlock after other + tasks succeed. +11. Sync the HVT module directly to the HVT unit or units. Validation: @@ -600,6 +624,8 @@ Setup: 9. Place one or more enemy groups or units to use as wave templates. 10. Sync any unit from each enemy group to the defend module. 11. Set reward funds, rating gain/loss, and end-state behavior. +12. Set `Prerequisite Task IDs` only if this defend task should unlock after + other tasks succeed. Validation: @@ -652,7 +678,8 @@ Before publishing a mission, verify: - Grouping modules are synced in the correct direction. - Success and fail limits match the number of required entities. - Reward funds and rating changes are intentional. -- The task appears in CAD when created. +- Unchained tasks appear in CAD when created. +- Chained tasks remain hidden until all prerequisite task IDs succeed. - Assigned CAD tasks can be acknowledged, declined, and completed. ## Mission Validation Checklist diff --git a/docus/content/1.getting-started/5.player-guide.md b/docus/content/1.getting-started/5.player-guide.md index 803a90a..0482541 100644 --- a/docus/content/1.getting-started/5.player-guide.md +++ b/docus/content/1.getting-started/5.player-guide.md @@ -3,9 +3,6 @@ title: "Player Guide" description: "Use this guide as the player-facing overview for Forge systems. It explains what players interact with during normal missions, how task assignment works, and what persistent storage limits apply." --- -Player-guide screenshots are stored as JPG files under -`docus/public/images/player`. - ## Opening Forge Interactions Most Forge actions are opened from the actor interaction menu while standing diff --git a/docus/content/3.server-modules/11.task.md b/docus/content/3.server-modules/11.task.md index d13084c..f9731f3 100644 --- a/docus/content/3.server-modules/11.task.md +++ b/docus/content/3.server-modules/11.task.md @@ -27,6 +27,7 @@ when a catalog entry is inserted or ownership changes: - `accepted` - `requesterUid` - `orgID` +- `prerequisiteTaskIds` Ownership context: @@ -208,6 +209,10 @@ Available task modules: These modules delegate to `forge_server_task_fnc_startTask`. +Each task module also includes an optional chain field: + +- `Prerequisite Task IDs`: comma-separated task IDs that must succeed first. + ## Mission Designer Guide This section is the practical Eden setup guide for mission designers. @@ -229,6 +234,13 @@ Use these rules for every Forge task: 6. Grouping modules such as `Explosive Entities`, `Protected Entities`, `Cargo`, `Hostages`, and `Shooters` should be synced to real world objects, not other logic modules. +7. To chain tasks, set `Prerequisite Task IDs` on the dependent task module. + Use comma-separated IDs such as `attack_01, delivery_02`. The dependent + task stays hidden from CAD and cannot be assigned until every listed task + succeeds. +8. Reward class fields accept comma-separated class names without brackets, + such as `ItemGPS, FirstAidKit`. Legacy SQF array strings such as + `["ItemGPS","FirstAidKit"]` are still supported. ### Attack Task @@ -472,6 +484,7 @@ through `forge_server_task_fnc_handler`. createHashMapFromArray [ ["limitFail", 0], ["limitSuccess", 3], + ["prerequisiteTaskIds", ["recon_01"]], ["funds", 50000], ["ratingFail", -10], ["ratingSuccess", 20], @@ -483,6 +496,37 @@ through `forge_server_task_fnc_handler`. ] call forge_server_task_fnc_startTask; ``` +## Chained Tasks + +Use `prerequisiteTaskIds` when a task should stay hidden until one or more +other tasks succeed. The task is still registered during mission setup, but it +is stored with `locked` status, filtered out of CAD, blocked from assignment, +and its task logic does not start until every prerequisite task has completed +with `succeeded`. + +```sqf +[ + "delivery", + "supply_delivery_02", + getMarkerPos "delivery_zone_02", + "Deliver Medical Supplies", + "Move the cargo into the marked delivery area.", + createHashMapFromArray [["cargo", [cargoBox1, cargoBox2]]], + createHashMapFromArray [ + ["deliveryZone", "delivery_zone_02"], + ["limitSuccess", 2], + ["prerequisiteTaskIds", ["compound_attack_01"]], + ["funds", 30000] + ] +] call forge_server_task_fnc_startTask; +``` + +Notes: + +- `prerequisiteTaskIds` accepts either a string or an array of task ID strings. +- All prerequisite tasks must succeed before the chained task unlocks. +- If a prerequisite fails or never completes, the chained task remains locked. + ## Handler Calls Use `forge_server_task_fnc_handler` directly when the task entities are already diff --git a/lib/services/src/task.rs b/lib/services/src/task.rs index 3899b75..3f803d5 100644 --- a/lib/services/src/task.rs +++ b/lib/services/src/task.rs @@ -45,18 +45,14 @@ impl TaskStateService { pub fn list_active_catalog(&self) -> Result, String> { let catalog = self.repository.list_catalog()?; - let active_statuses = self.repository.list_active_statuses()?; let mut active_entries = Vec::new(); - for (task_id, status) in active_statuses { + for (task_id, entry) in catalog { + let status = self.derive_catalog_status(&task_id, &entry)?; if !matches!(status.as_str(), "available" | "assigned" | "active") { continue; } - let Some(entry) = catalog.get(&task_id) else { - continue; - }; - let mut entry = entry.fields.clone(); entry.insert("taskId".to_string(), Value::String(task_id.clone())); entry.insert("taskID".to_string(), Value::String(task_id)); @@ -173,10 +169,15 @@ impl TaskStateService { return Ok(status); } - Ok(self - .repository - .get_completed_status(&entry_id)? - .unwrap_or_default()) + if let Some(status) = self.repository.get_completed_status(&entry_id)? { + return Ok(status); + } + + let Some(entry) = self.repository.get_catalog_entry(&entry_id)? else { + return Ok(String::new()); + }; + + Ok(Self::default_catalog_status(&entry)) } pub fn clear_status(&self, entry_id: String) -> Result { @@ -245,6 +246,31 @@ impl TaskStateService { Ok(entry.into_value()) } + fn derive_catalog_status(&self, entry_id: &str, entry: &TaskRecord) -> Result { + if let Some(status) = self.repository.get_active_status(entry_id)? { + return Ok(status); + } + + if let Some(status) = self.repository.get_completed_status(entry_id)? { + return Ok(status); + } + + Ok(Self::default_catalog_status(entry)) + } + + fn default_catalog_status(entry: &TaskRecord) -> String { + if entry + .fields + .get("locked") + .and_then(Value::as_bool) + .unwrap_or(false) + { + "locked".to_string() + } else { + "available".to_string() + } + } + fn normalize_catalog_entry(entry: &mut TaskRecord, entry_id: &str) { let fields = &mut entry.fields; fields @@ -383,6 +409,34 @@ mod tests { ); } + #[test] + fn get_status_defaults_catalog_entries_to_available_or_locked() { + let service = TaskStateService::new(InMemoryTaskRepository::new()); + + service + .upsert_catalog_entry("task-open".to_string(), r#"{"title":"Open"}"#.to_string()) + .expect("open catalog upsert should succeed"); + service + .upsert_catalog_entry( + "task-locked".to_string(), + r#"{"title":"Locked","locked":true}"#.to_string(), + ) + .expect("locked catalog upsert should succeed"); + + assert_eq!( + service + .get_status("task-open".to_string()) + .expect("open status lookup should succeed"), + "available" + ); + assert_eq!( + service + .get_status("task-locked".to_string()) + .expect("locked status lookup should succeed"), + "locked" + ); + } + #[test] fn list_active_catalog_returns_assignable_and_active_entries() { let service = TaskStateService::new(InMemoryTaskRepository::new()); @@ -435,4 +489,31 @@ mod tests { assert!(task_ids.contains(&"task-assigned")); assert!(task_ids.contains(&"task-active")); } + + #[test] + fn list_active_catalog_includes_unstatused_unlocked_entries() { + let service = TaskStateService::new(InMemoryTaskRepository::new()); + + service + .upsert_catalog_entry("task-open".to_string(), r#"{"title":"Open"}"#.to_string()) + .expect("open catalog upsert should succeed"); + service + .upsert_catalog_entry( + "task-locked".to_string(), + r#"{"title":"Locked","locked":true}"#.to_string(), + ) + .expect("locked catalog upsert should succeed"); + + let active_catalog = service + .list_active_catalog() + .expect("active catalog should build"); + + let task_ids: Vec<_> = active_catalog + .iter() + .filter_map(|entry| entry.get("taskId").and_then(Value::as_str)) + .collect(); + + assert_eq!(active_catalog.len(), 1); + assert!(task_ids.contains(&"task-open")); + } }