From 47eff19cb6db50d3d8d9f8a28c6b032f20824921 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 5 May 2026 14:53:28 -0700 Subject: [PATCH] feat: WOB material serialization, FORMAT_SPEC v1.1, material tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WOB save/load now serializes Material struct fields (flags, shader, blendMode, texturePath) per group — was saving only texture paths - FORMAT_SPEC.md v1.1: documents WOT doodad/WMO placements, WOB material fields, doodad rotation, terrain stamps, WCP file list - Test coverage: 5 new assertions verify material round-trip (flags, shader, blendMode all preserved through save→load cycle) - 260 total assertions across 75 test cases, all passing --- src/pipeline/wowee_building.cpp | 29 +++++++++++++++++++++++++++++ tests/test_open_formats.cpp | 11 +++++++++++ tools/editor/FORMAT_SPEC.md | 30 ++++++++++++++++++++---------- 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/src/pipeline/wowee_building.cpp b/src/pipeline/wowee_building.cpp index a0d27aa9..13130b5f 100644 --- a/src/pipeline/wowee_building.cpp +++ b/src/pipeline/wowee_building.cpp @@ -64,6 +64,23 @@ WoweeBuilding WoweeBuildingLoader::load(const std::string& basePath) { f.read(tp.data(), tl); grp.texturePaths.push_back(tp); } + + // Read material data (v1.1+) + uint32_t mc = 0; + if (f.read(reinterpret_cast(&mc), 4) && mc > 0 && mc <= 256) { + for (uint32_t mi = 0; mi < mc; mi++) { + WoweeBuilding::Material mat; + uint16_t pl; + f.read(reinterpret_cast(&pl), 2); + mat.texturePath.resize(pl); + f.read(mat.texturePath.data(), pl); + f.read(reinterpret_cast(&mat.flags), 4); + f.read(reinterpret_cast(&mat.shader), 4); + f.read(reinterpret_cast(&mat.blendMode), 4); + grp.materials.push_back(mat); + } + } + bld.groups.push_back(std::move(grp)); } @@ -140,6 +157,18 @@ bool WoweeBuildingLoader::save(const WoweeBuilding& bld, const std::string& base f.write(reinterpret_cast(&tl), 2); f.write(tp.data(), tl); } + + // Write material data + uint32_t mc = static_cast(grp.materials.size()); + f.write(reinterpret_cast(&mc), 4); + for (const auto& mat : grp.materials) { + uint16_t pl = static_cast(mat.texturePath.size()); + f.write(reinterpret_cast(&pl), 2); + f.write(mat.texturePath.data(), pl); + f.write(reinterpret_cast(&mat.flags), 4); + f.write(reinterpret_cast(&mat.shader), 4); + f.write(reinterpret_cast(&mat.blendMode), 4); + } } for (const auto& portal : bld.portals) { diff --git a/tests/test_open_formats.cpp b/tests/test_open_formats.cpp index 6f4ef79f..d67604b4 100644 --- a/tests/test_open_formats.cpp +++ b/tests/test_open_formats.cpp @@ -34,6 +34,12 @@ TEST_CASE("WOB save and load round-trip", "[wob]") { grp.vertices.push_back({{0,1,0}, {0,0,1}, {0,1}, {1,1,1,1}}); grp.indices = {0, 1, 2}; grp.texturePaths.push_back("textures/wall.png"); + WoweeBuilding::Material mat; + mat.texturePath = "textures/wall.png"; + mat.flags = 0x10; + mat.shader = 3; + mat.blendMode = 1; + grp.materials.push_back(mat); bld.groups.push_back(grp); WoweeBuilding::Portal portal; @@ -63,6 +69,11 @@ TEST_CASE("WOB save and load round-trip", "[wob]") { REQUIRE(loaded.groups[0].vertices.size() == 3); REQUIRE(loaded.groups[0].indices.size() == 3); REQUIRE(loaded.groups[0].isOutdoor == false); + REQUIRE(loaded.groups[0].materials.size() == 1); + REQUIRE(loaded.groups[0].materials[0].texturePath == "textures/wall.png"); + REQUIRE(loaded.groups[0].materials[0].flags == 0x10); + REQUIRE(loaded.groups[0].materials[0].shader == 3); + REQUIRE(loaded.groups[0].materials[0].blendMode == 1); REQUIRE(loaded.portals.size() == 1); REQUIRE(loaded.portals[0].groupA == 0); REQUIRE(loaded.portals[0].vertices.size() == 4); diff --git a/tools/editor/FORMAT_SPEC.md b/tools/editor/FORMAT_SPEC.md index 0333e426..8de90adf 100644 --- a/tools/editor/FORMAT_SPEC.md +++ b/tools/editor/FORMAT_SPEC.md @@ -1,16 +1,18 @@ -# Wowee Open Format Specification v1.0 +# Wowee Open Format Specification v1.1 Novel file formats for custom WoW zone content. No Blizzard IP. ## WOT — Wowee Open Terrain (JSON metadata) - Extension: `.wot` -- Contains: tile coords, texture list, per-chunk layers/holes, water data +- Contains: tile coords, texture list, per-chunk layers/holes, water data, + doodad placements (M2 objects), WMO placements (buildings) - Key: `"format": "wot-1.0"` +- Placement fields: `doodadNames[]`, `doodads[]` (nameId, uniqueId, pos, rot, scale, flags) +- WMO fields: `wmoNames[]`, `wmos[]` (nameId, uniqueId, pos, rot, flags, doodadSet) ## WHM — Wowee HeightMap (binary) - Extension: `.whm` - Magic: `WHM1` (0x314D4857) -- Version: 1 (embedded in magic — WHM2 for future revisions) - Layout: magic(4) + chunks(4) + vertsPerChunk(4) + per-chunk data × 256 - Per-chunk: baseHeight(4) + heights[145](580) + alphaSize(4) + alphaData(alphaSize) - Alpha data: raw alpha blend maps for texture layers (same format as ADT MCAL) @@ -18,29 +20,32 @@ Novel file formats for custom WoW zone content. No Blizzard IP. ## WOM — Wowee Open Model (binary) - Extension: `.wom` -- Magic: `WOM1` (0x314D4F57) — version 1 -- Version: embedded in magic (WOM2 for future revisions with animation support) +- Magic: `WOM1` (0x314D4F57) - Layout: magic(4) + vertCount(4) + indexCount(4) + texCount(4) + bounds(28) + name + vertices + indices + texPaths - Vertex: position(vec3) + normal(vec3) + texCoord(vec2) = 32 bytes +- Note: geometry-only (no skeletal animation — WOM2 planned for bone data) ## WOB — Wowee Open Building (binary) - Extension: `.wob` - Magic: `WOB1` (0x31424F57) - Layout: magic(4) + groupCount(4) + portalCount(4) + doodadCount(4) + bounds(4) + name + groups + portals + doodads -- Group: name + vertices(pos+normal+uv+color) + indices + texPaths + bounds + outdoor flag +- Group: name + vertexCount(4) + indexCount(4) + texCount(4) + outdoor(1) + bounds(24) + + vertices(pos+normal+uv+color) + indices + texPaths + materialCount(4) + materials +- Material: texturePath + flags(4) + shader(4) + blendMode(4) +- Doodad: modelPath + position(12) + rotation(12) + scale(4) +- Portal: groupA(4) + groupB(4) + vertexCount(4) + vertices ## WCP — Wowee Content Pack (archive) - Extension: `.wcp` - Magic: `WCP1` (0x31504357) - Layout: magic(4) + fileCount(4) + infoJsonSize(4) + infoJSON + [pathLen(2) + path + dataSize(4) + data] × N +- Info JSON includes categorized file list (terrain/model/building/texture/data) ## zone.json — Map Definition - Replaces WDT -- Contains: mapName, mapId, tiles, biome, file references - -## zone.json Fields -- `mapName`, `displayName`, `mapId`, `biome`, `baseHeight` +- Fields: `mapName`, `displayName`, `mapId`, `biome`, `baseHeight` - `hasCreatures`, `description`, `tiles` array, `files` map +- `doodadNames[]`, `doodads[]`, `wmoNames[]`, `wmos[]` for placed objects - `editorVersion` for compatibility tracking ## JSON DBC — Data Table Replacement @@ -54,6 +59,11 @@ Novel file formats for custom WoW zone content. No Blizzard IP. - Standard PNG format, loaded by client's texture override system - Editor auto-converts BLP→PNG on export via stb_image_write +## Terrain Stamps (.json) +- Portable terrain feature snapshots (mountains, craters, etc.) +- Format: `{"format": "wowee-stamp-1.0", "vertices": [[dx, dy, height], ...]}` +- Can be saved/loaded across zones and sessions + ## Open Format Scoring (0-6) 1. WOT terrain metadata present 2. WHM heightmap with valid magic