Skip to content

Commit 76e40fe

Browse files
committed
refactor: share ASCII case-insensitive compare + DBC reverse token lookup
Add baselib/Ascii.h (header-only Ascii::ToLower / Ascii::EqualCI) and replace the two hand-rolled case-insensitive compares (frame/Attributes' EqI, taxi/Map's IEqual + AsciiLower). Add DBC::ClassIdForToken / RaceIdForToken (token -> ChrClasses/ChrRaces id, the reverse of ClassToken/RaceToken) backed by a shared IdForStringField walk using Ascii::EqualCI, and replace player/NameCache's two near-identical ResolveClassToken / ResolveRaceToken loops.
1 parent b72c52d commit 76e40fe

6 files changed

Lines changed: 105 additions & 113 deletions

File tree

src/baselib/Ascii.h

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// This file is part of ClassicAPI.
2+
//
3+
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
4+
// of the GNU General Public License as published by the Free Software Foundation, either
5+
// version 3 of the License, or (at your option) any later version.
6+
//
7+
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
8+
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
9+
// PURPOSE. See the GNU General Public License for more details.
10+
//
11+
// You should have received a copy of the GNU General Public License along with
12+
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
13+
14+
#pragma once
15+
16+
// Tiny ASCII string helpers. Header-only — no locale, no allocation, safe from
17+
// any context (hook callbacks, DBC walks). Use these instead of hand-rolling
18+
// yet another case-insensitive compare.
19+
20+
namespace Ascii {
21+
22+
// Fold a single ASCII char to lower case (non-letters unchanged).
23+
inline char ToLower(char c) {
24+
return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c;
25+
}
26+
27+
// Case-insensitive full-string equality over ASCII. Two null pointers compare
28+
// equal; a null vs non-null does not.
29+
inline bool EqualCI(const char *a, const char *b) {
30+
if (a == nullptr || b == nullptr)
31+
return a == b;
32+
for (; *a && *b; ++a, ++b)
33+
if (ToLower(*a) != ToLower(*b))
34+
return false;
35+
return *a == *b;
36+
}
37+
38+
} // namespace Ascii

src/dbc/Names.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,37 @@
1515

1616
#include "Lookup.h"
1717
#include "Offsets.h"
18+
#include "baselib/Ascii.h"
1819

1920
namespace DBC {
2021

22+
namespace {
23+
24+
// Reverse lookup shared by ClassIdForToken / RaceIdForToken: the id whose
25+
// single-string field at `offset` matches `token` (case-insensitive), or 0.
26+
uint32_t IdForStringField(uintptr_t recordsVar, uintptr_t countVar, int offset,
27+
const char *token) {
28+
if (token == nullptr || *token == '\0')
29+
return 0;
30+
const int count = *reinterpret_cast<const int *>(countVar);
31+
const uint8_t *const *records =
32+
*reinterpret_cast<const uint8_t *const *const *>(recordsVar);
33+
if (records == nullptr)
34+
return 0;
35+
for (int i = 1; i <= count; ++i) {
36+
const uint8_t *rec = records[i];
37+
if (rec == nullptr)
38+
continue;
39+
const char *field =
40+
*reinterpret_cast<const char *const *>(rec + offset);
41+
if (field != nullptr && Ascii::EqualCI(field, token))
42+
return static_cast<uint32_t>(i);
43+
}
44+
return 0;
45+
}
46+
47+
} // namespace
48+
2149
const char *ClassName(uint32_t classID) {
2250
if (classID == 0)
2351
return nullptr;
@@ -50,6 +78,18 @@ const char *RaceToken(uint32_t raceID) {
5078
Offsets::OFF_CHRRACES_FILENAME);
5179
}
5280

81+
uint32_t ClassIdForToken(const char *token) {
82+
return IdForStringField(Offsets::VAR_CHRCLASSES_RECORDS,
83+
Offsets::VAR_CHRCLASSES_COUNT,
84+
Offsets::OFF_CHRCLASSES_FILENAME, token);
85+
}
86+
87+
uint32_t RaceIdForToken(const char *token) {
88+
return IdForStringField(Offsets::VAR_CHRRACES_RECORDS,
89+
Offsets::VAR_CHRRACES_COUNT,
90+
Offsets::OFF_CHRRACES_FILENAME, token);
91+
}
92+
5393
const char *AreaName(uint32_t areaID, bool resolveToParent) {
5494
if (areaID == 0)
5595
return nullptr;

src/dbc/Names.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ const char *RaceName(uint32_t raceID);
3939
// `UnitRace`'s second return / GetPlayerInfoByGUID's englishRace use.
4040
const char *RaceToken(uint32_t raceID);
4141

42+
// Reverse of ClassToken / RaceToken: the `ChrClasses.dbc` / `ChrRaces.dbc` id
43+
// whose client-filename token matches `token` (case-insensitive), or 0 if none
44+
// matches. Walks the table (cheap — a handful of records).
45+
uint32_t ClassIdForToken(const char *token);
46+
uint32_t RaceIdForToken(const char *token);
47+
4248
// Localized zone name for an `AreaTable.dbc` id. When `resolveToParent` is
4349
// true, a sub-area reports its parent zone (so "Goldshire" → "Elwynn
4450
// Forest"); when false, the area's own name is returned (what the engine's

src/frame/Attributes.cpp

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696

9797
#include "Game.h"
9898
#include "Offsets.h"
99+
#include "baselib/Ascii.h"
99100
#include "cursor/Info.h"
100101
#include "spell/AtCursor.h"
101102
#include "spell/AtUnit.h"
@@ -147,15 +148,6 @@ void Compose3Lower(char *dst, size_t n, const char *a, const char *b, const char
147148
dst[i] = '\0';
148149
}
149150

150-
// ASCII case-insensitive full-string equality.
151-
bool EqI(const char *a, const char *b) {
152-
for (; *a && *b; ++a, ++b)
153-
if (std::tolower(static_cast<unsigned char>(*a)) !=
154-
std::tolower(static_cast<unsigned char>(*b)))
155-
return false;
156-
return *a == *b;
157-
}
158-
159151
// True if `lname` (already lowercase) is a click "type" action attribute: an
160152
// optional modifier prefix (alt-/ctrl-/shift-, any order/combo) followed by
161153
// "type" and an optional button digit — i.e. anything the click resolver
@@ -510,10 +502,10 @@ void BuildModifierPrefix(void *L, char *buf, size_t n) {
510502

511503
// Button name -> attribute suffix (retail's convention: the button number).
512504
const char *ButtonSuffix(const char *btn) {
513-
if (EqI(btn, "RightButton")) return "2";
514-
if (EqI(btn, "MiddleButton")) return "3";
515-
if (EqI(btn, "Button4")) return "4";
516-
if (EqI(btn, "Button5")) return "5";
505+
if (Ascii::EqualCI(btn, "RightButton")) return "2";
506+
if (Ascii::EqualCI(btn, "MiddleButton")) return "3";
507+
if (Ascii::EqualCI(btn, "Button4")) return "4";
508+
if (Ascii::EqualCI(btn, "Button5")) return "5";
517509
return "1"; // LeftButton / unknown
518510
}
519511

@@ -601,10 +593,10 @@ bool CallGlobalNum2(void *L, const char *name, double a, double b) {
601593
// Returns true if it owned the click (so the chained handler is skipped).
602594
bool DispatchVerb(void *L, int fi, const char *prefix, const char *suffix,
603595
const char *verb, const char *unit) {
604-
if (EqI(verb, "target")) {
596+
if (Ascii::EqualCI(verb, "target")) {
605597
if (!unit) return false;
606598
// `unit="none"` clears the target (retail's SecureActionButton behavior).
607-
if (EqI(unit, "none")) {
599+
if (Ascii::EqualCI(unit, "none")) {
608600
Game::Lua::CallGlobal(L, "ClearTarget");
609601
return true;
610602
}
@@ -621,25 +613,25 @@ bool DispatchVerb(void *L, int fi, const char *prefix, const char *suffix,
621613
Game::Lua::CallGlobalString(L, "TargetUnit", unit);
622614
return true;
623615
}
624-
if (EqI(verb, "assist")) {
616+
if (Ascii::EqualCI(verb, "assist")) {
625617
if (!unit) return false;
626618
Game::Lua::CallGlobalString(L, "AssistUnit", unit);
627619
return true;
628620
}
629-
if (EqI(verb, "focus")) {
621+
if (Ascii::EqualCI(verb, "focus")) {
630622
if (!unit) return false;
631623
Unit::Focus::Set(Unit::Identity::GuidForToken(unit));
632624
return true;
633625
}
634-
if (EqI(verb, "spell")) {
626+
if (Ascii::EqualCI(verb, "spell")) {
635627
if (!unit) return false;
636628
char spell[128];
637629
if (!ReadModAttr(L, fi, prefix, "spell", suffix, spell, sizeof spell))
638630
return false;
639631
Spell::AtUnit::CastByName(spell, unit);
640632
return true;
641633
}
642-
if (EqI(verb, "item")) {
634+
if (Ascii::EqualCI(verb, "item")) {
643635
char item[128];
644636
int bag, slot;
645637
if (ReadModAttr(L, fi, prefix, "item", suffix, item, sizeof item)) {
@@ -657,7 +649,7 @@ bool DispatchVerb(void *L, int fi, const char *prefix, const char *suffix,
657649
}
658650
return false;
659651
}
660-
if (EqI(verb, "macro")) {
652+
if (Ascii::EqualCI(verb, "macro")) {
661653
char macro[512];
662654
if (!ReadModAttr(L, fi, prefix, "macrotext", suffix, macro, sizeof macro) &&
663655
!ReadModAttr(L, fi, prefix, "macro", suffix, macro, sizeof macro))
@@ -669,11 +661,11 @@ bool DispatchVerb(void *L, int fi, const char *prefix, const char *suffix,
669661
RunMacroTextC(L, macro);
670662
return true;
671663
}
672-
if (EqI(verb, "stop") || EqI(verb, "stopcasting")) {
664+
if (Ascii::EqualCI(verb, "stop") || Ascii::EqualCI(verb, "stopcasting")) {
673665
Game::Lua::CallGlobal(L, "SpellStopCasting");
674666
return true;
675667
}
676-
if (EqI(verb, "menu") || EqI(verb, "togglemenu")) {
668+
if (Ascii::EqualCI(verb, "menu") || Ascii::EqualCI(verb, "togglemenu")) {
677669
if (!unit) return false;
678670
// The unit dropdown is pure FrameXML work (UnitPopup + ToggleDropDown),
679671
// so it lives in the !!!ClassicAPI addon; we just pop it at the cursor.
@@ -793,7 +785,7 @@ int __fastcall FrameResolver_h(void *frame, void *edx, const char *name) {
793785
const int slot = g_frameResolverOriginal(frame, edx, name);
794786
if (slot != 0) // a real base-frame / subtype script — leave it
795787
return slot;
796-
if (EqI(name, "onattributechanged"))
788+
if (Ascii::EqualCI(name, "onattributechanged"))
797789
return reinterpret_cast<int>(AttrSlotFor(frame, /*create*/ true));
798790
return 0;
799791
}

src/player/NameCache.cpp

Lines changed: 3 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666

6767
#include "Game.h"
6868
#include "Offsets.h"
69+
#include "dbc/Names.h"
6970
#include "guid/Guid.h"
7071
#include "settings/Account.h"
7172
#include "unit/Identity.h"
@@ -636,80 +637,6 @@ bool ParseGUID(const char *str, uint64_t *outGUID) {
636637
return Guid::Parse(str, outGUID);
637638
}
638639

639-
// Returns the ChrClasses.dbc record ID for a class token like
640-
// `"WARRIOR"`, `"MAGE"`, etc. Case-insensitive. 0 if not matched.
641-
// 1.12 has 9 player classes; we walk the DBC each call (cheap — 9
642-
// records, each filename a few bytes) rather than building a static
643-
// table that risks getting out-of-sync.
644-
uint32_t ResolveClassToken(const char *token) {
645-
if (token == nullptr || *token == '\0')
646-
return 0;
647-
const int count = *reinterpret_cast<const int *>(
648-
static_cast<uintptr_t>(Offsets::VAR_CHRCLASSES_COUNT));
649-
const uint8_t *const *records = *reinterpret_cast<const uint8_t *const *const *>(
650-
static_cast<uintptr_t>(Offsets::VAR_CHRCLASSES_RECORDS));
651-
if (records == nullptr)
652-
return 0;
653-
for (int i = 1; i <= count; ++i) {
654-
const uint8_t *rec = records[i];
655-
if (rec == nullptr)
656-
continue;
657-
const char *filename = *reinterpret_cast<const char *const *>(
658-
rec + Offsets::OFF_CHRCLASSES_FILENAME);
659-
if (filename == nullptr)
660-
continue;
661-
// Case-insensitive compare; vanilla tokens are uppercase
662-
// ("WARRIOR", "MAGE", ...) and modern Classic uses the same.
663-
const char *a = filename;
664-
const char *b = token;
665-
bool match = true;
666-
while (*a && *b) {
667-
char ca = *a, cb = *b;
668-
if (ca >= 'a' && ca <= 'z') ca -= 32;
669-
if (cb >= 'a' && cb <= 'z') cb -= 32;
670-
if (ca != cb) { match = false; break; }
671-
++a; ++b;
672-
}
673-
if (match && *a == '\0' && *b == '\0')
674-
return static_cast<uint32_t>(i);
675-
}
676-
return 0;
677-
}
678-
679-
// Same shape as ResolveClassToken but walks ChrRaces.dbc.
680-
uint32_t ResolveRaceToken(const char *token) {
681-
if (token == nullptr || *token == '\0')
682-
return 0;
683-
const int count = *reinterpret_cast<const int *>(
684-
static_cast<uintptr_t>(Offsets::VAR_CHRRACES_COUNT));
685-
const uint8_t *const *records = *reinterpret_cast<const uint8_t *const *const *>(
686-
static_cast<uintptr_t>(Offsets::VAR_CHRRACES_RECORDS));
687-
if (records == nullptr)
688-
return 0;
689-
for (int i = 1; i <= count; ++i) {
690-
const uint8_t *rec = records[i];
691-
if (rec == nullptr)
692-
continue;
693-
const char *filename = *reinterpret_cast<const char *const *>(
694-
rec + Offsets::OFF_CHRRACES_FILENAME);
695-
if (filename == nullptr)
696-
continue;
697-
const char *a = filename;
698-
const char *b = token;
699-
bool match = true;
700-
while (*a && *b) {
701-
char ca = *a, cb = *b;
702-
if (ca >= 'a' && ca <= 'z') ca -= 32;
703-
if (cb >= 'a' && cb <= 'z') cb -= 32;
704-
if (ca != cb) { match = false; break; }
705-
++a; ++b;
706-
}
707-
if (match && *a == '\0' && *b == '\0')
708-
return static_cast<uint32_t>(i);
709-
}
710-
return 0;
711-
}
712-
713640
// `C_PlayerCache.RememberPlayer(guid, name, classToken [, raceToken
714641
// [, sex]])` — stores the entry in the persistent cache. Class and
715642
// race tokens are uppercase engine tokens (`"WARRIOR"`, `"NIGHTELF"`,
@@ -740,13 +667,13 @@ int __fastcall Script_C_PlayerCache_RememberPlayer(void *L) {
740667
return 1;
741668
}
742669
const char *classToken = Game::Lua::ToString(L, 3);
743-
const uint32_t classID = ResolveClassToken(classToken);
670+
const uint32_t classID = DBC::ClassIdForToken(classToken);
744671
// Optional race + sex. Missing / non-string / unknown values
745672
// resolve to 0, which Remember() treats as "leave existing value
746673
// alone" — so a 3-arg call preserves any prior race/sex data.
747674
uint32_t raceID = 0;
748675
if (Game::Lua::IsString(L, 4))
749-
raceID = ResolveRaceToken(Game::Lua::ToString(L, 4));
676+
raceID = DBC::RaceIdForToken(Game::Lua::ToString(L, 4));
750677
uint32_t sex = 0;
751678
if (Game::Lua::IsNumber(L, 5)) {
752679
const double raw = Game::Lua::ToNumber(L, 5);

src/taxi/Map.cpp

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
#include "Game.h"
3636
#include "Offsets.h"
37+
#include "baselib/Ascii.h"
3738
#include "dbc/Lookup.h"
3839
#include "dbc/Names.h"
3940
#include "map/Area.h"
@@ -67,24 +68,12 @@ int IntField(const uint8_t *rec, int off) {
6768
// suffix against AreaTable and fall back to the geometric resolver only when a
6869
// node has no ", Zone" suffix or the zone name doesn't match.
6970

70-
char AsciiLower(char c) { return (c >= 'A' && c <= 'Z') ? char(c - 'A' + 'a') : c; }
71-
72-
bool IEqual(const char *a, const char *b) {
73-
while (*a && *b) {
74-
if (AsciiLower(*a) != AsciiLower(*b))
75-
return false;
76-
++a;
77-
++b;
78-
}
79-
return *a == *b;
80-
}
81-
8271
// True if `name` begins with `prefix` followed by a space — a word-prefix, so
8372
// the abbreviated taxi suffixes ("Redridge", "Arathi") match the full
8473
// AreaTable names ("Redridge Mountains", "Arathi Highlands").
8574
bool IStartsWord(const char *name, const char *prefix) {
8675
while (*prefix) {
87-
if (!*name || AsciiLower(*name) != AsciiLower(*prefix))
76+
if (!*name || Ascii::ToLower(*name) != Ascii::ToLower(*prefix))
8877
return false;
8978
++name;
9079
++prefix;
@@ -135,7 +124,7 @@ int ResolveZoneByName(const char *zone) {
135124
DBC::AreaName(static_cast<uint32_t>(id), /*resolveToParent=*/false);
136125
if (nm == nullptr)
137126
continue;
138-
if (IEqual(nm, zone))
127+
if (Ascii::EqualCI(nm, zone))
139128
return id; // exact wins outright
140129
if (prefixHit == 0 && IStartsWord(nm, zone))
141130
prefixHit = id;

0 commit comments

Comments
 (0)