Fix CharSections.dbc field layout for classic/tbc/turtle expansions

The binary DBC files for all expansions use the same field ordering
(VariationIndex=4, ColorIndex=5, Texture1=6), but classic/tbc/turtle
dbc_layouts.json had swapped texture and variation/color fields, causing
all skin/face/hair/underwear lookups to fail. Also adds generalized
NxN texture scaling and a second video to README.
This commit is contained in:
Kelsi 2026-02-17 03:18:01 -08:00
parent 85714fd7f6
commit b8f1f15eb4
6 changed files with 93 additions and 28 deletions

View file

@ -12,8 +12,9 @@
}, },
"CharSections": { "CharSections": {
"RaceID": 1, "SexID": 2, "BaseSection": 3, "RaceID": 1, "SexID": 2, "BaseSection": 3,
"Texture1": 4, "Texture2": 5, "Texture3": 6, "VariationIndex": 4, "ColorIndex": 5,
"Flags": 7, "VariationIndex": 8, "ColorIndex": 9 "Texture1": 6, "Texture2": 7, "Texture3": 8,
"Flags": 9
}, },
"SpellIcon": { "ID": 0, "Path": 1 }, "SpellIcon": { "ID": 0, "Path": 1 },
"FactionTemplate": { "FactionTemplate": {

View file

@ -12,8 +12,9 @@
}, },
"CharSections": { "CharSections": {
"RaceID": 1, "SexID": 2, "BaseSection": 3, "RaceID": 1, "SexID": 2, "BaseSection": 3,
"Texture1": 4, "Texture2": 5, "Texture3": 6, "VariationIndex": 4, "ColorIndex": 5,
"Flags": 7, "VariationIndex": 8, "ColorIndex": 9 "Texture1": 6, "Texture2": 7, "Texture3": 8,
"Flags": 9
}, },
"SpellIcon": { "ID": 0, "Path": 1 }, "SpellIcon": { "ID": 0, "Path": 1 },
"FactionTemplate": { "FactionTemplate": {

View file

@ -12,8 +12,9 @@
}, },
"CharSections": { "CharSections": {
"RaceID": 1, "SexID": 2, "BaseSection": 3, "RaceID": 1, "SexID": 2, "BaseSection": 3,
"Texture1": 4, "Texture2": 5, "Texture3": 6, "VariationIndex": 4, "ColorIndex": 5,
"Flags": 7, "VariationIndex": 8, "ColorIndex": 9 "Texture1": 6, "Texture2": 7, "Texture3": 8,
"Flags": 9
}, },
"SpellIcon": { "ID": 0, "Path": 1 }, "SpellIcon": { "ID": 0, "Path": 1 },
"FactionTemplate": { "FactionTemplate": {

View file

@ -8,6 +8,8 @@ A native C++ World of Warcraft client with a custom OpenGL renderer.
[![Watch the video](https://img.youtube.com/vi/Pd9JuYYxu0o/maxresdefault.jpg)](https://youtu.be/Pd9JuYYxu0o) [![Watch the video](https://img.youtube.com/vi/Pd9JuYYxu0o/maxresdefault.jpg)](https://youtu.be/Pd9JuYYxu0o)
[![Watch the video](https://img.youtube.com/vi/J4NXegzqWSQ/maxresdefault.jpg)](https://youtu.be/J4NXegzqWSQ)
Primary target today is **WotLK 3.3.5a**, with active work to broaden compatibility across **Vanilla (Classic) + TBC + WotLK**. Primary target today is **WotLK 3.3.5a**, with active work to broaden compatibility across **Vanilla (Classic) + TBC + WotLK**.
> **Legal Disclaimer**: This is an educational/research project. It does not include any Blizzard Entertainment assets, data files, or proprietary code. World of Warcraft and all related assets are the property of Blizzard Entertainment, Inc. This project is not affiliated with or endorsed by Blizzard Entertainment. Users are responsible for supplying their own legally obtained game data files and for ensuring compliance with all applicable laws in their jurisdiction. > **Legal Disclaimer**: This is an educational/research project. It does not include any Blizzard Entertainment assets, data files, or proprietary code. World of Warcraft and all related assets are the property of Blizzard Entertainment, Inc. This project is not affiliated with or endorsed by Blizzard Entertainment. Users are responsible for supplying their own legally obtained game data files and for ensuring compliance with all applicable laws in their jurisdiction.

View file

@ -33,6 +33,7 @@
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include <cstdlib> #include <cstdlib>
#include <fstream>
#include <limits> #include <limits>
namespace wowee { namespace wowee {
@ -431,21 +432,22 @@ static void blitOverlay(std::vector<uint8_t>& composite, int compW, int compH,
} }
} }
// Nearest-neighbor 2x scale blit of overlay onto composite at (dstX, dstY) // Nearest-neighbor NxN scale blit of overlay onto composite at (dstX, dstY)
static void blitOverlayScaled2x(std::vector<uint8_t>& composite, int compW, int compH, static void blitOverlayScaledN(std::vector<uint8_t>& composite, int compW, int compH,
const pipeline::BLPImage& overlay, int dstX, int dstY) { const pipeline::BLPImage& overlay, int dstX, int dstY, int scale) {
if (scale < 1) scale = 1;
for (int sy = 0; sy < overlay.height; sy++) { for (int sy = 0; sy < overlay.height; sy++) {
for (int sx = 0; sx < overlay.width; sx++) { for (int sx = 0; sx < overlay.width; sx++) {
size_t srcIdx = (static_cast<size_t>(sy) * overlay.width + sx) * 4; size_t srcIdx = (static_cast<size_t>(sy) * overlay.width + sx) * 4;
uint8_t srcA = overlay.data[srcIdx + 3]; uint8_t srcA = overlay.data[srcIdx + 3];
if (srcA == 0) continue; if (srcA == 0) continue;
// Write to 2x2 block of destination pixels // Write to scale×scale block of destination pixels
for (int dy2 = 0; dy2 < 2; dy2++) { for (int dy2 = 0; dy2 < scale; dy2++) {
int dy = dstY + sy * 2 + dy2; int dy = dstY + sy * scale + dy2;
if (dy < 0 || dy >= compH) continue; if (dy < 0 || dy >= compH) continue;
for (int dx2 = 0; dx2 < 2; dx2++) { for (int dx2 = 0; dx2 < scale; dx2++) {
int dx = dstX + sx * 2 + dx2; int dx = dstX + sx * scale + dx2;
if (dx < 0 || dx >= compW) continue; if (dx < 0 || dx >= compW) continue;
size_t dstIdx = (static_cast<size_t>(dy) * compW + dx) * 4; size_t dstIdx = (static_cast<size_t>(dy) * compW + dx) * 4;
@ -468,6 +470,12 @@ static void blitOverlayScaled2x(std::vector<uint8_t>& composite, int compW, int
} }
} }
// Legacy 2x wrapper
static void blitOverlayScaled2x(std::vector<uint8_t>& composite, int compW, int compH,
const pipeline::BLPImage& overlay, int dstX, int dstY) {
blitOverlayScaledN(composite, compW, compH, overlay, dstX, dstY, 2);
}
GLuint CharacterRenderer::compositeTextures(const std::vector<std::string>& layerPaths) { GLuint CharacterRenderer::compositeTextures(const std::vector<std::string>& layerPaths) {
if (layerPaths.empty() || !assetManager || !assetManager->isInitialized()) { if (layerPaths.empty() || !assetManager || !assetManager->isInitialized()) {
return whiteTexture; return whiteTexture;
@ -502,13 +510,22 @@ GLuint CharacterRenderer::compositeTextures(const std::vector<std::string>& laye
// Pelvis Lower 128 160 128 64 // Pelvis Lower 128 160 128 64
// Foot 128 224 128 32 // Foot 128 224 128 32
// Scale factor: base texture may be larger than the 256x256 reference atlas
int coordScale = width / 256;
if (coordScale < 1) coordScale = 1;
// Atlas region sizes at 256x256 base (w, h) for known regions
struct AtlasRegion { int x, y, w, h; };
static const AtlasRegion faceLowerRegion256 = {0, 192, 128, 64};
static const AtlasRegion faceUpperRegion256 = {0, 160, 128, 32};
// Alpha-blend each overlay onto the composite // Alpha-blend each overlay onto the composite
for (size_t layer = 1; layer < layerPaths.size(); layer++) { for (size_t layer = 1; layer < layerPaths.size(); layer++) {
if (layerPaths[layer].empty()) continue; if (layerPaths[layer].empty()) continue;
auto overlay = assetManager->loadTexture(layerPaths[layer]); auto overlay = assetManager->loadTexture(layerPaths[layer]);
if (!overlay.isValid()) { if (!overlay.isValid()) {
core::Logger::getInstance().warning("Composite: failed to load overlay: ", layerPaths[layer]); core::Logger::getInstance().warning("Composite: FAILED to load overlay: ", layerPaths[layer]);
continue; continue;
} }
@ -521,39 +538,82 @@ GLuint CharacterRenderer::compositeTextures(const std::vector<std::string>& laye
} else { } else {
// Determine region by filename keywords // Determine region by filename keywords
// Coordinates scale with base texture size (256x256 is reference) // Coordinates scale with base texture size (256x256 is reference)
float s = width / 256.0f;
int dstX = 0, dstY = 0; int dstX = 0, dstY = 0;
int expectedW256 = 0, expectedH256 = 0; // Expected size at 256-base
std::string pathLower = layerPaths[layer]; std::string pathLower = layerPaths[layer];
for (auto& c : pathLower) c = std::tolower(c); for (auto& c : pathLower) c = std::tolower(c);
if (pathLower.find("faceupper") != std::string::npos) { if (pathLower.find("faceupper") != std::string::npos) {
dstX = 0; dstY = static_cast<int>(160 * s); dstX = faceUpperRegion256.x; dstY = faceUpperRegion256.y;
expectedW256 = faceUpperRegion256.w; expectedH256 = faceUpperRegion256.h;
} else if (pathLower.find("facelower") != std::string::npos) { } else if (pathLower.find("facelower") != std::string::npos) {
dstX = 0; dstY = static_cast<int>(192 * s); dstX = faceLowerRegion256.x; dstY = faceLowerRegion256.y;
expectedW256 = faceLowerRegion256.w; expectedH256 = faceLowerRegion256.h;
} else if (pathLower.find("pelvis") != std::string::npos) { } else if (pathLower.find("pelvis") != std::string::npos) {
dstX = static_cast<int>(128 * s); dstX = 128; dstY = 96;
dstY = static_cast<int>(96 * s); expectedW256 = 128; expectedH256 = 64;
} else if (pathLower.find("torso") != std::string::npos) { } else if (pathLower.find("torso") != std::string::npos) {
dstX = static_cast<int>(128 * s); dstX = 128; dstY = 0;
dstY = 0; expectedW256 = 128; expectedH256 = 64;
} else if (pathLower.find("armupper") != std::string::npos) { } else if (pathLower.find("armupper") != std::string::npos) {
dstX = 0; dstY = 0; dstX = 0; dstY = 0;
expectedW256 = 128; expectedH256 = 64;
} else if (pathLower.find("armlower") != std::string::npos) { } else if (pathLower.find("armlower") != std::string::npos) {
dstX = 0; dstY = static_cast<int>(64 * s); dstX = 0; dstY = 64;
expectedW256 = 128; expectedH256 = 64;
} else if (pathLower.find("hand") != std::string::npos) { } else if (pathLower.find("hand") != std::string::npos) {
dstX = 0; dstY = static_cast<int>(128 * s); dstX = 0; dstY = 128;
expectedW256 = 128; expectedH256 = 32;
} else if (pathLower.find("foot") != std::string::npos || pathLower.find("feet") != std::string::npos) { } else if (pathLower.find("foot") != std::string::npos || pathLower.find("feet") != std::string::npos) {
dstX = static_cast<int>(128 * s); dstY = static_cast<int>(224 * s); dstX = 128; dstY = 224;
expectedW256 = 128; expectedH256 = 32;
} else if (pathLower.find("legupper") != std::string::npos || pathLower.find("leg") != std::string::npos) { } else if (pathLower.find("legupper") != std::string::npos || pathLower.find("leg") != std::string::npos) {
dstX = static_cast<int>(128 * s); dstY = static_cast<int>(160 * s); dstX = 128; dstY = 160;
expectedW256 = 128; expectedH256 = 64;
} else { } else {
// Unknown — center placement as fallback // Unknown — center placement as fallback
dstX = (width - overlay.width) / 2; dstX = (width - overlay.width) / 2;
dstY = (height - overlay.height) / 2; dstY = (height - overlay.height) / 2;
core::Logger::getInstance().info("Composite: UNKNOWN region for '",
layerPaths[layer], "', centering at (", dstX, ",", dstY, ")");
blitOverlay(composite, width, height, overlay, dstX, dstY);
continue;
} }
core::Logger::getInstance().info("Composite: placing '", layerPaths[layer], "' at (", dstX, ",", dstY, ") on ", width, "x", height); // Scale coordinates from 256-base to actual canvas
blitOverlay(composite, width, height, overlay, dstX, dstY); dstX *= coordScale;
dstY *= coordScale;
// If overlay is 256-base sized but canvas is larger, scale the overlay up
int expectedW = expectedW256 * coordScale;
int expectedH = expectedH256 * coordScale;
bool needsScale = (coordScale > 1 &&
overlay.width == expectedW256 && overlay.height == expectedH256);
core::Logger::getInstance().info("Composite: placing '", layerPaths[layer],
"' (", overlay.width, "x", overlay.height,
") at (", dstX, ",", dstY, ") on ", width, "x", height,
" expected=", expectedW, "x", expectedH,
needsScale ? " [SCALING]" : "");
if (needsScale) {
blitOverlayScaledN(composite, width, height, overlay, dstX, dstY, coordScale);
} else {
blitOverlay(composite, width, height, overlay, dstX, dstY);
}
}
}
// Debug: dump composite to /tmp for visual inspection
{
std::string dumpPath = "/tmp/wowee_composite_debug_" +
std::to_string(width) + "x" + std::to_string(height) + ".raw";
std::ofstream dump(dumpPath, std::ios::binary);
if (dump) {
dump.write(reinterpret_cast<const char*>(composite.data()),
static_cast<std::streamsize>(composite.size()));
core::Logger::getInstance().info("Composite debug dump: ", dumpPath,
" (", width, "x", height, ", ", composite.size(), " bytes)");
} }
} }