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
This commit is contained in:
Jacob Schmidt 2026-04-05 21:44:10 -05:00
parent 1d54cc70c3
commit cd3e937cdc
8 changed files with 212 additions and 38 deletions

View File

@ -55,7 +55,7 @@ if (isNil QGVAR(VGRepository)) then { call FUNC(initVGRepository); };
}] call CFUNC(addEventHandler); }] call CFUNC(addEventHandler);
[{ [{
EGVAR(bank,BankRepository) get "isLoaded"; EGVAR(actor,ActorRepository) get "isLoaded";
}, { }, {
[QGVAR(initGarage), []] call CFUNC(localEvent); [QGVAR(initGarage), []] call CFUNC(localEvent);
}] call CFUNC(waitUntilAndExecute); }] call CFUNC(waitUntilAndExecute);

View File

@ -36,7 +36,7 @@ if (isNil QGVAR(VARepository)) then { call FUNC(initVARepository); };
}] call CFUNC(addEventHandler); }] call CFUNC(addEventHandler);
[{ [{
EGVAR(garage,GarageRepository) get "isLoaded"; EGVAR(actor,ActorRepository) get "isLoaded";
}, { }, {
[QGVAR(initLocker), []] call CFUNC(localEvent); [QGVAR(initLocker), []] call CFUNC(localEvent);
}] call CFUNC(waitUntilAndExecute); }] call CFUNC(waitUntilAndExecute);

View File

@ -51,7 +51,7 @@ if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initUIBridge); };
}] call CFUNC(addEventHandler); }] call CFUNC(addEventHandler);
[{ [{
EGVAR(locker,VARepository) get "isLoaded"; EGVAR(actor,ActorRepository) get "isLoaded";
}, { }, {
[QGVAR(initOrg), []] call CFUNC(localEvent); [QGVAR(initOrg), []] call CFUNC(localEvent);
}] call CFUNC(waitUntilAndExecute); }] call CFUNC(waitUntilAndExecute);

View File

@ -4,7 +4,7 @@
* File: fnc_initActorStore.sqf * File: fnc_initActorStore.sqf
* Author: IDSolutions * Author: IDSolutions
* Date: 2025-12-17 * Date: 2025-12-17
* Last Update: 2026-04-01 * Last Update: 2026-04-05
* Public: Yes * Public: Yes
* *
* Description: * Description:
@ -153,12 +153,140 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
params [["_uid", "", [""]], ["_initialize", false, [false]]]; params [["_uid", "", [""]], ["_initialize", false, [false]]];
if (_uid isEqualTo "") exitWith { createHashMap }; 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 _command = ["actor:hot:get", "actor:hot:init"] select _initialize;
private _actor = _self call ["callHotActor", [_command, [_uid]]]; private _actor = _self call ["callHotActor", [_command, [_uid]]];
if (_actor isEqualTo createHashMap) exitWith { _actor }; 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 { ["init", compileFinal {
params [["_uid", "", [""]]]; params [["_uid", "", [""]]];
@ -182,14 +310,11 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
_finalActor = _self call ["loadHotActor", [_uid, true]]; _finalActor = _self call ["loadHotActor", [_uid, true]];
["INFO", format ["Found actor for %1", _uid]] call EFUNC(common,log); ["INFO", format ["Found actor for %1", _uid]] call EFUNC(common,log);
} else { } else {
_finalActor = GVAR(ActorModel) call ["fromPlayer", [_player]]; if !(_self call ["ensurePersistentActor", [_uid]]) exitWith {
_finalActor set ["uid", _uid];
private _json = _self call ["toJSON", [_finalActor]];
["actor:create", [_uid, _json]] call EFUNC(extension,extCall) params ["_createResult", "_createSuccess"];
if (!_createSuccess) exitWith {
["ERROR", format ["Failed to create actor %1! Using fallback actor.", _uid]] call EFUNC(common,log); ["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]]; _finalActor = _self call ["cacheActor", [_uid, _finalActor]];
[CRPC(actor,responseInitActor), [_finalActor], _player] call CFUNC(targetEvent); [CRPC(actor,responseInitActor), [_finalActor], _player] call CFUNC(targetEvent);
_finalActor _finalActor
@ -358,6 +483,9 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
_finalActor set ["rank", rank _player]; _finalActor set ["rank", rank _player];
_finalActor set ["state", lifeState _player]; _finalActor set ["state", lifeState _player];
_finalActor set ["loadout", getUnitLoadout _player]; _finalActor set ["loadout", getUnitLoadout _player];
if ((_finalActor getOrDefault ["organization", ""]) isEqualTo "") then {
_finalActor set ["organization", "default"];
};
} else { } else {
["WARNING", format ["No player object found for %1 during actor snapshot, using cached values.", _uid]] call EFUNC(common,log); ["WARNING", format ["No player object found for %1 during actor snapshot, using cached values.", _uid]] call EFUNC(common,log);
}; };

View File

@ -31,17 +31,18 @@ private _chunkPrefix = "FORGE_TRANSPORT_CHUNK:";
private _chunkPrefixLength = count toArray _chunkPrefix; private _chunkPrefixLength = count toArray _chunkPrefix;
private _unsupportedRoutePrefix = "Error: Unsupported transport route"; private _unsupportedRoutePrefix = "Error: Unsupported transport route";
private _requestChunkSize = 12000; 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 = [ private _transportResponseFunctions = [
"actor:get", "actor:get",
"actor:create",
"actor:update",
"actor:hot:init", "actor:hot:init",
"actor:hot:get", "actor:hot:get",
"actor:hot:keys", "actor:hot:keys",
"actor:hot:save", "actor:hot:save",
"bank:get", "bank:get",
"bank:create",
"bank:update",
"bank:hot:init", "bank:hot:init",
"bank:hot:get", "bank:hot:get",
"bank:hot:save", "bank:hot:save",
@ -127,7 +128,10 @@ private _checkRedisAvailability = {
}; };
private _buildTransportArgumentsJson = { private _buildTransportArgumentsJson = {
params [["_rawArguments", [], [[]]]]; private _rawArguments = _this;
if !(_rawArguments isEqualType []) then {
_rawArguments = [_rawArguments];
};
private _stringArguments = _rawArguments apply { private _stringArguments = _rawArguments apply {
if (_x isEqualType "") exitWith { _x }; if (_x isEqualType "") exitWith { _x };
@ -162,10 +166,12 @@ if (_functionLower in ["status", "version"]) exitWith {
[_function, _arguments] call _callExtensionCommand [_function, _arguments] call _callExtensionCommand
}; };
private _argumentsJson = [_arguments] call _buildTransportArgumentsJson; private _argumentsJson = _arguments call _buildTransportArgumentsJson;
private _usesTransportResponse = _functionLower in _transportResponseFunctions; private _usesTransportResponse = _functionLower in _transportResponseFunctions;
private _usesChunkedRequest = (count toArray _argumentsJson) > _requestChunkSize; 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 { if !(_usesTransportResponse || { _usesChunkedRequest }) exitWith {
[_function, _arguments] call _callExtensionCommand [_function, _arguments] call _callExtensionCommand
}; };

View File

@ -262,7 +262,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]]; params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]];
private _memberName = _actor getOrDefault ["name", ""]; 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; _memberName = name _player;
}; };
if (_memberName isEqualTo "") then { _memberName = "Unknown"; }; if (_memberName isEqualTo "") then { _memberName = "Unknown"; };
@ -273,7 +273,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
if (_uid isEqualTo "" || { _orgID isEqualTo "" }) exitWith { createHashMap }; 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]]; private _updatedActor = EGVAR(actor,ActorStore) call ["load", [_uid]];
if ( if (
!(_updatedActor isEqualType createHashMap) !(_updatedActor isEqualType createHashMap)
@ -287,7 +287,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
}; };
_forcedActor set ["organization", _orgID]; _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 { if (_updatedActor isEqualType createHashMap && { _updatedActor isNotEqualTo createHashMap }) then {
_actorPatch = createHashMapFromArray [["organization", _orgID]]; _actorPatch = createHashMapFromArray [["organization", _orgID]];
}; };
@ -752,12 +752,35 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["existingOrgId", _existingOrgID] ["existingOrgId", _existingOrgID]
]; ];
private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:register", [toJSON _context]]]; ["org:hot:register", [toJSON _context]] call EFUNC(extension,extCall) params ["_rawResult", "_isSuccess"];
if (_envelope isEqualTo createHashMap) exitWith { if !_isSuccess exitWith {
_result set ["message", "Organization registration failed."]; _result set ["message", "Organization service was unavailable during registration."];
_result _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]]; private _actorPatch = _self call ["applyActorOrganization", [_uid, _envelope getOrDefault ["actorOrganization", _orgID], _actor]];
if (_actorPatch isEqualTo createHashMap) exitWith { if (_actorPatch isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to assign the player to the new organization."]; _result set ["message", "Failed to assign the player to the new organization."];

View File

@ -42,20 +42,27 @@ impl<R: ActorRepository, H: ActorHotRepository> ActorHotStateService<R, H> {
return Ok(actor); return Ok(actor);
} }
let actor = match self.service.repository.get_by_id(&key)? { let actor = self
Some(actor) => actor, .service
None => { .repository
let actor = Actor::new(key.clone()).map_err(|e| e.to_string())?; .get_by_id(&key)?
self.service.repository.create(&actor)?; .ok_or_else(|| format!("Actor with UID '{}' was not found", key))?;
actor
}
};
self.repository.save(&actor)?; self.repository.save(&actor)?;
Ok(actor) Ok(actor)
} }
pub fn get_actor(&self, key: String) -> Result<Actor, String> { pub fn get_actor(&self, key: String) -> Result<Actor, String> {
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<Actor, String> { pub fn override_actor(&self, key: String, json_data: String) -> Result<Actor, String> {

View File

@ -423,12 +423,22 @@ impl<R: OrgRepository, H: OrgHotRepository> OrgHotStateService<R, H> {
} }
let mut org = self.get_org(context.org_id)?; 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() {
let member_name = if context.member_name.trim().is_empty() { "Unknown".to_string()
"Unknown".to_string() } else {
} else { context.member_name
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( org.members.insert(
context.member_uid.clone(), context.member_uid.clone(),
MemberSummary { MemberSummary {