новая структура проекта

This commit is contained in:
2026-04-20 21:04:25 +04:00
parent 1fe2a72ef1
commit 1a56b22e38
1932 changed files with 1886 additions and 22779 deletions
View File
+79
View File
@@ -0,0 +1,79 @@
cmake_minimum_required(VERSION 3.22)
project(mpq_extractor VERSION 1.0.0)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ── Paths ──────────────────────────────────────────────────────────────────────
set(GODOT_CPP_DIR "${CMAKE_SOURCE_DIR}/../../third_party/godot-cpp" CACHE PATH "Path to godot-cpp")
set(STORMLIB_DIR "${CMAKE_SOURCE_DIR}/../../third_party/StormLib" CACHE PATH "Path to StormLib")
# ── godot-cpp ──────────────────────────────────────────────────────────────────
add_subdirectory(${GODOT_CPP_DIR} godot-cpp)
# ── StormLib ───────────────────────────────────────────────────────────────────
# Disable StormLib's own tests/examples
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(STORMLIB_BUILD_TESTS OFF CACHE BOOL "" FORCE)
add_subdirectory(${STORMLIB_DIR} StormLib)
# ── Extension library ──────────────────────────────────────────────────────────
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
add_library(mpq_extractor SHARED ${SOURCES})
target_include_directories(mpq_extractor PRIVATE
src
${STORMLIB_DIR}/src
)
target_link_libraries(mpq_extractor PRIVATE
godot-cpp
storm
)
# ── Output: addons/mpq_extractor/bin/ ─────────────────────────────────────────
set(OUTPUT_DIR "${CMAKE_SOURCE_DIR}/../../addons/mpq_extractor/bin")
set_target_properties(mpq_extractor PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${OUTPUT_DIR}"
RUNTIME_OUTPUT_DIRECTORY "${OUTPUT_DIR}"
LIBRARY_OUTPUT_DIRECTORY_DEBUG "${OUTPUT_DIR}"
LIBRARY_OUTPUT_DIRECTORY_RELEASE "${OUTPUT_DIR}"
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${OUTPUT_DIR}"
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${OUTPUT_DIR}"
)
# Platform-specific naming (Godot convention: lib<name>.<platform>.<arch>.dll/so)
if(WIN32)
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(PLATFORM_SUFFIX "windows.x86_64")
else()
set(PLATFORM_SUFFIX "windows.x86_32")
endif()
set_target_properties(mpq_extractor PROPERTIES
PREFIX ""
OUTPUT_NAME "libmpq_extractor.${PLATFORM_SUFFIX}"
SUFFIX ".dll"
)
elseif(UNIX AND NOT APPLE)
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(PLATFORM_SUFFIX "linux.x86_64")
else()
set(PLATFORM_SUFFIX "linux.x86_32")
endif()
set_target_properties(mpq_extractor PROPERTIES
PREFIX ""
OUTPUT_NAME "libmpq_extractor.${PLATFORM_SUFFIX}"
SUFFIX ".so"
)
elseif(APPLE)
set_target_properties(mpq_extractor PROPERTIES
PREFIX ""
OUTPUT_NAME "libmpq_extractor.macos.universal"
SUFFIX ".dylib"
)
endif()
# ── Install (optional) ─────────────────────────────────────────────────────────
message(STATUS "mpq_extractor → ${OUTPUT_DIR}")
+78
View File
@@ -0,0 +1,78 @@
@echo off
setlocal
rem Build script for mpq_extractor GDExtension on Windows.
rem Requires Visual Studio 2022 with the C++ workload installed.
set BUILD_TYPE=%1
if "%BUILD_TYPE%"=="" set BUILD_TYPE=Release
set SCRIPT_DIR=%~dp0
if "%SCRIPT_DIR:~-1%"=="\" set SCRIPT_DIR=%SCRIPT_DIR:~0,-1%
set BUILD_DIR=%SCRIPT_DIR%\build
echo [mpq_extractor] BUILD_TYPE=%BUILD_TYPE%
rem Locate cmake.exe, first from PATH and then from common VS2022 installs.
set VS_CMAKE=
for /f "delims=" %%i in ('where cmake 2^>nul') do (
set VS_CMAKE=%%i
goto :cmake_found
)
set VSCMAKE_GUESS=C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe
if exist "%VSCMAKE_GUESS%" (
set VS_CMAKE=%VSCMAKE_GUESS%
goto :cmake_found
)
set VSCMAKE_GUESS=C:\Program Files\Microsoft Visual Studio\2022\Professional\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe
if exist "%VSCMAKE_GUESS%" (
set VS_CMAKE=%VSCMAKE_GUESS%
goto :cmake_found
)
set VSCMAKE_GUESS=C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe
if exist "%VSCMAKE_GUESS%" (
set VS_CMAKE=%VSCMAKE_GUESS%
goto :cmake_found
)
echo ERROR: cmake.exe not found.
echo Install CMake or add it to PATH.
exit /b 1
:cmake_found
echo [mpq_extractor] cmake: %VS_CMAKE%
rem Initialize submodules when needed.
if not exist "%SCRIPT_DIR%\..\..\third_party\godot-cpp\CMakeLists.txt" (
echo [mpq_extractor] Initializing submodules...
cd /d "%SCRIPT_DIR%\..\.."
git submodule update --init --recursive
if errorlevel 1 (
echo ERROR: git submodule update failed.
exit /b 1
)
cd /d "%SCRIPT_DIR%"
)
rem Configure.
"%VS_CMAKE%" -S "%SCRIPT_DIR%" -B "%BUILD_DIR%" -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=%BUILD_TYPE%
if errorlevel 1 (
echo ERROR: cmake configure failed.
exit /b 1
)
rem Build.
"%VS_CMAKE%" --build "%BUILD_DIR%" --config %BUILD_TYPE% --parallel
if errorlevel 1 (
echo ERROR: cmake build failed.
exit /b 1
)
echo.
echo [mpq_extractor] Done. DLL is in:
echo %SCRIPT_DIR%\..\..\addons\mpq_extractor\bin\
endlocal
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Build script for mpq_extractor GDExtension on Linux/macOS
set -e
BUILD_TYPE="${1:-Release}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
BUILD_DIR="$SCRIPT_DIR/build"
echo "[mpq_extractor] BUILD_TYPE=$BUILD_TYPE"
# Init submodules if needed
if [ ! -f "$SCRIPT_DIR/../../third_party/godot-cpp/CMakeLists.txt" ]; then
echo "[mpq_extractor] Initializing submodules..."
cd "$SCRIPT_DIR/../.."
git submodule update --init --recursive
fi
# Configure
cmake -S "$SCRIPT_DIR" -B "$BUILD_DIR" \
-DCMAKE_BUILD_TYPE="$BUILD_TYPE"
# Build
cmake --build "$BUILD_DIR" --config "$BUILD_TYPE" --parallel "$(nproc 2>/dev/null || sysctl -n hw.logicalcpu)"
echo ""
echo "[mpq_extractor] Done. Library is in:"
echo " $SCRIPT_DIR/../../addons/mpq_extractor/bin/"
+643
View File
@@ -0,0 +1,643 @@
#include "adt_loader.h"
#include "wow_chunk_reader.h"
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <godot_cpp/variant/vector3.hpp>
#include <fstream>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace godot;
// ─────────────────────────────────────────────────────────────────────────────
// ADT constants
// ─────────────────────────────────────────────────────────────────────────────
static constexpr float TILE_SIZE = 533.33333f; // yards per map tile
static constexpr float CHUNK_SIZE = TILE_SIZE / 16.f; // 33.333 yards
static constexpr float UNIT_SIZE = CHUNK_SIZE / 8.f; // 4.166 yards (between outer verts)
// ─────────────────────────────────────────────────────────────────────────────
// Raw structures
// ─────────────────────────────────────────────────────────────────────────────
#pragma pack(push, 1)
struct MDDFEntry { // M2 doodad placement
uint32_t nameId;
uint32_t uniqueId;
float pos[3]; // WoW world coords
float rot[3]; // degrees
uint16_t scale; // 1024 = 1.0
uint16_t flags;
};
struct MODFEntry { // WMO placement
uint32_t nameId;
uint32_t uniqueId;
float pos[3];
float rot[3];
float bboxMin[3];
float bboxMax[3];
uint16_t flags;
uint16_t doodadSet;
uint16_t nameSet;
uint16_t padding;
};
struct MCNKHeader {
uint32_t flags;
uint32_t indexX;
uint32_t indexY;
uint32_t nLayers;
uint32_t nDoodadRefs;
uint32_t ofsMCVT;
uint32_t ofsMCNR;
uint32_t ofsMCLY;
uint32_t ofsMCRF;
uint32_t ofsMCAL;
uint32_t sizeMCAL;
uint32_t ofsMCSH;
uint32_t sizeMCSH;
uint32_t areaId;
uint32_t nMapObjRefs;
uint32_t holes; // low 16 bits are the classic 4x4 hole mask
uint16_t doodadMapping[8];
uint8_t doodadStencil[8];
uint32_t ofsMCSE;
uint32_t nSoundEmitters;
uint32_t ofsMCLQ;
uint32_t sizeMCLQ;
float zpos;
float xpos;
float ypos;
uint32_t ofsMCCV;
uint32_t unused1;
uint32_t unused2;
};
struct MCLYEntry {
uint32_t textureId;
uint32_t flags;
uint32_t ofsMCAL;
int32_t effectId;
};
struct MH2OHeader {
uint32_t ofsInformation;
uint32_t nLayers;
uint32_t ofsAttributes;
};
struct MH2OInformation {
uint16_t liquidId;
uint16_t liquidVertexFormat;
float minHeight;
float maxHeight;
uint8_t xOffset;
uint8_t yOffset;
uint8_t width;
uint8_t height;
uint32_t ofsInfoMask;
uint32_t ofsHeightMap;
};
#pragma pack(pop)
// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
std::vector<uint8_t> ADTLoader::read_file(const std::string &path) {
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f) return {};
auto sz = f.tellg(); f.seekg(0);
std::vector<uint8_t> buf(sz);
f.read(reinterpret_cast<char *>(buf.data()), sz);
return buf;
}
std::string ADTLoader::to_std(const String &s) {
return std::string(s.utf8().get_data());
}
String ADTLoader::to_godot(const std::string &s) {
return String(s.c_str());
}
PackedStringArray ADTLoader::parse_names_with_offsets(
const WoWChunk &names_chunk, const WoWChunk &ids_chunk)
{
PackedStringArray result;
if (names_chunk.size == 0 || ids_chunk.size == 0) return result;
const char *ndata = reinterpret_cast<const char *>(names_chunk.data);
const uint32_t *offsets = ids_chunk.array<uint32_t>();
uint32_t n = ids_chunk.size / 4;
for (uint32_t i = 0; i < n; ++i) {
uint32_t ofs = offsets[i];
if (ofs >= names_chunk.size) { result.push_back(""); continue; }
result.push_back(String(ndata + ofs));
}
return result;
}
namespace {
static constexpr uint32_t MCNK_FLAG_DO_NOT_FIX_ALPHA_MAP = 0x8000;
static constexpr uint32_t MCLY_FLAG_ALPHA_COMPRESSED = 0x200;
static constexpr size_t MCNK_CHUNK_HEADER_SIZE = 8;
bool mcnk_subchunk_fits(uint32_t offset_from_chunk_start, size_t bytes_needed, size_t mcnk_payload_size) {
if (offset_from_chunk_start < MCNK_CHUNK_HEADER_SIZE) {
return false;
}
const size_t offset_in_payload = (size_t)offset_from_chunk_start - MCNK_CHUNK_HEADER_SIZE;
return offset_in_payload + bytes_needed <= mcnk_payload_size;
}
bool mh2o_region_fits(uint32_t offset_in_payload, size_t bytes_needed, size_t mh2o_payload_size) {
const size_t offset = (size_t)offset_in_payload;
return offset <= mh2o_payload_size && bytes_needed <= mh2o_payload_size - offset;
}
const uint8_t *mcnk_subchunk_ptr(const uint8_t *mcnk_payload, uint32_t offset_from_chunk_start) {
if (offset_from_chunk_start < MCNK_CHUNK_HEADER_SIZE) {
return nullptr;
}
return mcnk_payload + ((size_t)offset_from_chunk_start - MCNK_CHUNK_HEADER_SIZE);
}
PackedByteArray decode_mcal_big_alpha(const uint8_t *src, size_t available_bytes) {
PackedByteArray out;
if (available_bytes < 64 * 64) {
return out;
}
out.resize(64 * 64);
std::memcpy(out.ptrw(), src, 64 * 64);
return out;
}
PackedByteArray decode_mcal_old_alpha(const uint8_t *src, size_t available_bytes, bool fix_edges) {
PackedByteArray out;
if (available_bytes < 64 * 32) {
return out;
}
out.resize(64 * 64);
uint8_t *dst = out.ptrw();
// Legacy MCAL uses the old 4-bit layout.
// Noggit decodes it into a transposed linear buffer (x-major, not row-major)
// and uploads that buffer directly to the GPU. Reproducing that layout keeps
// layer placement aligned with Noggit / the original client for classic ADTs.
size_t pos = 0;
for (int x = 0; x < 64; ++x) {
for (int y = 0; y < 64; y += 2) {
if (pos >= available_bytes) {
return PackedByteArray();
}
const uint8_t value = src[pos++];
const uint8_t low = value & 0x0F;
const uint8_t high = (value >> 4) & 0x0F;
dst[x * 64 + (y + 0)] = (low << 4) | low;
dst[x * 64 + (y + 1)] = (high << 4) | high;
}
}
if (fix_edges) {
for (int i = 0; i < 64; ++i) {
dst[i * 64 + 63] = dst[i * 64 + 62];
dst[63 * 64 + i] = dst[62 * 64 + i];
}
dst[63 * 64 + 63] = dst[62 * 64 + 62];
}
return out;
}
PackedByteArray decode_mcal_compressed_alpha(const uint8_t *src, size_t available_bytes) {
PackedByteArray out;
out.resize(64 * 64);
uint8_t *dst = out.ptrw();
size_t in_pos = 0;
size_t out_pos = 0;
while (in_pos < available_bytes && out_pos < 64 * 64) {
const uint8_t header = src[in_pos++];
const size_t count = header & 0x7F;
const bool fill = (header & 0x80) != 0;
if (count == 0) {
continue;
}
const size_t write_count = std::min<size_t>(count, 64 * 64 - out_pos);
if (fill) {
if (in_pos >= available_bytes) {
return PackedByteArray();
}
std::memset(dst + out_pos, src[in_pos++], write_count);
} else {
if (in_pos + count > available_bytes) {
return PackedByteArray();
}
std::memcpy(dst + out_pos, src + in_pos, write_count);
in_pos += count;
}
out_pos += write_count;
}
if (out_pos != 64 * 64) {
return PackedByteArray();
}
return out;
}
// ADT placement tables (MDDF/MODF) do not use the same world-space basis as
// MCNK origins. In 3.3.5a they are already expressed in tile/world space that
// matches the streamed terrain layout, with the vector stored as X, Z, Y.
inline void adt_placement_pos_to_godot(float px, float pz, float py,
float &gx, float &gy, float &gz) {
gx = px;
gy = pz;
gz = py;
}
// MDDF/MODF Euler angles are stored in degrees using the same axis layout as
// placement positions: Y-up, so rot[1] is yaw. Map each axis straight through
// to Godot's Y-up Euler space — the position conversion is identity, so the
// rotation conversion is identity too. rot[1] gets +180° later for WMOs.
inline void adt_placement_rot_to_godot(float rx, float ry_yaw, float rz,
float &gx, float &gy, float &gz) {
constexpr float D2R = (float)(M_PI / 180.0);
gx = rx * D2R;
gy = ry_yaw * D2R;
gz = rz * D2R;
}
} // namespace
// ─────────────────────────────────────────────────────────────────────────────
// Main loader
// ─────────────────────────────────────────────────────────────────────────────
Dictionary ADTLoader::load_adt_from_bytes(const PackedByteArray &bytes) {
if (bytes.is_empty()) return Dictionary();
// Wrap in a vector for the shared parse path
std::vector<uint8_t> buf(bytes.size());
std::memcpy(buf.data(), bytes.ptr(), bytes.size());
// Reuse the body of load_adt by calling the internal parse
// (inline here to avoid duplicating the reader setup)
const uint8_t *raw = buf.data();
size_t len = buf.size();
Dictionary result;
_parse_adt(raw, len, result);
return result;
}
Dictionary ADTLoader::load_adt(const String &path) {
Dictionary result;
auto buf = read_file(to_std(path));
if (buf.empty()) {
UtilityFunctions::push_error("ADTLoader: cannot read ", path);
return result;
}
_parse_adt(buf.data(), buf.size(), result);
return result;
}
void ADTLoader::_parse_adt(const uint8_t *raw, size_t len, Dictionary &result) {
ChunkReader reader(raw, len);
WoWChunk chunk;
// ── Pass 1: collect named chunks ──────────────────────────────────────────
WoWChunk cMTEX{}, cMMDX{}, cMMID{}, cMWMO{}, cMWID{}, cMDDF{}, cMODF{}, cMH2O{};
std::vector<WoWChunk> mcnk_chunks;
mcnk_chunks.reserve(256);
while (reader.next(chunk)) {
if (chunk.is("MTEX")) cMTEX = chunk;
else if (chunk.is("MMDX")) cMMDX = chunk;
else if (chunk.is("MMID")) cMMID = chunk;
else if (chunk.is("MWMO")) cMWMO = chunk;
else if (chunk.is("MWID")) cMWID = chunk;
else if (chunk.is("MDDF")) cMDDF = chunk;
else if (chunk.is("MODF")) cMODF = chunk;
else if (chunk.is("MH2O")) cMH2O = chunk;
else if (chunk.is("MCNK")) mcnk_chunks.push_back(chunk);
}
// ── Textures ──────────────────────────────────────────────────────────────
PackedStringArray textures;
if (cMTEX.size > 0) {
const char *p = reinterpret_cast<const char *>(cMTEX.data);
const char *end = p + cMTEX.size;
while (p < end) {
textures.push_back(String(p));
p += strlen(p) + 1;
}
}
result["textures"] = textures;
// ── M2 / WMO names ────────────────────────────────────────────────────────
result["m2_names"] = (cMMDX.size && cMMID.size)
? parse_names_with_offsets(cMMDX, cMMID)
: PackedStringArray();
result["wmo_names"] = (cMWMO.size && cMWID.size)
? parse_names_with_offsets(cMWMO, cMWID)
: PackedStringArray();
// ── M2 placements ─────────────────────────────────────────────────────────
Array m2_placements;
if (cMDDF.size > 0) {
uint32_t n = cMDDF.count_of(sizeof(MDDFEntry));
auto *dd = cMDDF.array<MDDFEntry>();
for (uint32_t i = 0; i < n; ++i) {
float gx, gy, gz, rx, ry, rz;
adt_placement_pos_to_godot(dd[i].pos[0], dd[i].pos[1], dd[i].pos[2], gx, gy, gz);
adt_placement_rot_to_godot(dd[i].rot[0], dd[i].rot[1], dd[i].rot[2], rx, ry, rz);
Dictionary p;
p["name_id"] = (int)dd[i].nameId;
p["pos"] = Vector3(gx, gy, gz);
p["rot"] = Vector3(rx, ry, rz);
p["scale"] = dd[i].scale / 1024.f;
m2_placements.push_back(p);
}
}
result["m2_placements"] = m2_placements;
// ── WMO placements ────────────────────────────────────────────────────────
Array wmo_placements;
if (cMODF.size > 0) {
uint32_t n = cMODF.count_of(sizeof(MODFEntry));
auto *od = cMODF.array<MODFEntry>();
for (uint32_t i = 0; i < n; ++i) {
float gx, gy, gz, rx, ry, rz;
adt_placement_pos_to_godot(od[i].pos[0], od[i].pos[1], od[i].pos[2], gx, gy, gz);
adt_placement_rot_to_godot(od[i].rot[0], od[i].rot[1], od[i].rot[2], rx, ry, rz);
Dictionary p;
p["name_id"] = (int)od[i].nameId;
p["pos"] = Vector3(gx, gy, gz);
p["rot"] = Vector3(rx, ry, rz);
p["scale"] = 1.0f;
wmo_placements.push_back(p);
}
}
result["wmo_placements"] = wmo_placements;
// ── MCNK chunks ───────────────────────────────────────────────────────────
Array chunks_array;
chunks_array.resize((int)mcnk_chunks.size());
for (int ci = 0; ci < (int)mcnk_chunks.size(); ++ci) {
const WoWChunk &mc = mcnk_chunks[ci];
if (mc.size < sizeof(MCNKHeader)) { chunks_array[ci] = Dictionary(); continue; }
const MCNKHeader &hdr = *reinterpret_cast<const MCNKHeader *>(mc.data);
const uint8_t *base = mc.data;
// World origin of this chunk (MCNK stores z/x/y, convert to Godot)
float ox, oy, oz;
wow_to_godot(hdr.zpos, hdr.xpos, hdr.ypos, ox, oy, oz);
// ── Heights (MCVT) ───────────────────────────────────────────────
PackedFloat32Array heights;
PackedVector3Array normals_arr;
if (hdr.ofsMCVT && mcnk_subchunk_fits(hdr.ofsMCVT, 8 + 145 * 4, mc.size)) {
// MCVT sub-chunk: 8-byte header + 145 floats
const uint8_t *p = mcnk_subchunk_ptr(base, hdr.ofsMCVT);
// skip sub-chunk magic(4) + size(4)
const float *hdata = reinterpret_cast<const float *>(p + 8);
heights.resize(145);
for (int j = 0; j < 145; ++j)
heights[j] = hdata[j];
}
if (hdr.ofsMCNR && mcnk_subchunk_fits(hdr.ofsMCNR, 8 + 145 * 3, mc.size)) {
const uint8_t *p = mcnk_subchunk_ptr(base, hdr.ofsMCNR) + 8; // skip sub-chunk header
normals_arr.resize(145);
for (int j = 0; j < 145; ++j) {
// MCNR stores signed bytes in WoW-axis order X, Z, Y.
// Terrain positions are converted with wow_to_godot(wx, wy, wz),
// so normals must be transformed with the same basis change:
// Godot = (-WoW.Y, WoW.Z, -WoW.X).
float wx = (int8_t)p[j * 3 + 0] / 127.f;
float wz = (int8_t)p[j * 3 + 1] / 127.f;
float wy = (int8_t)p[j * 3 + 2] / 127.f;
normals_arr[j] = Vector3(-wy, wz, -wx).normalized();
}
}
// ── Texture layers (MCLY) ────────────────────────────────────────
Array layers;
if (hdr.ofsMCLY && hdr.nLayers > 0 &&
mcnk_subchunk_fits(hdr.ofsMCLY, 8 + hdr.nLayers * sizeof(MCLYEntry), mc.size))
{
const MCLYEntry *ly = reinterpret_cast<const MCLYEntry *>(
mcnk_subchunk_ptr(base, hdr.ofsMCLY) + 8);
for (uint32_t li = 0; li < hdr.nLayers; ++li) {
Dictionary ld;
ld["texture_id"] = (int)ly[li].textureId;
ld["flags"] = (int)ly[li].flags;
ld["effect_id"] = (int)ly[li].effectId;
ld["alpha_offset"] = (int)ly[li].ofsMCAL;
layers.push_back(ld);
}
}
Array alpha_maps;
if (hdr.ofsMCAL && hdr.sizeMCAL >= 8 &&
mcnk_subchunk_fits(hdr.ofsMCAL, hdr.sizeMCAL, mc.size) &&
layers.size() > 1)
{
const uint8_t *mcal_data = mcnk_subchunk_ptr(base, hdr.ofsMCAL) + 8;
const size_t mcal_size = hdr.sizeMCAL - 8;
const bool fix_old_edges = (hdr.flags & MCNK_FLAG_DO_NOT_FIX_ALPHA_MAP) == 0;
bool has_compressed_alpha = false;
for (int li = 1; li < layers.size(); ++li) {
Dictionary layer = layers[li];
if (((int)layer.get("flags", 0) & (int)MCLY_FLAG_ALPHA_COMPRESSED) != 0) {
has_compressed_alpha = true;
break;
}
}
const int alpha_layer_count = layers.size() - 1;
const bool use_big_alpha = has_compressed_alpha ||
mcal_size >= (size_t)(alpha_layer_count * 4096);
for (int li = 1; li < layers.size(); ++li) {
Dictionary layer = layers[li];
const int flags = (int)layer.get("flags", 0);
const size_t start = (size_t)(int)layer.get("alpha_offset", 0);
size_t end = mcal_size;
PackedByteArray alpha_data;
for (int next_li = li + 1; next_li < layers.size(); ++next_li) {
Dictionary next_layer = layers[next_li];
const size_t next_start = (size_t)(int)next_layer.get("alpha_offset", 0);
if (next_start > start) {
end = std::min(end, next_start);
break;
}
}
if (start >= mcal_size || end <= start) {
alpha_maps.push_back(PackedByteArray());
continue;
}
const size_t available = end - start;
const uint8_t *src = mcal_data + start;
if ((flags & (int)MCLY_FLAG_ALPHA_COMPRESSED) != 0) {
alpha_data = decode_mcal_compressed_alpha(src, available);
} else if (use_big_alpha) {
alpha_data = decode_mcal_big_alpha(src, available);
} else {
alpha_data = decode_mcal_old_alpha(src, available, fix_old_edges);
}
alpha_maps.push_back(alpha_data);
}
}
// ── Build chunk dictionary ───────────────────────────────────────
Array liquids;
if (cMH2O.size >= 256 * sizeof(MH2OHeader)) {
const size_t header_index = (size_t)hdr.indexY * 16 + hdr.indexX;
const size_t header_offset = header_index * sizeof(MH2OHeader);
if (header_index < 256 && header_offset + sizeof(MH2OHeader) <= cMH2O.size) {
const MH2OHeader &water_header =
*reinterpret_cast<const MH2OHeader *>(cMH2O.data + header_offset);
if (water_header.nLayers > 0 &&
water_header.ofsInformation > 0 &&
mh2o_region_fits(
water_header.ofsInformation,
(size_t)water_header.nLayers * sizeof(MH2OInformation),
cMH2O.size))
{
for (uint32_t li = 0; li < water_header.nLayers; ++li) {
const uint32_t info_offset =
water_header.ofsInformation + li * sizeof(MH2OInformation);
const MH2OInformation &info =
*reinterpret_cast<const MH2OInformation *>(cMH2O.data + info_offset);
if (info.width == 0 || info.height == 0 ||
info.xOffset > 8 || info.yOffset > 8 ||
info.xOffset + info.width > 8 ||
info.yOffset + info.height > 8)
{
continue;
}
PackedByteArray liquid_mask;
liquid_mask.resize(8 * 8);
uint8_t *mask_ptr = liquid_mask.ptrw();
std::memset(mask_ptr, 0, 8 * 8);
uint64_t info_mask = 0xFFFFFFFFFFFFFFFFull;
const size_t mask_bits = (size_t)info.width * info.height;
const size_t mask_bytes = (mask_bits + 7) / 8;
if (info.ofsInfoMask > 0) {
if (!mh2o_region_fits(info.ofsInfoMask, mask_bytes, cMH2O.size)) {
continue;
}
info_mask = 0;
std::memcpy(&info_mask, cMH2O.data + info.ofsInfoMask, mask_bytes);
}
size_t bit_index = 0;
for (int z = 0; z < info.height; ++z) {
for (int x = 0; x < info.width; ++x, ++bit_index) {
if (((info_mask >> bit_index) & 1ull) == 0) {
continue;
}
mask_ptr[(info.yOffset + z) * 8 + (info.xOffset + x)] = 255;
}
}
PackedFloat32Array liquid_heights;
liquid_heights.resize(9 * 9);
float *height_ptr = liquid_heights.ptrw();
for (int i = 0; i < 9 * 9; ++i) {
height_ptr[i] = info.minHeight;
}
const bool has_height_values =
info.ofsHeightMap > 0 &&
(info.liquidVertexFormat == 0 ||
info.liquidVertexFormat == 1 ||
info.liquidVertexFormat == 3);
if (has_height_values) {
const size_t vertex_count = (size_t)(info.width + 1) * (info.height + 1);
const size_t height_bytes = vertex_count * sizeof(float);
if (!mh2o_region_fits(info.ofsHeightMap, height_bytes, cMH2O.size)) {
continue;
}
const float *src_heights =
reinterpret_cast<const float *>(cMH2O.data + info.ofsHeightMap);
for (int z = 0; z <= info.height; ++z) {
for (int x = 0; x <= info.width; ++x) {
const size_t src_index = (size_t)z * (info.width + 1) + x;
const size_t dst_index =
(size_t)(info.yOffset + z) * 9 + (info.xOffset + x);
height_ptr[dst_index] =
std::clamp(src_heights[src_index], info.minHeight, info.maxHeight);
}
}
}
Dictionary liquid;
liquid["liquid_id"] = (int)info.liquidId;
liquid["vertex_format"] = (int)info.liquidVertexFormat;
liquid["min_height"] = info.minHeight;
liquid["max_height"] = info.maxHeight;
liquid["x_offset"] = (int)info.xOffset;
liquid["y_offset"] = (int)info.yOffset;
liquid["width"] = (int)info.width;
liquid["height"] = (int)info.height;
liquid["mask"] = liquid_mask;
liquid["heights"] = liquid_heights;
liquids.push_back(liquid);
}
}
}
}
Dictionary cd;
cd["index_x"] = (int)hdr.indexX;
cd["index_y"] = (int)hdr.indexY;
cd["origin"] = Vector3(ox, oy, oz);
cd["heights"] = heights;
cd["normals"] = normals_arr;
cd["holes"] = (int)(hdr.holes & 0xFFFF);
cd["layers"] = layers;
cd["alpha_maps"] = alpha_maps;
cd["liquids"] = liquids;
chunks_array[ci] = cd;
}
result["chunks"] = chunks_array;
}
// ─────────────────────────────────────────────────────────────────────────────
void ADTLoader::_bind_methods() {
ClassDB::bind_method(D_METHOD("load_adt", "path"), &ADTLoader::load_adt);
ClassDB::bind_method(D_METHOD("load_adt_from_bytes", "bytes"), &ADTLoader::load_adt_from_bytes);
}
+89
View File
@@ -0,0 +1,89 @@
#pragma once
#include <vector>
#include <string>
#include <cstdint>
#include <godot_cpp/classes/ref_counted.hpp>
#include <godot_cpp/variant/dictionary.hpp>
#include <godot_cpp/variant/array.hpp>
#include <godot_cpp/variant/string.hpp>
#include <godot_cpp/variant/packed_float32_array.hpp>
#include <godot_cpp/variant/packed_vector3_array.hpp>
#include <godot_cpp/variant/packed_byte_array.hpp>
#include <godot_cpp/variant/packed_string_array.hpp>
#include "wow_chunk_reader.h"
namespace godot {
// ─────────────────────────────────────────────────────────────────────────────
// ADTLoader
//
// Loads WoW 3.3.5a ADT terrain tiles.
//
// Usage (GDScript):
// var adt = ADTLoader.new()
// var data = adt.load_adt("C:/wow/Data/World/Maps/Azeroth/Azeroth_32_48.adt")
//
// Return Dictionary:
// {
// "textures": PackedStringArray, # all texture filenames in MTEX
// "m2_names": PackedStringArray, # M2 model filenames
// "wmo_names": PackedStringArray, # WMO filenames
// "m2_placements": Array[Dictionary],
// "wmo_placements": Array[Dictionary],
// "chunks": Array[Dictionary], # 16×16 = 256 entries, row-major
// }
//
// Placement Dictionary (both M2 and WMO):
// {
// "name_id": int, # index into m2_names / wmo_names
// "pos": Vector3, # Godot world coords
// "rot": Vector3, # Euler angles (radians)
// "scale": float, # M2 only (WMO always 1.0)
// }
//
// Chunk Dictionary:
// {
// "index_x": int, # 0-15
// "index_y": int, # 0-15
// "origin": Vector3, # Godot world position of chunk origin
// "heights": PackedFloat32Array, # 145 values (9×9 outer + 8×8 inner grid)
// "normals": PackedVector3Array, # 145 normals
// "holes": int, # hole bit mask (low 16 bits)
// "layers": Array[Dictionary], # texture layer definitions
// "alpha_maps": Array[PackedByteArray], # one per layer (layer 0 has none)
// }
//
// Layer Dictionary:
// {
// "texture_id": int,
// "flags": int,
// "effect_id": int,
// "alpha_offset": int,
// }
// ─────────────────────────────────────────────────────────────────────────────
class ADTLoader : public RefCounted {
GDCLASS(ADTLoader, RefCounted)
public:
Dictionary load_adt(const String &path);
Dictionary load_adt_from_bytes(const PackedByteArray &bytes);
protected:
static void _bind_methods();
private:
static std::vector<uint8_t> read_file(const std::string &path);
static std::string to_std(const String &s);
static String to_godot(const std::string &s);
// Shared parse implementation
static void _parse_adt(const uint8_t *raw, size_t len, Dictionary &result);
// Parse names from MMDX/MWMO (null-separated) using MMID/MWID offset tables
static PackedStringArray parse_names_with_offsets(
const WoWChunk &names_chunk, const WoWChunk &ids_chunk);
};
} // namespace godot
+248
View File
@@ -0,0 +1,248 @@
#include "blp_loader.h"
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <fstream>
#include <cstring>
#include <algorithm>
using namespace godot;
// ─────────────────────────────────────────────────────────────────────────────
// BLP2 binary structures
// ─────────────────────────────────────────────────────────────────────────────
#pragma pack(push, 1)
struct BLP2Header {
char magic[4]; // "BLP2"
uint32_t type; // 0=JPEG 1=palette 2=DXT
uint8_t encoding; // 1=raw(palette) 2=DXT 3=raw(BGRA)
uint8_t alphaDepth; // 0,1,4,8
uint8_t alphaEncoding; // 0=DXT1 1=DXT3 7=DXT5
uint8_t hasMipmaps;
uint32_t width;
uint32_t height;
uint32_t mipmapOffset[16];
uint32_t mipmapSize[16];
};
#pragma pack(pop)
// ─────────────────────────────────────────────────────────────────────────────
// DXT colour block helpers
// ─────────────────────────────────────────────────────────────────────────────
static void unpack_565(uint16_t c, uint8_t &r, uint8_t &g, uint8_t &b) {
r = (c >> 11) & 0x1F; r = (r << 3) | (r >> 2);
g = (c >> 5) & 0x3F; g = (g << 2) | (g >> 4);
b = c & 0x1F; b = (b << 3) | (b >> 2);
}
// Decode a 4×4 DXT1 colour block into dst (RGBA row-major, y=block_y*4+row)
static void decode_dxt1_block(const uint8_t *src,
uint8_t *dst, int pitch, bool has_alpha)
{
uint16_t c0 = src[0] | (src[1] << 8);
uint16_t c1 = src[2] | (src[3] << 8);
uint8_t r[4], g[4], b[4], a[4];
unpack_565(c0, r[0], g[0], b[0]); a[0] = 255;
unpack_565(c1, r[1], g[1], b[1]); a[1] = 255;
if (c0 > c1 || !has_alpha) {
r[2] = (2*r[0]+r[1])/3; g[2] = (2*g[0]+g[1])/3; b[2] = (2*b[0]+b[1])/3; a[2] = 255;
r[3] = (r[0]+2*r[1])/3; g[3] = (g[0]+2*g[1])/3; b[3] = (b[0]+2*b[1])/3; a[3] = 255;
} else {
r[2] = (r[0]+r[1])/2; g[2] = (g[0]+g[1])/2; b[2] = (b[0]+b[1])/2; a[2] = 255;
r[3] = 0; g[3] = 0; b[3] = 0; a[3] = 0;
}
uint32_t indices = src[4] | (src[5]<<8) | (src[6]<<16) | (src[7]<<24);
for (int row = 0; row < 4; ++row) {
for (int col = 0; col < 4; ++col) {
int i = (indices >> (2*(row*4+col))) & 3;
uint8_t *p = dst + row * pitch + col * 4;
p[0] = r[i]; p[1] = g[i]; p[2] = b[i]; p[3] = a[i];
}
}
}
// DXT3 explicit alpha block (16 bytes: 8 alpha + 8 colour)
static void decode_dxt3_block(const uint8_t *src, uint8_t *dst, int pitch) {
uint8_t alpha[16];
for (int i = 0; i < 8; ++i) {
alpha[i*2] = (src[i] & 0x0F) * 17;
alpha[i*2+1] = (src[i] >> 4) * 17;
}
decode_dxt1_block(src + 8, dst, pitch, false);
for (int row = 0; row < 4; ++row)
for (int col = 0; col < 4; ++col)
dst[row * pitch + col * 4 + 3] = alpha[row*4+col];
}
// DXT5 interpolated alpha block (16 bytes: 8 alpha + 8 colour)
static void decode_dxt5_block(const uint8_t *src, uint8_t *dst, int pitch) {
uint8_t a0 = src[0], a1 = src[1];
uint8_t atable[8];
atable[0] = a0; atable[1] = a1;
if (a0 > a1) {
for (int i = 2; i < 8; ++i)
atable[i] = ((8-i)*a0 + (i-1)*a1) / 7;
} else {
for (int i = 2; i < 6; ++i)
atable[i] = ((6-i)*a0 + (i-1)*a1) / 5;
atable[6] = 0; atable[7] = 255;
}
uint64_t abits = 0;
for (int i = 0; i < 6; ++i) abits |= ((uint64_t)src[2+i]) << (i*8);
decode_dxt1_block(src + 8, dst, pitch, false);
for (int row = 0; row < 4; ++row)
for (int col = 0; col < 4; ++col) {
int bit = row*4+col;
int idx = (abits >> (bit*3)) & 7;
dst[row*pitch + col*4 + 3] = atable[idx];
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Full mip-level decoders
// ─────────────────────────────────────────────────────────────────────────────
void BLPLoader::decode_dxt1(const uint8_t *src, int w, int h,
std::vector<uint8_t> &dst, bool has_alpha)
{
int bw = (w + 3) / 4, bh = (h + 3) / 4;
dst.assign(w * h * 4, 0);
for (int by = 0; by < bh; ++by)
for (int bx = 0; bx < bw; ++bx, src += 8) {
uint8_t *d = dst.data() + (by*4*w + bx*4) * 4;
decode_dxt1_block(src, d, w*4, has_alpha);
}
}
void BLPLoader::decode_dxt3(const uint8_t *src, int w, int h,
std::vector<uint8_t> &dst)
{
int bw = (w+3)/4, bh = (h+3)/4;
dst.assign(w * h * 4, 0);
for (int by = 0; by < bh; ++by)
for (int bx = 0; bx < bw; ++bx, src += 16) {
uint8_t *d = dst.data() + (by*4*w + bx*4)*4;
decode_dxt3_block(src, d, w*4);
}
}
void BLPLoader::decode_dxt5(const uint8_t *src, int w, int h,
std::vector<uint8_t> &dst)
{
int bw = (w+3)/4, bh = (h+3)/4;
dst.assign(w * h * 4, 0);
for (int by = 0; by < bh; ++by)
for (int bx = 0; bx < bw; ++bx, src += 16) {
uint8_t *d = dst.data() + (by*4*w + bx*4)*4;
decode_dxt5_block(src, d, w*4);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Main parser
// ─────────────────────────────────────────────────────────────────────────────
Ref<Image> BLPLoader::parse(const uint8_t *data, size_t len) {
if (len < sizeof(BLP2Header)) return Ref<Image>();
const BLP2Header &hdr = *reinterpret_cast<const BLP2Header *>(data);
if (std::memcmp(hdr.magic, "BLP2", 4) != 0) {
// Try BLP1 magic
if (std::memcmp(hdr.magic, "BLP1", 4) != 0) return Ref<Image>();
}
uint32_t w = hdr.width, h = hdr.height;
if (w == 0 || h == 0) return Ref<Image>();
// Use mip level 0 only
uint32_t mip_ofs = hdr.mipmapOffset[0];
uint32_t mip_size = hdr.mipmapSize[0];
if (mip_ofs + mip_size > len) return Ref<Image>();
const uint8_t *mip = data + mip_ofs;
std::vector<uint8_t> rgba;
Image::Format fmt = Image::FORMAT_RGBA8;
if (hdr.encoding == 2) {
// DXT compressed
bool has_alpha = hdr.alphaDepth > 0;
switch (hdr.alphaEncoding) {
case 0: decode_dxt1(mip, w, h, rgba, has_alpha); break;
case 1: decode_dxt3(mip, w, h, rgba); break;
case 7: decode_dxt5(mip, w, h, rgba); break;
default: decode_dxt1(mip, w, h, rgba, has_alpha); break;
}
} else if (hdr.encoding == 1) {
// Palettized: 256×4 BGRA palette at offset 0xA4, then index data
const size_t palette_ofs = sizeof(BLP2Header);
if (palette_ofs + 256*4 > len) return Ref<Image>();
const uint8_t *palette = data + palette_ofs;
rgba.resize(w * h * 4);
for (uint32_t i = 0; i < w * h && i < mip_size; ++i) {
uint8_t idx = mip[i];
rgba[i*4+0] = palette[idx*4+2]; // R (from BGR)
rgba[i*4+1] = palette[idx*4+1]; // G
rgba[i*4+2] = palette[idx*4+0]; // B
rgba[i*4+3] = (hdr.alphaDepth == 0) ? 255 : mip[w*h + i];
}
} else if (hdr.encoding == 3) {
// Uncompressed BGRA
rgba.resize(w * h * 4);
for (uint32_t i = 0; i < w * h * 4 && i + 3 < mip_size; i += 4) {
rgba[i+0] = mip[i+2]; // R
rgba[i+1] = mip[i+1]; // G
rgba[i+2] = mip[i+0]; // B
rgba[i+3] = mip[i+3]; // A
}
} else {
return Ref<Image>();
}
if (rgba.empty()) return Ref<Image>();
PackedByteArray pba;
pba.resize((int)rgba.size());
std::memcpy(pba.ptrw(), rgba.data(), rgba.size());
return Image::create_from_data(w, h, false, fmt, pba);
}
// ─────────────────────────────────────────────────────────────────────────────
// Public API
// ─────────────────────────────────────────────────────────────────────────────
Ref<Image> BLPLoader::load_image(const String &path) {
auto buf = read_file(to_std(path));
if (buf.empty()) {
UtilityFunctions::push_error("BLPLoader: cannot read ", path);
return Ref<Image>();
}
return parse(buf.data(), buf.size());
}
Ref<Image> BLPLoader::load_image_from_bytes(const PackedByteArray &bytes) {
if (bytes.is_empty()) return Ref<Image>();
return parse(bytes.ptr(), bytes.size());
}
std::vector<uint8_t> BLPLoader::read_file(const std::string &path) {
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f) return {};
auto sz = f.tellg(); f.seekg(0);
std::vector<uint8_t> buf(sz);
f.read(reinterpret_cast<char *>(buf.data()), sz);
return buf;
}
std::string BLPLoader::to_std(const String &s) {
return std::string(s.utf8().get_data());
}
void BLPLoader::_bind_methods() {
ClassDB::bind_method(D_METHOD("load_image", "path"), &BLPLoader::load_image);
ClassDB::bind_method(D_METHOD("load_image_from_bytes", "bytes"), &BLPLoader::load_image_from_bytes);
}
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <vector>
#include <string>
#include <cstdint>
#include <godot_cpp/classes/ref_counted.hpp>
#include <godot_cpp/classes/image.hpp>
#include <godot_cpp/classes/image_texture.hpp>
#include <godot_cpp/variant/string.hpp>
#include <godot_cpp/variant/packed_byte_array.hpp>
namespace godot {
// ─────────────────────────────────────────────────────────────────────────────
// BLPLoader — WoW BLP2 texture → Godot Image
//
// Supports all formats used by WoW 3.3.5a:
// • DXT1 / DXT3 / DXT5 (encoding=2, most common)
// • Palettized BGRA (encoding=1)
// • Uncompressed BGRA (encoding=3, rare)
//
// Usage (GDScript):
// var img = BLPLoader.new().load_image("C:/path/texture.blp")
// var tex = ImageTexture.create_from_image(img)
// ─────────────────────────────────────────────────────────────────────────────
class BLPLoader : public RefCounted {
GDCLASS(BLPLoader, RefCounted)
public:
// Load from absolute filesystem path
Ref<Image> load_image(const String &path);
// Load from raw bytes (e.g. from MPQManager::read_file)
Ref<Image> load_image_from_bytes(const PackedByteArray &bytes);
protected:
static void _bind_methods();
private:
static Ref<Image> parse(const uint8_t *data, size_t len);
// DXT decoders (output RGBA8)
static void decode_dxt1(const uint8_t *src, int w, int h,
std::vector<uint8_t> &dst, bool has_alpha);
static void decode_dxt3(const uint8_t *src, int w, int h,
std::vector<uint8_t> &dst);
static void decode_dxt5(const uint8_t *src, int w, int h,
std::vector<uint8_t> &dst);
static std::vector<uint8_t> read_file(const std::string &path);
static std::string to_std(const String &s);
};
} // namespace godot
+339
View File
@@ -0,0 +1,339 @@
#include "m2_loader.h"
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <godot_cpp/variant/packed_string_array.hpp>
#include <godot_cpp/variant/packed_vector3_array.hpp>
#include <godot_cpp/variant/packed_vector2_array.hpp>
#include <godot_cpp/variant/packed_int32_array.hpp>
#include <fstream>
#include <cstring>
#include <cstdio>
using namespace godot;
// ─────────────────────────────────────────────────────────────────────────────
// Raw structures (WoW 3.3.5a, little-endian, packed)
// ─────────────────────────────────────────────────────────────────────────────
#pragma pack(push, 1)
// M2 file header — starts at offset 0, no chunk wrapper in 3.3.5a
struct M2Header {
uint32_t magic; // 'MD20' LE = 0x3032444D
uint32_t version;
uint32_t nName;
uint32_t ofsName;
uint32_t flags;
uint32_t nGlobalLoops;
uint32_t ofsGlobalLoops;
uint32_t nAnimations;
uint32_t ofsAnimations;
uint32_t nAnimLookup;
uint32_t ofsAnimLookup;
uint32_t nBones;
uint32_t ofsBones;
uint32_t nKeyBoneLookup; // unused for static render
uint32_t ofsKeyBoneLookup;
uint32_t nVertices; // offset 60
uint32_t ofsVertices; // offset 64
uint32_t nViews; // offset 68 — number of .skin files
uint32_t nColors;
uint32_t ofsColors;
uint32_t nTextures; // offset 80
uint32_t ofsTextures; // offset 84
uint32_t nTexWeights; // unused
uint32_t ofsTexWeights;
uint32_t nTexTransforms; // unused
uint32_t ofsTexTransforms;
uint32_t nReplTexLookup; // unused
uint32_t ofsReplTexLookup;
uint32_t nMaterials; // offset 112
uint32_t ofsMaterials; // offset 116
uint32_t nBoneCombos; // unused
uint32_t ofsBoneCombos;
uint32_t nTextureCombos; // offset 128
uint32_t ofsTextureCombos; // offset 132
};
// Total: 34 × 4 = 136 bytes
struct M2Vertex {
float pos[3];
uint8_t boneWeights[4];
uint8_t boneIndices[4];
float normal[3];
float texCoords[2];
float texCoords2[2];
};
// Total: 48 bytes
struct M2Texture {
uint32_t type; // 0 = hardcoded filename
uint32_t flags;
uint32_t nFilename;
uint32_t ofsFilename;
};
struct M2Material {
uint16_t flags;
uint16_t blendingMode; // 0=opaque 1=alpha_key 2=alpha_blend
};
struct SkinHeader {
uint32_t magic; // 'SKIN' LE = 0x4E494B53
uint32_t nIndices;
uint32_t ofsIndices;
uint32_t nTriangles;
uint32_t ofsTriangles;
uint32_t nProperties;
uint32_t ofsProperties;
uint32_t nSubMeshes;
uint32_t ofsSubMeshes;
uint32_t nTextureUnits;
uint32_t ofsTextureUnits;
uint32_t LOD;
};
// Total: 48 bytes
struct SkinSubMesh {
uint16_t submeshID;
uint16_t level;
uint16_t vertexStart;
uint16_t vertexCount;
uint16_t triangleStart;
uint16_t triangleCount;
uint16_t boneCount;
uint16_t boneStart;
uint16_t boneInfluences;
uint16_t centerBoneIndex;
float centerPosition[3];
float sortCenterPosition[3];
float sortRadius;
};
// Total: 10×2 + 7×4 = 48 bytes
struct SkinTextureUnit {
uint8_t flags;
uint8_t priority;
uint16_t shaderID;
uint16_t skinSectionIndex;
uint16_t flags2;
uint16_t colorIndex;
uint16_t materialIndex;
uint16_t materialLayer;
uint16_t textureCount;
uint16_t textureComboIndex;
uint16_t textureCoordComboIndex;
uint16_t textureWeightComboIndex;
uint16_t textureTransformComboIndex;
};
// Total: 2 + 11×2 = 24 bytes
#pragma pack(pop)
static constexpr uint32_t MAGIC_MD20 = 0x3032444D; // 'MD20' LE
static constexpr uint32_t MAGIC_SKIN = 0x4E494B53; // 'SKIN' LE
// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
std::vector<uint8_t> M2Loader::read_file(const std::string &path) {
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f) return {};
auto sz = f.tellg();
f.seekg(0);
std::vector<uint8_t> buf(sz);
f.read(reinterpret_cast<char *>(buf.data()), sz);
return buf;
}
std::string M2Loader::to_std(const String &s) {
return std::string(s.utf8().get_data());
}
String M2Loader::to_godot(const std::string &s) {
return String(s.c_str());
}
// Safe bounds-checked array accessor into a buffer
template<typename T>
static const T *safe_array(const std::vector<uint8_t> &buf, uint32_t ofs, uint32_t count) {
if (count == 0) return nullptr;
uint64_t end = (uint64_t)ofs + (uint64_t)count * sizeof(T);
if (end > buf.size()) return nullptr;
return reinterpret_cast<const T *>(buf.data() + ofs);
}
// ─────────────────────────────────────────────────────────────────────────────
// Core parser
// ─────────────────────────────────────────────────────────────────────────────
Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string &path) {
if (buf.size() < sizeof(M2Header)) return Dictionary();
const auto &hdr = *reinterpret_cast<const M2Header *>(buf.data());
if (hdr.magic != MAGIC_MD20) {
UtilityFunctions::push_error("M2Loader: unexpected magic, expected MD20");
return Dictionary();
}
// ── Vertices ─────────────────────────────────────────────────────────────
PackedVector3Array vertices, normals;
PackedVector2Array uvs;
const auto *verts = safe_array<M2Vertex>(buf, hdr.ofsVertices, hdr.nVertices);
if (verts && hdr.nVertices > 0) {
vertices.resize(hdr.nVertices);
normals.resize(hdr.nVertices);
uvs.resize(hdr.nVertices);
for (uint32_t i = 0; i < hdr.nVertices; ++i) {
const auto &v = verts[i];
// WoW model space (X right, Y forward, Z up) → Godot (X right, Y up, Z back)
vertices[i] = Vector3( v.pos[0], v.pos[2], -v.pos[1]);
normals[i] = Vector3( v.normal[0], v.normal[2], -v.normal[1]);
// WoW V origin is top-left; Godot expects bottom-left → flip V
uvs[i] = Vector2(v.texCoords[0], 1.0f - v.texCoords[1]);
}
}
// ── Textures ─────────────────────────────────────────────────────────────
PackedStringArray textures;
const auto *tex_arr = safe_array<M2Texture>(buf, hdr.ofsTextures, hdr.nTextures);
if (tex_arr) {
for (uint32_t i = 0; i < hdr.nTextures; ++i) {
const auto &t = tex_arr[i];
std::string fname;
if (t.type == 0 && t.nFilename > 0) {
uint64_t end = (uint64_t)t.ofsFilename + t.nFilename;
if (end <= buf.size()) {
fname = std::string(
reinterpret_cast<const char *>(buf.data() + t.ofsFilename),
t.nFilename);
// trim null terminators
while (!fname.empty() && fname.back() == '\0')
fname.pop_back();
for (auto &c : fname) if (c == '\\') c = '/';
}
}
textures.push_back(to_godot(fname));
}
}
// ── Materials ────────────────────────────────────────────────────────────
Array materials;
const auto *mat_arr = safe_array<M2Material>(buf, hdr.ofsMaterials, hdr.nMaterials);
if (mat_arr) {
for (uint32_t i = 0; i < hdr.nMaterials; ++i) {
Dictionary m;
m["flags"] = (int)mat_arr[i].flags;
m["blend_mode"] = (int)mat_arr[i].blendingMode;
materials.push_back(m);
}
}
// ── Texture combos ───────────────────────────────────────────────────────
PackedInt32Array texture_combos;
const auto *tc_arr = safe_array<uint16_t>(buf, hdr.ofsTextureCombos, hdr.nTextureCombos);
if (tc_arr) {
for (uint32_t i = 0; i < hdr.nTextureCombos; ++i)
texture_combos.push_back((int)tc_arr[i]);
}
// ── Skin file ────────────────────────────────────────────────────────────
// Find <basename>00.skin in the same directory as the .m2
std::string skin_path = path;
{
auto dot = skin_path.rfind('.');
if (dot != std::string::npos)
skin_path = skin_path.substr(0, dot);
skin_path += "00.skin";
}
auto skin_buf = read_file(skin_path);
PackedInt32Array indices;
Array batches;
if (skin_buf.size() >= sizeof(SkinHeader)) {
const auto &skin = *reinterpret_cast<const SkinHeader *>(skin_buf.data());
if (skin.magic == MAGIC_SKIN) {
const uint16_t *skin_idx = safe_array<uint16_t>(skin_buf, skin.ofsIndices, skin.nIndices);
const uint16_t *skin_tri = safe_array<uint16_t>(skin_buf, skin.ofsTriangles, skin.nTriangles);
const SkinSubMesh *sms = safe_array<SkinSubMesh> (skin_buf, skin.ofsSubMeshes, skin.nSubMeshes);
const SkinTextureUnit *tu = safe_array<SkinTextureUnit>(skin_buf, skin.ofsTextureUnits, skin.nTextureUnits);
if (skin_idx && skin_tri && sms && tu) {
for (uint32_t u = 0; u < skin.nTextureUnits; ++u) {
uint32_t sm_idx = tu[u].skinSectionIndex;
if (sm_idx >= skin.nSubMeshes) continue;
const SkinSubMesh &sm = sms[sm_idx];
// triangleStart may be extended by level in large models
uint32_t tri_start = (uint32_t)sm.triangleStart + ((uint32_t)sm.level << 16);
uint32_t tri_count = sm.triangleCount;
if (tri_start + tri_count > skin.nTriangles) continue;
int idx_start = (int)indices.size();
// Each group of 3 triangle-array entries forms one triangle.
// Remap: final_vertex = skin_idx[skin_tri[t]]
// Reverse winding (WoW CW → Godot CCW).
for (uint32_t t = 0; t + 2 < tri_count; t += 3) {
uint16_t a = skin_tri[tri_start + t];
uint16_t b = skin_tri[tri_start + t + 1];
uint16_t c = skin_tri[tri_start + t + 2];
if (a >= skin.nIndices || b >= skin.nIndices || c >= skin.nIndices)
continue;
indices.push_back((int)skin_idx[a]);
indices.push_back((int)skin_idx[c]); // reversed
indices.push_back((int)skin_idx[b]);
}
int idx_count = (int)indices.size() - idx_start;
if (idx_count <= 0) continue;
Dictionary batch;
batch["index_start"] = idx_start;
batch["index_count"] = idx_count;
batch["material_id"] = (int)tu[u].materialIndex;
batch["texture_combo_index"] = (int)tu[u].textureComboIndex;
batches.push_back(batch);
}
}
} else {
UtilityFunctions::push_warning("M2Loader: skin file missing or invalid: ",
to_godot(skin_path));
}
} else {
UtilityFunctions::push_warning("M2Loader: skin file not found: ",
to_godot(skin_path));
}
Dictionary result;
result["textures"] = textures;
result["materials"] = materials;
result["texture_combos"] = texture_combos;
result["vertices"] = vertices;
result["normals"] = normals;
result["uvs"] = uvs;
result["indices"] = indices;
result["batches"] = batches;
return result;
}
// ─────────────────────────────────────────────────────────────────────────────
// Public API
// ─────────────────────────────────────────────────────────────────────────────
Dictionary M2Loader::load_m2(const String &path) {
std::string spath = to_std(path);
auto buf = read_file(spath);
if (buf.empty()) {
UtilityFunctions::push_error("M2Loader: cannot read ", path);
return Dictionary();
}
return parse_m2(buf, spath);
}
void M2Loader::_bind_methods() {
ClassDB::bind_method(D_METHOD("load_m2", "path"), &M2Loader::load_m2);
}
+61
View File
@@ -0,0 +1,61 @@
#pragma once
#include <vector>
#include <string>
#include <cstdint>
#include <godot_cpp/classes/ref_counted.hpp>
#include <godot_cpp/variant/dictionary.hpp>
#include <godot_cpp/variant/string.hpp>
#include "wow_chunk_reader.h"
namespace godot {
// ─────────────────────────────────────────────────────────────────────────────
// M2Loader
//
// Loads WoW 3.3.5a .m2 model files (raw format, starts with MD20 magic).
// Parses the first skin file (<basename>00.skin) for render geometry.
//
// Usage (GDScript):
// var loader = M2Loader.new()
// var data = loader.load_m2("C:/wow/extracted/World/Doodad/Gnome/Gnome.m2")
//
// Return Dictionary:
// {
// "textures": PackedStringArray, # .blp texture paths (may be empty for replaceable)
// "materials": Array[Dictionary], # [{flags, blend_mode}]
// "texture_combos": PackedInt32Array, # textureCombos[i] = index into textures
// "vertices": PackedVector3Array, # Godot-space positions
// "normals": PackedVector3Array,
// "uvs": PackedVector2Array,
// "indices": PackedInt32Array, # flat triangle list into vertex array
// "batches": Array[Dictionary], # render batches
// }
//
// Each batch Dictionary:
// {
// "index_start": int, # offset into indices array
// "index_count": int, # number of indices for this batch
// "material_id": int, # index into materials
// "texture_combo_index": int, # index into texture_combos → texture
// }
// ─────────────────────────────────────────────────────────────────────────────
class M2Loader : public RefCounted {
GDCLASS(M2Loader, RefCounted)
public:
Dictionary load_m2(const String &path);
protected:
static void _bind_methods();
private:
Dictionary parse_m2(const std::vector<uint8_t> &buf, const std::string &path);
static std::vector<uint8_t> read_file(const std::string &path);
static std::string to_std(const String &s);
static String to_godot(const std::string &s);
};
} // namespace godot
+324
View File
@@ -0,0 +1,324 @@
#include "mpq_manager.h"
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <godot_cpp/classes/dir_access.hpp>
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <unordered_set>
namespace fs = std::filesystem;
using namespace godot;
// ─────────────────────────────────────────────────────────────────────────────
// WoW 3.3.5a (build 12340) MPQ load order
// Source: Blizzard client, TrinityCore/MaNGOS toolchain
//
// Priority value = open order (higher = wins).
// Base archives 17, locale archives 1020 (locale always wins over base).
// ─────────────────────────────────────────────────────────────────────────────
std::vector<std::pair<std::string, int>>
MPQManager::build_archive_list(const std::string &data_dir, const std::string &locale)
{
// {relative path from data_dir, priority}
std::vector<std::pair<std::string, int>> list = {
// ── Base archives (lowest priority) ───────────────────────────────
{ "common.mpq", 1 },
{ "common-2.mpq", 2 },
{ "expansion.mpq", 3 },
{ "lichking.mpq", 4 },
{ "patch.mpq", 5 },
{ "patch-2.mpq", 6 },
{ "patch-3.mpq", 7 },
// ── Locale archives (higher priority) ─────────────────────────────
{ locale + "/locale-" + locale + ".mpq", 10 },
{ locale + "/speech-" + locale + ".mpq", 11 },
{ locale + "/expansion-locale-" + locale + ".mpq", 12 },
{ locale + "/lichking-locale-" + locale + ".mpq", 13 },
{ locale + "/patch-" + locale + ".mpq", 14 },
{ locale + "/patch-" + locale + "-2.mpq", 15 },
{ locale + "/patch-" + locale + "-3.mpq", 16 },
};
// Prepend data_dir to each entry
for (auto &entry : list) {
entry.first = data_dir + "/" + entry.first;
}
return list;
}
// ─────────────────────────────────────────────────────────────────────────────
MPQManager::MPQManager() = default;
MPQManager::~MPQManager() {
close();
}
// ─────────────────────────────────────────────────────────────────────────────
int MPQManager::open_client(const String &base_path, const String &locale)
{
close();
std::string data_dir = to_std(base_path) + "/Data";
std::string loc = to_std(locale);
auto archive_list = build_archive_list(data_dir, loc);
int opened = 0;
for (auto &[path, priority] : archive_list) {
if (!fs::exists(path)) {
// Not every archive is required to be present
continue;
}
if (open_archive(String(path.c_str()), priority)) {
++opened;
}
}
if (opened == 0) {
UtilityFunctions::push_error("MPQManager: no archives found in ", base_path);
} else {
UtilityFunctions::print("MPQManager: opened ", opened, " archives from ", base_path);
}
return opened;
}
// ─────────────────────────────────────────────────────────────────────────────
bool MPQManager::open_archive(const String &path, int priority)
{
std::string spath = to_std(path);
HANDLE hMpq = nullptr;
if (!SFileOpenArchive(spath.c_str(), 0, MPQ_OPEN_READ_ONLY | STREAM_FLAG_READ_ONLY, &hMpq)) {
UtilityFunctions::push_warning("MPQManager: failed to open ", path,
" (error ", (int)GetLastError(), ")");
return false;
}
MPQArchiveEntry entry;
entry.handle = hMpq;
entry.path = path;
entry.priority = priority;
// Insert sorted ascending by priority so index 0 = lowest
auto it = std::lower_bound(
m_archives.begin(), m_archives.end(), priority,
[](const MPQArchiveEntry &e, int p){ return e.priority < p; });
m_archives.insert(it, entry);
return true;
}
// ─────────────────────────────────────────────────────────────────────────────
void MPQManager::close()
{
for (auto &entry : m_archives) {
if (entry.handle) {
SFileCloseArchive(entry.handle);
entry.handle = nullptr;
}
}
m_archives.clear();
}
// ─────────────────────────────────────────────────────────────────────────────
// Internal: search from highest priority (back of vector) to lowest
HANDLE MPQManager::find_file(const std::string &internal_path) const
{
for (auto it = m_archives.rbegin(); it != m_archives.rend(); ++it) {
HANDLE hFile = nullptr;
if (SFileOpenFileEx(it->handle, internal_path.c_str(), 0, &hFile)) {
SFileCloseFile(hFile);
return it->handle;
}
}
return nullptr;
}
// ─────────────────────────────────────────────────────────────────────────────
bool MPQManager::has_file(const String &internal_path) const
{
return find_file(to_std(internal_path)) != nullptr;
}
// ─────────────────────────────────────────────────────────────────────────────
PackedByteArray MPQManager::read_file(const String &internal_path) const
{
std::string spath = to_std(internal_path);
HANDLE hArchive = find_file(spath);
if (!hArchive) {
UtilityFunctions::push_error("MPQManager: file not found: ", internal_path);
return PackedByteArray();
}
HANDLE hFile = nullptr;
if (!SFileOpenFileEx(hArchive, spath.c_str(), 0, &hFile)) {
return PackedByteArray();
}
DWORD file_size = SFileGetFileSize(hFile, nullptr);
if (file_size == SFILE_INVALID_SIZE || file_size == 0) {
SFileCloseFile(hFile);
return PackedByteArray();
}
PackedByteArray result;
result.resize((int)file_size);
uint8_t *ptr = result.ptrw();
DWORD read = 0;
bool ok = SFileReadFile(hFile, ptr, file_size, &read, nullptr);
SFileCloseFile(hFile);
if (!ok || read != file_size) {
UtilityFunctions::push_error("MPQManager: read error on ", internal_path);
return PackedByteArray();
}
return result;
}
// ─────────────────────────────────────────────────────────────────────────────
bool MPQManager::extract_file(const String &internal_path, const String &output_path) const
{
std::string spath = to_std(internal_path);
std::string outpath = to_std(output_path);
HANDLE hArchive = find_file(spath);
if (!hArchive) {
UtilityFunctions::push_error("MPQManager: file not found: ", internal_path);
return false;
}
// Ensure parent directory exists
fs::path out(outpath);
fs::create_directories(out.parent_path());
return SFileExtractFile(hArchive, spath.c_str(), outpath.c_str(), 0);
}
// ─────────────────────────────────────────────────────────────────────────────
int MPQManager::extract_files(const String &filter, const String &output_dir) const
{
if (m_archives.empty()) return 0;
std::string out_dir = to_std(output_dir);
std::string sfilter = to_std(filter);
// Collect unique file names from all archives (highest priority wins)
// We iterate archives highest→lowest and track already-seen paths.
std::unordered_set<std::string> seen;
int count = 0;
for (auto it = m_archives.rbegin(); it != m_archives.rend(); ++it) {
SFILE_FIND_DATA fd;
HANDLE hFind = SFileFindFirstFile(it->handle, sfilter.c_str(), &fd, nullptr);
if (!hFind) continue;
do {
std::string fname(fd.cFileName);
if (seen.count(fname)) continue; // already extracted from higher-priority archive
seen.insert(fname);
// Build output path mirroring internal path
fs::path out_path = fs::path(out_dir) / fs::path(fname);
fs::create_directories(out_path.parent_path());
if (SFileExtractFile(it->handle, fname.c_str(), out_path.string().c_str(), 0)) {
++count;
} else {
UtilityFunctions::push_warning("MPQManager: failed to extract ", String(fname.c_str()));
}
} while (SFileFindNextFile(hFind, &fd));
SFileFindClose(hFind);
}
UtilityFunctions::print("MPQManager: extracted ", count, " files → ", output_dir);
return count;
}
// ─────────────────────────────────────────────────────────────────────────────
PackedStringArray MPQManager::list_files(const String &filter) const
{
std::string sfilter = to_std(filter.is_empty() ? String("*") : filter);
std::unordered_set<std::string> seen;
PackedStringArray result;
// Iterate highest priority first so duplicates are attributed to winner
for (auto it = m_archives.rbegin(); it != m_archives.rend(); ++it) {
SFILE_FIND_DATA fd;
HANDLE hFind = SFileFindFirstFile(it->handle, sfilter.c_str(), &fd, nullptr);
if (!hFind) continue;
do {
std::string fname(fd.cFileName);
if (!seen.count(fname)) {
seen.insert(fname);
result.push_back(String(fname.c_str()));
}
} while (SFileFindNextFile(hFind, &fd));
SFileFindClose(hFind);
}
return result;
}
// ─────────────────────────────────────────────────────────────────────────────
int MPQManager::get_archive_count() const {
return (int)m_archives.size();
}
Array MPQManager::get_archive_info() const {
Array out;
for (auto it = m_archives.rbegin(); it != m_archives.rend(); ++it) {
DWORD file_count = 0;
SFileGetFileInfo(it->handle, SFileMpqNumberOfFiles, &file_count, sizeof(file_count), nullptr);
Dictionary d;
d["path"] = it->path;
d["priority"] = it->priority;
d["file_count"] = (int)file_count;
out.push_back(d);
}
return out;
}
// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
String MPQManager::to_godot(const std::string &s) {
return String(s.c_str());
}
std::string MPQManager::to_std(const String &s) {
return std::string(s.utf8().get_data());
}
// ─────────────────────────────────────────────────────────────────────────────
// GDScript bindings
// ─────────────────────────────────────────────────────────────────────────────
void MPQManager::_bind_methods() {
ClassDB::bind_method(D_METHOD("open_client", "base_path", "locale"),
&MPQManager::open_client);
ClassDB::bind_method(D_METHOD("open_archive", "path", "priority"),
&MPQManager::open_archive);
ClassDB::bind_method(D_METHOD("close"),
&MPQManager::close);
ClassDB::bind_method(D_METHOD("has_file", "internal_path"),
&MPQManager::has_file);
ClassDB::bind_method(D_METHOD("read_file", "internal_path"),
&MPQManager::read_file);
ClassDB::bind_method(D_METHOD("extract_file", "internal_path", "output_path"),
&MPQManager::extract_file);
ClassDB::bind_method(D_METHOD("extract_files", "filter", "output_dir"),
&MPQManager::extract_files);
ClassDB::bind_method(D_METHOD("list_files", "filter"),
&MPQManager::list_files, DEFVAL("*"));
ClassDB::bind_method(D_METHOD("get_archive_count"),
&MPQManager::get_archive_count);
ClassDB::bind_method(D_METHOD("get_archive_info"),
&MPQManager::get_archive_info);
}
+102
View File
@@ -0,0 +1,102 @@
#pragma once
#include <godot_cpp/classes/ref_counted.hpp>
#include <godot_cpp/variant/string.hpp>
#include <godot_cpp/variant/packed_string_array.hpp>
#include <godot_cpp/variant/packed_byte_array.hpp>
#include <godot_cpp/variant/dictionary.hpp>
#include <StormLib.h>
#include <vector>
#include <string>
namespace godot {
// ─────────────────────────────────────────────────────────────────────────────
// MPQArchiveEntry one open archive with its priority
// ─────────────────────────────────────────────────────────────────────────────
struct MPQArchiveEntry {
HANDLE handle = nullptr;
String path;
int priority = 0; // higher = wins
};
// ─────────────────────────────────────────────────────────────────────────────
// MPQManager GDScript-accessible class
// Usage:
// var mgr = MPQManager.new()
// mgr.open_client("res://sources", "ruRU")
// var data = mgr.read_file("Interface\\FrameXML\\GlobalStrings.lua")
// mgr.extract_all("DBFilesClient", "res://data/extracted/dbc")
// mgr.close()
// ─────────────────────────────────────────────────────────────────────────────
class MPQManager : public RefCounted {
GDCLASS(MPQManager, RefCounted)
public:
MPQManager();
~MPQManager();
// Open all MPQ archives from a WoW 3.3.5a client folder.
// base_path folder containing Data/
// locale e.g. "ruRU", "enUS", "deDE"
// Returns number of successfully opened archives (0 on total failure).
int open_client(const String &base_path, const String &locale);
// Open a single MPQ archive explicitly (for custom workflows).
bool open_archive(const String &path, int priority);
// Close all open archives.
void close();
// Check whether a file exists in any archive.
bool has_file(const String &internal_path) const;
// Read a file from the highest-priority archive that contains it.
PackedByteArray read_file(const String &internal_path) const;
// Extract a single file to output_path (creates parent dirs).
bool extract_file(const String &internal_path, const String &output_path) const;
// Extract all files matching filter (e.g. "DBFilesClient\\*.dbc")
// into output_dir, preserving sub-paths.
// Returns number of extracted files.
int extract_files(const String &filter, const String &output_dir) const;
// List all files in all archives matching filter. Deduplicates by name,
// keeping only the entry from the highest-priority archive.
PackedStringArray list_files(const String &filter = "*") const;
// Info: how many archives are open
int get_archive_count() const;
// Info: array of Dictionaries { "path": ..., "priority": ..., "file_count": ... }
Array get_archive_info() const;
protected:
static void _bind_methods();
private:
std::vector<MPQArchiveEntry> m_archives; // sorted ascending by priority
// Returns the handle of the highest-priority archive that contains the file.
// Returns nullptr if not found.
HANDLE find_file(const std::string &internal_path) const;
// Helper: std::string → Godot String
static String to_godot(const std::string &s);
// Helper: Godot String → std::string (ANSI/UTF8 path)
static std::string to_std(const String &s);
// Ensure output directory exists (creates recursively)
static bool ensure_dir(const std::string &path);
// WoW 3.3.5a archive load order (lowest→highest priority)
// locale archives always have higher priority than base archives
static std::vector<std::pair<std::string,int>> build_archive_list(
const std::string &data_dir,
const std::string &locale);
};
} // namespace godot
+40
View File
@@ -0,0 +1,40 @@
#include "register_types.h"
#include "mpq_manager.h"
#include "wmo_loader.h"
#include "adt_loader.h"
#include "blp_loader.h"
#include "m2_loader.h"
#include <godot_cpp/core/defs.hpp>
#include <godot_cpp/godot.hpp>
using namespace godot;
void initialize_mpq_extractor_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) return;
ClassDB::register_class<MPQManager>();
ClassDB::register_class<WMOLoader>();
ClassDB::register_class<ADTLoader>();
ClassDB::register_class<BLPLoader>();
ClassDB::register_class<M2Loader>();
}
void uninitialize_mpq_extractor_module(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) return;
}
extern "C" {
GDExtensionBool GDE_EXPORT mpq_extractor_init(
GDExtensionInterfaceGetProcAddress p_get_proc_address,
const GDExtensionClassLibraryPtr p_library,
GDExtensionInitialization *r_initialization)
{
godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization);
init_obj.register_initializer(initialize_mpq_extractor_module);
init_obj.register_terminator(uninitialize_mpq_extractor_module);
init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE);
return init_obj.init();
}
} // extern "C"
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include <godot_cpp/core/class_db.hpp>
void initialize_mpq_extractor_module(godot::ModuleInitializationLevel p_level);
void uninitialize_mpq_extractor_module(godot::ModuleInitializationLevel p_level);
+355
View File
@@ -0,0 +1,355 @@
#include "wmo_loader.h"
#include "wow_chunk_reader.h"
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <godot_cpp/variant/packed_string_array.hpp>
#include <fstream>
#include <filesystem>
#include <cstring>
#include <cstdio>
namespace fs = std::filesystem;
using namespace godot;
// ─────────────────────────────────────────────────────────────────────────────
// Raw WMO binary structures (packed, no padding)
// ─────────────────────────────────────────────────────────────────────────────
#pragma pack(push, 1)
struct MOHDChunk { // Root header
uint32_t nMaterials;
uint32_t nGroups;
uint32_t nPortals;
uint32_t nLights;
uint32_t nModels;
uint32_t nDoodads;
uint32_t nSets;
uint32_t ambColor;
uint32_t areaID;
float bboxMin[3];
float bboxMax[3];
uint16_t flags;
uint16_t numLiquids;
};
struct MOMTEntry { // Material
uint32_t flags;
uint32_t shader;
uint32_t blendMode;
uint32_t texUnit0; // index into MOTX
uint32_t sidnColor;
uint32_t frameSidnColor;
uint32_t texUnit1;
uint32_t diffColor;
uint32_t groundType;
uint32_t texUnit2;
uint32_t color2;
uint32_t flags2;
uint32_t runTimeData[4];
};
struct MOGPHeader { // Group header (inside MOGP chunk)
uint32_t groupNameOfs;
uint32_t descNameOfs;
uint32_t flags;
float bboxMin[3];
float bboxMax[3];
uint16_t portalStart;
uint16_t portalCount;
uint16_t transBatchCount;
uint16_t intBatchCount;
uint16_t extBatchCount;
uint16_t padding;
uint8_t fogIds[4];
uint32_t liquidType;
uint32_t groupID;
uint32_t unk32a;
uint32_t unk32b;
};
struct MOBAEntry { // Render batch
int16_t bboxMin[3];
int16_t bboxMax[3];
uint32_t indexStart;
uint16_t indexCount;
uint16_t vertexStart;
uint16_t vertexEnd;
uint8_t flags;
uint8_t materialId;
};
struct MOVTEntry { float x, y, z; };
struct MONREntry { float x, y, z; };
struct MOTVEntry { float u, v; };
struct MOCVEntry { uint8_t b, g, r, a; };
#pragma pack(pop)
// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
std::vector<uint8_t> WMOLoader::read_file(const std::string &path) {
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f) return {};
auto sz = f.tellg();
f.seekg(0);
std::vector<uint8_t> buf(sz);
f.read(reinterpret_cast<char *>(buf.data()), sz);
return buf;
}
std::string WMOLoader::to_std(const String &s) {
return std::string(s.utf8().get_data());
}
String WMOLoader::to_godot(const std::string &s) {
return String(s.c_str());
}
// ─────────────────────────────────────────────────────────────────────────────
// Root parser
// ─────────────────────────────────────────────────────────────────────────────
Dictionary WMOLoader::parse_root(const std::vector<uint8_t> &buf) {
Dictionary root;
if (buf.empty()) return root;
ChunkReader reader(buf.data(), buf.size());
WoWChunk chunk;
PackedStringArray textures;
Array materials;
std::vector<std::string> tex_strings;
while (reader.next(chunk)) {
if (chunk.is("MOTX")) {
// Null-separated texture paths
auto names = parse_string_block(chunk);
tex_strings = names;
for (auto &n : names)
textures.push_back(to_godot(n));
} else if (chunk.is("MOMT")) {
uint32_t n = chunk.count_of(sizeof(MOMTEntry));
auto *mt = chunk.array<MOMTEntry>();
for (uint32_t i = 0; i < n; ++i) {
Dictionary mat;
mat["flags"] = (int)mt[i].flags;
mat["shader"] = (int)mt[i].shader;
mat["blend_mode"] = (int)mt[i].blendMode;
// texUnit offsets → find index in tex_strings
auto tex_idx = [&](uint32_t ofs) -> int {
uint32_t cur = 0;
for (int j = 0; j < (int)tex_strings.size(); ++j) {
if (cur == ofs) return j;
cur += (uint32_t)tex_strings[j].size() + 1;
}
return -1;
};
mat["texture0"] = tex_idx(mt[i].texUnit0);
mat["texture1"] = tex_idx(mt[i].texUnit1);
materials.push_back(mat);
}
}
}
root["textures"] = textures;
root["materials"] = materials;
return root;
}
// ─────────────────────────────────────────────────────────────────────────────
// Group parser
// ─────────────────────────────────────────────────────────────────────────────
Dictionary WMOLoader::parse_group(const std::vector<uint8_t> &buf) {
Dictionary group;
if (buf.empty()) return group;
ChunkReader top(buf.data(), buf.size());
WoWChunk chunk;
// Skip MVER, find MOGP
while (top.next(chunk)) {
if (!chunk.is("MOGP")) continue;
// MOGP data = MOGPHeader + sub-chunks
if (chunk.size < sizeof(MOGPHeader)) break;
// const auto &hdr = *reinterpret_cast<const MOGPHeader *>(chunk.data);
// Sub-chunks start after the header
size_t sub_ofs = sizeof(MOGPHeader);
ChunkReader sub(chunk.data + sub_ofs, chunk.size - sub_ofs);
WoWChunk sc;
PackedVector3Array vertices, normals;
PackedVector2Array uvs;
PackedColorArray colors;
PackedInt32Array indices;
Array batches;
while (sub.next(sc)) {
if (sc.is("MOVT")) {
uint32_t n = sc.count_of(sizeof(MOVTEntry));
auto *v = sc.array<MOVTEntry>();
vertices.resize(n);
for (uint32_t i = 0; i < n; ++i) {
float gx, gy, gz;
// WMO local coords → negate Z for Godot Y-up
gx = -v[i].y;
gy = v[i].z;
gz = -v[i].x;
vertices[i] = Vector3(gx, gy, gz);
}
} else if (sc.is("MONR")) {
uint32_t n = sc.count_of(sizeof(MONREntry));
auto *v = sc.array<MONREntry>();
normals.resize(n);
for (uint32_t i = 0; i < n; ++i)
normals[i] = Vector3(-v[i].y, v[i].z, -v[i].x);
} else if (sc.is("MOTV")) {
uint32_t n = sc.count_of(sizeof(MOTVEntry));
auto *v = sc.array<MOTVEntry>();
uvs.resize(n);
for (uint32_t i = 0; i < n; ++i)
uvs[i] = Vector2(v[i].u, v[i].v);
} else if (sc.is("MOVI")) {
uint32_t n = sc.size / 2;
auto *idx = sc.array<uint16_t>();
indices.resize(n);
for (uint32_t i = 0; i < n; ++i)
indices[i] = (int)idx[i];
} else if (sc.is("MOCV")) {
uint32_t n = sc.count_of(sizeof(MOCVEntry));
auto *v = sc.array<MOCVEntry>();
colors.resize(n);
for (uint32_t i = 0; i < n; ++i)
colors[i] = Color(v[i].r / 255.f, v[i].g / 255.f,
v[i].b / 255.f, v[i].a / 255.f);
} else if (sc.is("MOBA")) {
uint32_t n = sc.count_of(sizeof(MOBAEntry));
auto *ba = sc.array<MOBAEntry>();
for (uint32_t i = 0; i < n; ++i) {
Dictionary b;
b["index_start"] = (int)ba[i].indexStart;
b["index_count"] = (int)ba[i].indexCount;
b["vertex_start"] = (int)ba[i].vertexStart;
b["vertex_count"] = (int)ba[i].vertexEnd - (int)ba[i].vertexStart + 1;
b["material_id"] = (int)ba[i].materialId;
b["flags"] = (int)ba[i].flags;
batches.push_back(b);
}
}
}
group["vertices"] = vertices;
group["normals"] = normals;
group["uvs"] = uvs;
group["colors"] = colors;
group["indices"] = indices;
group["batches"] = batches;
break; // only one MOGP per group file
}
return group;
}
// ─────────────────────────────────────────────────────────────────────────────
// Public API
// ─────────────────────────────────────────────────────────────────────────────
Dictionary WMOLoader::load_root(const String &path) {
auto buf = read_file(to_std(path));
if (buf.empty()) {
UtilityFunctions::push_error("WMOLoader: cannot read ", path);
return Dictionary();
}
return parse_root(buf);
}
Dictionary WMOLoader::load_group(const String &path) {
auto buf = read_file(to_std(path));
if (buf.empty()) {
UtilityFunctions::push_error("WMOLoader: cannot read ", path);
return Dictionary();
}
return parse_group(buf);
}
Dictionary WMOLoader::load_wmo(const String &path) {
std::string spath = to_std(path);
auto root_buf = read_file(spath);
if (root_buf.empty()) {
UtilityFunctions::push_error("WMOLoader: cannot read root ", path);
return Dictionary();
}
Dictionary result = parse_root(root_buf);
// Count groups from MOHD
uint32_t nGroups = 0;
{
ChunkReader r(root_buf.data(), root_buf.size());
WoWChunk c;
if (r.find("MOHD", c) && c.size >= sizeof(MOHDChunk))
nGroups = c.as<MOHDChunk>().nGroups;
}
// Build base path (strip .wmo extension)
std::string base = spath;
if (base.size() > 4 && base.substr(base.size() - 4) == ".wmo")
base = base.substr(0, base.size() - 4);
Array groups;
for (uint32_t i = 0; i < nGroups; ++i) {
char suffix[16];
std::snprintf(suffix, sizeof(suffix), "_%03u.wmo", i);
std::string gpath = base + suffix;
auto gbuf = read_file(gpath);
if (gbuf.empty()) {
UtilityFunctions::push_warning("WMOLoader: group not found: ",
to_godot(gpath));
groups.push_back(Dictionary());
continue;
}
groups.push_back(parse_group(gbuf));
}
result["groups"] = groups;
UtilityFunctions::print("WMOLoader: loaded ", (int)nGroups, " groups from ", path);
return result;
}
Dictionary WMOLoader::load_wmo_from_bytes(const PackedByteArray &root_bytes,
const Array &group_bytes_array)
{
std::vector<uint8_t> rbuf(root_bytes.size());
std::memcpy(rbuf.data(), root_bytes.ptr(), root_bytes.size());
Dictionary result = parse_root(rbuf);
Array groups;
for (int i = 0; i < group_bytes_array.size(); ++i) {
PackedByteArray gb = group_bytes_array[i];
if (gb.is_empty()) { groups.push_back(Dictionary()); continue; }
std::vector<uint8_t> gbuf(gb.size());
std::memcpy(gbuf.data(), gb.ptr(), gb.size());
groups.push_back(parse_group(gbuf));
}
result["groups"] = groups;
return result;
}
// ─────────────────────────────────────────────────────────────────────────────
void WMOLoader::_bind_methods() {
ClassDB::bind_method(D_METHOD("load_wmo", "path"), &WMOLoader::load_wmo);
ClassDB::bind_method(D_METHOD("load_root", "path"), &WMOLoader::load_root);
ClassDB::bind_method(D_METHOD("load_group", "path"), &WMOLoader::load_group);
ClassDB::bind_method(D_METHOD("load_wmo_from_bytes", "root_bytes", "group_bytes_array"), &WMOLoader::load_wmo_from_bytes);
}
+95
View File
@@ -0,0 +1,95 @@
#pragma once
#include <vector>
#include <string>
#include <cstdint>
#include <godot_cpp/classes/ref_counted.hpp>
#include <godot_cpp/variant/dictionary.hpp>
#include <godot_cpp/variant/array.hpp>
#include <godot_cpp/variant/string.hpp>
#include <godot_cpp/variant/packed_vector3_array.hpp>
#include <godot_cpp/variant/packed_vector2_array.hpp>
#include <godot_cpp/variant/packed_int32_array.hpp>
#include <godot_cpp/variant/packed_color_array.hpp>
#include <godot_cpp/variant/packed_byte_array.hpp>
#include "wow_chunk_reader.h"
namespace godot {
// ─────────────────────────────────────────────────────────────────────────────
// WMOLoader
//
// Loads WoW 3.3.5a WMO files (root + group files).
//
// Usage (GDScript):
// var wmo = WMOLoader.new()
// var data = wmo.load_wmo("C:/wow/Data/World/wmo/Azeroth/Buildings/StormwindHouse/StormwindHouse.wmo")
//
// Return value is a Dictionary:
// {
// "textures": PackedStringArray, # texture filenames referenced by materials
// "materials": Array[Dictionary], # material definitions
// "groups": Array[Dictionary], # one entry per group file
// }
//
// Each group Dictionary:
// {
// "name": String,
// "vertices": PackedVector3Array,
// "normals": PackedVector3Array,
// "uvs": PackedVector2Array,
// "colors": PackedColorArray, # may be empty
// "indices": PackedInt32Array,
// "batches": Array[Dictionary], # render batches
// }
//
// Each batch Dictionary:
// {
// "index_start": int,
// "index_count": int,
// "vertex_start": int,
// "vertex_count": int,
// "material_id": int,
// }
//
// Each material Dictionary:
// {
// "texture0": int, # index into textures array (-1 = none)
// "texture1": int,
// "blend_mode": int, # 0=opaque 1=alpha key 2=alpha blend
// "flags": int,
// }
// ─────────────────────────────────────────────────────────────────────────────
class WMOLoader : public RefCounted {
GDCLASS(WMOLoader, RefCounted)
public:
// Load root WMO + all group files from the same directory.
// path must be an absolute OS path.
Dictionary load_wmo(const String &path);
// Load only the root file (no groups).
Dictionary load_root(const String &path);
// Load a single group file.
Dictionary load_group(const String &path);
// Load from raw bytes (e.g. from MPQManager::read_file).
// group_bytes: Array of PackedByteArray, one per group file.
Dictionary load_wmo_from_bytes(const PackedByteArray &root_bytes,
const Array &group_bytes_array);
protected:
static void _bind_methods();
private:
Dictionary parse_root(const std::vector<uint8_t> &buf);
Dictionary parse_group(const std::vector<uint8_t> &buf);
static std::vector<uint8_t> read_file(const std::string &path);
static std::string to_std(const String &s);
static String to_godot(const std::string &s);
};
} // namespace godot
+112
View File
@@ -0,0 +1,112 @@
#pragma once
#include <cstdint>
#include <cstring>
#include <vector>
#include <string>
// ─────────────────────────────────────────────────────────────────────────────
// Lightweight in-memory chunk reader for WoW chunked binary formats
// (WMO, ADT, WDT, etc.)
// ─────────────────────────────────────────────────────────────────────────────
struct WoWChunk {
uint32_t magic = 0;
uint32_t size = 0;
const uint8_t *data = nullptr; // points into the owning buffer
// Compare magic as 4 chars (e.g. "MVER").
// WoW stores chunk IDs reversed in the file (MVER → REVM bytes).
// After memcpy into uint32_t LE the logical name sits MSB→LSB,
// so we compare each nibble from the top.
bool is(const char *tag) const {
return ((magic >> 24) & 0xFF) == (uint8_t)tag[0] &&
((magic >> 16) & 0xFF) == (uint8_t)tag[1] &&
((magic >> 8) & 0xFF) == (uint8_t)tag[2] &&
((magic >> 0) & 0xFF) == (uint8_t)tag[3];
}
template<typename T>
const T &as() const { return *reinterpret_cast<const T *>(data); }
template<typename T>
const T *array() const { return reinterpret_cast<const T *>(data); }
uint32_t count_of(size_t elem_size) const {
return size / (uint32_t)elem_size;
}
};
// Reads all top-level chunks from a buffer.
// For WMO groups, use ChunkReader on the MOGP payload to get sub-chunks.
class ChunkReader {
public:
explicit ChunkReader(const uint8_t *buf, size_t len)
: m_buf(buf), m_len(len) {}
// Returns true if chunk at current position was read into `out`
bool next(WoWChunk &out) {
if (m_pos + 8 > m_len) return false;
std::memcpy(&out.magic, m_buf + m_pos, 4);
std::memcpy(&out.size, m_buf + m_pos + 4, 4);
m_pos += 8;
if (m_pos + out.size > m_len) return false;
out.data = m_buf + m_pos;
m_pos += out.size;
return true;
}
bool eof() const { return m_pos >= m_len; }
void reset() { m_pos = 0; }
// Find first chunk with given 4-char magic, reset on miss
bool find(const char *tag, WoWChunk &out) {
reset();
WoWChunk c;
while (next(c)) {
if (c.is(tag)) { out = c; return true; }
}
return false;
}
private:
const uint8_t *m_buf;
size_t m_len;
size_t m_pos = 0;
};
// ─── Helpers ──────────────────────────────────────────────────────────────────
// WoW world coordinates → Godot Y-up
// WoW: X=North, Y=West, Z=Up, world center at (17066.666, 17066.666)
inline void wow_to_godot(float wx, float wy, float wz,
float &gx, float &gy, float &gz)
{
gx = -(wy - 17066.666f);
gy = wz;
gz = -(wx - 17066.666f);
}
// WoW rotation (degrees, ZXY order) → Godot Euler (radians, XYZ)
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
inline void wow_rot_to_godot(float rx, float ry, float rz,
float &gx, float &gy, float &gz)
{
constexpr float D2R = (float)(M_PI / 180.0);
gx = rx * D2R;
gy = (rz - 90.0f) * D2R; // WoW Z-rot maps to Godot Y
gz = -ry * D2R;
}
// Collect null-separated strings from a chunk into a vector
inline std::vector<std::string> parse_string_block(const WoWChunk &c) {
std::vector<std::string> result;
const char *p = reinterpret_cast<const char *>(c.data);
const char *end = p + c.size;
while (p < end) {
result.emplace_back(p);
p += result.back().size() + 1;
}
return result;
}