From cd3e937cdc2ae552d076de6e8a0cace0b97d0782 Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Sun, 5 Apr 2026 21:44:10 -0500 Subject: [PATCH] Hydrate missing actor and org records from live data - Require actor records to exist in storage before hot load - Fall back to player snapshots to fill missing actor fields and org defaults - Refresh org member names when a better value is available - Keep bootstrap extension calls on the direct path by default --- .../addons/garage/XEH_postInitClient.sqf | 2 +- .../addons/locker/XEH_postInitClient.sqf | 2 +- arma/client/addons/org/XEH_postInitClient.sqf | 2 +- .../actor/functions/fnc_initActorStore.sqf | 144 +++++++++++++++++- .../extension/functions/fnc_extCall.sqf | 18 ++- .../addons/org/functions/fnc_initOrgStore.sqf | 35 ++++- lib/services/src/actor.rs | 25 +-- lib/services/src/org.rs | 22 ++- 8 files changed, 212 insertions(+), 38 deletions(-) diff --git a/arma/client/addons/garage/XEH_postInitClient.sqf b/arma/client/addons/garage/XEH_postInitClient.sqf index 76cd623..e187c2d 100644 --- a/arma/client/addons/garage/XEH_postInitClient.sqf +++ b/arma/client/addons/garage/XEH_postInitClient.sqf @@ -55,7 +55,7 @@ if (isNil QGVAR(VGRepository)) then { call FUNC(initVGRepository); }; }] call CFUNC(addEventHandler); [{ - EGVAR(bank,BankRepository) get "isLoaded"; + EGVAR(actor,ActorRepository) get "isLoaded"; }, { [QGVAR(initGarage), []] call CFUNC(localEvent); }] call CFUNC(waitUntilAndExecute); diff --git a/arma/client/addons/locker/XEH_postInitClient.sqf b/arma/client/addons/locker/XEH_postInitClient.sqf index 20123fa..a0a878d 100644 --- a/arma/client/addons/locker/XEH_postInitClient.sqf +++ b/arma/client/addons/locker/XEH_postInitClient.sqf @@ -36,7 +36,7 @@ if (isNil QGVAR(VARepository)) then { call FUNC(initVARepository); }; }] call CFUNC(addEventHandler); [{ - EGVAR(garage,GarageRepository) get "isLoaded"; + EGVAR(actor,ActorRepository) get "isLoaded"; }, { [QGVAR(initLocker), []] call CFUNC(localEvent); }] call CFUNC(waitUntilAndExecute); diff --git a/arma/client/addons/org/XEH_postInitClient.sqf b/arma/client/addons/org/XEH_postInitClient.sqf index 5ddcfc0..cd70afc 100644 --- a/arma/client/addons/org/XEH_postInitClient.sqf +++ b/arma/client/addons/org/XEH_postInitClient.sqf @@ -51,7 +51,7 @@ if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initUIBridge); }; }] call CFUNC(addEventHandler); [{ - EGVAR(locker,VARepository) get "isLoaded"; + EGVAR(actor,ActorRepository) get "isLoaded"; }, { [QGVAR(initOrg), []] call CFUNC(localEvent); }] call CFUNC(waitUntilAndExecute); diff --git a/arma/server/addons/actor/functions/fnc_initActorStore.sqf b/arma/server/addons/actor/functions/fnc_initActorStore.sqf index 7a83da8..d48a960 100644 --- a/arma/server/addons/actor/functions/fnc_initActorStore.sqf +++ b/arma/server/addons/actor/functions/fnc_initActorStore.sqf @@ -4,7 +4,7 @@ * File: fnc_initActorStore.sqf * Author: IDSolutions * Date: 2025-12-17 - * Last Update: 2026-04-01 + * Last Update: 2026-04-05 * Public: Yes * * Description: @@ -153,12 +153,140 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [ params [["_uid", "", [""]], ["_initialize", false, [false]]]; if (_uid isEqualTo "") exitWith { createHashMap }; + if (_initialize) then { + private _ensureResult = _self call ["ensurePersistentActor", [_uid]]; + if !(_ensureResult isEqualType true && { _ensureResult }) exitWith { createHashMap }; + }; private _command = ["actor:hot:get", "actor:hot:init"] select _initialize; private _actor = _self call ["callHotActor", [_command, [_uid]]]; if (_actor isEqualTo createHashMap) exitWith { _actor }; - _self call ["cacheActor", [_uid, _actor]] + _self call ["hydrateActorIfNeeded", [_uid, _actor, true]] + }], + ["ensurePersistentActor", compileFinal { + params [["_uid", "", [""]]]; + + if (_uid isEqualTo "") exitWith { false }; + + ["actor:exists", [_uid]] call EFUNC(extension,extCall) params ["_existsResult", "_existsSuccess"]; + if (!_existsSuccess || { !(_existsResult isEqualType "") }) exitWith { + ["ERROR", format ["Failed to verify persistent actor state for %1.", _uid]] call EFUNC(common,log); + false + }; + + if (_existsResult isEqualTo "true") exitWith { true }; + + private _player = [_uid] call EFUNC(common,getPlayer); + private _actor = GVAR(ActorModel) call ["fromPlayer", [_player]]; + _actor set ["uid", _uid]; + + if ((_actor getOrDefault ["organization", ""]) isEqualTo "") then { + _actor set ["organization", "default"]; + }; + + private _json = _self call ["toJSON", [_actor]]; + ["actor:create", [_uid, _json]] call EFUNC(extension,extCall) params ["_createResult", "_createSuccess"]; + + if (!_createSuccess || { !(_createResult isEqualType "") }) exitWith { + ["ERROR", format ["Failed to create actor %1 from server snapshot.", _uid]] call EFUNC(common,log); + false + }; + + if ((_createResult find "Error:") == 0) exitWith { + ["ERROR", format ["Actor create for %1 failed: %2", _uid, _createResult]] call EFUNC(common,log); + false + }; + + true + }], + ["hydrateActorIfNeeded", compileFinal { + params [["_uid", "", [""]], ["_actor", createHashMap, [createHashMap]], ["_save", true, [false]]]; + + if (_uid isEqualTo "" || { !(_actor isEqualType createHashMap) } || { _actor isEqualTo createHashMap }) exitWith { + createHashMap + }; + + private _hydratedActor = GVAR(ActorModel) call ["migrate", [+_actor]]; + private _defaults = GVAR(ActorModel) call ["defaults", []]; + private _player = [_uid] call EFUNC(common,getPlayer); + private _needsPersist = false; + + if ((_hydratedActor getOrDefault ["uid", ""]) isEqualTo "") then { + _hydratedActor set ["uid", _uid]; + _needsPersist = true; + }; + if ((_hydratedActor getOrDefault ["organization", ""]) isEqualTo "") then { + _hydratedActor set ["organization", "default"]; + _needsPersist = true; + }; + + { + private _value = _hydratedActor getOrDefault [_x, ""]; + if !(_value isEqualType "") then { + _hydratedActor set [_x, _defaults getOrDefault [_x, ""]]; + _needsPersist = true; + }; + } forEach ["phone_number", "email"]; + + if (_player isNotEqualTo objNull) then { + private _snapshot = GVAR(ActorModel) call ["fromPlayer", [_player]]; + private _name = _hydratedActor getOrDefault ["name", ""]; + if ( + !(_name isEqualType "") + || { _name isEqualTo "" } + || { toLowerANSI _name isEqualTo "unknown" } + ) then { + _hydratedActor set ["name", _snapshot getOrDefault ["name", name _player]]; + _needsPersist = true; + }; + + private _position = _hydratedActor getOrDefault ["position", []]; + if !(_position isEqualType [] && { count _position isEqualTo 3 }) then { + _hydratedActor set ["position", _snapshot getOrDefault ["position", getPosASL _player]]; + _needsPersist = true; + }; + + private _direction = _hydratedActor getOrDefault ["direction", 0]; + if !(_direction isEqualType 0) then { + _hydratedActor set ["direction", _snapshot getOrDefault ["direction", getDir _player]]; + _needsPersist = true; + }; + + { + private _fieldValue = _hydratedActor getOrDefault [_x, ""]; + if (!(_fieldValue isEqualType "") || { _fieldValue isEqualTo "" }) then { + _hydratedActor set [_x, _snapshot getOrDefault [_x, _defaults getOrDefault [_x, ""]]]; + _needsPersist = true; + }; + } forEach ["stance", "rank", "state"]; + + private _loadout = _hydratedActor getOrDefault ["loadout", []]; + if !(_loadout isEqualType [] && { count _loadout > 0 }) then { + _hydratedActor set ["loadout", getUnitLoadout _player]; + _needsPersist = true; + }; + } else { + { + private _fieldValue = _hydratedActor getOrDefault [_x, ""]; + if (!(_fieldValue isEqualType "") || { _fieldValue isEqualTo "" }) then { + _hydratedActor set [_x, _defaults getOrDefault [_x, ""]]; + _needsPersist = true; + }; + } forEach ["stance", "rank", "state"]; + }; + + if !_needsPersist exitWith { + _self call ["cacheActor", [_uid, _hydratedActor]] + }; + + private _updatedActor = _self call ["override", [_uid, _hydratedActor, _save]]; + if (_updatedActor isEqualType createHashMap && { _updatedActor isNotEqualTo createHashMap }) exitWith { + _self call ["cacheActor", [_uid, _updatedActor]] + }; + + ["WARNING", format ["Failed to hydrate actor %1 from player snapshot.", _uid]] call EFUNC(common,log); + _self call ["cacheActor", [_uid, _hydratedActor]] }], ["init", compileFinal { params [["_uid", "", [""]]]; @@ -182,14 +310,11 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [ _finalActor = _self call ["loadHotActor", [_uid, true]]; ["INFO", format ["Found actor for %1", _uid]] call EFUNC(common,log); } else { - _finalActor = GVAR(ActorModel) call ["fromPlayer", [_player]]; - _finalActor set ["uid", _uid]; - - private _json = _self call ["toJSON", [_finalActor]]; - ["actor:create", [_uid, _json]] call EFUNC(extension,extCall) params ["_createResult", "_createSuccess"]; - if (!_createSuccess) exitWith { + if !(_self call ["ensurePersistentActor", [_uid]]) exitWith { ["ERROR", format ["Failed to create actor %1! Using fallback actor.", _uid]] call EFUNC(common,log); + _finalActor = GVAR(ActorModel) call ["fromPlayer", [_player]]; + _finalActor set ["uid", _uid]; _finalActor = _self call ["cacheActor", [_uid, _finalActor]]; [CRPC(actor,responseInitActor), [_finalActor], _player] call CFUNC(targetEvent); _finalActor @@ -358,6 +483,9 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [ _finalActor set ["rank", rank _player]; _finalActor set ["state", lifeState _player]; _finalActor set ["loadout", getUnitLoadout _player]; + if ((_finalActor getOrDefault ["organization", ""]) isEqualTo "") then { + _finalActor set ["organization", "default"]; + }; } else { ["WARNING", format ["No player object found for %1 during actor snapshot, using cached values.", _uid]] call EFUNC(common,log); }; diff --git a/arma/server/addons/extension/functions/fnc_extCall.sqf b/arma/server/addons/extension/functions/fnc_extCall.sqf index 7d7ec29..e7145b6 100644 --- a/arma/server/addons/extension/functions/fnc_extCall.sqf +++ b/arma/server/addons/extension/functions/fnc_extCall.sqf @@ -31,17 +31,18 @@ private _chunkPrefix = "FORGE_TRANSPORT_CHUNK:"; private _chunkPrefixLength = count toArray _chunkPrefix; private _unsupportedRoutePrefix = "Error: Unsupported transport route"; private _requestChunkSize = 12000; +// Keep bootstrap create/update calls on the direct extension path by default. +// Actor/bank initialization payloads are small enough for normal callExtension +// usage, and their correctness depends on preserving the native argument shape +// of [uid, json]. Transport remains available automatically for genuinely large +// requests through the chunked-request path below. private _transportResponseFunctions = [ "actor:get", - "actor:create", - "actor:update", "actor:hot:init", "actor:hot:get", "actor:hot:keys", "actor:hot:save", "bank:get", - "bank:create", - "bank:update", "bank:hot:init", "bank:hot:get", "bank:hot:save", @@ -127,7 +128,10 @@ private _checkRedisAvailability = { }; private _buildTransportArgumentsJson = { - params [["_rawArguments", [], [[]]]]; + private _rawArguments = _this; + if !(_rawArguments isEqualType []) then { + _rawArguments = [_rawArguments]; + }; private _stringArguments = _rawArguments apply { if (_x isEqualType "") exitWith { _x }; @@ -162,10 +166,12 @@ if (_functionLower in ["status", "version"]) exitWith { [_function, _arguments] call _callExtensionCommand }; -private _argumentsJson = [_arguments] call _buildTransportArgumentsJson; +private _argumentsJson = _arguments call _buildTransportArgumentsJson; private _usesTransportResponse = _functionLower in _transportResponseFunctions; private _usesChunkedRequest = (count toArray _argumentsJson) > _requestChunkSize; +// Most calls should stay direct unless they either need chunked response +// assembly or the request body is large enough to require staging. if !(_usesTransportResponse || { _usesChunkedRequest }) exitWith { [_function, _arguments] call _callExtensionCommand }; diff --git a/arma/server/addons/org/functions/fnc_initOrgStore.sqf b/arma/server/addons/org/functions/fnc_initOrgStore.sqf index f0de6a0..dd4b2a0 100644 --- a/arma/server/addons/org/functions/fnc_initOrgStore.sqf +++ b/arma/server/addons/org/functions/fnc_initOrgStore.sqf @@ -262,7 +262,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]]; private _memberName = _actor getOrDefault ["name", ""]; - if (_memberName isEqualTo "" && { _player isNotEqualTo objNull }) then { + if ((_memberName isEqualTo "" || { toLowerANSI _memberName isEqualTo "unknown" }) && { _player isNotEqualTo objNull }) then { _memberName = name _player; }; if (_memberName isEqualTo "") then { _memberName = "Unknown"; }; @@ -273,7 +273,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ if (_uid isEqualTo "" || { _orgID isEqualTo "" }) exitWith { createHashMap }; - private _actorPatch = EGVAR(actor,ActorStore) call ["set", [_uid, "organization", _orgID, false]]; + private _actorPatch = EGVAR(actor,ActorStore) call ["set", [_uid, "organization", _orgID, true]]; private _updatedActor = EGVAR(actor,ActorStore) call ["load", [_uid]]; if ( !(_updatedActor isEqualType createHashMap) @@ -287,7 +287,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ }; _forcedActor set ["organization", _orgID]; - _updatedActor = EGVAR(actor,ActorStore) call ["override", [_uid, _forcedActor, false]]; + _updatedActor = EGVAR(actor,ActorStore) call ["override", [_uid, _forcedActor, true]]; if (_updatedActor isEqualType createHashMap && { _updatedActor isNotEqualTo createHashMap }) then { _actorPatch = createHashMapFromArray [["organization", _orgID]]; }; @@ -752,12 +752,35 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [ ["existingOrgId", _existingOrgID] ]; - private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:register", [toJSON _context]]]; - if (_envelope isEqualTo createHashMap) exitWith { - _result set ["message", "Organization registration failed."]; + ["org:hot:register", [toJSON _context]] call EFUNC(extension,extCall) params ["_rawResult", "_isSuccess"]; + if !_isSuccess exitWith { + _result set ["message", "Organization service was unavailable during registration."]; _result }; + if !(_rawResult isEqualType "") exitWith { + _result set ["message", "Organization service returned an invalid registration response."]; + _result + }; + + if ((_rawResult find "Error:") == 0) exitWith { + _result set ["message", _rawResult select [7]]; + _result + }; + + private _envelope = fromJSON _rawResult; + if !(_envelope isEqualType createHashMap) exitWith { + _result set ["message", "Organization service returned malformed registration data."]; + _result + }; + + if ("org" in _envelope) then { + private _syncedOrg = _self call ["syncHotOrg", [_envelope getOrDefault ["org", createHashMap]]]; + if (_syncedOrg isNotEqualTo createHashMap) then { + _envelope set ["org", _syncedOrg]; + }; + }; + private _actorPatch = _self call ["applyActorOrganization", [_uid, _envelope getOrDefault ["actorOrganization", _orgID], _actor]]; if (_actorPatch isEqualTo createHashMap) exitWith { _result set ["message", "Failed to assign the player to the new organization."]; diff --git a/lib/services/src/actor.rs b/lib/services/src/actor.rs index 040660b..103330b 100644 --- a/lib/services/src/actor.rs +++ b/lib/services/src/actor.rs @@ -42,20 +42,27 @@ impl ActorHotStateService { return Ok(actor); } - 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 - } - }; + let actor = self + .service + .repository + .get_by_id(&key)? + .ok_or_else(|| format!("Actor with UID '{}' was not found", key))?; self.repository.save(&actor)?; Ok(actor) } pub fn get_actor(&self, key: String) -> Result { - self.init_actor(key) + if let Some(actor) = self.repository.get(&key)? { + return Ok(actor); + } + + let actor = self + .service + .repository + .get_by_id(&key)? + .ok_or_else(|| format!("Actor with UID '{}' was not found", key))?; + self.repository.save(&actor)?; + Ok(actor) } pub fn override_actor(&self, key: String, json_data: String) -> Result { diff --git a/lib/services/src/org.rs b/lib/services/src/org.rs index 96e00e1..dfbda93 100644 --- a/lib/services/src/org.rs +++ b/lib/services/src/org.rs @@ -423,12 +423,22 @@ impl OrgHotStateService { } let mut org = self.get_org(context.org_id)?; - if !org.members.contains_key(&context.member_uid) { - let member_name = if context.member_name.trim().is_empty() { - "Unknown".to_string() - } else { - context.member_name - }; + let member_name = if context.member_name.trim().is_empty() { + "Unknown".to_string() + } else { + context.member_name + }; + let should_refresh_member_name = org + .members + .get(&context.member_uid) + .map(|member| { + let existing_name = member.name.trim(); + !member_name.eq_ignore_ascii_case("unknown") + && (existing_name.is_empty() || existing_name.eq_ignore_ascii_case("unknown")) + }) + .unwrap_or(false); + + if !org.members.contains_key(&context.member_uid) || should_refresh_member_name { org.members.insert( context.member_uid.clone(), MemberSummary {