Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions addons/recorder/XEH_prep.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ PREP(eh_killed);
PREP(getInstigator);
PREP(getEventWeaponText);
PREP(getWeaponDisplayData);
PREP(getConfigDisplayName);

PREP(entityMonitors);
PREP(telemetryLoop);
Expand Down
71 changes: 71 additions & 0 deletions addons/recorder/fnc_getConfigDisplayName.sqf
Original file line number Diff line number Diff line change
@@ -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 <OCAP_recorder_fnc_installPlayerTrackingClients>
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
45 changes: 23 additions & 22 deletions addons/recorder/fnc_installPlayerTrackingClients.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {""};
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)];
13 changes: 6 additions & 7 deletions addons/recorder/fnc_recordExplosion.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
6 changes: 3 additions & 3 deletions addons/recorder/fnc_submitStaticWeapon.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down
12 changes: 6 additions & 6 deletions addons/recorder/fnc_trackConstructions.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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;
};
Expand Down
14 changes: 7 additions & 7 deletions addons/recorder/fnc_trackServicing.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -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", []]];
Expand All @@ -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];
Expand Down Expand Up @@ -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;
};

Expand Down
5 changes: 3 additions & 2 deletions addons/recorder/fnc_trackTrenches.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down
18 changes: 14 additions & 4 deletions docs/player-detail-tracking.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading