2020// code → .lua / .xml / .toc / .xsd → BlizzardInterfaceCode\...
2121// art → .blp / .tga → BlizzardInterfaceArt\...
2222//
23+ // `ExportDBCFiles` — ClassicAPI-original companion that dumps every
24+ // `.dbc` table the client loads to `DBFilesClient\`. It unions the MPQ
25+ // `(listfile)` with a scan of `.text` for the DBC path-getter pattern
26+ // (`mov eax, &"DBFilesClient\X.dbc"; ret`) — the latter is the
27+ // authoritative "what does this build load" list, since the master DBC
28+ // init is straight-line (no name table), and it catches the ~18 DBCs the
29+ // listfile doesn't index. See `ScanPathGetters`.
30+ //
2331// Files land relative to the client's working directory, mirroring the
2432// retail command's `BlizzardInterface{Code,Art}\<relative-path>`
2533// layout. On completion it writes a "wrote N files" line back to the
5563
5664#include < cstdint>
5765#include < cstdio>
66+ #include < cstring>
5867#include < string>
5968#include < unordered_set>
6069#include < vector>
@@ -145,69 +154,123 @@ bool WriteFileToDisk(const std::string &relPath, const void *data, unsigned int
145154 return ok;
146155}
147156
148- // Length of the "Interface\" prefix we strip from MPQ paths before
149- // re-rooting under the destination folder.
150- constexpr size_t kRootPrefixLen = 10 ; // strlen("Interface\\")
151-
152157const char *const kCodeExts [] = {" lua" , " xml" , " toc" , " xsd" };
153158const char *const kArtExts [] = {" blp" , " tga" };
159+ const char *const kDbcExts [] = {" dbc" };
154160
155161enum Mode { MODE_CODE , MODE_ART };
156162
157- // Collector for the MPQ enumeration callback. File-static because
158- // FUN_00401470 hands the callback only the path string; enumeration is
159- // synchronous and single-threaded, so a file-static sink is safe.
163+ std::string LowerCopy (const char *s) {
164+ std::string out;
165+ for (const char *p = s; *p; ++p) {
166+ char c = *p;
167+ if (c >= ' A' && c <= ' Z' ) c = static_cast <char >(c + (' a' - ' A' ));
168+ out.push_back (c);
169+ }
170+ return out;
171+ }
172+
173+ // Collector for the MPQ enumeration callback (and the path-getter scan).
174+ // File-static because FUN_00401470 hands the callback only the path
175+ // string; enumeration is synchronous and single-threaded, so a
176+ // file-static sink is safe.
160177struct Collector {
161178 const char *const *exts = nullptr ;
162179 int extCount = 0 ;
163180 std::vector<std::string> files;
164- std::unordered_set<std::string> seen; // dedup across archives
181+ std::unordered_set<std::string> seen; // lowercased keys — dedup across sources
182+
183+ // Case-insensitive dedup so the listfile and the path-getter scan
184+ // (which may differ in casing) never write the same DBC twice.
185+ void Add (const char *path) {
186+ if (seen.insert (LowerCopy (path)).second )
187+ files.emplace_back (path);
188+ }
165189};
166190Collector *g_collector = nullptr ;
167191
168192// MPQ enumeration callback — one call per listfile entry under the
169- // "Interface\" prefix. Keep going (return 1) regardless.
193+ // active prefix. Keep going (return 1) regardless.
170194int __fastcall CollectCb (const char *fullPath) {
171195 Collector *c = g_collector;
172196 if (c != nullptr && fullPath != nullptr &&
173- HasExtension (fullPath, c->exts , c->extCount )) {
174- if (c->seen .insert (fullPath).second )
175- c->files .emplace_back (fullPath);
176- }
197+ HasExtension (fullPath, c->exts , c->extCount ))
198+ c->Add (fullPath);
177199 return 1 ; // 0 would stop enumeration
178200}
179201
180- // Enumerate every MPQ file under Interface\ whose extension matches
181- // `mode`, read each, and write it under the destination root. Returns
182- // the number of files successfully written.
183- int ExportTree (Mode mode) {
184- auto MpqEnumFiles = reinterpret_cast <MpqEnumFiles_t>(Offsets::FUN_MPQ_ENUM_FILES );
185- auto FileRead = reinterpret_cast <FileRead_t>(Offsets::FUN_FILE_READ );
186- auto SMemFree = reinterpret_cast <SMemFree_t>(Offsets::FUN_STORM_SMEM_FREE );
187-
188- const char *dstRoot =
189- (mode == MODE_ART ) ? " BlizzardInterfaceArt" : " BlizzardInterfaceCode" ;
202+ // --- DBC path-getter scan ---------------------------------------------
203+ //
204+ // The DBC loaders aren't driven by any iterable name table — the master
205+ // init (`FUN_0053f8b0`) is straight-line, calling ~150 per-DBC loaders in
206+ // sequence, each of which calls its own one-instruction path-getter:
207+ //
208+ // B8 <&"DBFilesClient\Foo.dbc"> mov eax, <strVA>
209+ // C3 ret
210+ //
211+ // So the authoritative, dynamic list of "what DBCs does this build load"
212+ // is the set of those getters. We recover it the same way docs/DBCs.md
213+ // did — scan `.text` for the `B8 imm32 C3` pattern and keep matches whose
214+ // immediate points at a `.dbc` string. This catches every loaded table,
215+ // including the ~18 the MPQ `(listfile)` doesn't index.
216+
217+ // Build-specific bounds (this 1.12.1 client). `.text` per CLAUDE.md; the
218+ // image range bounds candidate string pointers so the suffix check never
219+ // dereferences unmapped memory. Getter strings live in `.data` (e.g.
220+ // Spell.dbc at 0x00859E30), comfortably inside the image.
221+ constexpr uintptr_t kTextStart = 0x00401000 ;
222+ constexpr uintptr_t kTextEnd = 0x007FEDAC ;
223+ constexpr uintptr_t kImageStart = 0x00400000 ;
224+ constexpr uintptr_t kImageEnd = 0x00D26000 ;
225+
226+ // True if `s` (bounded, never reading past the image) names a ".dbc" file.
227+ bool LooksLikeDbcString (const uint8_t *s) {
228+ const uint8_t *limit = reinterpret_cast <const uint8_t *>(kImageEnd );
229+ size_t n = 0 ;
230+ while (n < 260 && s + n < limit && s[n] != ' \0 ' )
231+ ++n;
232+ if (s + n >= limit || s[n] != ' \0 ' || n < 4 ) // not terminated, or too short
233+ return false ;
234+ return EqualsCI (reinterpret_cast <const char *>(s + n - 4 ), " .dbc" );
235+ }
190236
191- Collector collector;
192- collector.exts = (mode == MODE_ART ) ? kArtExts : kCodeExts ;
193- collector.extCount = (mode == MODE_ART ) ? 2 : 4 ;
237+ // Scan .text for the DBC path-getter pattern and add each referenced
238+ // ".dbc" path to the collector. Synchronous, reads only our own image.
239+ void ScanPathGetters (Collector &c) {
240+ const uint8_t *p = reinterpret_cast <const uint8_t *>(kTextStart );
241+ const uint8_t *end = reinterpret_cast <const uint8_t *>(kTextEnd ) - 6 ;
242+ for (; p <= end; ++p) {
243+ if (p[0 ] != 0xB8 || p[5 ] != 0xC3 ) // mov eax, imm32 ; ret
244+ continue ;
245+ uint32_t imm;
246+ std::memcpy (&imm, p + 1 , sizeof (imm));
247+ if (imm < kImageStart || imm >= kImageEnd )
248+ continue ;
249+ const uint8_t *s = reinterpret_cast <const uint8_t *>(imm);
250+ if (LooksLikeDbcString (s))
251+ c.Add (reinterpret_cast <const char *>(s));
252+ }
253+ }
194254
195- g_collector = &collector;
196- MpqEnumFiles (kArchiveSelector , " Interface\\ " , &CollectCb, nullptr );
197- g_collector = nullptr ;
255+ // Read each collected source path from the MPQs and write it under
256+ // `dstRoot`, re-rooted by stripping the leading `prefixLen` chars (the
257+ // "Interface\" / "DBFilesClient\" prefix). Returns the number written.
258+ int WriteFiles (const std::vector<std::string> &files, size_t prefixLen,
259+ const char *dstRoot) {
260+ auto FileRead = reinterpret_cast <FileRead_t>(Offsets::FUN_FILE_READ );
261+ auto SMemFree = reinterpret_cast <SMemFree_t>(Offsets::FUN_STORM_SMEM_FREE );
198262
199- const std::vector<std::string> &files = collector.files ;
200263 int written = 0 ;
201264 for (const std::string &src : files) {
202265 void *buf = nullptr ;
203266 unsigned int size = 0 ;
204267 if (FileRead (0 , src.c_str (), &buf, &size, 0 , 1 , 0 ) == 0 || buf == nullptr )
205268 continue ;
206269
207- // Re-root: "Interface\FrameXML\ Foo.lua " -> "<dstRoot>\FrameXML \Foo.lua ".
270+ // Re-root: "<prefix>Sub\ Foo.ext " -> "<dstRoot>\Sub \Foo.ext ".
208271 std::string dst = dstRoot;
209272 dst.push_back (' \\ ' );
210- dst.append (src, kRootPrefixLen , std::string::npos);
273+ dst.append (src, prefixLen , std::string::npos);
211274
212275 if (WriteFileToDisk (dst, buf, size))
213276 ++written;
@@ -217,6 +280,47 @@ int ExportTree(Mode mode) {
217280 return written;
218281}
219282
283+ // Enumerate every MPQ file under `pathPrefix` whose extension matches
284+ // one of `exts`, read each, and write it under `dstRoot`. Returns the
285+ // number of files successfully written.
286+ int ExportTree (const char *pathPrefix, const char *const *exts, int extCount,
287+ const char *dstRoot) {
288+ auto MpqEnumFiles = reinterpret_cast <MpqEnumFiles_t>(Offsets::FUN_MPQ_ENUM_FILES );
289+
290+ Collector collector;
291+ collector.exts = exts;
292+ collector.extCount = extCount;
293+
294+ g_collector = &collector;
295+ MpqEnumFiles (kArchiveSelector , pathPrefix, &CollectCb, nullptr );
296+ g_collector = nullptr ;
297+
298+ return WriteFiles (collector.files , std::strlen (pathPrefix), dstRoot);
299+ }
300+
301+ // Export the DBC tables. Unions two sources, deduped case-insensitively:
302+ // 1. the MPQ (listfile) under DBFilesClient\ (catches files that ship
303+ // in the archives without a loader, e.g. wowerror_strings.dbc), and
304+ // 2. the .text path-getter scan (the authoritative set the client
305+ // actually loads — catches the ~18 DBCs absent from the listfile).
306+ // Every DBC path is "DBFilesClient\Name.dbc", so re-rooting strips that
307+ // 14-char prefix and writes under `dstRoot`.
308+ int ExportDBC (const char *dstRoot) {
309+ auto MpqEnumFiles = reinterpret_cast <MpqEnumFiles_t>(Offsets::FUN_MPQ_ENUM_FILES );
310+
311+ Collector collector;
312+ collector.exts = kDbcExts ;
313+ collector.extCount = 1 ;
314+
315+ g_collector = &collector;
316+ MpqEnumFiles (kArchiveSelector , " DBFilesClient\\ " , &CollectCb, nullptr );
317+ g_collector = nullptr ;
318+
319+ ScanPathGetters (collector);
320+
321+ return WriteFiles (collector.files , std::strlen (" DBFilesClient\\ " ), dstRoot);
322+ }
323+
220324// Console-command handler ABI: edx = the argument text after the
221325// command name ("" when bare). Mirrors 4.3.4's `art`/`code` dispatch.
222326bool FirstTokenEquals (const char *s, const char *token) {
@@ -246,7 +350,11 @@ int __fastcall Console_ExportInterfaceFiles(void * /*unused*/, const char *args)
246350 return 1 ;
247351 }
248352
249- const int written = ExportTree (mode);
353+ const int written = ExportTree (
354+ " Interface\\ " ,
355+ (mode == MODE_ART ) ? kArtExts : kCodeExts ,
356+ (mode == MODE_ART ) ? 2 : 4 ,
357+ (mode == MODE_ART ) ? " BlizzardInterfaceArt" : " BlizzardInterfaceCode" );
250358
251359 char msg[160 ];
252360 snprintf (msg, sizeof (msg), " ExportInterfaceFiles: wrote %d %s file(s) to %s\\ " ,
@@ -256,6 +364,21 @@ int __fastcall Console_ExportInterfaceFiles(void * /*unused*/, const char *args)
256364 return 1 ;
257365}
258366
367+ // `ExportDBCFiles` — ClassicAPI-original companion to ExportInterfaceFiles.
368+ // Dumps every `.dbc` table the client loads to `DBFilesClient\` under the
369+ // working directory. No subcommand — there's only one thing to export.
370+ // Unions the MPQ (listfile) with a `.text` path-getter scan so it gets
371+ // the complete set the client loads, including DBCs the listfile omits.
372+ int __fastcall Console_ExportDBCFiles (void * /* unused*/ , const char * /* args*/ ) {
373+ const int written = ExportDBC (" DBFilesClient" );
374+
375+ char msg[96 ];
376+ snprintf (msg, sizeof (msg),
377+ " ExportDBCFiles: wrote %d .dbc file(s) to DBFilesClient\\ " , written);
378+ Game::Console::Write (msg);
379+ return 1 ;
380+ }
381+
259382// Register the console command. A console command is process-global
260383// (not Lua-state-bound) and persists for the whole session, so the
261384// single registration at glue boot — the earliest hook, firing at
@@ -272,6 +395,11 @@ void EnsureRegistered() {
272395 Game::Console::CATEGORY_DEBUG ,
273396 " Extracts Blizzard's UI files from the MPQs to disk. "
274397 " Usage: ExportInterfaceFiles art|code" );
398+ Game::Console::RegisterCommand (
399+ " ExportDBCFiles" , &Console_ExportDBCFiles,
400+ Game::Console::CATEGORY_DEBUG ,
401+ " Extracts the client's .dbc tables from the MPQs to disk. "
402+ " Usage: ExportDBCFiles" );
275403}
276404
277405const Game::GlueModuleAutoRegister _autoreg{&EnsureRegistered};
0 commit comments