Skip to content

Commit 762968d

Browse files
committed
Add ExportDBCFiles console command
Companion to ExportInterfaceFiles: dumps every .dbc table the client loads to DBFilesClient\ next to the exe. Reuses the same MPQ enumeration + file-read pipeline (factored into a shared WriteFiles), and unions two sources for a complete set: 1. MPQ (listfile) under DBFilesClient\ — catches archive files with no loader (e.g. wowerror_strings.dbc). 2. A .text scan for the DBC path-getter pattern (mov eax, &"DBFilesClient\X.dbc"; ret) — the authoritative list of what the build loads. The master DBC init (FUN_0053f8b0) is straight-line with no iterable name table, so the per-DBC getters are the registry; scanning for them is the technique that built docs/DBCs.md. Bounded reads, can't fault. Listfile alone yielded 137; the union yields 155 (+18 cosmetic anim/sound/lookup DBCs the listfile omits, 0 lost). Results deduped case-insensitively. Verified in-game: old vs new extract diff is exactly the 18 expected additions.
1 parent d2672d8 commit 762968d

3 files changed

Lines changed: 194 additions & 33 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ when launching with `-console`), not as Lua functions. See the
9999
|---------|--------------|
100100
| `ExportInterfaceFiles code` | Extracts Blizzard's UI source (`.lua`/`.xml`/`.toc`/`.xsd`) from the MPQs to `BlizzardInterfaceCode\` |
101101
| `ExportInterfaceFiles art` | Extracts Blizzard's UI art (`.blp`/`.tga`) from the MPQs to `BlizzardInterfaceArt\` |
102+
| `ExportDBCFiles` | Extracts the client's `.dbc` tables from the MPQs to `DBFilesClient\` |
102103

103104
### Events
104105

docs/API.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ build instructions.
3535

3636
- [Console](#console)
3737
- [`ExportInterfaceFiles art|code` (console command)](#exportinterfacefiles-artcode-console-command)
38+
- [`ExportDBCFiles` (console command)](#exportdbcfiles-console-command)
3839

3940
- [Container](#container)
4041
- [`C_Container.GetContainerItemID(bagIndex, slotIndex)`](#c_containergetcontaineritemidbagindex-slotindex)
@@ -878,6 +879,37 @@ Notes:
878879
- It runs synchronously and briefly freezes the client while it walks
879880
`Interface\` — expected for a one-shot extraction.
880881

882+
### `ExportDBCFiles` (console command)
883+
884+
A ClassicAPI-original companion to `ExportInterfaceFiles` that dumps
885+
every `.dbc` table the client loads to `DBFilesClient\` under the working
886+
directory (next to `WoW.exe`). No subcommand — there's only one thing to
887+
export.
888+
889+
```
890+
> ExportDBCFiles
891+
ExportDBCFiles: wrote 155 .dbc file(s) to DBFilesClient\
892+
```
893+
894+
Unlike the MPQ `(listfile)` alone (which omits ~18 of the tables the
895+
client actually loads — mostly cosmetic animation/sound/lookup DBCs that
896+
Blizzard left out of the index), this unions two sources for a complete
897+
set:
898+
899+
1. the **MPQ `(listfile)`** under `DBFilesClient\` — catches files that
900+
ship in the archives without a loader (e.g. `wowerror_strings.dbc`);
901+
2. a **`.text` scan for the DBC path-getter pattern**
902+
(`mov eax, &"DBFilesClient\X.dbc"; ret`) — the authoritative list of
903+
what this build loads. The master DBC init (`FUN_0053f8b0`) is
904+
straight-line with no iterable name table, so the per-DBC getters
905+
*are* the registry; scanning for them is the same technique that
906+
built [docs/DBCs.md](DBCs.md).
907+
908+
Results are deduped case-insensitively. Handy for a local DBC dump to
909+
inspect schemas/values directly — e.g. verifying a record's column
910+
layout against a known row, the way `C_Item.GetEnchantInfo`'s effect
911+
columns were confirmed — without a standalone MPQ extraction tool.
912+
881913
## Container
882914

883915
### `C_Container.GetContainerItemID(bagIndex, slotIndex)`

src/interface/Export.cpp

Lines changed: 161 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@
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
@@ -55,6 +63,7 @@
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-
152157
const char *const kCodeExts[] = {"lua", "xml", "toc", "xsd"};
153158
const char *const kArtExts[] = {"blp", "tga"};
159+
const char *const kDbcExts[] = {"dbc"};
154160

155161
enum 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.
160177
struct 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
};
166190
Collector *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.
170194
int __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.
222326
bool 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

277405
const Game::GlueModuleAutoRegister _autoreg{&EnsureRegistered};

0 commit comments

Comments
 (0)