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:
parent
1d54cc70c3
commit
cd3e937cdc
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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."];
|
||||
|
||||
@ -42,20 +42,27 @@ impl<R: ActorRepository, H: ActorHotRepository> ActorHotStateService<R, H> {
|
||||
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<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> {
|
||||
|
||||
@ -423,12 +423,22 @@ impl<R: OrgRepository, H: OrgHotRepository> OrgHotStateService<R, H> {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user