From 07d5422091ab25ed629fdaeb37d236fb76fd88fa Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Tue, 28 Apr 2026 23:04:22 -0500 Subject: [PATCH] Refactor task management system with object prototypes and enhanced logging - Updated fnc_extCall.sqf to suppress logging for specific functions. - Added object model prototypes for task instances in prototypes/taskObjectPrototypes.sqf. - Enhanced README.md to document the new object model and its purpose. - Modified XEH_postInit.sqf to improve event handling for defuse tasks. - Updated various task functions (fnc_attack, fnc_defend, fnc_defuse, fnc_delivery, fnc_destroy, fnc_heartBeat, fnc_hostage, fnc_hvt) to include task acceptance checks. - Improved fnc_makeHostage and fnc_makeIED to ensure proper task registration and state management. - Introduced new methods in task object prototypes for better state management and task flow control. --- .../extension/functions/fnc_extCall.sqf | 9 +- arma/server/addons/task/README.md | 11 + arma/server/addons/task/XEH_postInit.sqf | 16 +- .../addons/task/functions/fnc_attack.sqf | 8 + .../addons/task/functions/fnc_defend.sqf | 32 +- .../addons/task/functions/fnc_defuse.sqf | 28 +- .../task/functions/fnc_defuseModule.sqf | 12 +- .../addons/task/functions/fnc_delivery.sqf | 8 + .../addons/task/functions/fnc_destroy.sqf | 8 + .../addons/task/functions/fnc_heartBeat.sqf | 11 +- .../addons/task/functions/fnc_hostage.sqf | 29 +- .../task/functions/fnc_hostageModule.sqf | 16 +- arma/server/addons/task/functions/fnc_hvt.sqf | 8 + .../task/functions/fnc_initTaskStore.sqf | 72 ++++- .../addons/task/functions/fnc_makeHostage.sqf | 9 +- .../addons/task/functions/fnc_makeIED.sqf | 2 +- arma/server/addons/task/prototypes/README.md | 26 ++ .../task/prototypes/taskObjectPrototypes.sqf | 290 ++++++++++++++++++ 18 files changed, 554 insertions(+), 41 deletions(-) create mode 100644 arma/server/addons/task/prototypes/README.md create mode 100644 arma/server/addons/task/prototypes/taskObjectPrototypes.sqf diff --git a/arma/server/addons/extension/functions/fnc_extCall.sqf b/arma/server/addons/extension/functions/fnc_extCall.sqf index 40754af..426a95b 100644 --- a/arma/server/addons/extension/functions/fnc_extCall.sqf +++ b/arma/server/addons/extension/functions/fnc_extCall.sqf @@ -4,7 +4,7 @@ * File: fnc_extCall.sqf * Author: IDSolutions * Date: 2026-01-03 - * Last Update: 2026-04-01 + * Last Update: 2026-04-28 * Public: No * * Description: @@ -24,9 +24,12 @@ params [["_function", "", [""]], ["_arguments", [], [[]]]]; -["INFO", format ["Calling function: %1", _function], nil, nil] call EFUNC(common,log); - +private _quietFunctionLogs = ["task:defuse:get"]; private _functionLower = toLower _function; +if !(_functionLower in _quietFunctionLogs) then { + ["INFO", format ["Calling function: %1", _function], nil, nil] call EFUNC(common,log); +}; + private _chunkPrefix = "FORGE_TRANSPORT_CHUNK:"; private _chunkPrefixLength = count toArray _chunkPrefix; private _unsupportedRoutePrefix = "Error: Unsupported transport route"; diff --git a/arma/server/addons/task/README.md b/arma/server/addons/task/README.md index d054b60..be4e43a 100644 --- a/arma/server/addons/task/README.md +++ b/arma/server/addons/task/README.md @@ -41,6 +41,17 @@ system intentionally starts clean after each server or mission restart. - defuse progress - per-task entity registries for cargo, hostages, HVTs, IEDs, protected entities, shooters, and targets +### Object Model Prototype +A review-only prototype for object-based task instances lives under +`prototypes/`. It is not wired into runtime. + +- `prototypes/taskObjectPrototypes.sqf` +- `prototypes/README.md` + +The prototype sketches `TaskInstanceBaseClass`, `HostageTaskBaseClass`, and +`DefuseTaskBaseClass` using `createHashMapObject` so the team can review a +stateful per-task design without replacing the current procedural task flows. + ### Reward Handling `fnc_handleTaskRewards.sqf` applies org-owned rewards: - `funds` -> org funds diff --git a/arma/server/addons/task/XEH_postInit.sqf b/arma/server/addons/task/XEH_postInit.sqf index aadd2db..3d29ab1 100644 --- a/arma/server/addons/task/XEH_postInit.sqf +++ b/arma/server/addons/task/XEH_postInit.sqf @@ -2,13 +2,27 @@ ["ace_explosives_defuse", { private _taskID = ""; + private _explosive = objNull; { if (_x isEqualType objNull && { !isNull _x }) then { + if (isNull _explosive) then { _explosive = _x; }; _taskID = _x getVariable ["assignedTask", ""]; if (_taskID isNotEqualTo "") exitWith {}; }; } forEach _this; - if (_taskID isEqualTo "") exitWith {}; + if (_taskID isEqualTo "" && { !isNull _explosive }) then { + _taskID = GVAR(TaskStore) call ["findTaskEntityOwner", ["ieds", _explosive]]; + }; + + if (_taskID isEqualTo "") exitWith { + ["WARNING", format [ + "ACE Defuse Event Ignored: No assignedTask found. Explosive=%1, Type=%2, NetID=%3", + _explosive, + typeOf _explosive, + netId _explosive + ]] call EFUNC(common,log); + }; + GVAR(TaskStore) call ["incrementDefuseCount", [_taskID]]; }] call CFUNC(addEventHandler); diff --git a/arma/server/addons/task/functions/fnc_attack.sqf b/arma/server/addons/task/functions/fnc_attack.sqf index a4893fc..c3909ee 100644 --- a/arma/server/addons/task/functions/fnc_attack.sqf +++ b/arma/server/addons/task/functions/fnc_attack.sqf @@ -58,6 +58,14 @@ waitUntil { }; _targets = GVAR(TaskStore) call ["getTaskEntities", ["targets", _taskID]]; + +if (_timeLimit isNotEqualTo 0) then { + waitUntil { + sleep 1; + GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] + }; +}; + private _startTime = if (_timeLimit isNotEqualTo 0) then { floor(time) } else { nil }; waitUntil { diff --git a/arma/server/addons/task/functions/fnc_defend.sqf b/arma/server/addons/task/functions/fnc_defend.sqf index c1e7b20..e5c4f07 100644 --- a/arma/server/addons/task/functions/fnc_defend.sqf +++ b/arma/server/addons/task/functions/fnc_defend.sqf @@ -55,17 +55,41 @@ if (_defenseZone == "" || !(markerShape _defenseZone in ["RECTANGLE", "ELLIPSE"] }; private _result = 0; -private _startTime = time; -private _nextWaveTime = _startTime; +private _startTime = -1; +private _nextWaveTime = -1; private _currentWave = 0; private _zoneEmptyCounter = 0; private _warningIssued = false; +private _defenseStarted = false; + +waitUntil { + sleep 1; + GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] +}; + +waitUntil { + sleep 1; + GVAR(TaskStore) call ["trackParticipants", [_taskID, [], _defenseZone, 0]]; + + private _bluforInZone = count (allUnits select { _x isKindOf "CAManBase" && { side _x == west } && { alive _x }} inAreaArray _defenseZone); + private _readyToStart = _bluforInZone >= _minBlufor; + + if (_readyToStart) then { + _defenseStarted = true; + _startTime = time; + _nextWaveTime = _startTime; + + GVAR(TaskStore) call ["notifyParticipants", [_taskID, "info", "Tasks", "Defense has started. Hold the zone."]]; + }; + + _readyToStart +}; waitUntil { sleep 1; GVAR(TaskStore) call ["trackParticipants", [_taskID, [], _defenseZone, 0]]; private _bluforInZone = count (allUnits select { _x isKindOf "CAManBase" && { side _x == west } && { alive _x }} inAreaArray _defenseZone); - private _timeElapsed = time - _startTime; + private _timeElapsed = if (_defenseStarted) then { time - _startTime } else { 0 }; if (_bluforInZone < _minBlufor) then { _zoneEmptyCounter = _zoneEmptyCounter + 1; @@ -79,7 +103,7 @@ waitUntil { _warningIssued = false; }; - if (_currentWave < _waveCount && time >= _nextWaveTime) then { + if (_currentWave < _waveCount && _defenseStarted && { time >= _nextWaveTime }) then { [_defenseZone, _taskID, _currentWave] call FUNC(spawnEnemyWave); _currentWave = _currentWave + 1; diff --git a/arma/server/addons/task/functions/fnc_defuse.sqf b/arma/server/addons/task/functions/fnc_defuse.sqf index 1c4d6b0..1019bcf 100644 --- a/arma/server/addons/task/functions/fnc_defuse.sqf +++ b/arma/server/addons/task/functions/fnc_defuse.sqf @@ -46,7 +46,6 @@ params [ private _result = 0; private _ieds = []; -private _entities = []; waitUntil { sleep 1; @@ -54,25 +53,30 @@ waitUntil { count _ieds > 0 }; -waitUntil { - sleep 1; - _entities = GVAR(TaskStore) call ["getTaskEntities", ["entities", _taskID]]; - GVAR(TaskStore) call ["trackParticipants", [_taskID, _ieds + _entities, "", 250]]; - count _entities > 0 -}; - _ieds = GVAR(TaskStore) call ["getTaskEntities", ["ieds", _taskID]]; -_entities = GVAR(TaskStore) call ["getTaskEntities", ["entities", _taskID]]; +private _entities = GVAR(TaskStore) call ["getTaskEntities", ["entities", _taskID]]; +private _requiredDefusals = if (_limitSuccess < 0) then { count _ieds } else { _limitSuccess }; +private _maxProtectedLosses = if (_limitFail < 0) then { count _entities } else { _limitFail }; +private _entitiesDestroyed = 0; +private _defusedCount = 0; +private _shouldFail = false; +private _shouldSucceed = false; +private _done = false; waitUntil { sleep 1; GVAR(TaskStore) call ["trackParticipants", [_taskID, _ieds + _entities, "", 250]]; - private _entitiesDestroyed = ({ !alive _x } count _entities); + _entitiesDestroyed = ({ !alive _x } count _entities); + _defusedCount = GVAR(TaskStore) call ["getDefuseCount", [_taskID]]; + _shouldFail = (_maxProtectedLosses > 0) && { _entitiesDestroyed >= _maxProtectedLosses }; + _shouldSucceed = (_requiredDefusals > 0) && { _defusedCount >= _requiredDefusals } && { (_maxProtectedLosses <= 0) || { _entitiesDestroyed < _maxProtectedLosses } }; + _done = false; - if (_entitiesDestroyed >= _limitFail) then { _result = 1; }; + if (_shouldFail) then { _result = 1; }; + if ((_result == 1) or _shouldSucceed) then { _done = true; }; - (_result == 1) or ((GVAR(TaskStore) call ["getDefuseCount", [_taskID]]) >= _limitSuccess && (_entitiesDestroyed < _limitFail)) + _done }; if (_result == 1) then { diff --git a/arma/server/addons/task/functions/fnc_defuseModule.sqf b/arma/server/addons/task/functions/fnc_defuseModule.sqf index 3324c02..36bf58c 100644 --- a/arma/server/addons/task/functions/fnc_defuseModule.sqf +++ b/arma/server/addons/task/functions/fnc_defuseModule.sqf @@ -30,8 +30,16 @@ if (_taskID isEqualTo "") exitWith { private _syncedModules = synchronizedObjects _logic; private _iedModule = (_syncedModules select { typeOf _x isEqualTo "FORGE_Module_Explosives" }) param [0, objNull]; private _protectedModule = (_syncedModules select { typeOf _x isEqualTo "FORGE_Module_Protected" }) param [0, objNull]; -private _iedEntities = if (!isNull _iedModule) then { synchronizedObjects _iedModule } else { [] }; -private _protectedEntities = if (!isNull _protectedModule) then { synchronizedObjects _protectedModule } else { [] }; +private _iedEntities = if (!isNull _iedModule) then { + synchronizedObjects _iedModule select { !(_x isKindOf "Logic") } +} else { + [] +}; +private _protectedEntities = if (!isNull _protectedModule) then { + synchronizedObjects _protectedModule select { !(_x isKindOf "Logic") } +} else { + [] +}; ["INFO", format [ "Defuse Module: TaskID: %1, IEDs: %2, Protected: %3, IED timer: %4s", diff --git a/arma/server/addons/task/functions/fnc_delivery.sqf b/arma/server/addons/task/functions/fnc_delivery.sqf index 426e10a..d47cb5a 100644 --- a/arma/server/addons/task/functions/fnc_delivery.sqf +++ b/arma/server/addons/task/functions/fnc_delivery.sqf @@ -60,6 +60,14 @@ waitUntil { }; _cargo = GVAR(TaskStore) call ["getTaskEntities", ["cargo", _taskID]]; + +if (_timeLimit isNotEqualTo 0) then { + waitUntil { + sleep 1; + GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] + }; +}; + private _startTime = if (_timeLimit isNotEqualTo 0) then { floor(time) } else { nil }; waitUntil { diff --git a/arma/server/addons/task/functions/fnc_destroy.sqf b/arma/server/addons/task/functions/fnc_destroy.sqf index 2284dda..3a24228 100644 --- a/arma/server/addons/task/functions/fnc_destroy.sqf +++ b/arma/server/addons/task/functions/fnc_destroy.sqf @@ -58,6 +58,14 @@ waitUntil { }; _targets = GVAR(TaskStore) call ["getTaskEntities", ["targets", _taskID]]; + +if (_timeLimit isNotEqualTo 0) then { + waitUntil { + sleep 1; + GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] + }; +}; + private _startTime = if (_timeLimit isNotEqualTo 0) then { floor(time) } else { nil }; waitUntil { diff --git a/arma/server/addons/task/functions/fnc_heartBeat.sqf b/arma/server/addons/task/functions/fnc_heartBeat.sqf index 7a57941..bd36a3a 100644 --- a/arma/server/addons/task/functions/fnc_heartBeat.sqf +++ b/arma/server/addons/task/functions/fnc_heartBeat.sqf @@ -38,7 +38,8 @@ switch (_typeOf) do { [_entity] joinSilent (group _nearPlayer); - _entity setCaptive false; + // Keep rescued hostages protected while they follow the player group. + _entity setCaptive true; _entity enableAIFeature ["MOVE", true]; _entity playMove "acts_executionvictim_unbow"; }; @@ -53,6 +54,14 @@ switch (_typeOf) do { doStop _entity; }; case "ied": { + private _taskID = _entity getVariable ["assignedTask", ""]; + if (_taskID isNotEqualTo "") then { + waitUntil { + sleep 1; + GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] + }; + }; + while { alive _entity && _time > 0} do { if (_time > 10) then { _entity say3D "FORGE_timerBeep" }; if (_time <= 10 && _time > 5) then { _entity say3D "FORGE_timerBeepShort" }; diff --git a/arma/server/addons/task/functions/fnc_hostage.sqf b/arma/server/addons/task/functions/fnc_hostage.sqf index 66c4071..d631137 100644 --- a/arma/server/addons/task/functions/fnc_hostage.sqf +++ b/arma/server/addons/task/functions/fnc_hostage.sqf @@ -75,13 +75,26 @@ waitUntil { _hostages = GVAR(TaskStore) call ["getTaskEntities", ["hostages", _taskID]]; _shooters = GVAR(TaskStore) call ["getTaskEntities", ["shooters", _taskID]]; +private _requiredRescues = if (_limitSuccess < 0) then { count _hostages } else { _limitSuccess }; +private _maxHostageLosses = if (_limitFail < 0) then { count _hostages } else { _limitFail }; + +if (_timeLimit isNotEqualTo 0) then { + waitUntil { + sleep 1; + GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] + }; +}; + private _startTime = if (_timeLimit isNotEqualTo 0) then { floor(time) } else { nil }; waitUntil { sleep 1; GVAR(TaskStore) call ["trackParticipants", [_taskID, _hostages + _shooters, _extZone, 250]]; - private _hostagesFreed = ({ !captive _x } count _hostages); + private _playerGroups = allPlayers apply { group _x }; + private _hostagesFreed = ({ + alive _x && { ((group _x) in _playerGroups) || { !captive _x } } + } count _hostages); private _hostagesInZone = ({ _x inArea _extZone } count _hostages); private _hostagesKilled = ({ !alive _x } count _hostages); private _shootersAlive = ({ alive _x } count _shooters); @@ -89,18 +102,18 @@ waitUntil { if (_timeLimit isNotEqualTo 0) then { private _timeExpired = (floor time - _startTime >= _timeLimit); - if (_hostagesFreed < _limitSuccess && _timeExpired) then { _result = 1; }; - if (_hostagesKilled >= _limitFail) then { _result = 1; }; + if (_hostagesFreed < _requiredRescues && _timeExpired) then { _result = 1; }; + if (_hostagesKilled >= _maxHostageLosses) then { _result = 1; }; (_result == 1) or - ((_hostagesInZone >= _limitSuccess) && (_hostagesKilled < _limitFail)) or - ((!isNil "_shooters") && (_shootersAlive <= 0) && (_hostagesInZone >= _limitSuccess) && (_hostagesKilled < _limitFail)) + ((_hostagesInZone >= _requiredRescues) && (_hostagesKilled < _maxHostageLosses)) or + ((!isNil "_shooters") && (_shootersAlive <= 0) && (_hostagesInZone >= _requiredRescues) && (_hostagesKilled < _maxHostageLosses)) } else { - if (_hostagesKilled >= _limitFail) then { _result = 1; }; + if (_hostagesKilled >= _maxHostageLosses) then { _result = 1; }; (_result == 1) or - ((_hostagesInZone >= _limitSuccess) && (_hostagesKilled < _limitFail)) or - ((!isNil "_shooters") && (_shootersAlive <= 0) && (_hostagesInZone >= _limitSuccess) && (_hostagesKilled < _limitFail)) + ((_hostagesInZone >= _requiredRescues) && (_hostagesKilled < _maxHostageLosses)) or + ((!isNil "_shooters") && (_shootersAlive <= 0) && (_hostagesInZone >= _requiredRescues) && (_hostagesKilled < _maxHostageLosses)) }; }; diff --git a/arma/server/addons/task/functions/fnc_hostageModule.sqf b/arma/server/addons/task/functions/fnc_hostageModule.sqf index 8a90a5f..9e9040b 100644 --- a/arma/server/addons/task/functions/fnc_hostageModule.sqf +++ b/arma/server/addons/task/functions/fnc_hostageModule.sqf @@ -30,8 +30,20 @@ if (_taskID isEqualTo "") exitWith { private _syncedModules = synchronizedObjects _logic; private _hostageModule = (_syncedModules select { typeOf _x isEqualTo "FORGE_Module_Hostages" }) param [0, objNull]; private _shooterModule = (_syncedModules select { typeOf _x isEqualTo "FORGE_Module_Shooters" }) param [0, objNull]; -private _hostageEntities = if (!isNull _hostageModule) then { synchronizedObjects _hostageModule } else { [] }; -private _shooterEntities = if (!isNull _shooterModule) then { synchronizedObjects _shooterModule } else { [] }; +private _hostageEntities = if (!isNull _hostageModule) then { + synchronizedObjects _hostageModule select { + (_x isKindOf "CAManBase") && { !(_x isKindOf "Logic") } + } +} else { + [] +}; +private _shooterEntities = if (!isNull _shooterModule) then { + synchronizedObjects _shooterModule select { + (_x isKindOf "CAManBase") && { !(_x isKindOf "Logic") } + } +} else { + [] +}; ["INFO", format [ "Hostage Module: TaskID: %1, ExtZone: %2, Hostages: %3, Shooters: %4, CBRN: %5, Execution: %6", diff --git a/arma/server/addons/task/functions/fnc_hvt.sqf b/arma/server/addons/task/functions/fnc_hvt.sqf index 97f54c8..a7548a1 100644 --- a/arma/server/addons/task/functions/fnc_hvt.sqf +++ b/arma/server/addons/task/functions/fnc_hvt.sqf @@ -66,6 +66,14 @@ waitUntil { }; _hvts = GVAR(TaskStore) call ["getTaskEntities", ["hvts", _taskID]]; + +if (_timeLimit isNotEqualTo 0) then { + waitUntil { + sleep 1; + GVAR(TaskStore) call ["isTaskAccepted", [_taskID]] + }; +}; + private _startTime = if (_timeLimit isNotEqualTo 0) then { floor(time) } else { nil }; waitUntil { diff --git a/arma/server/addons/task/functions/fnc_initTaskStore.sqf b/arma/server/addons/task/functions/fnc_initTaskStore.sqf index 5de0c4e..369000b 100644 --- a/arma/server/addons/task/functions/fnc_initTaskStore.sqf +++ b/arma/server/addons/task/functions/fnc_initTaskStore.sqf @@ -176,6 +176,26 @@ GVAR(TaskStore) = createHashMapObject [[ private _entry = _self call ["callTaskState", ["task:catalog:get", [_taskID], objNull]]; _entry isEqualType createHashMap }], + ["getTaskCatalogEntry", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { createHashMap }; + + private _entry = _self call ["callTaskState", ["task:catalog:get", [_taskID], createHashMap]]; + if !(_entry isEqualType createHashMap) exitWith { createHashMap }; + + _entry + }], + ["isTaskAccepted", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + private _entry = _self call ["getTaskCatalogEntry", [_taskID]]; + if (_entry isEqualTo createHashMap) exitWith { false }; + + (_entry getOrDefault ["accepted", false]) || { (_entry getOrDefault ["requesterUid", ""]) isNotEqualTo "" } + }], ["acceptTask", compileFinal { params [["_taskID", "", [""]], ["_requesterUid", "", [""]]]; @@ -278,6 +298,35 @@ GVAR(TaskStore) = createHashMapObject [[ +(_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", "", [""]]]; @@ -382,20 +431,31 @@ GVAR(TaskStore) = createHashMapObject [[ if (_taskID isEqualTo "") exitWith { 0 }; - private _nextCount = _self call ["callTaskState", ["task:defuse:increment", [_taskID], 0]]; - if !(_nextCount isEqualType 0) exitWith { 0 }; + ["task:defuse:increment", [_taskID]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; - _nextCount + 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 }], ["getDefuseCount", compileFinal { params [["_taskID", "", [""]]]; if (_taskID isEqualTo "") exitWith { 0 }; - private _defuseCount = _self call ["callTaskState", ["task:defuse:get", [_taskID], 0]]; - if !(_defuseCount isEqualType 0) exitWith { 0 }; + ["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 + }; - _defuseCount + parseNumber _result }], ["notifyParticipants", compileFinal { params [ diff --git a/arma/server/addons/task/functions/fnc_makeHostage.sqf b/arma/server/addons/task/functions/fnc_makeHostage.sqf index 4644ac8..3a34ed1 100644 --- a/arma/server/addons/task/functions/fnc_makeHostage.sqf +++ b/arma/server/addons/task/functions/fnc_makeHostage.sqf @@ -27,4 +27,11 @@ if (_taskID == "") exitWith { ["ERROR", "No task ID provided for entity"] call E SETVAR(_entity,assignedTask,_taskID); GVAR(TaskStore) call ["registerTaskEntity", ["hostages", _taskID, _entity]]; -if (alive _entity) then { [_entity, "hostage"] spawn FUNC(heartBeat); }; +if (alive _entity) then { + // Apply hostage protection immediately so nearby hostile AI cannot kill the + // unit before the scheduled heartbeat initializes the animation state. + _entity setCaptive true; + _entity enableAIFeature ["MOVE", false]; + + [_entity, "hostage"] spawn FUNC(heartBeat); +}; diff --git a/arma/server/addons/task/functions/fnc_makeIED.sqf b/arma/server/addons/task/functions/fnc_makeIED.sqf index 6f185ee..cf3f6ee 100644 --- a/arma/server/addons/task/functions/fnc_makeIED.sqf +++ b/arma/server/addons/task/functions/fnc_makeIED.sqf @@ -26,7 +26,7 @@ if (_time <= 0) exitWith { ["ERROR", "Invalid time provided for IED"] call EFUNC ["INFO", format ["Make IED: %1", _this]] call EFUNC(common,log); -SETVAR(_entity,assignedTask,_taskID); +SETPVAR(_entity,assignedTask,_taskID); GVAR(TaskStore) call ["registerTaskEntity", ["ieds", _taskID, _entity]]; if (alive _entity) then { [_entity, "ied", _time] spawn FUNC(heartBeat); }; diff --git a/arma/server/addons/task/prototypes/README.md b/arma/server/addons/task/prototypes/README.md new file mode 100644 index 0000000..1bb039a --- /dev/null +++ b/arma/server/addons/task/prototypes/README.md @@ -0,0 +1,26 @@ +# Task Object Prototypes + +This folder contains review-only `createHashMapObject` prototypes for task +instances. They are not part of runtime initialization. + +Current prototypes: +- `TaskInstanceBaseClass` +- `HostageTaskBaseClass` +- `DefuseTaskBaseClass` + +Source: +- [taskObjectPrototypes.sqf](./taskObjectPrototypes.sqf) + +Purpose: +- show what per-task instance objects could look like +- separate state ownership from the current long procedural functions +- avoid committing the live addon to a large refactor before the model is + reviewed + +Important design choice: +- these prototypes use explicit `markSucceeded`, `markFailed`, and `cleanup` + methods instead of relying on `#delete` + +That is intentional. `createHashMapObject` destructor timing is reference-based, +so `#delete` is not a good primitive for mission-critical task completion or +reward flow. diff --git a/arma/server/addons/task/prototypes/taskObjectPrototypes.sqf b/arma/server/addons/task/prototypes/taskObjectPrototypes.sqf new file mode 100644 index 0000000..f87bc57 --- /dev/null +++ b/arma/server/addons/task/prototypes/taskObjectPrototypes.sqf @@ -0,0 +1,290 @@ +#include "..\script_component.hpp" + +/* + * Review-only prototype for object-based task instances. + * + * This file is intentionally not referenced by XEH_PREP or runtime init. + * It exists so the current procedural task flows can be compared against + * a createHashMapObject-based design before any live refactor is attempted. + * + * Usage in debug/testing: + * private _prototypes = call compile preprocessFileLineNumbers + * "\forge\forge_server\addons\task\prototypes\taskObjectPrototypes.sqf"; + * + * private _task = createHashMapObject [ + * _prototypes get "HostageTaskBaseClass", + * [ + * "task_hostage_review", + * createHashMapFromArray [ + * ["hostages", [hostage1, hostage2]], + * ["shooters", [shooter1, shooter2]] + * ], + * createHashMapFromArray [ + * ["extractionZone", "hostage_extract"], + * ["limitSuccess", 2], + * ["limitFail", 1], + * ["execution", true], + * ["timeLimit", 900] + * ] + * ] + * ]; + */ + +#pragma hemtt ignore_variables ["_self"] + +GVAR(TaskInstanceBaseClass) = createHashMapFromArray [ + ["#type", "TaskInstanceBaseClass"], + ["#create", compileFinal { + params [ + ["_taskID", "", [""]], + ["_taskType", "", [""]], + ["_entities", createHashMap, [createHashMap]], + ["_taskParams", createHashMap, [createHashMap]] + ]; + + _self set ["taskID", _taskID]; + _self set ["taskType", _taskType]; + _self set ["entities", _entities]; + _self set ["taskParams", _taskParams]; + _self set ["status", "created"]; + _self set ["startedAt", -1]; + _self set ["finishedAt", -1]; + _self set ["failureReason", ""]; + _self set ["outcomeData", createHashMap]; + }], + ["getTaskID", compileFinal { + _self getOrDefault ["taskID", ""] + }], + ["getTaskType", compileFinal { + _self getOrDefault ["taskType", ""] + }], + ["getStatus", compileFinal { + _self getOrDefault ["status", "created"] + }], + ["markActive", compileFinal { + _self set ["status", "active"]; + _self set ["startedAt", serverTime]; + true + }], + ["markSucceeded", compileFinal { + params [["_outcomeData", createHashMap, [createHashMap]]]; + + _self set ["status", "succeeded"]; + _self set ["finishedAt", serverTime]; + _self set ["outcomeData", _outcomeData]; + true + }], + ["markFailed", compileFinal { + params [["_reason", "", [""]], ["_outcomeData", createHashMap, [createHashMap]]]; + + _self set ["status", "failed"]; + _self set ["finishedAt", serverTime]; + _self set ["failureReason", _reason]; + _self set ["outcomeData", _outcomeData]; + true + }], + ["cleanup", compileFinal { + false + }], + ["tick", compileFinal { + createHashMap + }], + ["runLoop", compileFinal { + false + }] +]; + +GVAR(HostageTaskBaseClass) = createHashMapFromArray [ + ["#base", GVAR(TaskInstanceBaseClass)], + ["#type", "HostageTaskBaseClass"], + ["#create", compileFinal { + params [ + ["_taskID", "", [""]], + ["_entities", createHashMap, [createHashMap]], + ["_taskParams", createHashMap, [createHashMap]] + ]; + + _self set ["taskID", _taskID]; + _self set ["taskType", "hostage"]; + _self set ["entities", _entities]; + _self set ["taskParams", _taskParams]; + _self set ["status", "created"]; + _self set ["startedAt", -1]; + _self set ["finishedAt", -1]; + _self set ["failureReason", ""]; + _self set ["outcomeData", createHashMap]; + + private _hostages = +(_entities getOrDefault ["hostages", []]); + private _shooters = +(_entities getOrDefault ["shooters", []]); + private _requiredRescues = _taskParams getOrDefault ["limitSuccess", -1]; + if (_requiredRescues < 0) then { _requiredRescues = count _hostages; }; + + private _maxHostageLosses = _taskParams getOrDefault ["limitFail", -1]; + if (_maxHostageLosses < 0) then { _maxHostageLosses = count _hostages; }; + + _self set ["hostages", _hostages]; + _self set ["shooters", _shooters]; + _self set ["extractionZone", _taskParams getOrDefault ["extractionZone", ""]]; + _self set ["timeLimit", _taskParams getOrDefault ["timeLimit", 0]]; + _self set ["execution", _taskParams getOrDefault ["execution", false]]; + _self set ["cbrn", _taskParams getOrDefault ["cbrn", false]]; + _self set ["cbrnZone", _taskParams getOrDefault ["cbrnZone", ""]]; + _self set ["requiredRescues", _requiredRescues]; + _self set ["maxHostageLosses", _maxHostageLosses]; + }], + ["countFreedHostages", compileFinal { + private _playerGroups = allPlayers apply { group _x }; + private _hostages = _self getOrDefault ["hostages", []]; + + { + alive _x && { ((group _x) in _playerGroups) || { !captive _x } } + } count _hostages + }], + ["countHostagesInZone", compileFinal { + private _extZone = _self getOrDefault ["extractionZone", ""]; + private _hostages = _self getOrDefault ["hostages", []]; + + if (_extZone isEqualTo "") exitWith { 0 }; + { _x inArea _extZone } count _hostages + }], + ["countKilledHostages", compileFinal { + private _hostages = _self getOrDefault ["hostages", []]; + { !alive _x } count _hostages + }], + ["countAliveShooters", compileFinal { + private _shooters = _self getOrDefault ["shooters", []]; + { alive _x } count _shooters + }], + ["tick", compileFinal { + private _startedAt = _self getOrDefault ["startedAt", -1]; + private _timeLimit = _self getOrDefault ["timeLimit", 0]; + private _killed = _self call ["countKilledHostages", []]; + private _freed = _self call ["countFreedHostages", []]; + private _inZone = _self call ["countHostagesInZone", []]; + private _shootersAlive = _self call ["countAliveShooters", []]; + private _requiredRescues = _self getOrDefault ["requiredRescues", 0]; + private _maxHostageLosses = _self getOrDefault ["maxHostageLosses", 0]; + private _timeExpired = false; + + if (_timeLimit > 0 && { _startedAt >= 0 }) then { + _timeExpired = (serverTime - _startedAt) >= _timeLimit; + }; + + createHashMapFromArray [ + ["freed", _freed], + ["inZone", _inZone], + ["killed", _killed], + ["shootersAlive", _shootersAlive], + ["requiredRescues", _requiredRescues], + ["maxHostageLosses", _maxHostageLosses], + ["timeExpired", _timeExpired], + ["shouldFail", (_killed >= _maxHostageLosses) || { _timeExpired && { _freed < _requiredRescues } }], + ["shouldSucceed", (_inZone >= _requiredRescues) && { _killed < _maxHostageLosses }] + ] + }], + ["runLoop", compileFinal { + _self call ["markActive", []]; + + while { (_self call ["getStatus", []]) isEqualTo "active" } do { + private _snapshot = _self call ["tick", []]; + + if (_snapshot getOrDefault ["shouldFail", false]) exitWith { + _self call ["markFailed", ["Hostage fail conditions met.", _snapshot]]; + }; + + if (_snapshot getOrDefault ["shouldSucceed", false]) exitWith { + _self call ["markSucceeded", [_snapshot]]; + }; + + sleep 1; + }; + + true + }] +]; + +GVAR(DefuseTaskBaseClass) = createHashMapFromArray [ + ["#base", GVAR(TaskInstanceBaseClass)], + ["#type", "DefuseTaskBaseClass"], + ["#create", compileFinal { + params [ + ["_taskID", "", [""]], + ["_entities", createHashMap, [createHashMap]], + ["_taskParams", createHashMap, [createHashMap]] + ]; + + _self set ["taskID", _taskID]; + _self set ["taskType", "defuse"]; + _self set ["entities", _entities]; + _self set ["taskParams", _taskParams]; + _self set ["status", "created"]; + _self set ["startedAt", -1]; + _self set ["finishedAt", -1]; + _self set ["failureReason", ""]; + _self set ["outcomeData", createHashMap]; + + private _ieds = +(_entities getOrDefault ["ieds", []]); + private _protected = +(_entities getOrDefault ["protected", []]); + private _requiredDefusals = _taskParams getOrDefault ["limitSuccess", -1]; + if (_requiredDefusals < 0) then { _requiredDefusals = count _ieds; }; + + private _maxProtectedLosses = _taskParams getOrDefault ["limitFail", -1]; + if (_maxProtectedLosses < 0) then { _maxProtectedLosses = count _protected; }; + + _self set ["ieds", _ieds]; + _self set ["protected", _protected]; + _self set ["requiredDefusals", _requiredDefusals]; + _self set ["maxProtectedLosses", _maxProtectedLosses]; + _self set ["iedTimer", _taskParams getOrDefault ["iedTimer", 300]]; + }], + ["countProtectedDestroyed", compileFinal { + private _protected = _self getOrDefault ["protected", []]; + { !alive _x } count _protected + }], + ["getDefuseCount", compileFinal { + private _taskID = _self getOrDefault ["taskID", ""]; + if (_taskID isEqualTo "") exitWith { 0 }; + + GVAR(TaskStore) call ["getDefuseCount", [_taskID]] + }], + ["tick", compileFinal { + private _defusedCount = _self call ["getDefuseCount", []]; + private _protectedDestroyed = _self call ["countProtectedDestroyed", []]; + private _requiredDefusals = _self getOrDefault ["requiredDefusals", 0]; + private _maxProtectedLosses = _self getOrDefault ["maxProtectedLosses", 0]; + + createHashMapFromArray [ + ["defusedCount", _defusedCount], + ["protectedDestroyed", _protectedDestroyed], + ["requiredDefusals", _requiredDefusals], + ["maxProtectedLosses", _maxProtectedLosses], + ["shouldFail", (_protectedDestroyed >= _maxProtectedLosses) && { _maxProtectedLosses > 0 }], + ["shouldSucceed", (_defusedCount >= _requiredDefusals) && { _requiredDefusals > 0 } && { _protectedDestroyed < _maxProtectedLosses || { _maxProtectedLosses <= 0 } }] + ] + }], + ["runLoop", compileFinal { + _self call ["markActive", []]; + + while { (_self call ["getStatus", []]) isEqualTo "active" } do { + private _snapshot = _self call ["tick", []]; + + if (_snapshot getOrDefault ["shouldFail", false]) exitWith { + _self call ["markFailed", ["Defuse fail conditions met.", _snapshot]]; + }; + + if (_snapshot getOrDefault ["shouldSucceed", false]) exitWith { + _self call ["markSucceeded", [_snapshot]]; + }; + + sleep 1; + }; + + true + }] +]; + +createHashMapFromArray [ + ["TaskInstanceBaseClass", GVAR(TaskInstanceBaseClass)], + ["HostageTaskBaseClass", GVAR(HostageTaskBaseClass)], + ["DefuseTaskBaseClass", GVAR(DefuseTaskBaseClass)] +]