diff --git a/arma/server/addons/actor/XEH_preInit.sqf b/arma/server/addons/actor/XEH_preInit.sqf index 46cf2f8..d47f97b 100644 --- a/arma/server/addons/actor/XEH_preInit.sqf +++ b/arma/server/addons/actor/XEH_preInit.sqf @@ -18,7 +18,7 @@ PREP_RECOMPILE_END; if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid UID!" }; - private _finalData = GVAR(ActorStore) call ["get", [GVAR(Registry), _uid, _field]]; + private _finalData = GVAR(ActorStore) call ["get", [_uid, _field]]; private _player = [_uid] call EFUNC(common,getPlayer); [CRPC(actor,responseSyncActor), [_finalData], _player] call CFUNC(targetEvent); @@ -29,7 +29,7 @@ PREP_RECOMPILE_END; if (_uid isEqualTo "" || _field isEqualTo "") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid UID or Key!" }; - private _hashMap = GVAR(ActorStore) call ["set", [GVAR(Registry), "actor:update", _uid, _field, _value, _sync]]; + private _hashMap = GVAR(ActorStore) call ["set", [_uid, _field, _value, _sync]]; private _player = [_uid] call EFUNC(common,getPlayer); [CRPC(actor,responseSyncActor), [_hashMap], _player] call CFUNC(targetEvent); @@ -41,7 +41,7 @@ PREP_RECOMPILE_END; if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid UID!" }; if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid field pairs!" }; - private _hashMap = GVAR(ActorStore) call ["mset", [GVAR(Registry), "actor:update", _uid, _fieldValuePairs, _sync]]; + private _hashMap = GVAR(ActorStore) call ["mset", [_uid, _fieldValuePairs, _sync]]; private _player = [_uid] call EFUNC(common,getPlayer); [CRPC(actor,responseSyncActor), [_hashMap], _player] call CFUNC(targetEvent); @@ -53,7 +53,7 @@ PREP_RECOMPILE_END; if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid UID!" }; GVAR(ActorStore) call ["snapshot", [_uid]]; - private _finalData = GVAR(ActorStore) call ["save", [GVAR(Registry), "actor:update", _uid]]; + private _finalData = GVAR(ActorStore) call ["save", [_uid]]; private _player = [_uid] call EFUNC(common,getPlayer); [CRPC(actor,responseSyncActor), [_finalData], _player] call CFUNC(targetEvent); @@ -63,5 +63,5 @@ PREP_RECOMPILE_END; params [["_uid", "", [""]]]; if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid UID!" }; - GVAR(ActorStore) call ["remove", [GVAR(Registry), _uid]]; + GVAR(ActorStore) call ["remove", [_uid]]; }] call CFUNC(addEventHandler); diff --git a/arma/server/addons/actor/functions/fnc_initActorStore.sqf b/arma/server/addons/actor/functions/fnc_initActorStore.sqf index 5dbba52..7a83da8 100644 --- a/arma/server/addons/actor/functions/fnc_initActorStore.sqf +++ b/arma/server/addons/actor/functions/fnc_initActorStore.sqf @@ -9,8 +9,8 @@ * * Description: * Initializes the actor store for managing player actor data. - * Actor hot state is owned by the extension; SQF maintains a compatibility - * mirror for engine-adjacent consumers. + * Actor hot state is owned by the extension; SQF acts as a thin bridge for + * engine-adjacent reads, snapshots, and response fan-out. * * Arguments: * None @@ -38,7 +38,7 @@ GVAR(ActorModel) = compileFinal createHashMapObject [[ _actor set ["state", "HEALTHY"]; _actor set ["phone_number", ""]; _actor set ["email", ""]; - _actor set ["organization", ""]; + _actor set ["organization", "default"]; _actor set ["holster", true]; _actor @@ -109,7 +109,6 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [ ["#base", EGVAR(common,BaseStore)], ["#type", "ActorBaseStore"], ["#create", compileFinal { - GVAR(Registry) = createHashMap; ["INFO", "Actor Store Initialized!"] call EFUNC(common,log); }], ["cacheActor", compileFinal { @@ -117,9 +116,7 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [ if (_uid isEqualTo "" || { !(_actor isEqualType createHashMap) }) exitWith { createHashMap }; - private _finalActor = GVAR(ActorModel) call ["migrate", [+_actor]]; - GVAR(Registry) set [_uid, _finalActor]; - _finalActor + GVAR(ActorModel) call ["migrate", [+_actor]] }], ["callHotActor", compileFinal { params [["_function", "", [""]], ["_arguments", [], [[]]]]; @@ -138,6 +135,20 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [ if !(_data isEqualType createHashMap) exitWith { createHashMap }; _data }], + ["listHotUids", compileFinal { + ["actor:hot:keys", []] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; + if !(_isSuccess) exitWith { [] }; + if !(_result isEqualType "") exitWith { [] }; + if ((_result find "Error:") == 0) exitWith { + ["ERROR", format ["Actor extension call '%1' failed: %2", "actor:hot:keys", _result]] call EFUNC(common,log); + [] + }; + + private _uids = fromJSON _result; + if !(_uids isEqualType []) exitWith { [] }; + + _uids select { _x isEqualType "" && { _x isNotEqualTo "" } } + }], ["loadHotActor", compileFinal { params [["_uid", "", [""]], ["_initialize", false, [false]]]; @@ -149,75 +160,10 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [ _self call ["cacheActor", [_uid, _actor]] }], - ["normalizeGetArgs", compileFinal { - params ["_rawArguments"]; - - if ((_rawArguments param [0, createHashMap]) isEqualType createHashMap) exitWith { - [ - _rawArguments param [1, "", [""]], - _rawArguments param [2, "", [""]] - ] - }; - - [ - _rawArguments param [0, "", [""]], - _rawArguments param [1, "", [""]] - ] - }], - ["normalizeSetArgs", compileFinal { - params ["_rawArguments"]; - - if ((_rawArguments param [0, createHashMap]) isEqualType createHashMap) exitWith { - [ - _rawArguments param [2, "", [""]], - _rawArguments param [3, "", [""]], - _rawArguments param [4, nil, [0, "", [], false, createHashMap, objNull, grpNull]], - _rawArguments param [5, false, [false]] - ] - }; - - [ - _rawArguments param [0, "", [""]], - _rawArguments param [1, "", [""]], - _rawArguments param [2, nil, [0, "", [], false, createHashMap, objNull, grpNull]], - _rawArguments param [3, false, [false]] - ] - }], - ["normalizeMSetArgs", compileFinal { - params ["_rawArguments"]; - - if ((_rawArguments param [0, createHashMap]) isEqualType createHashMap) exitWith { - [ - _rawArguments param [2, "", [""]], - _rawArguments param [3, createHashMap, [createHashMap]], - _rawArguments param [4, false, [false]] - ] - }; - - [ - _rawArguments param [0, "", [""]], - _rawArguments param [1, createHashMap, [createHashMap]], - _rawArguments param [2, false, [false]] - ] - }], - ["normalizeUidArg", compileFinal { - params ["_rawArguments"]; - - if ((_rawArguments param [0, createHashMap]) isEqualType createHashMap) exitWith { - _rawArguments param [1, "", [""]] - }; - - _rawArguments param [0, "", [""]] - }], ["init", compileFinal { params [["_uid", "", [""]]]; private _player = [_uid] call EFUNC(common,getPlayer); - private _cached = GVAR(Registry) getOrDefault [_uid, nil]; - if !(isNil { _cached }) exitWith { - [CRPC(actor,responseInitActor), [_cached], _player] call CFUNC(targetEvent); - _cached - }; ["actor:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; if !(_isSuccess) exitWith { @@ -264,7 +210,7 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [ _finalActor }], ["get", compileFinal { - call (_self get "normalizeGetArgs") params ["_uid", "_field"]; + params [["_uid", "", [""]], ["_field", "", [""]]]; private _actor = _self call ["loadHotActor", [_uid, false]]; if (_actor isEqualTo createHashMap) then { @@ -274,6 +220,50 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [ if (_field isEqualTo "") exitWith { _actor }; _actor getOrDefault [_field, nil] }], + ["load", compileFinal { + params [["_uid", "", [""]]]; + + private _actor = _self call ["get", [_uid, ""]]; + if !(_actor isEqualType createHashMap) exitWith { createHashMap }; + + _actor + }], + ["getFieldOrDefault", compileFinal { + params [["_uid", "", [""]], ["_field", "", [""]], ["_default", nil]]; + + if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { _default }; + + private _actor = _self call ["load", [_uid]]; + if !(_actor isEqualType createHashMap) exitWith { _default }; + if (_actor isEqualTo createHashMap) exitWith { _default }; + + _actor getOrDefault [_field, _default] + }], + ["getOrganization", compileFinal { + params [["_uid", "", [""]], ["_default", "default", [""]]]; + + private _orgID = _self call ["getFieldOrDefault", [_uid, "organization", _default]]; + if !(_orgID isEqualType "") exitWith { _default }; + if (_orgID isEqualTo "") exitWith { _default }; + + _orgID + }], + ["getName", compileFinal { + params [["_uid", "", [""]], ["_default", "", [""]]]; + + private _name = _self call ["getFieldOrDefault", [_uid, "name", _default]]; + if !(_name isEqualType "") exitWith { _default }; + + _name + }], + ["getPhoneNumber", compileFinal { + params [["_uid", "", [""]], ["_default", "", [""]]]; + + private _phoneNumber = _self call ["getFieldOrDefault", [_uid, "phone_number", _default]]; + if !(_phoneNumber isEqualType "") exitWith { _default }; + + _phoneNumber + }], ["override", compileFinal { params [ ["_uid", "", [""]], @@ -297,7 +287,12 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [ _self call ["cacheActor", [_uid, _actor]] }], ["set", compileFinal { - call (_self get "normalizeSetArgs") params ["_uid", "_field", "_value", "_sync"]; + params [ + ["_uid", "", [""]], + ["_field", "", [""]], + ["_value", nil, [0, "", [], false, createHashMap, objNull, grpNull]], + ["_sync", false, [false]] + ]; if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap }; @@ -312,7 +307,7 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [ createHashMapFromArray [[_field, _updatedActor getOrDefault [_field, _value]]] }], ["mset", compileFinal { - call (_self get "normalizeMSetArgs") params ["_uid", "_fieldValuePairs", "_sync"]; + params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]]; if (_uid isEqualTo "" || { !(_fieldValuePairs isEqualType createHashMap) }) exitWith { createHashMap }; @@ -327,7 +322,7 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [ +_fieldValuePairs }], ["save", compileFinal { - private _uid = call (_self get "normalizeUidArg"); + params [["_uid", "", [""]]]; if (_uid isEqualTo "") exitWith { createHashMap }; private _actor = _self call ["callHotActor", ["actor:hot:save", [_uid]]]; @@ -336,11 +331,10 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [ _self call ["cacheActor", [_uid, _actor]] }], ["remove", compileFinal { - private _uid = call (_self get "normalizeUidArg"); + params [["_uid", "", [""]]]; if (_uid isEqualTo "") exitWith { false }; - GVAR(Registry) deleteAt _uid; ["actor:hot:remove", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; _isSuccess && { _result isEqualTo "OK" } }], diff --git a/arma/server/addons/bank/functions/fnc_initPayloadBuilder.sqf b/arma/server/addons/bank/functions/fnc_initPayloadBuilder.sqf index 6cd9bcd..24f8615 100644 --- a/arma/server/addons/bank/functions/fnc_initPayloadBuilder.sqf +++ b/arma/server/addons/bank/functions/fnc_initPayloadBuilder.sqf @@ -62,10 +62,7 @@ GVAR(BankPayloadBuilder) = createHashMapObject [[ ]; if (_uid isEqualTo "") exitWith { _defaultState }; - private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; - private _orgID = _actor getOrDefault ["organization", "default"]; - if (_orgID isEqualTo "") then { _orgID = "default"; }; - + private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_uid]]; private _org = EGVAR(org,OrgStore) call ["loadById", [_orgID]]; if (_org isEqualTo createHashMap) then { _org = EGVAR(org,OrgStore) call ["loadById", ["default"]]; diff --git a/arma/server/addons/bank/functions/fnc_initSessionManager.sqf b/arma/server/addons/bank/functions/fnc_initSessionManager.sqf index dc9077e..7d4774b 100644 --- a/arma/server/addons/bank/functions/fnc_initSessionManager.sqf +++ b/arma/server/addons/bank/functions/fnc_initSessionManager.sqf @@ -24,16 +24,20 @@ #pragma hemtt ignore_variables ["_self"] GVAR(BankSessionManager) = createHashMapObject [[ ["#type", "BankSessionManager"], + ["#create", compileFinal { + _self set ["sessions", createHashMap]; + }], ["getSessionState", compileFinal { params [["_uid", "", [""]]]; - private _session = GVAR(SessionRegistry) getOrDefault [_uid, createHashMap]; + private _sessions = _self getOrDefault ["sessions", createHashMap]; + private _session = _sessions getOrDefault [_uid, createHashMap]; if (_session isEqualTo createHashMap) then { _session = createHashMapFromArray [ ["atmAuthorized", false], ["mode", "bank"] ]; - GVAR(SessionRegistry) set [_uid, _session]; + _sessions set [_uid, _session]; }; _session @@ -44,9 +48,10 @@ GVAR(BankSessionManager) = createHashMapObject [[ if (_uid isEqualTo "") exitWith { createHashMap }; private _session = +(_self call ["getSessionState", [_uid]]); + private _sessions = _self getOrDefault ["sessions", createHashMap]; { _session set [_x, _y]; } forEach _fieldValuePairs; - GVAR(SessionRegistry) set [_uid, _session]; + _sessions set [_uid, _session]; _session }], ["resolveMode", compileFinal { diff --git a/arma/server/addons/bank/functions/fnc_initStore.sqf b/arma/server/addons/bank/functions/fnc_initStore.sqf index 507f236..d04e37b 100644 --- a/arma/server/addons/bank/functions/fnc_initStore.sqf +++ b/arma/server/addons/bank/functions/fnc_initStore.sqf @@ -18,7 +18,6 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [ ["#base", EGVAR(common,BaseStore)], ["#type", "BankBaseStore"], ["#create", compileFinal { - GVAR(SessionRegistry) = createHashMap; ["INFO", "Bank Store Initialized!"] call EFUNC(common,log); }], ["normalizeAccount", compileFinal { @@ -228,6 +227,17 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [ false }; + private _persistenceFailures = []; + private _savedBank = _self call ["save", [_uid]]; + if (_savedBank isEqualTo createHashMap) then { + _persistenceFailures pushBack "bank"; + }; + + private _orgPersistenceMessage = _orgResult getOrDefault ["persistenceMessage", ""]; + if !(_orgResult getOrDefault ["persisted", false]) then { + _persistenceFailures pushBack "organization"; + }; + GVAR(BankMessenger) call ["sendAccountSync", [_uid, _bankPatch]]; GVAR(BankMessenger) call ["sendNotification", [_uid, "info", "Bank", _orgResult getOrDefault ["message", format ["Repaid $%1 toward the organization credit line.", [_amount] call EFUNC(common,formatNumber)]]]]; @@ -241,6 +251,19 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [ } forEach (_orgResult getOrDefault ["memberUids", []]); }; + if (_persistenceFailures isNotEqualTo []) then { + private _warning = format [ + "Credit repayment posted, but durable save failed for: %1.", + _persistenceFailures joinString ", " + ]; + if (_orgPersistenceMessage isNotEqualTo "") then { + _warning = format ["%1 %2", _warning, _orgPersistenceMessage]; + }; + + ["ERROR", format ["Credit repayment for %1 completed with persistence failures: %2", _uid, _persistenceFailures joinString ", "]] call EFUNC(common,log); + GVAR(BankMessenger) call ["sendAlert", [_uid, "warning", _warning]]; + }; + _self call ["hydrateSession", [_uid, "", false]]; true }], diff --git a/arma/server/addons/cad/functions/fnc_initActivityRepository.sqf b/arma/server/addons/cad/functions/fnc_initActivityRepository.sqf index 3fab742..fa9b05b 100644 --- a/arma/server/addons/cad/functions/fnc_initActivityRepository.sqf +++ b/arma/server/addons/cad/functions/fnc_initActivityRepository.sqf @@ -22,48 +22,19 @@ #pragma hemtt ignore_variables ["_self"] GVAR(ActivityRepositoryBaseClass) = compileFinal createHashMapFromArray [ ["#type", "CadActivityRepositoryBaseClass"], - ["#create", compileFinal { - _self set ["activityRegistry", []]; - _self set ["persistenceLoaded", false]; - }], - ["restorePersistedActivity", compileFinal { - if (_self getOrDefault ["persistenceLoaded", false]) exitWith { true }; - - private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; - if (_persistenceService isEqualTo createHashMap) exitWith { false }; - - private _result = _persistenceService call ["loadActivity", []]; - if !(_result getOrDefault ["success", false]) exitWith { false }; - - _self set ["activityRegistry", +(_result getOrDefault ["data", []])]; - _self set ["persistenceLoaded", true]; - true - }], ["appendEntry", compileFinal { params [["_entry", createHashMap, [createHashMap]]]; if (_entry isEqualTo createHashMap) exitWith { false }; - - _self call ["restorePersistedActivity", []]; - - private _activityRegistry = +(_self getOrDefault ["activityRegistry", []]); private _finalEntry = +_entry; if ((_finalEntry getOrDefault ["timestamp", -1]) < 0) then { _finalEntry set ["timestamp", serverTime]; }; - _activityRegistry pushBack _finalEntry; - - if ((count _activityRegistry) > 50) then { - _activityRegistry deleteRange [0, (count _activityRegistry) - 50]; - }; - - _self set ["activityRegistry", _activityRegistry]; private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; - if (_persistenceService isNotEqualTo createHashMap) then { - _persistenceService call ["appendActivity", [_finalEntry]]; - }; - true + if (_persistenceService isEqualTo createHashMap) exitWith { false }; + + _persistenceService call ["appendActivity", [_finalEntry]] }], ["appendActivity", compileFinal { params [ @@ -85,8 +56,13 @@ GVAR(ActivityRepositoryBaseClass) = compileFinal createHashMapFromArray [ _self call ["appendEntry", [_entry]] }], ["getActivity", compileFinal { - _self call ["restorePersistedActivity", []]; - +(_self getOrDefault ["activityRegistry", []]) + private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; + if (_persistenceService isEqualTo createHashMap) exitWith { [] }; + + private _result = _persistenceService call ["loadActivity", []]; + if !(_result getOrDefault ["success", false]) exitWith { [] }; + + +(_result getOrDefault ["data", []]) }] ]; diff --git a/arma/server/addons/cad/functions/fnc_initAssignmentRepository.sqf b/arma/server/addons/cad/functions/fnc_initAssignmentRepository.sqf index cb3d5be..b8ab1ae 100644 --- a/arma/server/addons/cad/functions/fnc_initAssignmentRepository.sqf +++ b/arma/server/addons/cad/functions/fnc_initAssignmentRepository.sqf @@ -24,15 +24,51 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [ ["#type", "CadAssignmentRepositoryBaseClass"], ["#create", compileFinal { - _self set ["assignmentRegistry", createHashMap]; - _self set ["dispatchOrderRegistry", createHashMap]; - _self set ["persistenceLoaded", false]; + _self set ["ownershipHydrated", false]; + }], + ["loadState", compileFinal { + private _result = createHashMapFromArray [ + ["success", false], + ["assignments", createHashMap], + ["dispatchOrders", createHashMap] + ]; + + private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; + if (_persistenceService isEqualTo createHashMap) exitWith { _result }; + + private _assignmentsResult = _persistenceService call ["loadAssignments", []]; + if !(_assignmentsResult getOrDefault ["success", false]) exitWith { _result }; + + private _ordersResult = _persistenceService call ["loadDispatchOrders", []]; + if !(_ordersResult getOrDefault ["success", false]) exitWith { _result }; + + private _assignmentRegistry = +(_assignmentsResult getOrDefault ["data", createHashMap]); + private _dispatchOrderRegistry = +(_ordersResult getOrDefault ["data", createHashMap]); + + if !(_self getOrDefault ["ownershipHydrated", false]) then { + { + if ((_y getOrDefault ["state", ""]) isNotEqualTo "acknowledged") then { continue; }; + if ((_y getOrDefault ["acknowledgedByUid", ""]) isEqualTo "") then { continue; }; + if ((_dispatchOrderRegistry getOrDefault [_x, createHashMap]) isNotEqualTo createHashMap) then { continue; }; + if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) isNotEqualTo "active") then { continue; }; + + EGVAR(task,TaskStore) call ["bindTaskOwnership", [_x, _y getOrDefault ["acknowledgedByUid", ""]]]; + } forEach _assignmentRegistry; + + _self set ["ownershipHydrated", true]; + }; + + _result set ["success", true]; + _result set ["assignments", _assignmentRegistry]; + _result set ["dispatchOrders", _dispatchOrderRegistry]; + _result }], ["pruneAssignments", compileFinal { - _self call ["restorePersistedState", []]; + private _state = _self call ["loadState", []]; + if !(_state getOrDefault ["success", false]) exitWith { 0 }; - private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap]; - private _dispatchOrderRegistry = _self getOrDefault ["dispatchOrderRegistry", createHashMap]; + private _assignmentRegistry = _state getOrDefault ["assignments", createHashMap]; + private _dispatchOrderRegistry = _state getOrDefault ["dispatchOrders", createHashMap]; private _keysToRemove = []; { @@ -46,12 +82,6 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [ }; } forEach _assignmentRegistry; - { - _assignmentRegistry deleteAt _x; - } forEach _keysToRemove; - - _self set ["assignmentRegistry", _assignmentRegistry]; - private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; if (_persistenceService isNotEqualTo createHashMap) then { { @@ -62,50 +92,73 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [ count _keysToRemove }], ["getAssignments", compileFinal { - _self call ["restorePersistedState", []]; - values (_self getOrDefault ["assignmentRegistry", createHashMap]) + private _state = _self call ["loadState", []]; + if !(_state getOrDefault ["success", false]) exitWith { [] }; + + values (_state getOrDefault ["assignments", createHashMap]) }], ["isDispatchOrder", compileFinal { params [["_taskID", "", [""]]]; if (_taskID isEqualTo "") exitWith { false }; - ((_self getOrDefault ["dispatchOrderRegistry", createHashMap]) getOrDefault [_taskID, createHashMap]) isNotEqualTo createHashMap + private _state = _self call ["loadState", []]; + if !(_state getOrDefault ["success", false]) exitWith { false }; + + ((_state getOrDefault ["dispatchOrders", createHashMap]) getOrDefault [_taskID, createHashMap]) isNotEqualTo createHashMap }], - ["restorePersistedState", compileFinal { - if (_self getOrDefault ["persistenceLoaded", false]) exitWith { true }; + ["getAssignmentByTaskId", compileFinal { + params [["_taskID", "", [""]]]; - private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; - if (_persistenceService isEqualTo createHashMap) exitWith { false }; + if (_taskID isEqualTo "") exitWith { createHashMap }; - private _assignmentsResult = _persistenceService call ["loadAssignments", []]; - if !(_assignmentsResult getOrDefault ["success", false]) exitWith { false }; + private _state = _self call ["loadState", []]; + if !(_state getOrDefault ["success", false]) exitWith { createHashMap }; - private _ordersResult = _persistenceService call ["loadDispatchOrders", []]; - if !(_ordersResult getOrDefault ["success", false]) exitWith { false }; + +((_state getOrDefault ["assignments", createHashMap]) getOrDefault [_taskID, createHashMap]) + }], + ["getDispatchOrderByTaskId", compileFinal { + params [["_taskID", "", [""]]]; - private _assignmentRegistry = +(_assignmentsResult getOrDefault ["data", createHashMap]); - private _dispatchOrderRegistry = +(_ordersResult getOrDefault ["data", createHashMap]); + if (_taskID isEqualTo "") exitWith { createHashMap }; - _self set ["assignmentRegistry", _assignmentRegistry]; - _self set ["dispatchOrderRegistry", _dispatchOrderRegistry]; - _self set ["persistenceLoaded", true]; + private _state = _self call ["loadState", []]; + if !(_state getOrDefault ["success", false]) exitWith { createHashMap }; + + +((_state getOrDefault ["dispatchOrders", createHashMap]) getOrDefault [_taskID, createHashMap]) + }], + ["getCurrentTaskIdForGroup", compileFinal { + params [["_groupID", "", [""]]]; + + if (_groupID isEqualTo "") exitWith { "" }; + + private _state = _self call ["loadState", []]; + if !(_state getOrDefault ["success", false]) exitWith { "" }; + + private _assignmentRegistry = _state getOrDefault ["assignments", createHashMap]; + private _dispatchOrderRegistry = _state getOrDefault ["dispatchOrders", createHashMap]; + private _taskID = ""; { - if ((_y getOrDefault ["state", ""]) isNotEqualTo "acknowledged") then { continue; }; - if (((_y getOrDefault ["acknowledgedByUid", ""]) isEqualTo "")) then { continue; }; - if ((_dispatchOrderRegistry getOrDefault [_x, createHashMap]) isNotEqualTo createHashMap) then { continue; }; - if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) isNotEqualTo "active") then { continue; }; - EGVAR(task,TaskStore) call ["bindTaskOwnership", [_x, _y getOrDefault ["acknowledgedByUid", ""]]]; + if ((_y getOrDefault ["groupId", ""]) isNotEqualTo _groupID) then { continue; }; + if !((_y getOrDefault ["state", ""]) in ["assigned", "acknowledged"]) then { continue; }; + + private _dispatchOrder = +(_dispatchOrderRegistry getOrDefault [_x, createHashMap]); + if (_dispatchOrder isEqualTo createHashMap) then { + if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) isNotEqualTo "active") then { continue; }; + _taskID = _x; + } else { + _taskID = _dispatchOrder getOrDefault ["title", _x]; + }; } forEach _assignmentRegistry; - true + _taskID }], ["buildDispatchOrderEntry", compileFinal { params [ ["_taskID", "", [""]], ["_order", createHashMap, [createHashMap]], - ["_assignmentRegistry", createHashMap, [createHashMap]], + ["_assignment", createHashMap, [createHashMap]], ["_groupRepository", createHashMap, [createHashMap]] ]; @@ -127,7 +180,6 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [ }; }; - private _assignment = _assignmentRegistry getOrDefault [_taskID, createHashMap]; _entry set ["taskId", _taskID]; _entry set ["taskID", _taskID]; _entry set ["type", _entry getOrDefault ["type", "dispatch_order"]]; @@ -136,6 +188,23 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [ _entry set ["assignmentState", [_assignment getOrDefault ["state", ""], "unassigned"] select (_assignment isEqualTo createHashMap)]; _entry }], + ["buildDispatchOrderEntryForTask", compileFinal { + params [ + ["_taskID", "", [""]], + ["_groupRepository", createHashMap, [createHashMap]] + ]; + + if (_taskID isEqualTo "") exitWith { createHashMap }; + + private _state = _self call ["loadState", []]; + if !(_state getOrDefault ["success", false]) exitWith { createHashMap }; + + private _order = +((_state getOrDefault ["dispatchOrders", createHashMap]) getOrDefault [_taskID, createHashMap]); + if (_order isEqualTo createHashMap) exitWith { createHashMap }; + + private _assignment = +((_state getOrDefault ["assignments", createHashMap]) getOrDefault [_taskID, createHashMap]); + _self call ["buildDispatchOrderEntry", [_taskID, _order, _assignment, _groupRepository]] + }], ["assignTaskToGroup", compileFinal { params [ ["_requesterUid", "", [""]], @@ -150,16 +219,20 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [ ["assignment", createHashMap] ]; - _self call ["restorePersistedState", []]; - private _permissionService = _self getOrDefault ["permissionService", createHashMap]; if !(_permissionService call ["canDispatch", [_requesterUid]]) exitWith { _result set ["message", "You are not authorized to assign contracts."]; _result }; - private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap]; - private _dispatchOrderRegistry = _self getOrDefault ["dispatchOrderRegistry", createHashMap]; + private _state = _self call ["loadState", []]; + if !(_state getOrDefault ["success", false]) exitWith { + _result set ["message", "CAD extension state is unavailable."]; + _result + }; + + private _assignmentRegistry = _state getOrDefault ["assignments", createHashMap]; + private _dispatchOrderRegistry = _state getOrDefault ["dispatchOrders", createHashMap]; private _isDispatchOrder = (_dispatchOrderRegistry getOrDefault [_taskID, createHashMap]) isNotEqualTo createHashMap; if (!_isDispatchOrder && { (EGVAR(task,TaskStore) call ["getTaskStatus", [_taskID]]) isNotEqualTo "active" }) exitWith { @@ -221,9 +294,6 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [ _result }; - _assignmentRegistry set [_taskID, _assignment]; - _self set ["assignmentRegistry", _assignmentRegistry]; - private _activityEntry = +(_assignData getOrDefault ["activity", createHashMap]); if (_activityEntry isNotEqualTo createHashMap) then { private _activityRepository = _self getOrDefault ["activityRepository", createHashMap]; @@ -235,6 +305,9 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [ _result set ["assignment", _assignment]; _result set ["leaderUid", _leaderUid]; _result set ["isDispatchOrder", _isDispatchOrder]; + if (_isDispatchOrder) then { + _result set ["order", +(_dispatchOrderRegistry getOrDefault [_taskID, createHashMap])]; + }; _result }], ["createDispatchOrder", compileFinal { @@ -254,8 +327,6 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [ ["order", createHashMap] ]; - _self call ["restorePersistedState", []]; - private _permissionService = _self getOrDefault ["permissionService", createHashMap]; if !(_permissionService call ["canDispatch", [_requesterUid]]) exitWith { _result set ["message", "You are not authorized to create dispatch orders."]; @@ -337,14 +408,6 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [ _result }; - private _dispatchOrderRegistry = _self getOrDefault ["dispatchOrderRegistry", createHashMap]; - _dispatchOrderRegistry set [_taskID, _order]; - _self set ["dispatchOrderRegistry", _dispatchOrderRegistry]; - - private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap]; - _assignmentRegistry set [_taskID, _assignment]; - _self set ["assignmentRegistry", _assignmentRegistry]; - private _activityEntry = +(_createData getOrDefault ["activity", createHashMap]); if (_activityEntry isNotEqualTo createHashMap) then { private _activityRepository = _self getOrDefault ["activityRepository", createHashMap]; @@ -368,23 +431,25 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [ ["assignment", createHashMap] ]; - _self call ["restorePersistedState", []]; - private _permissionService = _self getOrDefault ["permissionService", createHashMap]; if !(_permissionService call ["canDispatch", [_requesterUid]]) exitWith { _result set ["message", "You are not authorized to close dispatch orders."]; _result }; - private _dispatchOrderRegistry = _self getOrDefault ["dispatchOrderRegistry", createHashMap]; - private _order = +(_dispatchOrderRegistry getOrDefault [_taskID, createHashMap]); + private _state = _self call ["loadState", []]; + if !(_state getOrDefault ["success", false]) exitWith { + _result set ["message", "CAD extension state is unavailable."]; + _result + }; + + private _order = +((_state getOrDefault ["dispatchOrders", createHashMap]) getOrDefault [_taskID, createHashMap]); if (_order isEqualTo createHashMap) exitWith { _result set ["message", "Dispatch order could not be resolved."]; _result }; - private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap]; - private _assignment = +(_assignmentRegistry getOrDefault [_taskID, createHashMap]); + private _assignment = +((_state getOrDefault ["assignments", createHashMap]) getOrDefault [_taskID, createHashMap]); private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; if (_persistenceService isEqualTo createHashMap) exitWith { @@ -399,14 +464,8 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [ }; private _closeData = +(_closeResult getOrDefault ["data", createHashMap]); - _order = +(_closeData getOrDefault ["order", _order]); _assignment = +(_closeData getOrDefault ["assignment", _assignment]); - _dispatchOrderRegistry deleteAt _taskID; - _self set ["dispatchOrderRegistry", _dispatchOrderRegistry]; - _assignmentRegistry deleteAt _taskID; - _self set ["assignmentRegistry", _assignmentRegistry]; - private _activityEntry = +(_closeData getOrDefault ["activity", createHashMap]); if (_activityEntry isNotEqualTo createHashMap) then { _activityEntry set ["actorUid", _requesterUid]; @@ -430,12 +489,14 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [ ]; private _transition = _this param [2, "acknowledge", [""]]; + private _state = _self call ["loadState", []]; + if !(_state getOrDefault ["success", false]) exitWith { + _result set ["message", "CAD extension state is unavailable."]; + _result + }; - _self call ["restorePersistedState", []]; - - private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap]; - private _assignment = +(_assignmentRegistry getOrDefault [_taskID, createHashMap]); - private _isDispatchOrder = _self call ["isDispatchOrder", [_taskID]]; + private _assignment = +((_state getOrDefault ["assignments", createHashMap]) getOrDefault [_taskID, createHashMap]); + private _isDispatchOrder = ((_state getOrDefault ["dispatchOrders", createHashMap]) getOrDefault [_taskID, createHashMap]) isNotEqualTo createHashMap; if (_assignment isEqualTo createHashMap) exitWith { _result set ["message", "Task is not assigned."]; _result @@ -508,16 +569,6 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [ _result }; - switch (_transition) do { - case "decline": { - _assignmentRegistry deleteAt _taskID; - }; - default { - _assignmentRegistry set [_taskID, _assignment]; - }; - }; - _self set ["assignmentRegistry", _assignmentRegistry]; - private _activityEntry = +(_transitionData getOrDefault ["activity", createHashMap]); if (_activityEntry isNotEqualTo createHashMap) then { if (_isDispatchOrder) then { diff --git a/arma/server/addons/cad/functions/fnc_initCadStore.sqf b/arma/server/addons/cad/functions/fnc_initCadStore.sqf index 07db1f0..ad70578 100644 --- a/arma/server/addons/cad/functions/fnc_initCadStore.sqf +++ b/arma/server/addons/cad/functions/fnc_initCadStore.sqf @@ -10,6 +10,11 @@ * Initializes the CAD store as a coordinator over activity, group, * assignment, and permission domain objects. * + * CAD operational state is extension-backed but intentionally transient. + * Orders, requests, assignments, hydrate state, and recent activity are + * scoped to the active server/mission lifecycle and start fresh after a + * restart. + * * Arguments: * None * @@ -133,17 +138,13 @@ GVAR(CadStoreBaseClass) = compileFinal createHashMapFromArray [ private _leaderUid = _result getOrDefault ["leaderUid", ""]; if (_leaderUid isEqualTo "") exitWith { false }; + private _assignmentRepository = _self get "AssignmentRepository"; private _message = if (_result getOrDefault ["isDispatchOrder", false]) then { private _order = _result getOrDefault ["order", createHashMap]; if (_order isEqualTo createHashMap) then { private _assignment = _result getOrDefault ["assignment", createHashMap]; private _taskID = _assignment getOrDefault ["taskId", ""]; - _order = (_self get "AssignmentRepository") call ["buildDispatchOrderEntry", [ - _taskID, - ((_self get "AssignmentRepository") getOrDefault ["dispatchOrderRegistry", createHashMap]) getOrDefault [_taskID, createHashMap], - (_self get "AssignmentRepository") getOrDefault ["assignmentRegistry", createHashMap], - _self get "GroupRepository" - ]]; + _order = _assignmentRepository call ["buildDispatchOrderEntryForTask", [_taskID, _self get "GroupRepository"]]; }; format ["Dispatch order assigned: %1. Open CAD to review and acknowledge.", _order getOrDefault ["title", "Dispatch Order"]] @@ -203,15 +204,10 @@ GVAR(CadStoreBaseClass) = compileFinal createHashMapFromArray [ private _permissionService = _self get "PermissionService"; private _groupRepository = _self get "GroupRepository"; - private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; - if (_actor isEqualTo createHashMap && { _uid isNotEqualTo "" }) then { - _actor = EGVAR(actor,ActorStore) call ["init", [_uid]]; - }; - private _groupID = _groupRepository call ["getPlayerGroupId", [_uid]]; private _session = createHashMapFromArray [ ["uid", _uid], - ["orgId", _actor getOrDefault ["organization", "default"]], + ["orgId", EGVAR(actor,ActorStore) call ["getOrganization", [_uid]]], ["isDispatcher", _permissionService call ["canDispatch", [_uid]]], ["groupId", _groupID], ["isLeader", _groupRepository call ["isGroupLeader", [_uid, _groupID]]] diff --git a/arma/server/addons/cad/functions/fnc_initGroupRepository.sqf b/arma/server/addons/cad/functions/fnc_initGroupRepository.sqf index 3a119a7..9d6f8dc 100644 --- a/arma/server/addons/cad/functions/fnc_initGroupRepository.sqf +++ b/arma/server/addons/cad/functions/fnc_initGroupRepository.sqf @@ -24,8 +24,6 @@ GVAR(GroupRepositoryBaseClass) = compileFinal createHashMapFromArray [ ["#type", "CadGroupRepositoryBaseClass"], ["#create", compileFinal { - _self set ["groupRegistry", createHashMap]; - _self set ["groupProfileRegistry", createHashMap]; _self set ["validStatuses", [ "available", "en_route", @@ -63,31 +61,11 @@ GVAR(GroupRepositoryBaseClass) = compileFinal createHashMapFromArray [ if (_groupID isEqualTo "") exitWith { "" }; private _assignmentRepository = _self getOrDefault ["assignmentRepository", createHashMap]; - private _assignmentRegistry = _assignmentRepository getOrDefault ["assignmentRegistry", createHashMap]; - private _dispatchOrderRegistry = _assignmentRepository getOrDefault ["dispatchOrderRegistry", createHashMap]; - private _taskID = ""; + if (_assignmentRepository isEqualTo createHashMap) exitWith { "" }; - { - if ((_y getOrDefault ["groupId", ""]) isNotEqualTo _groupID) then { continue; }; - if !((_y getOrDefault ["state", ""]) in ["assigned", "acknowledged"]) then { continue; }; - private _dispatchOrder = +(_dispatchOrderRegistry getOrDefault [_x, createHashMap]); - if (_dispatchOrder isEqualTo createHashMap) then { - if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) isNotEqualTo "active") then { continue; }; - _taskID = _x; - } else { - _taskID = _dispatchOrder getOrDefault ["title", _x]; - }; - - } forEach _assignmentRegistry; - - _taskID + _assignmentRepository call ["getCurrentTaskIdForGroup", [_groupID]] }], ["syncGroups", compileFinal { - private _assignmentRepository = _self getOrDefault ["assignmentRepository", createHashMap]; - if (_assignmentRepository isNotEqualTo createHashMap) then { - _assignmentRepository call ["restorePersistedState", []]; - }; - private _liveGroups = []; { @@ -106,16 +84,9 @@ GVAR(GroupRepositoryBaseClass) = compileFinal createHashMapFromArray [ if (_groupID isEqualTo "") then { continue; }; private _leaderUid = getPlayerUID _leader; - private _actor = EGVAR(actor,Registry) getOrDefault [_leaderUid, createHashMap]; - if (_actor isEqualTo createHashMap && { _leaderUid isNotEqualTo "" }) then { - _actor = EGVAR(actor,ActorStore) call ["init", [_leaderUid]]; - }; - - private _orgID = _actor getOrDefault ["organization", "default"]; - if (_orgID isEqualTo "") then { _orgID = "default"; }; + private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_leaderUid]]; private _memberUids = []; private _memberRoster = []; - { private _memberUid = getPlayerUID _x; private _memberState = toLowerANSI (lifeState _x); @@ -158,7 +129,6 @@ GVAR(GroupRepositoryBaseClass) = compileFinal createHashMapFromArray [ }; private _nextRegistry = createHashMap; - private _profileRegistry = createHashMap; { if !(_x isEqualType createHashMap) then { continue; }; private _groupID = _x getOrDefault ["groupId", ""]; @@ -166,15 +136,8 @@ GVAR(GroupRepositoryBaseClass) = compileFinal createHashMapFromArray [ private _groupRecord = +_x; _nextRegistry set [_groupID, _groupRecord]; - _profileRegistry set [_groupID, createHashMapFromArray [ - ["groupId", _groupID], - ["role", _groupRecord getOrDefault ["role", "infantry"]], - ["status", _groupRecord getOrDefault ["status", "available"]] - ]]; } forEach _mergedGroups; - _self set ["groupProfileRegistry", _profileRegistry]; - _self set ["groupRegistry", _nextRegistry]; _nextRegistry }], ["getGroupRecord", compileFinal { @@ -309,12 +272,6 @@ GVAR(GroupRepositoryBaseClass) = compileFinal createHashMapFromArray [ _groupRecord set ["status", _profile getOrDefault ["status", _groupRecord getOrDefault ["status", "available"]]]; _groupRecord set ["lastUpdate", serverTime]; - private _profileRegistry = _self getOrDefault ["groupProfileRegistry", createHashMap]; - _groupRegistry set [_groupID, _groupRecord]; - _self set ["groupRegistry", _groupRegistry]; - _profileRegistry set [_groupID, _profile]; - _self set ["groupProfileRegistry", _profileRegistry]; - private _activityEntry = +(_profileData getOrDefault ["activity", createHashMap]); if (_activityEntry isNotEqualTo createHashMap) then { private _activityRepository = _self getOrDefault ["activityRepository", createHashMap]; diff --git a/arma/server/addons/cad/functions/fnc_initPermissionService.sqf b/arma/server/addons/cad/functions/fnc_initPermissionService.sqf index 07fea10..f27f215 100644 --- a/arma/server/addons/cad/functions/fnc_initPermissionService.sqf +++ b/arma/server/addons/cad/functions/fnc_initPermissionService.sqf @@ -27,10 +27,7 @@ GVAR(PermissionServiceBaseClass) = compileFinal createHashMapFromArray [ if (_uid isEqualTo "") exitWith { false }; - private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; - if (_actor isEqualTo createHashMap) exitWith { false }; - - private _orgID = _actor getOrDefault ["organization", "default"]; + private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_uid]]; private _org = EGVAR(org,OrgStore) call ["loadById", [_orgID]]; if (_org isEqualTo createHashMap) exitWith { false }; diff --git a/arma/server/addons/cad/functions/fnc_initPersistenceService.sqf b/arma/server/addons/cad/functions/fnc_initPersistenceService.sqf index dd4c053..e1ad1e1 100644 --- a/arma/server/addons/cad/functions/fnc_initPersistenceService.sqf +++ b/arma/server/addons/cad/functions/fnc_initPersistenceService.sqf @@ -10,6 +10,10 @@ * Initializes the CAD extension-state service that bridges live SQF * state to the Rust extension for hot CAD storage and recent history. * + * This is a live operational cache, not a durable persistence layer. + * CAD extension state is expected to reset with the current server or + * mission lifecycle. + * * Arguments: * None * diff --git a/arma/server/addons/cad/functions/fnc_initRequestRepository.sqf b/arma/server/addons/cad/functions/fnc_initRequestRepository.sqf index 7a6f89b..eeedad4 100644 --- a/arma/server/addons/cad/functions/fnc_initRequestRepository.sqf +++ b/arma/server/addons/cad/functions/fnc_initRequestRepository.sqf @@ -24,8 +24,6 @@ GVAR(RequestRepositoryBaseClass) = compileFinal createHashMapFromArray [ ["#type", "CadRequestRepositoryBaseClass"], ["#create", compileFinal { - _self set ["requestRegistry", createHashMap]; - _self set ["persistenceLoaded", false]; _self set ["validTypes", [ "medevac_9line", "ace_lace", @@ -39,20 +37,14 @@ GVAR(RequestRepositoryBaseClass) = compileFinal createHashMapFromArray [ "emergency" ]]; }], - ["restorePersistedState", compileFinal { - if (_self getOrDefault ["persistenceLoaded", false]) exitWith { true }; - + ["loadRequestRegistry", compileFinal { private _persistenceService = _self getOrDefault ["persistenceService", createHashMap]; - if (_persistenceService isEqualTo createHashMap) exitWith { false }; + if (_persistenceService isEqualTo createHashMap) exitWith { createHashMap }; private _result = _persistenceService call ["loadRequests", []]; - if !(_result getOrDefault ["success", false]) exitWith { false }; + if !(_result getOrDefault ["success", false]) exitWith { createHashMap }; - private _requestRegistry = +(_result getOrDefault ["data", createHashMap]); - - _self set ["requestRegistry", _requestRegistry]; - _self set ["persistenceLoaded", true]; - true + +(_result getOrDefault ["data", createHashMap]) }], ["submitRequest", compileFinal { params [ @@ -68,8 +60,6 @@ GVAR(RequestRepositoryBaseClass) = compileFinal createHashMapFromArray [ ["request", createHashMap] ]; - _self call ["restorePersistedState", []]; - private _finalType = toLowerANSI _type; if !(_finalType in (_self getOrDefault ["validTypes", []])) exitWith { _result set ["message", "Invalid support request type."]; @@ -132,10 +122,6 @@ GVAR(RequestRepositoryBaseClass) = compileFinal createHashMapFromArray [ _result }; - private _requestRegistry = _self getOrDefault ["requestRegistry", createHashMap]; - _requestRegistry set [_requestID, _request]; - _self set ["requestRegistry", _requestRegistry]; - private _activityEntry = +(_submitData getOrDefault ["activity", createHashMap]); if (_activityEntry isNotEqualTo createHashMap) then { private _activityRepository = _self getOrDefault ["activityRepository", createHashMap]; @@ -156,9 +142,7 @@ GVAR(RequestRepositoryBaseClass) = compileFinal createHashMapFromArray [ ["request", createHashMap] ]; - _self call ["restorePersistedState", []]; - - private _requestRegistry = _self getOrDefault ["requestRegistry", createHashMap]; + private _requestRegistry = _self call ["loadRequestRegistry", []]; private _request = +(_requestRegistry getOrDefault [_requestID, createHashMap]); if (_request isEqualTo createHashMap) exitWith { _result set ["message", "Support request could not be resolved."]; @@ -188,8 +172,6 @@ GVAR(RequestRepositoryBaseClass) = compileFinal createHashMapFromArray [ private _closeData = +(_closeResult getOrDefault ["data", createHashMap]); _request = +(_closeData getOrDefault ["request", _request]); - _requestRegistry deleteAt _requestID; - _self set ["requestRegistry", _requestRegistry]; private _activityEntry = +(_closeData getOrDefault ["activity", createHashMap]); if (_activityEntry isNotEqualTo createHashMap) then { diff --git a/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf b/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf index 830cee0..613ef18 100644 --- a/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf +++ b/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf @@ -25,7 +25,7 @@ GVAR(FEconomyStore) = createHashMapObject [[ ["#type", "IFuelEconomy"], ["#create", { GVAR(FuelCost) = 5; - GVAR(FuelRegistry) = createHashMap; + _self set ["fuelRegistry", createHashMap]; ["INFO", "Fuel Store Initialized!", nil, nil] call EFUNC(common,log); }], @@ -34,15 +34,17 @@ GVAR(FEconomyStore) = createHashMapObject [[ private _index = netId _target; private _uid = getPlayerUID _unit; + private _fuelRegistry = _self getOrDefault ["fuelRegistry", createHashMap]; - GVAR(FuelRegistry) set [_index, _uid]; + _fuelRegistry set [_index, _uid]; SETVAR(_target,liters,0); }], ["stop", { params ["_source", "_target"]; private _index = netId _target; - private _uid = GVAR(FuelRegistry) get _index; + private _fuelRegistry = _self getOrDefault ["fuelRegistry", createHashMap]; + private _uid = _fuelRegistry get _index; private _player = [_uid] call EFUNC(common,getPlayer); private _totalLiters = GETVAR(_target,liters,0); @@ -51,7 +53,7 @@ GVAR(FEconomyStore) = createHashMapObject [[ private _formattedTotalLiters = _totalLiters toFixed 2; [CRPC(notifications,recieveNotification), ["info", "Refueling", format ["Refueling complete: %1L
Total Cost: $%2", _formattedTotalLiters, _formattedTotalCost]], _player] call CFUNC(targetEvent); - GVAR(FuelRegistry) deleteAt _index; + _fuelRegistry deleteAt _index; }] ]]; diff --git a/arma/server/addons/extension/functions/fnc_extCall.sqf b/arma/server/addons/extension/functions/fnc_extCall.sqf index e91ae1a..7d7ec29 100644 --- a/arma/server/addons/extension/functions/fnc_extCall.sqf +++ b/arma/server/addons/extension/functions/fnc_extCall.sqf @@ -37,6 +37,7 @@ private _transportResponseFunctions = [ "actor:update", "actor:hot:init", "actor:hot:get", + "actor:hot:keys", "actor:hot:save", "bank:get", "bank:create", diff --git a/arma/server/addons/locker/functions/fnc_initVAStore.sqf b/arma/server/addons/locker/functions/fnc_initVAStore.sqf index 567010b..ecbc234 100644 --- a/arma/server/addons/locker/functions/fnc_initVAStore.sqf +++ b/arma/server/addons/locker/functions/fnc_initVAStore.sqf @@ -4,7 +4,7 @@ * File: fnc_initVAStore.sqf * Author: IDSolutions * Date: 2025-12-17 - * Last Update: 2026-04-01 + * Last Update: 2026-04-05 * Public: No * * Description: @@ -28,7 +28,7 @@ GVAR(VArsenalModel) = compileFinal createHashMapObject [[ private _vArsenal = createHashMap; _vArsenal set ["backpacks", ["B_AssaultPack_rgr"]]; - _vArsenal set ["items", ["FirstAidKit", "G_Combat", "H_Cap_blk_ION", "H_HelmetB", "ItemCompass", "ItemGPS", "ItemMap", "ItemRadio", "ItemWatch", "U_IG_Guerrilla_6_1", "V_TacVest_oli", "ACE_EarPlugs"]]; + _vArsenal set ["items", ["FirstAidKit", "G_Combat", "H_Cap_blk_ION", "H_HelmetB", "ItemCompass", "ItemGPS", "ItemMap", "ItemRadio", "ItemWatch", "U_BG_Guerrilla_6_1", "V_TacVest_oli", "ACE_EarPlugs"]]; _vArsenal set ["magazines", ["16Rnd_9x21_Mag", "30Rnd_65x39_caseless_black_mag", "Chemlight_blue", "Chemlight_green", "Chemlight_red", "Chemlight_yellow", "HandGrenade", "SmokeShell", "SmokeShellBlue", "SmokeShellGreen", "SmokeShellOrange", "SmokeShellPurple", "SmokeShellRed", "SmokeShellYellow"]]; _vArsenal set ["weapons", ["arifle_MX_F", "hgun_P07_F"]]; diff --git a/arma/server/addons/main/XEH_PREP.hpp b/arma/server/addons/main/XEH_PREP.hpp index 3a1cf90..f4df82a 100644 --- a/arma/server/addons/main/XEH_PREP.hpp +++ b/arma/server/addons/main/XEH_PREP.hpp @@ -1,2 +1,3 @@ PREP(initStores); +PREP(initValidationHarness); PREP(saveHotState); diff --git a/arma/server/addons/main/XEH_preInit.sqf b/arma/server/addons/main/XEH_preInit.sqf index 929f1ff..be9e270 100644 --- a/arma/server/addons/main/XEH_preInit.sqf +++ b/arma/server/addons/main/XEH_preInit.sqf @@ -4,8 +4,6 @@ PREP_RECOMPILE_START; #include "XEH_PREP.hpp" PREP_RECOMPILE_END; -GVAR(PlayerBootstrapRegistry) = createHashMap; - ["forge_icom_event", { params [["_event", "", [""]], ["_data", createHashMap, [createHashMap]]]; diff --git a/arma/server/addons/main/functions/fnc_initStores.sqf b/arma/server/addons/main/functions/fnc_initStores.sqf index 69f5259..d1b9275 100644 --- a/arma/server/addons/main/functions/fnc_initStores.sqf +++ b/arma/server/addons/main/functions/fnc_initStores.sqf @@ -47,3 +47,6 @@ if (isNil QEGVAR(org,OrgStore)) then { call EFUNC(org,initOrgStore); }; // Store if (isNil QEGVAR(store,StoreStore)) then { call EFUNC(store,initStoreStore); }; + +// Validation Harness +if (isNil QGVAR(ValidationHarness)) then { call FUNC(initValidationHarness); }; diff --git a/arma/server/addons/main/functions/fnc_initValidationHarness.sqf b/arma/server/addons/main/functions/fnc_initValidationHarness.sqf new file mode 100644 index 0000000..786531b --- /dev/null +++ b/arma/server/addons/main/functions/fnc_initValidationHarness.sqf @@ -0,0 +1,213 @@ +#include "..\script_component.hpp" + +/* + * Author: IDSolutions + * Initializes the server-side validation harness for targeted runtime smoke + * checks around high-risk multi-module flows. + * + * Arguments: + * None + * + * Return Value: + * Validation harness object + * + * Example: + * call forge_server_main_fnc_initValidationHarness; + * + * Public: No + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(ValidationHarness) = createHashMapObject [[ + ["#type", "ValidationHarness"], + ["buildResult", compileFinal { + params [ + ["_action", "", [""]], + ["_success", false, [false]], + ["_message", "", [""]], + ["_data", createHashMap, [createHashMap]] + ]; + + createHashMapFromArray [ + ["action", _action], + ["success", _success], + ["message", _message], + ["data", _data] + ] + }], + ["logResult", compileFinal { + params [["_result", createHashMap, [createHashMap]]]; + + if (_result isEqualTo createHashMap) exitWith { _result }; + + private _level = ["WARNING", "INFO"] select (_result getOrDefault ["success", false]); + private _action = _result getOrDefault ["action", "validation"]; + private _message = _result getOrDefault ["message", ""]; + [_level, format ["Validation harness '%1': %2", _action, _message]] call EFUNC(common,log); + + _result + }], + ["normalizeMapArg", compileFinal { + params [ + ["_value", createHashMap, [createHashMap, ""]], + ["_fallback", createHashMap, [createHashMap]] + ]; + + if (_value isEqualType createHashMap) exitWith { +_value }; + if !(_value isEqualType "") exitWith { +_fallback }; + if (_value isEqualTo "") exitWith { +_fallback }; + + private _parsed = fromJSON _value; + if !(_parsed isEqualType createHashMap) exitWith { +_fallback }; + + _parsed + }], + ["run", compileFinal { + params [["_action", "", [""]], ["_arguments", [], [[]]]]; + + private _actionLower = toLowerANSI _action; + if (_actionLower isEqualTo "") exitWith { + _self call ["logResult", [_self call ["buildResult", ["unknown", false, "A validation action is required.", createHashMap]]]] + }; + + switch (_actionLower) do { + case "save_hot_state": { + _arguments params [["_uid", "", [""]]]; + + private _success = [_uid] call FUNC(saveHotState); + private _message = [ + format ["Hot-state save failed for '%1'.", _uid], + format ["Hot-state save completed for '%1'.", [_uid, "all hot state"] select (_uid isEqualTo "")] + ] select _success; + + _self call ["logResult", [_self call ["buildResult", [ + _actionLower, + _success, + _message, + createHashMapFromArray [["uid", _uid]] + ]]]] + }; + case "store_checkout": { + _arguments params [["_uid", "", [""]], ["_payload", createHashMap, [createHashMap, ""]]]; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (_uid isEqualTo "" || { isNull _player }) exitWith { + _self call ["logResult", [_self call ["buildResult", [_actionLower, false, "A valid online player UID is required for store checkout validation.", createHashMap]]]] + }; + + private _payloadMap = _self call ["normalizeMapArg", [_payload, createHashMap]]; + if (_payloadMap isEqualTo createHashMap) exitWith { + _self call ["logResult", [_self call ["buildResult", [_actionLower, false, "Store checkout validation payload was invalid.", createHashMap]]]] + }; + + private _result = EGVAR(store,StoreStore) call ["checkout", [_uid, _player, toJSON _payloadMap]]; + private _success = _result getOrDefault ["success", false]; + private _message = _result getOrDefault ["message", "Store checkout validation completed."]; + + _self call ["logResult", [_self call ["buildResult", [_actionLower, _success, _message, _result]]]] + }; + case "org_assign_credit_line": { + _arguments params [ + ["_requesterUid", "", [""]], + ["_memberUid", "", [""]], + ["_memberName", "", [""]], + ["_amount", 0, [0]] + ]; + + private _result = EGVAR(org,OrgStore) call ["assignCreditLine", [_requesterUid, _memberUid, _memberName, _amount]]; + private _success = _result getOrDefault ["success", false]; + private _message = _result getOrDefault ["message", "Credit line validation completed."]; + + _self call ["logResult", [_self call ["buildResult", [_actionLower, _success, _message, _result]]]] + }; + case "bank_credit_repayment": { + _arguments params [["_uid", "", [""]], ["_amount", 0, [0]]]; + + if (_uid isEqualTo "") exitWith { + _self call ["logResult", [_self call ["buildResult", [_actionLower, false, "A valid UID is required for bank credit repayment validation.", createHashMap]]]] + }; + + private _beforeAccount = EGVAR(bank,BankStore) call ["get", [_uid, ""]]; + private _beforeOrgState = EGVAR(bank,BankPayloadBuilder) call ["resolveOrgState", [_uid]]; + private _success = EGVAR(bank,BankStore) call ["repayCreditLine", [_uid, _amount]]; + private _afterAccount = EGVAR(bank,BankStore) call ["get", [_uid, ""]]; + private _afterOrgState = EGVAR(bank,BankPayloadBuilder) call ["resolveOrgState", [_uid]]; + + private _message = [ + format ["Bank credit repayment validation failed for %1.", _uid], + format ["Bank credit repayment validation completed for %1.", _uid] + ] select _success; + + _self call ["logResult", [_self call ["buildResult", [ + _actionLower, + _success, + _message, + createHashMapFromArray [ + ["beforeAccount", _beforeAccount], + ["afterAccount", _afterAccount], + ["beforeOrgState", _beforeOrgState], + ["afterOrgState", _afterOrgState] + ] + ]]]] + }; + case "task_reward_context": { + _arguments params [["_taskID", "", [""]]]; + + private _context = EGVAR(task,TaskStore) call ["resolveRewardContext", [_taskID]]; + private _success = _taskID isNotEqualTo "" && { (_context getOrDefault ["orgID", ""]) isNotEqualTo "" }; + private _message = [ + format ["No reward context was available for task %1.", _taskID], + format ["Resolved reward context for task %1.", _taskID] + ] select _success; + + _self call ["logResult", [_self call ["buildResult", [_actionLower, _success, _message, _context]]]] + }; + case "task_apply_rating": { + _arguments params [["_taskID", "", [""]], ["_delta", 0, [0]]]; + + private _result = EGVAR(task,TaskStore) call ["applyRatingOutcome", [_taskID, _delta]]; + private _success = _result getOrDefault ["success", true]; + private _message = [ + _result getOrDefault ["message", format ["Task rating validation failed for %1.", _taskID]], + format ["Task rating validation completed for %1.", _taskID] + ] select _success; + + _self call ["logResult", [_self call ["buildResult", [_actionLower, _success, _message, _result]]]] + }; + case "task_apply_rewards": { + _arguments params [["_taskID", "", [""]], ["_rewards", createHashMap, [createHashMap, ""]]]; + + private _rewardsMap = _self call ["normalizeMapArg", [_rewards, createHashMap]]; + if (_taskID isEqualTo "" || { _rewardsMap isEqualTo createHashMap }) exitWith { + _self call ["logResult", [_self call ["buildResult", [_actionLower, false, "Task reward validation requires a task ID and reward payload.", createHashMap]]]] + }; + + private _rewardContext = EGVAR(task,TaskStore) call ["resolveRewardContext", [_taskID]]; + private _beforeOrg = EGVAR(org,OrgStore) call ["loadById", [_rewardContext getOrDefault ["orgID", ""]]]; + private _success = [_taskID, _rewardsMap] call EFUNC(task,handleTaskRewards); + private _afterOrg = EGVAR(org,OrgStore) call ["loadById", [_rewardContext getOrDefault ["orgID", ""]]]; + + private _message = [ + format ["Task reward validation failed for %1.", _taskID], + format ["Task reward validation completed for %1.", _taskID] + ] select _success; + + _self call ["logResult", [_self call ["buildResult", [ + _actionLower, + _success, + _message, + createHashMapFromArray [ + ["rewardContext", _rewardContext], + ["beforeOrg", _beforeOrg], + ["afterOrg", _afterOrg] + ] + ]]]] + }; + default { + _self call ["logResult", [_self call ["buildResult", [_actionLower, false, format ["Unknown validation action '%1'.", _actionLower], createHashMap]]]] + }; + }; + }] +]]; + +GVAR(ValidationHarness) diff --git a/arma/server/addons/main/functions/fnc_saveHotState.sqf b/arma/server/addons/main/functions/fnc_saveHotState.sqf index 5fbfb98..dc819da 100644 --- a/arma/server/addons/main/functions/fnc_saveHotState.sqf +++ b/arma/server/addons/main/functions/fnc_saveHotState.sqf @@ -28,12 +28,12 @@ if (_uid isEqualTo "") then { }; } forEach allPlayers; - if !(isNil QEGVAR(actor,Registry)) then { + if !(isNil QEGVAR(actor,ActorStore)) then { { if (_x isNotEqualTo "") then { _uids pushBackUnique _x; }; - } forEach keys EGVAR(actor,Registry); + } forEach (EGVAR(actor,ActorStore) call ["listHotUids", []]); }; } else { _uids pushBack _uid; diff --git a/arma/server/addons/org/functions/fnc_initOrgStore.sqf b/arma/server/addons/org/functions/fnc_initOrgStore.sqf index 230564e..f0de6a0 100644 --- a/arma/server/addons/org/functions/fnc_initOrgStore.sqf +++ b/arma/server/addons/org/functions/fnc_initOrgStore.sqf @@ -4,7 +4,7 @@ * File: fnc_initOrgStore.sqf * Author: IDSolutions * Date: 2026-02-13 - * Last Update: 2026-04-01 + * Last Update: 2026-04-04 * Public: Yes * * Description: @@ -256,10 +256,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ if (_uid isEqualTo "") exitWith { "default" }; - private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; - private _orgID = _actor getOrDefault ["organization", "default"]; - if (_orgID isEqualTo "") then { _orgID = "default"; }; - _orgID + EGVAR(actor,ActorStore) call ["getOrganization", [_uid]] }], ["resolveActorName", compileFinal { params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]]; @@ -276,8 +273,8 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ if (_uid isEqualTo "" || { _orgID isEqualTo "" }) exitWith { createHashMap }; - private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", _orgID, false]]; - private _updatedActor = EGVAR(actor,ActorStore) call ["get", [_uid, ""]]; + private _actorPatch = EGVAR(actor,ActorStore) call ["set", [_uid, "organization", _orgID, false]]; + private _updatedActor = EGVAR(actor,ActorStore) call ["load", [_uid]]; if ( !(_updatedActor isEqualType createHashMap) || { _updatedActor isEqualTo createHashMap } @@ -375,8 +372,8 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ }; private _player = [_uid] call EFUNC(common,getPlayer); - private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; - private _orgID = _actor getOrDefault ["organization", "default"]; + private _actor = EGVAR(actor,ActorStore) call ["load", [_uid]]; + private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_uid]]; private _memberName = _self call ["resolveActorName", [_uid, _player, _actor]]; private _context = createHashMapFromArray [ ["requesterUid", _uid], @@ -418,8 +415,8 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ }; private _player = [_uid] call EFUNC(common,getPlayer); - private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; - private _orgID = _actor getOrDefault ["organization", "default"]; + private _actor = EGVAR(actor,ActorStore) call ["load", [_uid]]; + private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_uid]]; private _memberName = _self call ["resolveActorName", [_uid, _player, _actor]]; private _context = createHashMapFromArray [ ["requesterUid", _uid], @@ -438,7 +435,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ private _memberUid = _x getOrDefault ["uid", ""]; if (_memberUid isEqualTo "") then { continue; }; - private _memberActor = EGVAR(actor,Registry) getOrDefault [_memberUid, createHashMap]; + private _memberActor = EGVAR(actor,ActorStore) call ["load", [_memberUid]]; private _actorPatch = _self call ["applyActorOrganization", [_memberUid, _x getOrDefault ["actorOrganization", "default"], _memberActor]]; if (_actorPatch isEqualTo createHashMap) then { ["WARNING", format ["Failed to restore actor organization for %1 after org disband.", _memberUid]] call EFUNC(common,log); @@ -471,7 +468,9 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ ["success", false], ["message", ""], ["patch", createHashMap], - ["memberUids", []] + ["memberUids", []], + ["persisted", false], + ["persistenceMessage", ""] ]; if (_requesterUid isEqualTo "" || { _memberUid isEqualTo "" } || { _amount <= 0 }) exitWith { @@ -479,10 +478,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _result }; - private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap]; - private _orgID = _requesterActor getOrDefault ["organization", "default"]; - if (_orgID isEqualTo "") then { _orgID = "default"; }; - + private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]]; private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer); private _requesterIsDefaultOrgCeo = ( _requesterPlayer isNotEqualTo objNull @@ -509,7 +505,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _result set ["message", _envelope getOrDefault ["message", "Credit line assigned."]]; _result set ["patch", _envelope getOrDefault ["patch", createHashMap]]; _result set ["memberUids", _envelope getOrDefault ["memberUids", []]]; - _result + _self call ["persistMutationResult", [_orgID, _result, "Credit line assignment"]] }], ["repayCreditLine", compileFinal { params [["_requesterUid", "", [""]], ["_amount", 0, [0]]]; @@ -518,7 +514,9 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ ["success", false], ["message", ""], ["patch", createHashMap], - ["memberUids", []] + ["memberUids", []], + ["persisted", false], + ["persistenceMessage", ""] ]; if (_requesterUid isEqualTo "" || { _amount <= 0 }) exitWith { @@ -526,10 +524,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _result }; - private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap]; - private _orgID = _requesterActor getOrDefault ["organization", "default"]; - if (_orgID isEqualTo "") then { _orgID = "default"; }; - + private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]]; private _context = createHashMapFromArray [ ["requesterUid", _requesterUid], ["orgId", _orgID], @@ -546,13 +541,38 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _result set ["message", _envelope getOrDefault ["message", "Credit repayment posted."]]; _result set ["patch", _envelope getOrDefault ["patch", createHashMap]]; _result set ["memberUids", _envelope getOrDefault ["memberUids", []]]; - _result + _self call ["persistMutationResult", [_orgID, _result, "Credit repayment"]] }], ["buildPortalPayload", compileFinal { params [["_uid", "", [""]]]; GVAR(OrgPayloadBuilder) call ["buildPortalPayload", [_uid]] }], + ["persistMutationResult", compileFinal { + params [ + ["_orgID", "", [""]], + ["_result", createHashMap, [createHashMap]], + ["_actionLabel", "Organization update", [""]] + ]; + + if (_orgID isEqualTo "" || { _result isEqualTo createHashMap }) exitWith { _result }; + + if !(_result getOrDefault ["success", false]) exitWith { _result }; + + _result set ["persisted", false]; + _result set ["persistenceMessage", ""]; + + private _savedOrg = _self call ["saveById", [_orgID]]; + if (_savedOrg isEqualTo createHashMap) exitWith { + private _message = format ["%1 applied, but durable save failed for organization %2.", _actionLabel, _orgID]; + ["ERROR", _message] call EFUNC(common,log); + _result set ["persistenceMessage", _message]; + _result + }; + + _result set ["persisted", true]; + _result + }], ["chargeCheckout", compileFinal { params [["_requesterUid", "", [""]], ["_requesterPlayer", objNull, [objNull]], ["_source", "org_funds", [""]], ["_amount", 0, [0]], ["_commit", false, [false]]]; @@ -560,13 +580,12 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ ["success", false], ["message", "Unable to process organization payment."], ["patch", createHashMap], - ["memberUids", []] + ["memberUids", []], + ["persisted", false], + ["persistenceMessage", ""] ]; - private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap]; - private _orgID = _requesterActor getOrDefault ["organization", "default"]; - if (_orgID isEqualTo "") then { _orgID = "default"; }; - + private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]]; private _requesterIsDefaultOrgCeo = ( _requesterPlayer isNotEqualTo objNull && { _orgID isEqualTo "default" } @@ -589,7 +608,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _result set ["message", _envelope getOrDefault ["message", ""]]; _result set ["patch", _envelope getOrDefault ["patch", createHashMap]]; _result set ["memberUids", _envelope getOrDefault ["memberUids", []]]; - _result + _self call ["persistMutationResult", [_orgID, _result, "Organization checkout charge"]] }], ["saveById", compileFinal { params [["_orgID", "", [""]]]; @@ -605,7 +624,9 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ ["success", false], ["message", "Unable to update organization assets."], ["patch", createHashMap], - ["memberUids", []] + ["memberUids", []], + ["persisted", false], + ["persistenceMessage", ""] ]; if (_assets isEqualTo []) exitWith { @@ -616,8 +637,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ private _resolvedOrgID = _orgID; if (_resolvedOrgID isEqualTo "") then { - private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap]; - _resolvedOrgID = _requesterActor getOrDefault ["organization", "default"]; + _resolvedOrgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]]; }; if (_resolvedOrgID isEqualTo "") then { _resolvedOrgID = "default"; }; @@ -644,7 +664,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _result set ["message", _envelope getOrDefault ["message", ""]]; _result set ["patch", _envelope getOrDefault ["patch", createHashMap]]; _result set ["memberUids", _envelope getOrDefault ["memberUids", []]]; - _result + _self call ["persistMutationResult", [_resolvedOrgID, _result, "Organization asset update"]] }], ["addFleetVehicles", compileFinal { params [["_requesterUid", "", [""]], ["_vehicles", [], [[]]], ["_commit", false, [false]], ["_orgID", "", [""]]]; @@ -653,7 +673,9 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ ["success", false], ["message", "Unable to update organization fleet."], ["patch", createHashMap], - ["memberUids", []] + ["memberUids", []], + ["persisted", false], + ["persistenceMessage", ""] ]; if (_vehicles isEqualTo []) exitWith { @@ -664,8 +686,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ private _resolvedOrgID = _orgID; if (_resolvedOrgID isEqualTo "") then { - private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap]; - _resolvedOrgID = _requesterActor getOrDefault ["organization", "default"]; + _resolvedOrgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]]; }; if (_resolvedOrgID isEqualTo "") then { _resolvedOrgID = "default"; }; @@ -691,7 +712,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _result set ["message", _envelope getOrDefault ["message", ""]]; _result set ["patch", _envelope getOrDefault ["patch", createHashMap]]; _result set ["memberUids", _envelope getOrDefault ["memberUids", []]]; - _result + _self call ["persistMutationResult", [_resolvedOrgID, _result, "Organization fleet update"]] }], ["loadById", compileFinal { params [["_orgID", "", [""]]]; @@ -715,10 +736,9 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ _result }; - private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; - private _existingOrgID = _actor getOrDefault ["organization", ""]; - - private _orgID = _actor getOrDefault ["phone_number", ""]; + private _actor = EGVAR(actor,ActorStore) call ["load", [_uid]]; + private _existingOrgID = EGVAR(actor,ActorStore) call ["getOrganization", [_uid, ""]]; + private _orgID = EGVAR(actor,ActorStore) call ["getPhoneNumber", [_uid]]; if (_orgID isEqualTo "") exitWith { _result set ["message", "Player phone number was not available for organization registration."]; _result @@ -754,12 +774,8 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ params [["_uid", "", [""]]]; private _player = [_uid] call EFUNC(common,getPlayer); - private _actor = EGVAR(actor,Registry) get _uid; - private _orgID = _actor get "organization"; - if (_orgID isEqualTo "") then { - _orgID = "default"; - }; - + private _actor = EGVAR(actor,ActorStore) call ["load", [_uid]]; + private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_uid]]; private _finalOrg = _self call ["loadById", [_orgID]]; if (_finalOrg isEqualTo createHashMap) then { ["WARNING", format ["No existing org found for %1, using default org.", _uid]] call EFUNC(common,log); diff --git a/arma/server/addons/org/functions/fnc_initPayloadBuilder.sqf b/arma/server/addons/org/functions/fnc_initPayloadBuilder.sqf index 19fe960..3e91122 100644 --- a/arma/server/addons/org/functions/fnc_initPayloadBuilder.sqf +++ b/arma/server/addons/org/functions/fnc_initPayloadBuilder.sqf @@ -160,10 +160,8 @@ GVAR(OrgPayloadBuilder) = createHashMapObject [[ private _player = [_uid] call EFUNC(common,getPlayer); if (isNull _player) exitWith { createHashMap }; - private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; - private _orgID = _actor getOrDefault ["organization", "default"]; - if (_orgID isEqualTo "") then { _orgID = "default"; }; - + private _actor = EGVAR(actor,ActorStore) call ["load", [_uid]]; + private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_uid]]; private _org = _self call ["resolveOrgForUid", [_uid]]; if (_org isEqualTo createHashMap) exitWith { createHashMap }; diff --git a/arma/server/addons/store/functions/fnc_initStoreStore.sqf b/arma/server/addons/store/functions/fnc_initStoreStore.sqf index 61fb751..b77f94f 100644 --- a/arma/server/addons/store/functions/fnc_initStoreStore.sqf +++ b/arma/server/addons/store/functions/fnc_initStoreStore.sqf @@ -4,7 +4,7 @@ * File: fnc_initStoreStore.sqf * Author: IDSolutions * Date: 2026-03-12 - * Last Update: 2026-03-14 + * Last Update: 2026-04-04 * Public: No * * Description: @@ -50,10 +50,7 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [ _bankBalance = _bankAccount getOrDefault ["bank", 0]; }; - private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; - private _orgId = _actor getOrDefault ["organization", "default"]; - if (_orgId isEqualTo "") then { _orgId = "default"; }; - + private _orgId = EGVAR(actor,ActorStore) call ["getOrganization", [_uid]]; private _org = EGVAR(org,OrgStore) call ["loadById", [_orgId]]; if (_org isEqualTo createHashMap) then { _org = EGVAR(org,OrgStore) call ["loadById", ["default"]]; @@ -160,7 +157,10 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [ ["vehicleGranted", []], ["bankPatch", createHashMap], ["orgPatch", createHashMap], - ["orgTargetUids", []] + ["orgTargetUids", []], + ["persistenceSucceeded", false], + ["persistenceFailures", []], + ["persistenceMessage", ""] ] }], ["formatCurrency", compileFinal { @@ -255,6 +255,69 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [ true }], + ["persistCheckoutState", compileFinal { + params [ + ["_uid", "", [""]], + ["_orgID", "", [""]], + ["_backendResult", createHashMap, [createHashMap]] + ]; + + private _result = createHashMapFromArray [ + ["success", true], + ["failures", []], + ["message", ""] + ]; + + if (_uid isEqualTo "" || { _backendResult isEqualTo createHashMap }) exitWith { + _result set ["success", false]; + _result set ["failures", ["checkout"]]; + _result set ["message", "Checkout persistence context was invalid."]; + _result + }; + + private _persistenceFailures = []; + + if ((keys (_backendResult getOrDefault ["lockerPatch", createHashMap])) isNotEqualTo []) then { + if ((EGVAR(locker,LockerStore) call ["save", [_uid]]) isEqualTo createHashMap) then { + _persistenceFailures pushBack "locker"; + }; + }; + + if ((keys (_backendResult getOrDefault ["vaPatch", createHashMap])) isNotEqualTo []) then { + if ((EGVAR(locker,VAStore) call ["save", [_uid]]) isEqualTo createHashMap) then { + _persistenceFailures pushBack "virtual_arsenal"; + }; + }; + + if ((keys (_backendResult getOrDefault ["vgaragePatch", createHashMap])) isNotEqualTo []) then { + if ((EGVAR(garage,VGarageStore) call ["save", [_uid]]) isEqualTo createHashMap) then { + _persistenceFailures pushBack "virtual_garage"; + }; + }; + + if ((keys (_backendResult getOrDefault ["bankPatch", createHashMap])) isNotEqualTo []) then { + if ((EGVAR(bank,BankStore) call ["save", [_uid]]) isEqualTo createHashMap) then { + _persistenceFailures pushBack "bank"; + }; + }; + + if (_orgID isNotEqualTo "" && { (keys (_backendResult getOrDefault ["orgPatch", createHashMap])) isNotEqualTo [] }) then { + if ((EGVAR(org,OrgStore) call ["saveById", [_orgID]]) isEqualTo createHashMap) then { + _persistenceFailures pushBack "organization"; + }; + }; + + if (_persistenceFailures isNotEqualTo []) then { + _result set ["success", false]; + _result set ["failures", _persistenceFailures]; + _result set ["message", format [ + "Checkout completed, but durable save failed for: %1.", + _persistenceFailures joinString ", " + ]]; + }; + + _result + }], ["checkout", compileFinal { params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_payloadJson", "", [""]]]; @@ -310,6 +373,14 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [ }; _self call ["syncCheckoutResult", [_player, _backendResult]]; + private _persistenceResult = _self call [ + "persistCheckoutState", + [ + _uid, + _checkoutContext getOrDefault ["orgId", ""], + _backendResult + ] + ]; _result set ["success", true]; _result set ["message", _backendResult getOrDefault ["message", format [ @@ -320,6 +391,16 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [ ]]]; _result set ["lockerGranted", _backendResult getOrDefault ["lockerGranted", []]]; _result set ["vehicleGranted", _backendResult getOrDefault ["vehicleGranted", []]]; + _result set ["persistenceSucceeded", _persistenceResult getOrDefault ["success", false]]; + _result set ["persistenceFailures", _persistenceResult getOrDefault ["failures", []]]; + _result set ["persistenceMessage", _persistenceResult getOrDefault ["message", ""]]; + + if !(_persistenceResult getOrDefault ["success", false]) then { + private _warning = _persistenceResult getOrDefault ["message", "Checkout completed with persistence failures."]; + ["ERROR", format ["Store checkout for %1 completed with persistence failures: %2", _uid, (_persistenceResult getOrDefault ["failures", []]) joinString ", "]] call EFUNC(common,log); + _result set ["message", format ["%1 %2", _result get "message", _warning]]; + }; + _result }] ]; diff --git a/arma/server/addons/task/README.md b/arma/server/addons/task/README.md index 3ea76e3..ef67658 100644 --- a/arma/server/addons/task/README.md +++ b/arma/server/addons/task/README.md @@ -3,6 +3,10 @@ ## Overview The task addon is a server-owned mission/task system for Forge. It manages task execution, task-owned state, participant tracking, contribution-based player earnings, and org-owned rewards. +Task operational state is mission-scoped. The extension-backed task catalog, +ownership, status, and defuse state are reset on task store startup, so the +system intentionally starts clean after each server or mission restart. + ## Responsibilities - spawn and monitor task flows on the server - track per-task entities through `TaskStore` @@ -95,6 +99,7 @@ If you want the accepting player's org to own the task rewards, use `fnc_handler - the dynamic mission manager in `fnc_missionManager.sqf` is now limited to attack missions only - it starts server-owned tasks through `fnc_handler.sqf` and binds them to the `default` org - task lifecycle for the mission manager is tracked through `TaskStore` status entries +- task backend state is intentionally transient and resets with the active server/mission lifecycle - task rewards are org-owned, not player-owned - participant notifications are sent through the notifications module, not through local server UI diff --git a/arma/server/addons/task/functions/fnc_handleTaskRewards.sqf b/arma/server/addons/task/functions/fnc_handleTaskRewards.sqf index e0e83bf..1ba8f83 100644 --- a/arma/server/addons/task/functions/fnc_handleTaskRewards.sqf +++ b/arma/server/addons/task/functions/fnc_handleTaskRewards.sqf @@ -51,6 +51,7 @@ if (_orgID isEqualTo "") exitWith { private _success = true; private _funds = _rewards getOrDefault ["funds", 0]; private _rewardMessages = []; +private _failureMessages = []; private _resolveRewardLabel = { params [["_className", "", [""]]]; @@ -115,8 +116,15 @@ if (_funds > 0) then { if (_updatedOrg isEqualTo createHashMap) then { ["ERROR", format ["Failed to update organization %1 funds for task %2.", _orgID, _taskID]] call EFUNC(common,log); _success = false; + _failureMessages pushBack "org funds update"; } else { private _patch = createHashMapFromArray [["funds", _nextFunds]]; + private _savedOrg = EGVAR(org,OrgStore) call ["saveById", [_orgID]]; + if (_savedOrg isEqualTo createHashMap) then { + ["ERROR", format ["Task %1 updated organization %2 funds, but durable save failed.", _taskID, _orgID]] call EFUNC(common,log); + _success = false; + _failureMessages pushBack "org funds persistence"; + }; [_patch] call _syncOrgPatch; _rewardMessages pushBack format ["$%1 org funds", [_funds] call EFUNC(common,formatNumber)]; @@ -141,8 +149,15 @@ private _grantOrgAssets = { if !(_grantResult getOrDefault ["success", false]) then { ["ERROR", format ["Failed to award %1 assets for task %2: %3", _category, _taskID, _grantResult getOrDefault ["message", "Unknown error."]]] call EFUNC(common,log); _success = false; + _failureMessages pushBack format ["%1 asset update", _category]; } else { [_grantResult getOrDefault ["patch", createHashMap]] call _syncOrgPatch; + if !(_grantResult getOrDefault ["persisted", false]) then { + private _persistenceMessage = _grantResult getOrDefault ["persistenceMessage", format ["%1 assets updated, but durable save failed.", _category]]; + ["ERROR", format ["Task %1 %2", _taskID, _persistenceMessage]] call EFUNC(common,log); + _success = false; + _failureMessages pushBack format ["%1 asset persistence", _category]; + }; private _labels = _items apply { [_x] call _resolveRewardLabel }; _rewardMessages pushBack format ["%1: %2", _category, _labels joinString ", "]; }; @@ -171,8 +186,15 @@ private _grantOrgFleet = { if !(_fleetResult getOrDefault ["success", false]) then { ["ERROR", format ["Failed to award vehicle rewards for task %2: %1", _fleetResult getOrDefault ["message", "Unknown error."], _taskID]] call EFUNC(common,log); _success = false; + _failureMessages pushBack "fleet update"; } else { [_fleetResult getOrDefault ["patch", createHashMap]] call _syncOrgPatch; + if !(_fleetResult getOrDefault ["persisted", false]) then { + private _persistenceMessage = _fleetResult getOrDefault ["persistenceMessage", "Fleet updated, but durable save failed."]; + ["ERROR", format ["Task %1 %2", _taskID, _persistenceMessage]] call EFUNC(common,log); + _success = false; + _failureMessages pushBack "fleet persistence"; + }; private _labels = _vehicles apply { [_x] call _resolveRewardLabel }; _rewardMessages pushBack format ["vehicles: %1", _labels joinString ", "]; }; @@ -219,7 +241,12 @@ if (_success) then { ["INFO", _message] call EFUNC(common,log); ["success", "Tasks", _message] call _notifyMembers; } else { - ["warning", "Tasks", format ["Task %1 completed, but one or more org rewards failed to apply.", _taskID]] call _notifyMembers; + private _warningMessage = format ["Task %1 completed, but one or more org rewards failed to apply.", _taskID]; + if (_failureMessages isNotEqualTo []) then { + _warningMessage = format ["%1 Failed areas: %2.", _warningMessage, _failureMessages joinString ", "]; + }; + + ["warning", "Tasks", _warningMessage] call _notifyMembers; }; _success diff --git a/arma/server/addons/task/functions/fnc_handler.sqf b/arma/server/addons/task/functions/fnc_handler.sqf index 0349b27..cffe3a1 100644 --- a/arma/server/addons/task/functions/fnc_handler.sqf +++ b/arma/server/addons/task/functions/fnc_handler.sqf @@ -27,14 +27,7 @@ if (_minRating > 0) then { if (_requesterUid isEqualTo "") then { ["WARNING", format ["Task %1 requires minimum reputation %2 but no requester UID was provided, skipping reputation gate.", _taskType, _minRating]] call EFUNC(common,log); } else { - private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap]; - if (_requesterActor isEqualTo createHashMap) then { - _requesterActor = EGVAR(actor,ActorStore) call ["init", [_requesterUid]]; - }; - - private _orgID = _requesterActor getOrDefault ["organization", "default"]; - if (_orgID isEqualTo "") then { _orgID = "default"; }; - + private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]]; private _org = EGVAR(org,OrgStore) call ["loadById", [_orgID]]; private _orgReputation = _org getOrDefault ["reputation", 0]; if (_orgReputation < _minRating) exitWith { diff --git a/arma/server/addons/task/functions/fnc_initTaskStore.sqf b/arma/server/addons/task/functions/fnc_initTaskStore.sqf index f0410f9..5de0c4e 100644 --- a/arma/server/addons/task/functions/fnc_initTaskStore.sqf +++ b/arma/server/addons/task/functions/fnc_initTaskStore.sqf @@ -5,6 +5,10 @@ * Initializes the task store for task entity tracking, participant * contribution tracking, and task outcome application. * + * Task metadata is extension-backed but intentionally transient. The + * task backend is reset when this store is created so task/catalog/status + * state starts clean for each server or mission lifecycle. + * * Arguments: * None * @@ -32,6 +36,8 @@ GVAR(TaskStore) = createHashMapObject [[ ["targets", createHashMap] ]]; + // Task extension state is mission-scoped and intentionally reset on + // startup rather than being treated as durable account data. ["task:reset", []] call EFUNC(extension,extCall) params ["_result", "_isSuccess"]; if ( !_isSuccess @@ -99,18 +105,14 @@ GVAR(TaskStore) = createHashMapObject [[ private _orgID = "default"; if (_requesterUid isNotEqualTo "") then { - private _actor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap]; - if (_actor isEqualTo createHashMap) then { - _actor = EGVAR(actor,ActorStore) call ["init", [_requesterUid]]; - }; + 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 = _actor getOrDefault ["organization", ""]; - if (_orgID isEqualTo "") then { _orgID = "default"; }; + _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]]; }; private _context = createHashMapFromArray [ @@ -166,6 +168,14 @@ GVAR(TaskStore) = createHashMapObject [[ _entries }], + ["hasTaskCatalogEntry", compileFinal { + params [["_taskID", "", [""]]]; + + if (_taskID isEqualTo "") exitWith { false }; + + private _entry = _self call ["callTaskState", ["task:catalog:get", [_taskID], objNull]]; + _entry isEqualType createHashMap + }], ["acceptTask", compileFinal { params [["_taskID", "", [""]], ["_requesterUid", "", [""]]]; @@ -180,17 +190,13 @@ GVAR(TaskStore) = createHashMapObject [[ _result }; - private _actor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap]; - if (_actor isEqualTo createHashMap) then { - _actor = EGVAR(actor,ActorStore) call ["init", [_requesterUid]]; - }; + 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 = _actor getOrDefault ["organization", ""]; - if (_orgID isEqualTo "") then { _orgID = "default"; }; + private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]]; private _context = createHashMapFromArray [ ["requesterUid", _requesterUid], @@ -418,6 +424,10 @@ GVAR(TaskStore) = createHashMapObject [[ if (_taskID isEqualTo "") exitWith { false }; + if !(isNil QGVAR(MissionManager)) then { + GVAR(MissionManager) call ["completeMission", [_taskID]]; + }; + private _participantRegistry = _self getOrDefault ["participantRegistry", createHashMap]; _participantRegistry deleteAt _taskID; _self set ["participantRegistry", _participantRegistry]; @@ -431,7 +441,11 @@ GVAR(TaskStore) = createHashMapObject [[ private _result = createHashMapFromArray [ ["participantUids", []], ["orgIds", []], - ["contributions", createHashMap] + ["contributions", createHashMap], + ["success", true], + ["mutationFailures", []], + ["persistenceFailures", []], + ["message", ""] ]; if (_taskID isEqualTo "" || { _delta isEqualTo 0 }) exitWith { _result }; @@ -462,6 +476,8 @@ GVAR(TaskStore) = createHashMapObject [[ private _orgIds = []; private _contributions = createHashMap; private _totalContribution = 0; + private _mutationFailures = []; + private _persistenceFailures = []; if (_delta > 0) then { { @@ -481,12 +497,7 @@ GVAR(TaskStore) = createHashMapObject [[ { private _uid = _x; - private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap]; - if (_actor isEqualTo createHashMap) then { - _actor = EGVAR(actor,ActorStore) call ["init", [_uid]]; - }; - - private _orgID = _actor getOrDefault ["organization", ""]; + private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_uid, ""]]; if (_orgID isNotEqualTo "") then { _orgIds pushBackUnique _orgID; }; @@ -517,6 +528,10 @@ GVAR(TaskStore) = createHashMapObject [[ if (_patch isEqualTo createHashMap) then { continue; }; EGVAR(bank,BankMessenger) call ["sendAccountSync", [_uid, _patch]]; + 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; @@ -547,8 +562,13 @@ GVAR(TaskStore) = createHashMapObject [[ } forEach _memberUids; _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]; }; }; }; @@ -556,6 +576,19 @@ GVAR(TaskStore) = createHashMapObject [[ _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 "; "]; + }; _result }] ]]; diff --git a/arma/server/addons/task/functions/fnc_missionManager.sqf b/arma/server/addons/task/functions/fnc_missionManager.sqf index a1aec02..e471b27 100644 --- a/arma/server/addons/task/functions/fnc_missionManager.sqf +++ b/arma/server/addons/task/functions/fnc_missionManager.sqf @@ -38,8 +38,25 @@ GVAR(MissionManagerBaseClass) = compileFinal createHashMapFromArray [ ["getMaxConcurrentMissions", compileFinal { private _maxConcurrent = _self getOrDefault ["maxConcurrentMissions", 1]; if (_maxConcurrent <= 0) then { _maxConcurrent = 1; }; + private _attackLocationCount = _self call ["getAttackLocationCount", []]; + if (_attackLocationCount > 0) then { + _maxConcurrent = _maxConcurrent min _attackLocationCount; + }; _maxConcurrent }], + ["getAttackLocationCount", compileFinal { + private _locationsConfig = _self getOrDefault ["locationsConfig", configNull]; + if (isNull _locationsConfig) exitWith { 0 }; + + private _count = 0; + { + if ("attack" in getArray (_x >> "suitable")) then { + _count = _count + 1; + }; + } forEach ("true" configClasses _locationsConfig); + + _count + }], ["getLocationReuseCooldown", compileFinal { private _missionConfig = _self getOrDefault ["missionConfig", configNull]; private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown"); @@ -352,7 +369,8 @@ GVAR(MissionManager) = createHashMapObject [GVAR(MissionManagerBaseClass)]; [{ { private _status = GVAR(TaskStore) call ["getTaskStatus", [_x]]; - if (_status in ["succeeded", "failed"]) then { + private _hasCatalogEntry = GVAR(TaskStore) call ["hasTaskCatalogEntry", [_x]]; + if (_status in ["succeeded", "failed"] || { _status isEqualTo "" && { !_hasCatalogEntry } }) then { GVAR(MissionManager) call ["completeMission", [_x]]; GVAR(TaskStore) call ["clearTaskStatus", [_x]]; }; diff --git a/arma/server/extension/src/actor.rs b/arma/server/extension/src/actor.rs index 53f3a68..7bbdf68 100644 --- a/arma/server/extension/src/actor.rs +++ b/arma/server/extension/src/actor.rs @@ -54,6 +54,7 @@ pub fn group() -> Group { Group::new() .command("init", init_hot_actor) .command("get", get_hot_actor) + .command("keys", list_hot_actor_keys) .command("override", override_hot_actor) .command("save", save_hot_actor) .command("remove", remove_hot_actor), @@ -91,6 +92,16 @@ pub(crate) fn get_hot_actor(call_context: CallContext, key: String) -> String { } } +pub(crate) fn list_hot_actor_keys() -> String { + match HOT_ACTOR_SERVICE.list_actor_keys() { + Ok(keys) => match serde_json::to_string(&keys) { + Ok(json) => json, + Err(error) => format!("Error: Failed to serialize actor hot-state keys: {}", error), + }, + Err(error) => format!("Error: {}", error), + } +} + pub(crate) fn override_hot_actor( call_context: CallContext, key: String, diff --git a/arma/server/extension/src/cad.rs b/arma/server/extension/src/cad.rs index 5984e99..d6c4223 100644 --- a/arma/server/extension/src/cad.rs +++ b/arma/server/extension/src/cad.rs @@ -3,6 +3,10 @@ //! The extension owns the in-memory CAD state store, while the shared service //! layer handles mutation rules and hydrate shaping. This keeps the extension //! surface thin and aligned with the workspace architecture. +//! +//! CAD state is intentionally transient operational state. It follows the +//! active server or mission lifecycle and is not treated as durable player or +//! organization persistence. use arma_rs::Group; use forge_repositories::InMemoryCadRepository; diff --git a/arma/server/extension/src/task.rs b/arma/server/extension/src/task.rs index 233d3dc..fbe2e40 100644 --- a/arma/server/extension/src/task.rs +++ b/arma/server/extension/src/task.rs @@ -2,6 +2,9 @@ //! //! The extension owns portable task metadata while SQF keeps Arma-only runtime //! state such as entity references and participant tracking. +//! +//! This state is intentionally transient and is reset during server task-store +//! initialization so tasks start clean for each server or mission lifecycle. use arma_rs::Group; use forge_repositories::InMemoryTaskRepository; diff --git a/arma/server/extension/src/transport.rs b/arma/server/extension/src/transport.rs index 0232ba4..6e3b32c 100644 --- a/arma/server/extension/src/transport.rs +++ b/arma/server/extension/src/transport.rs @@ -185,6 +185,10 @@ fn route_command( expect_arg_count(function_name, &arguments, 1)?; Ok(actor::get_hot_actor(call_context, arguments[0].clone())) } + "actor:hot:keys" => { + expect_arg_count(function_name, &arguments, 0)?; + Ok(actor::list_hot_actor_keys()) + } "actor:hot:override" => { expect_arg_count(function_name, &arguments, 2)?; Ok(actor::override_hot_actor( diff --git a/lib/models/src/actor.rs b/lib/models/src/actor.rs index b6e7c2b..203eed1 100644 --- a/lib/models/src/actor.rs +++ b/lib/models/src/actor.rs @@ -61,7 +61,7 @@ impl Actor { state: "HEALTHY".to_string(), holster: true, rank: None, - organization: "".to_string(), + organization: "default".to_string(), }; actor.validate()?; @@ -171,7 +171,7 @@ impl FromArma for Actor { })?; if actor.organization.trim().is_empty() { - actor.organization = String::new(); + actor.organization = "default".to_string(); } Ok(actor) diff --git a/lib/models/src/v_locker.rs b/lib/models/src/v_locker.rs index 2f68ffc..f67de35 100644 --- a/lib/models/src/v_locker.rs +++ b/lib/models/src/v_locker.rs @@ -35,7 +35,7 @@ impl VLocker { "ItemMap".to_string(), "ItemRadio".to_string(), "ItemWatch".to_string(), - "U_IG_Guerrilla_6_1".to_string(), + "U_BG_Guerrilla_6_1".to_string(), "V_TacVest_oli".to_string(), ], weapons: vec!["arifle_MX_F".to_string(), "hgun_P07_F".to_string()], diff --git a/lib/repositories/src/actor.rs b/lib/repositories/src/actor.rs index cca7867..72107e1 100644 --- a/lib/repositories/src/actor.rs +++ b/lib/repositories/src/actor.rs @@ -34,6 +34,7 @@ pub trait ActorRepository: Send + Sync { pub trait ActorHotRepository: Send + Sync { fn get(&self, id: &str) -> Result, String>; + fn keys(&self) -> Result, String>; fn save(&self, actor: &Actor) -> Result<(), String>; fn delete(&self, id: &str) -> Result<(), String>; } @@ -57,6 +58,13 @@ impl ActorHotRepository for InMemoryActorHotRepository { .map_err(|_| "Actor hot state lock poisoned.".to_string()) } + fn keys(&self) -> Result, String> { + self.state + .read() + .map(|state| state.keys().cloned().collect()) + .map_err(|_| "Actor hot state lock poisoned.".to_string()) + } + fn save(&self, actor: &Actor) -> Result<(), String> { self.state .write() diff --git a/lib/services/README.md b/lib/services/README.md index 78b64a1..cf88b86 100644 --- a/lib/services/README.md +++ b/lib/services/README.md @@ -26,6 +26,14 @@ graph TD - **Error Handling:** Converts technical errors into business-friendly messages. - **Data Transformation:** Handles JSON parsing and model conversion. +## Operational State Policy + +Most hot-state services in Forge back durable player or organization records and +are expected to flush through the save path. `CAD` and `Task` are the current +exceptions: they are extension-backed operational state services that are +intentionally transient and restart clean with the active server or mission +lifecycle. + ## Actor Service The `ActorService` manages player lifecycle and state. diff --git a/lib/services/src/actor.rs b/lib/services/src/actor.rs index 6f693b4..040660b 100644 --- a/lib/services/src/actor.rs +++ b/lib/services/src/actor.rs @@ -42,7 +42,14 @@ impl ActorHotStateService { return Ok(actor); } - let actor = self.service.get_actor(key)?; + let actor = match self.service.repository.get_by_id(&key)? { + Some(actor) => actor, + None => { + let actor = Actor::new(key.clone()).map_err(|e| e.to_string())?; + self.service.repository.create(&actor)?; + actor + } + }; self.repository.save(&actor)?; Ok(actor) } @@ -77,6 +84,10 @@ impl ActorHotStateService { Ok(saved_actor) } + pub fn list_actor_keys(&self) -> Result, String> { + self.repository.keys() + } + pub fn remove_actor(&self, key: String) -> Result<(), String> { self.repository.delete(&key) } @@ -114,6 +125,9 @@ impl ActorService { if actor.email.is_empty() { actor.email = generate_email(&actor.phone_number); } + if actor.organization.trim().is_empty() { + actor.organization = "default".to_string(); + } // Validate before persisting actor