@@ -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.
131132struct 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+
171193const 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