Skip to content

Commit 22605da

Browse files
committed
tooltip: format compare-item percent stats via PERCENTAGE_STRING
Block/dodge/parry chance and crit/hit are native vanilla percentages, so their compare-tooltip delta read as a bare number ("-5 Block"). Attach the percent via the locale-aware PERCENTAGE_STRING global so it renders "-5% Block" without touching any _SHORT label (correct in every locale). IsPercentStat marks the percent keys; FormatPercent substitutes the value into PERCENTAGE_STRING's first conversion slot by hand (never feeds the locale format to snprintf) with a plain "%" fallback. Flat stats (Defense, Attack Power, Block Value) stay bare; dropped the now-redundant trailing " %" from the English fallback labels.
1 parent 7378e0b commit 22605da

1 file changed

Lines changed: 86 additions & 16 deletions

File tree

src/tooltip/CompareItem.cpp

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,9 @@ int SlotsForInvType(int invType, int *out) {
126126
// English fallback labels, used when the client doesn't define the
127127
// modern `_G[key]` global-string (vanilla 1.12 largely doesn't). Phrasing
128128
// mirrors pfUI's eqcompare so the deltas read consistently on this
129-
// client. Percent stats keep a "%" suffix since the values are native
130-
// vanilla percentages.
129+
// client. Percent stats carry NO "%" here — `IsPercentStat` appends the
130+
// "%" to the value ("-5% Block"), so the unit is correct regardless of
131+
// which label (Blizzard `_SHORT` or this fallback) is used.
131132
struct KeyLabel {
132133
const char *key;
133134
const char *label;
@@ -149,25 +150,46 @@ constexpr KeyLabel kFallbackLabels[] = {
149150
{"RESISTANCE6_NAME", "Arcane Resistance"},
150151
{"ITEM_MOD_ATTACK_POWER_SHORT", "Attack Power"},
151152
{"ITEM_MOD_RANGED_ATTACK_POWER_SHORT", "Ranged Attack Power"},
152-
{"ITEM_MOD_CRIT_MELEE_RATING", "Melee Crit %"},
153-
{"ITEM_MOD_CRIT_RANGED_RATING", "Ranged Crit %"},
154-
{"ITEM_MOD_HIT_MELEE_RATING", "Melee Hit %"},
155-
{"ITEM_MOD_HIT_RANGED_RATING", "Ranged Hit %"},
156-
{"ITEM_MOD_HIT_SPELL_RATING", "Spell Hit %"},
157-
{"ITEM_MOD_CRIT_SPELL_RATING", "Spell Crit %"},
153+
{"ITEM_MOD_CRIT_MELEE_RATING", "Melee Crit"},
154+
{"ITEM_MOD_CRIT_RANGED_RATING", "Ranged Crit"},
155+
{"ITEM_MOD_HIT_MELEE_RATING", "Melee Hit"},
156+
{"ITEM_MOD_HIT_RANGED_RATING", "Ranged Hit"},
157+
{"ITEM_MOD_HIT_SPELL_RATING", "Spell Hit"},
158+
{"ITEM_MOD_CRIT_SPELL_RATING", "Spell Crit"},
158159
{"ITEM_MOD_SPELL_DAMAGE_DONE_SHORT", "Spell Damage"},
159160
{"ITEM_MOD_SPELL_HEALING_DONE_SHORT", "Spell Healing"},
160161
{"ITEM_MOD_MANA_REGENERATION", "Mana Regen"},
161162
{"ITEM_MOD_DEFENSE_SKILL_RATING", "Defense"},
162-
{"ITEM_MOD_DODGE_RATING", "Dodge %"},
163-
{"ITEM_MOD_PARRY_RATING", "Parry %"},
164-
{"ITEM_MOD_BLOCK_RATING", "Block %"},
163+
{"ITEM_MOD_DODGE_RATING", "Dodge"},
164+
{"ITEM_MOD_PARRY_RATING", "Parry"},
165+
{"ITEM_MOD_BLOCK_RATING", "Block"},
165166
{"ITEM_MOD_BLOCK_VALUE", "Block Value"},
166167
{"ITEM_MOD_DAMAGE_PER_SECOND_SHORT", "Damage Per Second"},
167168
{"ITEM_DELTA_DESCRIPTION",
168169
"If you replace this item, the following stat changes will occur:"},
169170
};
170171

172+
// Percent-valued stat keys: the block/dodge/parry *chances* and the
173+
// crit/hit percentages. Vanilla stores these as native percentages (a
174+
// value of 5 means 5%), so the delta must read "-5% Block", not "-5
175+
// Block". The "%" is attached to the NUMBER rather than the label, so it
176+
// stays correct in every locale and requires no `_SHORT` change.
177+
// NB: ITEM_MOD_DEFENSE_SKILL_RATING (skill points) and
178+
// ITEM_MOD_BLOCK_VALUE (flat block amount) are NOT percentages.
179+
bool IsPercentStat(const char *key) {
180+
static const char *const kPercent[] = {
181+
"ITEM_MOD_CRIT_MELEE_RATING", "ITEM_MOD_CRIT_RANGED_RATING",
182+
"ITEM_MOD_CRIT_SPELL_RATING", "ITEM_MOD_HIT_MELEE_RATING",
183+
"ITEM_MOD_HIT_RANGED_RATING", "ITEM_MOD_HIT_SPELL_RATING",
184+
"ITEM_MOD_DODGE_RATING", "ITEM_MOD_PARRY_RATING",
185+
"ITEM_MOD_BLOCK_RATING",
186+
};
187+
for (const char *k : kPercent)
188+
if (std::strcmp(k, key) == 0)
189+
return true;
190+
return false;
191+
}
192+
171193
const char *FallbackLabel(const char *key) {
172194
for (const auto &e : kFallbackLabels)
173195
if (std::strcmp(e.key, key) == 0)
@@ -199,22 +221,63 @@ bool EndsWithShort(const char *key) {
199221
return n >= 6 && std::strcmp(key + n - 6, "_SHORT") == 0;
200222
}
201223

202-
// Copies `_G[key]` into `out` iff it's a plain, non-empty, non-format
203-
// string. Direct globals-table access, no pcall. Returns whether it took.
204-
bool TryGlobalLabel(void *L, const char *key, char *out, int outSize) {
224+
// Raw `_G[key]` string fetch — no format-specifier filtering. Direct
225+
// globals-table access, no pcall. Returns whether a non-empty string was
226+
// copied into `out`.
227+
bool TryGlobalRaw(void *L, const char *key, char *out, int outSize) {
205228
const int saved = Game::Lua::GetTop(L);
206229
Game::Lua::PushString(L, key);
207230
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
208231
const char *g = (Game::Lua::Type(L, -1) == Game::Lua::TYPE_STRING)
209232
? Game::Lua::ToString(L, -1)
210233
: nullptr;
211-
const bool ok = (g != nullptr && *g != '\0' && !HasFormatSpecifier(g));
234+
const bool ok = (g != nullptr && *g != '\0');
212235
if (ok)
213236
std::snprintf(out, outSize, "%s", g);
214237
Game::Lua::SetTop(L, saved);
215238
return ok;
216239
}
217240

241+
// Copies `_G[key]` into `out` iff it's a plain, non-empty, non-format
242+
// string. Returns whether it took.
243+
bool TryGlobalLabel(void *L, const char *key, char *out, int outSize) {
244+
char tmp[192];
245+
if (!TryGlobalRaw(L, key, tmp, sizeof(tmp)) || HasFormatSpecifier(tmp))
246+
return false;
247+
std::snprintf(out, outSize, "%s", tmp);
248+
return true;
249+
}
250+
251+
// Formats a signed value token (e.g. "-5") as a localized percentage via
252+
// the PERCENTAGE_STRING global (typically "%s%%" → "-5%"). The locale
253+
// format is NEVER passed to snprintf — we substitute `num` into its first
254+
// conversion slot (`%s`/`%d`) by hand and collapse `%%` → `%`, so a
255+
// hostile or unexpected format can't misread the argument list. Falls back
256+
// to a plain "%" suffix when the global is absent.
257+
void FormatPercent(void *L, const char *num, char *out, int outSize) {
258+
char fmt[64];
259+
if (!TryGlobalRaw(L, "PERCENTAGE_STRING", fmt, sizeof(fmt))) {
260+
std::snprintf(out, outSize, "%s%%", num);
261+
return;
262+
}
263+
int o = 0;
264+
bool inserted = false;
265+
for (const char *p = fmt; *p != '\0' && o < outSize - 1; ++p) {
266+
if (p[0] == '%' && p[1] == '%') { // literal percent
267+
out[o++] = '%';
268+
++p;
269+
} else if (p[0] == '%' && p[1] != '\0' && !inserted) { // value slot
270+
for (const char *q = num; *q != '\0' && o < outSize - 1; ++q)
271+
out[o++] = *q;
272+
++p; // consume the conversion letter (s / d)
273+
inserted = true;
274+
} else {
275+
out[o++] = *p;
276+
}
277+
}
278+
out[o] = '\0';
279+
}
280+
218281
// Resolve a display label for a stat `key` into `out`, respecting
219282
// localization. The rating/regen tokens GetItemStats keys on
220283
// (`ITEM_MOD_CRIT_MELEE_RATING`, …) carry a "%s" *description* as their
@@ -285,7 +348,14 @@ void RenderDeltas(void *self, void *L, const Accum *acc, double dpsDelta) {
285348
if (v == 0)
286349
continue;
287350
LabelFor(L, acc[i].key, label, sizeof(label));
288-
std::snprintf(line, sizeof(line), "%s%ld %s", v > 0 ? "+" : "", v, label);
351+
char num[48];
352+
std::snprintf(num, sizeof(num), "%s%ld", v > 0 ? "+" : "", v);
353+
if (IsPercentStat(acc[i].key)) {
354+
char pct[64];
355+
FormatPercent(L, num, pct, sizeof(pct));
356+
std::snprintf(num, sizeof(num), "%s", pct);
357+
}
358+
std::snprintf(line, sizeof(line), "%s %s", num, label);
289359
AddColoredLine(self, line, v > 0 ? kGain : kLoss);
290360
}
291361
}

0 commit comments

Comments
 (0)