From 6e09fde00d498a6a556971057490bec091cfefe7 Mon Sep 17 00:00:00 2001 From: Smith Date: Tue, 20 Oct 2026 20:17:12 +0300 Subject: [PATCH] fix(recorder): evaluate __EVAL display names instead of recording their source getTextRaw returns the unevaluated source of a displayName written as __EVAL(...), so recordings showed e.g. 'format ["%1 [BLUE]", getText (...)]' as an item name. Add OCAP_recorder_fnc_getConfigDisplayName as the single rule for locale-safe config names, use it at every server site, and ship it to clients for gear and Zeus module names. --- addons/recorder/XEH_prep.sqf | 1 + addons/recorder/fnc_getConfigDisplayName.sqf | 71 +++++++++++++++++++ .../fnc_installPlayerTrackingClients.sqf | 45 ++++++------ addons/recorder/fnc_recordExplosion.sqf | 13 ++-- addons/recorder/fnc_submitStaticWeapon.sqf | 6 +- addons/recorder/fnc_trackConstructions.sqf | 12 ++-- addons/recorder/fnc_trackServicing.sqf | 14 ++-- addons/recorder/fnc_trackTrenches.sqf | 5 +- docs/player-detail-tracking.md | 18 +++-- 9 files changed, 134 insertions(+), 51 deletions(-) create mode 100644 addons/recorder/fnc_getConfigDisplayName.sqf diff --git a/addons/recorder/XEH_prep.sqf b/addons/recorder/XEH_prep.sqf index 28de4f0..944a8b8 100644 --- a/addons/recorder/XEH_prep.sqf +++ b/addons/recorder/XEH_prep.sqf @@ -35,6 +35,7 @@ PREP(eh_killed); PREP(getInstigator); PREP(getEventWeaponText); PREP(getWeaponDisplayData); +PREP(getConfigDisplayName); PREP(entityMonitors); PREP(telemetryLoop); diff --git a/addons/recorder/fnc_getConfigDisplayName.sqf b/addons/recorder/fnc_getConfigDisplayName.sqf new file mode 100644 index 0000000..22b4f5c --- /dev/null +++ b/addons/recorder/fnc_getConfigDisplayName.sqf @@ -0,0 +1,71 @@ +/* ---------------------------------------------------------------------------- +FILE: fnc_getConfigDisplayName.sqf + +FUNCTION: OCAP_recorder_fnc_getConfigDisplayName + +Description: + The display name of a config class, safe to store in a recording, or "" when + the config holds no name that is. + + Never store a localized string in a recording: getText resolves a stringtable + reference in the *recording server's* language, and the file is read in eight. + getTextRaw returns what the config actually holds: + + - a literal, the text the mod author typed, which is returned as-is. + - a "$STR_..." reference, which SQF can only read in the game's language. + "" is returned and the caller falls back to something English, usually + the class name. + - an __EVAL(...) expression, stored unevaluated: getTextRaw hands back its + source, `format ["%1 [BLUE]", getText (...)]`, and only getText runs it. + The evaluated name is returned, or "" if it came out non-ASCII, which + means the expression pulled in a localized name. + + The non-ASCII test is a heuristic, and only expressions go through it: + + - a localized name in a Latin-script language with no diacritics on that + server ("Gewehr [BLUE]" on a German one) is plain ASCII and gets recorded. + SQF cannot tell it from English. Cyrillic, Czech, Polish and the like are + caught. + - an English name with a typographic character ("7.62×51mm", "–") is + rejected, and the caller falls back to its class name. + + Clients have none of our functions, so + ships this one to them as OCAP_recorder_client_getConfigDisplayName. + +Parameters: + _config - The class whose displayName to read [Config] + +Returns: + The display name, or "" [String] + +Examples: + > private _name = [configOf _vehicle] call FUNC(getConfigDisplayName); + > if (_name isEqualTo "") then {_name = typeOf _vehicle}; + +Public: + No + +Author: + Smith +---------------------------------------------------------------------------- */ +#include "script_component.hpp" + +params [["_config", configNull, [configNull]]]; + +private _entry = _config >> "displayName"; +private _name = getTextRaw _entry; + +// For a literal getText agrees with getTextRaw; only an expression differs. +if (_name isEqualType "" && {_name isNotEqualTo ""} && {(_name select [0, 1]) isNotEqualTo "$"}) then { + private _evaluated = getText _entry; + if (_evaluated isNotEqualTo _name) then { + _name = ["", _evaluated] select (((toArray _evaluated) findIf {_x > 127}) == -1); + }; +}; + +// getTextRaw hands back an array for a config entry written as one. +if (_name isEqualType []) then {_name = _name param [0, ""]}; +if (!(_name isEqualType "")) exitWith {""}; +_name = trim _name; +if ((_name select [0, 1]) isEqualTo "$") exitWith {""}; +_name diff --git a/addons/recorder/fnc_installPlayerTrackingClients.sqf b/addons/recorder/fnc_installPlayerTrackingClients.sqf index d75697c..cda9f63 100644 --- a/addons/recorder/fnc_installPlayerTrackingClients.sqf +++ b/addons/recorder/fnc_installPlayerTrackingClients.sqf @@ -72,9 +72,14 @@ if (isNil QGVAR(playerTrackingSettingNames)) then { call GVAR(publishPlayerTrackingSettings); -{ +[[FUNC(getConfigDisplayName)], { + params ["_getConfigDisplayName"]; if (!hasInterface || {!isNil "OCAP_recorder_client_playerTrackingPFH"}) exitWith {}; + // Clients have none of our functions, so the server ships the one config-name + // rule along with this block instead of the sampler keeping its own copy. + OCAP_recorder_client_getConfigDisplayName = _getConfigDisplayName; + OCAP_recorder_client_hasAceMedical = isClass (configFile >> "CfgPatches" >> "ace_medical_engine"); OCAP_recorder_client_hasKatMedical = isClass (configFile >> "CfgPatches" >> "kat_main"); OCAP_recorder_client_hasAceFatigue = isClass (configFile >> "CfgPatches" >> "ace_advanced_fatigue"); @@ -202,11 +207,11 @@ call GVAR(publishPlayerTrackingSettings); // stringtable reference in the *recording server's* language, which froze Czech // item names into recordings the web UI then showed to viewers in every locale. // - // `getTextRaw` hands back what the config actually holds. A mod author who - // typed the name in is giving us English, so that is used as-is. A "$STR_..." - // reference cannot be read in another language from SQF, so the name is derived - // from the class instead, which is English by construction. Either way nothing - // localized reaches the recording. + // OCAP_recorder_fnc_getConfigDisplayName reads what the config actually holds. + // A mod author who typed the name in is giving us English, so that is used + // as-is, and an __EVAL(...) name is evaluated. A "$STR_..." reference cannot be + // read in another language from SQF, so the name is derived from the class + // instead, which is English by construction. // // Keep the derivation in step with ui/src/data/gearDisplayName.ts and // python/convert_recording_gear.py, which derive the same label. @@ -298,18 +303,21 @@ call GVAR(publishPlayerTrackingSettings); // // `getText` was the problem: it resolves a stringtable reference in the // *recording server's* language, which is how Czech item names reached viewers - // in eight locales. `getTextRaw` returns what the config actually holds, and - // that answers one of two ways: + // in eight locales. OCAP_recorder_fnc_getConfigDisplayName reads what the config + // actually holds, and that answers one of three ways: // // - a literal, which is the text the mod author typed and so is English. On a // real mission take this covers about 95% of the classes a player carries: // "5.56mm 30rnd Tracer Mag", "40mm HE Grenade Round (L123)". + // - an __EVAL(...) expression, which is evaluated. It is usually a literal + // parent name plus a suffix: "[US] MOLLE Sturmgepak (OCP/Rajio) [BLUE]". // - a "$STR_..." reference, which SQF can only read in the language the game // is running. There is no way to ask for English, so the name is built from // the class instead, which is English by construction: "ACE_elasticBandage" // becomes "Elastic Bandage" and "ItemMap" becomes "Map". // - // Either way the recording only ever holds English. + // An expression is the one way a localized name can still get through; see + // that function for the limits. OCAP_recorder_client_gearDisplayName = { params [["_className", "", [""]]]; if (_className isEqualTo "") exitWith {""}; @@ -321,16 +329,11 @@ call GVAR(publishPlayerTrackingSettings); { private _config = configFile >> _x >> _className; if (isClass _config) exitWith { - _raw = getTextRaw (_config >> "displayName"); + _raw = [_config] call OCAP_recorder_client_getConfigDisplayName; }; } forEach ["CfgWeapons", "CfgMagazines", "CfgVehicles", "CfgGlasses"]; - // getTextRaw hands back an array for a config entry written as one. - if (_raw isEqualType []) then {_raw = _raw param [0, ""]}; - if (!(_raw isEqualType "")) then {_raw = ""}; - _raw = trim _raw; - - private _name = if (_raw isEqualTo "" || {(_raw select [0, 1]) isEqualTo "$"}) then { + private _name = if (_raw isEqualTo "") then { [_className] call OCAP_recorder_client_deriveNameFromClass } else { _raw @@ -1708,11 +1711,9 @@ call GVAR(publishPlayerTrackingSettings); if ([_customName] call OCAP_recorder_client_isEnglishText) then {_customName} else {"Custom Module"} }; - private _raw = getTextRaw (configOf _object >> "displayName"); - if (_raw isEqualType []) then {_raw = _raw param [0, ""]}; - if (!(_raw isEqualType "")) then {_raw = ""}; - _raw = trim _raw; - if (_raw isNotEqualTo "" && {(_raw select [0, 1]) isNotEqualTo "$"} && {[_raw] call OCAP_recorder_client_isEnglishText}) then { + // Module names get the stricter isEnglishText on top of the shared rule. + private _raw = [configOf _object] call OCAP_recorder_client_getConfigDisplayName; + if (_raw isNotEqualTo "" && {[_raw] call OCAP_recorder_client_isEnglishText}) then { _raw } else { [_className] call OCAP_recorder_client_humanizeZeusModuleClass @@ -1903,4 +1904,4 @@ call GVAR(publishPlayerTrackingSettings); }] call CBA_fnc_addEventHandler; }; }; -} remoteExec ["call", [0, -2] select isDedicated, QGVAR(playerTrackingBootstrapJip)]; +}] remoteExec ["call", [0, -2] select isDedicated, QGVAR(playerTrackingBootstrapJip)]; diff --git a/addons/recorder/fnc_recordExplosion.sqf b/addons/recorder/fnc_recordExplosion.sqf index 92874a5..2c55fcc 100644 --- a/addons/recorder/fnc_recordExplosion.sqf +++ b/addons/recorder/fnc_recordExplosion.sqf @@ -79,18 +79,17 @@ if (isNil "_info") then { private _radius = getNumber (_cfg >> "indirectHitRange"); private _damage = getNumber (_cfg >> "indirectHit"); - // Never store a localized string in a recording: getText resolves in the - // recording server's language, getTextRaw returns the literal the mod author - // typed. A class whose name is only a $STR_ reference falls back to the class - // name, which is English by construction. See AGENTS.md. - private _name = getTextRaw (_cfg >> "displayName"); + // Never store a localized string in a recording, see FUNC(getConfigDisplayName). + // An ammo without a usable name tries its default magazine, then falls back + // to the class name, which is English by construction. + private _name = [_cfg] call FUNC(getConfigDisplayName); if (_name isEqualTo "") then { private _mag = getText (_cfg >> "defaultMagazine"); if (_mag isNotEqualTo "") then { - _name = getTextRaw (configFile >> "CfgMagazines" >> _mag >> "displayName"); + _name = [configFile >> "CfgMagazines" >> _mag] call FUNC(getConfigDisplayName); }; }; - if (_name isEqualTo "" || {(_name select [0, 1]) isEqualTo "$"}) then {_name = _ammo}; + if (_name isEqualTo "") then {_name = _ammo}; _info = [_radius, _damage, _name]; GVAR(explosionConfigCache) set [_ammo, _info]; diff --git a/addons/recorder/fnc_submitStaticWeapon.sqf b/addons/recorder/fnc_submitStaticWeapon.sqf index 1faa85c..563fdc1 100644 --- a/addons/recorder/fnc_submitStaticWeapon.sqf +++ b/addons/recorder/fnc_submitStaticWeapon.sqf @@ -110,9 +110,9 @@ private _stale = []; {GVAR(staticWeaponSeen) deleteAt _x} forEach _stale; private _class = typeOf _static; -// Never store a localized string in a recording -- see AGENTS.md. -private _name = getTextRaw (configFile >> "CfgVehicles" >> _class >> "displayName"); -if (_name isEqualTo "" || {(_name select [0, 1]) isEqualTo "$"}) then {_name = _class}; +// Never store a localized string in a recording -- see FUNC(getConfigDisplayName). +private _name = [configFile >> "CfgVehicles" >> _class] call FUNC(getConfigDisplayName); +if (_name isEqualTo "") then {_name = _class}; private _emit = { params ["_static", "_unitId", "_unitName", "_side", "_class", "_name", "_recordingStart"]; diff --git a/addons/recorder/fnc_trackConstructions.sqf b/addons/recorder/fnc_trackConstructions.sqf index d1a4d4b..ee01006 100644 --- a/addons/recorder/fnc_trackConstructions.sqf +++ b/addons/recorder/fnc_trackConstructions.sqf @@ -132,9 +132,9 @@ GVAR(submitFortification) = { }; private _class = typeOf _object; - // Never store a localized string in a recording -- see AGENTS.md. - private _name = getTextRaw (configFile >> "CfgVehicles" >> _class >> "displayName"); - if (_name isEqualTo "" || {(_name select [0, 1]) isEqualTo "$"}) then {_name = _class}; + // Never store a localized string in a recording -- see FUNC(getConfigDisplayName). + private _name = [configFile >> "CfgVehicles" >> _class] call FUNC(getConfigDisplayName); + if (_name isEqualTo "") then {_name = _class}; private _clock = [daytime, "HH:MM"] call BIS_fnc_timeToString; private _label = format ["%1 - %2 - %3", _name, name _unit, _clock]; @@ -221,9 +221,9 @@ GVAR(staticWeaponSnapshot) = createHashMap; private _known = _previous getOrDefault [_id, []]; private _name = ""; if (_known isEqualTo []) then { - // Never store a localized string in a recording -- see AGENTS.md. - _name = getTextRaw (configOf _x >> "displayName"); - if (_name isEqualTo "" || {(_name select [0, 1]) isEqualTo "$"}) then {_name = typeOf _x}; + // Never store a localized string in a recording -- see FUNC(getConfigDisplayName). + _name = [configOf _x] call FUNC(getConfigDisplayName); + if (_name isEqualTo "") then {_name = typeOf _x}; } else { _name = _known select 2; }; diff --git a/addons/recorder/fnc_trackServicing.sqf b/addons/recorder/fnc_trackServicing.sqf index 42610ce..7530cf9 100644 --- a/addons/recorder/fnc_trackServicing.sqf +++ b/addons/recorder/fnc_trackServicing.sqf @@ -123,9 +123,9 @@ GVAR(serviceAttribution) = { /* Function: OCAP_recorder_submitServiceEvent (local) - Writes one serviceEvent. The vehicle display name is resolved with getTextRaw - and cached on the object, because getText would write the recording server's - locale into a file read in eight languages (see AGENTS.md). + Writes one serviceEvent. The vehicle display name is resolved with + FUNC(getConfigDisplayName) and cached on the object, because getText would + write the recording server's locale into a file read in eight languages. */ GVAR(submitServiceEvent) = { params ["_kind", "_vehicle", "_vehicleId", "_unitId", "_unitName", "_side", "_from", "_to", ["_extra", []]]; @@ -140,8 +140,8 @@ GVAR(submitServiceEvent) = { _pos = getPosASL _vehicle; _vehicleName = _vehicle getVariable [QGVAR(serviceDisplayName), ""]; if (_vehicleName isEqualTo "") then { - _vehicleName = getTextRaw (configOf _vehicle >> "displayName"); - if (_vehicleName isEqualTo "" || {(_vehicleName select [0, 1]) isEqualTo "$"}) then { + _vehicleName = [configOf _vehicle] call FUNC(getConfigDisplayName); + if (_vehicleName isEqualTo "") then { _vehicleName = typeOf _vehicle; }; _vehicle setVariable [QGVAR(serviceDisplayName), _vehicleName]; @@ -316,8 +316,8 @@ if (isClass (configFile >> "CfgPatches" >> "ace_rearm")) then { _side = str (side group _unit); }; - private _magazineName = getTextRaw (configFile >> "CfgMagazines" >> _magazineClass >> "displayName"); - if (_magazineName isEqualTo "" || {(_magazineName select [0, 1]) isEqualTo "$"}) then { + private _magazineName = [configFile >> "CfgMagazines" >> _magazineClass] call FUNC(getConfigDisplayName); + if (_magazineName isEqualTo "") then { _magazineName = _magazineClass; }; diff --git a/addons/recorder/fnc_trackTrenches.sqf b/addons/recorder/fnc_trackTrenches.sqf index 98dada7..2a3d2ba 100644 --- a/addons/recorder/fnc_trackTrenches.sqf +++ b/addons/recorder/fnc_trackTrenches.sqf @@ -161,8 +161,9 @@ private _fnc_submitTrench = { private _name = ""; private _icon = "trench"; if (_info isEqualTo []) then { - // Unlisted class -- fall back to the server's locale and the generic icon. - _name = getText (configFile >> "CfgVehicles" >> _class >> "displayName"); + // Unlisted class -- fall back to the config name and the generic icon. Never + // store a localized string in a recording, see FUNC(getConfigDisplayName). + _name = [configFile >> "CfgVehicles" >> _class] call FUNC(getConfigDisplayName); if (_name isEqualTo "") then {_name = _class}; } else { _info params ["_localName", "_localIcon"]; diff --git a/docs/player-detail-tracking.md b/docs/player-detail-tracking.md index b087256..5e73003 100644 --- a/docs/player-detail-tracking.md +++ b/docs/player-detail-tracking.md @@ -96,10 +96,20 @@ holds: from the class instead, which is English by construction: `ACE_elasticBandage` becomes "Elastic Bandage" and `ItemMap` becomes "Map". This is what happens for vanilla and ACE items. - -Either way the recording holds an English name for every item, including the two -medical labels the recorder resolves itself (IV bags and medications). The web UI -only derives a name when a recording does not carry one at all. +- an **`__EVAL(...)` expression** — `getTextRaw` hands back its source, e.g. + `format ["%1 [BLUE]", getText (...)]`, and only `getText` runs it. The evaluated + name is recorded, unless it came out non-ASCII, which means the expression pulled + in a localized name; then the name is built from the class as above. + +The same rule, `OCAP_recorder_fnc_getConfigDisplayName`, names vehicles, +magazines, explosions, static weapons, fortifications and trenches on the server; +the server ships it to clients for gear and Zeus modules. + +The recording therefore holds an English name for every item, including the two +medical labels the recorder resolves itself (IV bags and medications), with one +gap: the non-ASCII test on an expression cannot catch a localized name that is +plain ASCII, such as German without umlauts. The web UI only derives a name when +a recording does not carry one at all. Recordings made before this change are converted by `python/convert_recording_gear.py`, which translates a recorded name into English