Commit graph

3076 commits

Author SHA1 Message Date
Kelsi
f0abd1794b fix(wom): sanitize bone pivot NaN + clamp out-of-range parentBone
Bones with NaN pivots produce broken skeleton matrices that ripple into
every child bone via the parent-chain multiplication. Out-of-range
parentBone indices would cause a use-after-free during bone-matrix
computation. Both now defensively clamped.
2026-05-06 05:19:24 -07:00
Kelsi
15648e21ec fix(wob): per-vertex NaN/inf scrub on load (matches WOM)
Same NaN guard as the just-applied WOM one. Sanitizes position/normal/
texCoord/color components after the bulk read. WMO renderer's matrix
math is sensitive — a single NaN position could desync the entire
group's draw state.
2026-05-06 05:16:16 -07:00
Kelsi
29ae850307 fix(wom): per-vertex NaN/inf scrub on load
Even after the bound-field guards, individual vertex floats (position,
normal, texCoord) could still poison the GPU. NaN positions would crash
the M2 vertex shader on some drivers (silent device-lost). Now each
component defaults to 0 (or 1 for normal Z) when non-finite — vertex
ends up at origin instead of corrupting the whole pipeline.
2026-05-06 05:14:35 -07:00
Kelsi
1467417b11 fix(wom): sanitize boundRadius / boundMin / boundMax against NaN/inf on load
WOM bound fields drive M2 culling and collision AABBs — non-finite
values would either cull the model out entirely or crash the cull math.
Now boundRadius defaults to 1.0 when invalid, and each boundMin/boundMax
component defaults to 0 when non-finite.
2026-05-06 05:12:53 -07:00
Kelsi
de983c2728 fix(whm): replace NaN/inf chunk base + per-vertex heights with 0.0 on load
A WHM with non-finite height values would produce non-finite vertex
positions in the terrain mesh, breaking collision queries, pathing,
and the GPU's matrix math. Both the chunk base (one float per chunk)
and the 145 per-vertex heights are now individually validated.
2026-05-06 05:10:04 -07:00
Kelsi
199f18cdac fix(wob): clamp doodad scale to 1.0 when loaded value is 0/NaN/inf
Without this guard, a corrupted or partially-written WoB with scale=0
would render the doodad at zero size (invisible) and a NaN/inf would
crash the renderer's matrix math. Now defaults to 1.0 for any non-
finite or near-zero value.
2026-05-06 04:57:09 -07:00
Kelsi
db068d480b feat(wob): tryLoadByGamePath helper, used by editor + terrain_manager
Mirrors the WOM tryLoadByGamePath API: probes custom_zones/buildings/ +
output/buildings/ by default, with optional extraPrefixes (e.g. per-zone
output/<map>/buildings/) checked first. Both the editor and the main
game's terrain_manager now use the helper, removing duplicate inline
lookup loops in two more places.
2026-05-06 04:10:12 -07:00
Kelsi
f36309a96f feat(wom): tryLoadByGamePath accepts extraPrefixes for per-zone search roots
The shared helper only probed custom_zones/models/ + output/models/, but
the editor's exportZone writes to output/<map>/models/. Added an
extraPrefixes parameter that's tried before the defaults — main game's
terrain_manager now passes ['output/<map>/models/', 'custom_zones/<map>/
models/'] so per-zone WOM exports override globals. Also removes the
last duplicate WOM-loading code from terrain_manager.
2026-05-06 04:07:16 -07:00
Kelsi
1d05bb0f13 fix(terrain): main game also propagates MODF scale to WMO instances
Mirrors the editor's WMO scale fix. WMOReady gains a scale field that
is computed from the loaded MODF placement.scale (u16 / 1024) and
forwarded to wmoRenderer->createInstance(). Without this the main
game ignored MODF scale even on WotLK ADTs that use it.
2026-05-06 03:55:14 -07:00
Kelsi
db1968f2cc feat(adt): preserve MODF nameSet + scale fields across load/save round-trip
WMOPlacement struct gains nameSet and scale fields (defaulting to 0 and
1024 = 1.0). The loader now reads them when the entry is the full 64
bytes (WotLK+); the writer emits the actual values rather than always
hard-coding (0, 1024). Older expansions still round-trip cleanly because
defaults match the previous behaviour.
2026-05-06 03:37:13 -07:00
Kelsi
5241bbd669 perf(editor): cache M2/WMO models across rebuilds, only clear instances
The editor's rebuildObjects path was destroying every cached model and
re-uploading it on every (debounced) change. Added M2Renderer::clearInstances
that drops only the instance list while keeping models loaded. Editor's
clearObjects switches to clearInstances (M2) + clearInstances (WMO),
and persistent path->modelId maps survive across rebuilds. clearTerrain
fully evicts when loading a new zone.
2026-05-06 03:23:06 -07:00
Kelsi
497997c50a fix(wob): doodad euler->quat uses glm convention to round-trip with fromWMO
Manual qx*qy*qz product was XYZ intrinsic, but fromWMO uses
glm::eulerAngles which returns YXZ Tait-Bryan extrinsic. The mismatch
introduced rotation drift on every save/load cycle. Using glm::quat
constructed from euler radians directly applies the inverse convention
of eulerAngles so the round-trip is clean.
2026-05-06 03:05:35 -07:00
Kelsi
560b4a9d35 feat(assets): PNG override probes custom_zones/textures and output/textures
The previous implementation required the .blp to exist in the asset
manifest before the PNG sidecar could be found. That prevented
custom-zone exports from shipping PNG-only textures (which is the whole
point of the open-format pipeline). Now if the manifest lookup fails
the loader probes well-known custom-zone roots so PNG-only assets work
out of the box.
2026-05-06 03:04:12 -07:00
Kelsi
a49b35e41b fix(wob): aggregate unique materials across ALL groups in toWMOModel
Previously only group[0]'s materials were emitted. Buildings with
group-specific materials (e.g., the indoor groups using a different
texture set than outdoor) lost those materials, leaving batches in
later groups pointing to materialIndex out of range. Now dedupes by
(texture, blend, flags) across every group.
2026-05-06 02:37:10 -07:00
Kelsi
eadb6a5886 feat(woc): add WMO collision meshes to exported zone collision
WoweeCollision previously only contained terrain triangles; placed WMO
buildings had no collision in the exported zone, so players could walk
through walls. Added WoweeCollisionBuilder::addMesh() that transforms a
local-space mesh into world space with slope-based walkability flags,
and the editor's exportZone now walks every placed WMO and feeds each
group's geometry through it. Indoor vs outdoor groups are tagged via
the WMO group flag.
2026-05-06 02:33:22 -07:00
Kelsi
4578bbc0d1 fix(wom): toM2 converts .png texture paths back to .blp for renderer
Same fix as WoB: M2Renderer's PNG override is keyed on .blp extension.
fromM2 writes .png paths to signal intent (PNG export pipeline), but
toM2 must convert back so the runtime engages the override and finds
the actual texture file.
2026-05-06 02:16:28 -07:00
Kelsi
aa6b692a58 fix(wob): convert .png texture paths back to .blp in toWMOModel
WMORenderer's PNG override system only triggers when the requested
texture path ends in .blp — it strips the .blp and probes for .png.
WoB stores .png paths (because fromWMO converts .blp->.png at conversion
time), so passing them straight through to the WMOModel meant the
override wasn't engaged and textures didn't load. Now converts back to
.blp so the existing override pipeline picks up the PNG file.
2026-05-06 02:15:13 -07:00
Kelsi
9d200fbe7b fix(wom): fromM2 always merges .skin file regardless of M2 isValid state
WotLK M2s store the header in .m2 but geometry in .skin. fromM2 only
loaded the skin when isValid() returned false, which it does for those
WotLK files — but missed the case where M2Loader::load happened to
populate enough that isValid() was true (older format M2s with newer
features). Now always merges skin data when present, matching the
editor's viewport loader behaviour.
2026-05-06 02:12:45 -07:00
Kelsi
318f255918 fix(wob): emit default doodadSet so WMO renderer draws WoB doodads
WMORenderer uses doodadSets[0] to select which slice of the doodads
array to render. Without a set entry the renderer skipped every doodad
even when toWMOModel populated them. Now emits a default set named
'Set_$DefaultGlobal' covering all doodads — matches Blizzard's default
set name and convention.
2026-05-06 02:09:24 -07:00
Kelsi
f2bbc8d60f feat(wob): toWMOModel restores doodad placements (interior props)
WoB stored doodad placements but toWMOModel never reconstructed them in
the WMOModel, so converted-back buildings rendered as empty shells.
Now rebuilds doodadNames + doodads with M2 path conversion (.wom -> .m2
for the runtime that hasn't picked up WOM-aware doodad loading yet) and
euler->quaternion rotation conversion.
2026-05-06 02:07:44 -07:00
Kelsi
804c7d3d60 fix(wom): toM2 handles WOM3 batches with empty textureLookup safely
Previously `m.textureLookup.size() - 1` would underflow to UINT_MAX when
texturePaths was empty, then std::min would clamp the bad value into the
batch. Renderer would either crash or sample bogus memory. Now treats an
empty lookup as textureIndex=0 (white-texture fallback path).
2026-05-06 02:00:52 -07:00
Kelsi
7d3eb59893 fix(wob): toWMOModel restores materials, textures, portals, group flags
Previously toWMOModel only copied vertex/index data — materials and
textures were dropped, so a converted-back WMO would render textureless
white walls. Now rebuilds the global texture index from material paths,
emits one WMOMaterial per WoB material, copies group bounds + outdoor
flag, and reconstructs MOPR portal refs from groupA/groupB so PVS data
survives round-trip.
2026-05-06 01:56:47 -07:00
Kelsi
a711a92875 refactor(terrain): use WoweeModelLoader::toM2 for WOM loading in main game
terrain_manager.cpp had a 70-line duplicate of the WOM->M2 conversion that
ignored WOM3 multi-batch support. Replaced with a single toM2() call.
Also extended toM2 to copy bones and animation sequence headers so the
shared helper now produces a fully renderable M2Model from any WOM
version, with main game and editor both using the same code path.
2026-05-06 01:38:31 -07:00
Kelsi
23951d4075 fix(wom): fromM2 sets version=3 when batches were extracted
Without this fromM2 always wrote version=2 even when batches were
populated, causing the version field on the in-memory model to lie
about its content. The save() magic-byte selection happens off the
batches/animation flags directly so the on-disk file is still correct,
but loaders that key off model.version saw stale info.
2026-05-06 01:36:37 -07:00
Kelsi
13a7adffab fix(wom): validate WOM3 batch ranges to reject corrupt files safely
Without bounds checks, a corrupted WOM3 file with invalid indexStart/
indexCount/textureIndex would feed bad ranges to the M2 renderer and
crash on draw. Now each batch is validated against the loaded indices
and texturePaths arrays; out-of-range batches are warned and dropped.
2026-05-06 01:32:59 -07:00
Kelsi
6f88eb270a feat(wob): extract portals from WMO during conversion
WoweeBuildingLoader::fromWMO previously left the portals array empty,
losing portal/PVS data essential for indoor visibility culling. Now reads
the WMO portal vertex polygons and pairs them with their two adjacent
groups via MOPR refs, populating WoweeBuilding::Portal fully.
2026-05-06 01:20:29 -07:00
Kelsi
e342f2ba55 chore(wmo): demote 'cleared all WMO models' message to INFO level 2026-05-06 01:16:18 -07:00
Kelsi
b736c6b2e1 feat(wom): add WOM3 multi-batch format for material-aware models
WOM1/WOM2 had a single mesh with one texture, which lost the multi-submesh
structure of complex M2 models (body+hair+eyes+armor each need different
textures and blend modes).

WOM3 adds a Batch array: each batch has indexStart/indexCount + a textureIndex
into texturePaths + blendMode + flags. Loader is fully backward compatible:
WOM1/WOM2 files still load, and WOM3 with no batches block falls back to a
single full-mesh batch. fromM2 now extracts batches with materials, and toM2
emits matching M2 batches so the renderer can draw them correctly.
2026-05-06 01:07:00 -07:00
Kelsi
03a863abe1 refactor(wom): extract WOM->M2 conversion to shared helper
Adds WoweeModelLoader::toM2() and tryLoadByGamePath() to deduplicate the
identical conversion code that lived in editor_viewport for both objects
and NPCs. Cuts ~70 lines of duplicated logic and makes WOM->M2 reusable
across the codebase.
2026-05-06 01:02:56 -07:00
Kelsi
1c3307a0b6 fix(editor): unload ghost preview model when path changes
setGhostPreview reused modelId 59999 for every preview, but loadModel
returns true without doing anything when the ID is already cached. So
selecting a new NPC kept the old ghost model in GPU memory and createInstance
used the stale model. Added M2Renderer::unloadModel public API and call it
from clearGhostPreview.
2026-05-06 00:51:22 -07:00
Kelsi
ca630c4e87 chore(editor): remove debug logging now that NPC rendering works 2026-05-06 00:48:41 -07:00
Kelsi
a6d6e0168a fix(editor): add filtered-out instance diagnostic in M2 render path 2026-05-06 00:38:41 -07:00
Kelsi
b1162a2ac5 fix(editor): add GPU buffer validation logging in M2 render path 2026-05-06 00:26:36 -07:00
Kelsi
67f4097e74 fix: resolve all GitHub CodeQL security/quality alerts
Fix 9 integer-multiplication-cast-to-long warnings across 6 files:
- wmo_renderer.cpp: grid cell count and height variance calculation
- composite_renderer.cpp: overlay tile grid allocation
- vk_texture.cpp: image size calculation (width*height*bpp)
- m2_renderer.cpp: collision grid cell allocation
- character_renderer.cpp: normal map buffer and height variance
- world_entry_callback_handler.cpp: tile reserve count

All fixes cast operands to size_t/double before multiplication to
prevent integer overflow when dimensions are large.
2026-05-05 22:49:21 -07:00
Kelsi
b439f12c36 fix: WOM2 bone data in client renderer, WOM tests, docs update
- Fix WOM→M2Model conversion: copy boneWeights/boneIndices from WOM2
  vertices (was dropping skeletal binding data, breaking animation)
- Copy bone hierarchy and animation sequences from WOM2 to M2Model
  so animated WOM2 models render with proper skeletal deformation
- Add 3 WOM format tests: WOM1 binary structure, WOM2 magic check,
  invalid magic rejection (6 new assertions)
- CHANGELOG: document WOM1/WOM2 animated model support
- README: clarify WOM1 (static) vs WOM2 (animated) models
- 334 total assertions across 87 test cases
2026-05-05 16:23:22 -07:00
Kelsi
f6dfc295ab feat: WOM2 animated model format with bones and keyframe animation
Upgrades WOM from geometry-only (WOM1) to fully animated (WOM2):

- WOM2 magic (0x324D4F57) for animated models, WOM1 for static
- Vertex extended: +boneWeights[4] +boneIndices[4] (40 bytes vs 32)
- Bone data: keyBoneId, parentBone, pivot, flags per bone
- Animation data: per-sequence per-bone keyframes with translation,
  rotation (quaternion), scale at millisecond timestamps
- fromM2() now preserves all skeletal data: bone hierarchy, weights,
  and per-sequence keyframes from M2 animation tracks
- Backward compatible: WOM1 files load without bone data (32-byte
  vertices read and padded with default bone weights)
- FORMAT_SPEC.md updated with WOM2 binary layout
2026-05-05 16:16:07 -07:00
Kelsi
cff8d359e4 feat: client-side WOC collision loading + walkability queries
- TerrainManager loads WOC collision meshes alongside WOT/WHM terrain
  from both custom_zones/ and output/ directories
- CollisionData stored per-tile with triangle array + bounds
- isPositionWalkable(x, y): returns whether a world position is on
  walkable terrain (barycentric point-in-triangle test)
- getCollisionFlags(x, y): returns per-triangle flags (walkable,
  water, steep, indoor) for movement system integration
- Defaults to walkable when no collision data is loaded (backward compat)
- Custom zone players now have proper terrain physics boundaries
2026-05-05 15:32:48 -07:00
Kelsi
4d5eef480e feat: WOC collision mesh format — 7th novel open format
New format: WOC (Wowee Open Collision) — binary collision mesh for
custom zone walkability. Magic WOC1 (0x31434F57).

- WoweeCollisionBuilder::fromTerrain() generates collision triangles
  from terrain heightmap with slope classification (50 deg threshold)
- Per-triangle flags: walkable (0x01), water (0x02), steep (0x04)
- Respects terrain holes (skips triangles in hole regions)
- Binary save/load with bounds, tile coords, triangle data
- Auto-exported on zone save alongside WOT/WHM/WOM/WOB
- Added to content pack validation (score now 0-7)
- FORMAT_SPEC.md v1.1 updated with WOC binary layout
- 19 new test assertions: flat terrain generation (32k tris all
  walkable), save/load round-trip, hole skipping
- 328 total assertions across 84 test cases
2026-05-05 15:23:58 -07:00
Kelsi
ad67700cb6 feat: WOM renderable batches, JSON DBC test coverage
- WOM→M2 conversion now creates proper render batch (indexStart,
  indexCount, vertexStart, vertexCount), texture references, material
  with opaque blend mode — WOM models are now actually visible in the
  client renderer instead of being invisible geometry-only shells
- 5 new JSON DBC test cases: basic load with strings/ints, float values,
  empty records rejection, missing records key rejection, findRecordById
  lookup across 3 records
- Total: 278 assertions across 80 test cases, all passing
2026-05-05 15:03:01 -07:00
Kelsi
47eff19cb6 feat: WOB material serialization, FORMAT_SPEC v1.1, material tests
- 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
2026-05-05 14:53:28 -07:00
Kelsi
ca15da5e9b feat: WOT doodad/WMO placements, WOB materials, deduplicate loader
Architecture fixes for open format data fidelity:

- WOT now serializes full doodad/WMO placement arrays (positions,
  rotations, scale, flags, doodad sets) — was only storing counts,
  causing all placed objects to be lost on WOT round-trip
- WOT loader parses placements back into ADTTerrain for client rendering
- WOB Material struct added: preserves WMO material flags, shader type,
  and blend mode during WMO→WOB conversion (was geometry-only)
- WOB doodad rotation: quaternion→euler conversion instead of hardcoded
  zero (placed doodads inside buildings now retain their orientation)
- importOpen() deduplicated: delegates to pipeline::WoweeTerrainLoader
  instead of duplicating 100 lines of parsing code
2026-05-05 14:44:46 -07:00
Kelsi
08500384e2 refactor: migrate all remaining JSON to nlohmann/json
- npc_spawner: save/load with proper JSON (25+ fields + patrol paths)
- zone_manifest: save/load with nlohmann (was naive string concat/parse)
  - load now parses all fields: mapId, baseHeight, tiles, hasCreatures
- custom_zone_discovery: parse zone.json with nlohmann, extract mapId
  and tile coordinates (was only reading name/author/description)
- object_placer: save/load with nlohmann (was substring parsing)
- editor_app: stats.json export uses nlohmann, score display now /6

Zero naive JSON string concatenation remains in the editor codebase.
2026-05-05 13:10:07 -07:00
Kelsi
815787933b feat: WHM alpha maps + nlohmann/json for WOT format
- WHM binary now includes per-chunk alpha map data (alphaSize + data)
  so custom zones render with proper texture blending in the client
- WOT exporter rewritten with nlohmann/json (was manual string concat)
- WOT loader rewritten with nlohmann/json (was naive substring parsing)
- Backward compatible: old WHM files without alpha data still load fine
2026-05-05 13:04:51 -07:00
Kelsi
4fc0361f7a feat: complete client integration for all 6 open formats
- Wire WOB buildings into WMO render pipeline (loads→converts→renders)
- Implement JSON DBC loading in DBCFile::loadJSON() with nlohmann/json
- Wire JSON DBC override into AssetManager (custom_zones/output scan)
- Add WMO→WOB conversion with full geometry (fromWMO)
- Replace placeholder WOB export with real WMO→WOB conversion in editor
- Add --convert-wmo CLI flag for batch WMO→WOB conversion
- Store discovered custom zones on Renderer with getCustomZones() accessor
- Add isCustomZone_ member to TerrainManager

All 6 Blizzard format replacements now fully load in the client:
  ADT→WOT/WHM, WDT→zone.json, BLP→PNG, DBC→JSON, M2→WOM, WMO→WOB
2026-05-05 12:41:19 -07:00
Kelsi
01d3638835 feat: WOB→WMO conversion and loading in client terrain manager
- WoweeBuildingLoader::toWMOModel() converts WOB groups to WMOModel
  with vertices, indices, normals, texCoords, and vertex colors
- TerrainManager now loads WOB files from custom_zones/buildings/
  and converts to WMOModel for the WMO renderer pipeline
- WMOGroup indices converted from uint32 to uint16 for renderer compat

Client open format support — 4 of 6 now loading:
- FULL: WOT/WHM terrain, PNG textures, WOM models
- LOAD: WOB buildings (converts to WMOModel, render pipeline TODO)
- DETECT: zone.json (scanned), JSON DBC (scanned)
2026-05-05 12:12:26 -07:00
Kelsi
aac854c8ad feat: client scans and logs available custom zones at startup
- Renderer::initializeRenderers() now scans custom_zones/ and output/
  for zone.json manifests on first initialization
- Logs all discovered custom zones with name, directory, and
  NPC/quest availability indicators
- One-time scan (cached after first run)
- Foundation for custom zone selection menu in client UI

Client open format support:
- FULL: WOT/WHM terrain, PNG textures, WOM models (load + render)
- DETECT: zone.json (scanned at startup), WOB buildings, JSON DBC
2026-05-05 12:05:00 -07:00
Kelsi
2d417aa125 feat: client detects WOB buildings and JSON DBCs from custom zones
- TerrainManager now checks for .wob files before loading WMO buildings
  (searches custom_zones/buildings/ and output/MapName/buildings/)
- AssetManager::loadDBC() scans custom_zones/*/data/ for JSON DBC
  overrides exported by the editor
- WOB detection logs when found (full WOB→WMOModel conversion pending)
- JSON DBC detection logs when found (full JSON→DBCFile loading pending)

Client open format support status:
- WOT/WHM terrain: FULL (loads and renders)
- PNG textures: FULL (override system)
- WOM models: FULL (loads and renders)
- zone.json: DETECTION (CustomZoneDiscovery scans)
- WOB buildings: DETECTION (found, conversion pending)
- JSON DBC: DETECTION (found, loading pending)
2026-05-05 12:00:31 -07:00
Kelsi
8517ae3778 feat: client loads WOM open models from custom zones automatically
- TerrainManager now checks for .wom files before loading M2 models
- Searches custom_zones/models/ and output/MapName/models/ directories
- Converts WOM vertices/indices to M2Model struct for the renderer
- Full pipeline: editor exports M2→WOM → client loads WOM directly
- Falls back to standard M2 loading if no WOM found

The client can now render custom zone content using entirely
open formats: WOT/WHM terrain + WOM models + PNG textures
2026-05-05 10:36:46 -07:00
Kelsi
71c3eb0fe6 feat: Wowee Open Building format (.wob) — novel WMO replacement
ALL 6 BLIZZARD FORMATS NOW HAVE OPEN REPLACEMENTS.

WOB format: binary building file with groups, portals, and doodads.
- WOB1 magic header
- Groups: vertices (pos+normal+uv+color), indices, texture paths,
  bounding box, indoor/outdoor flag
- Portals: connect groups with polygon boundaries
- Doodad placements: reference .wom models with transform

WoweeBuildingLoader: load/save/exists for .wob files.

Complete format replacement table:
- ADT → WOT/WHM (terrain heightmaps + metadata)
- WDT → zone.json (map definition)
- BLP → PNG (textures)
- DBC → JSON (data tables)
- M2 → WOM (static models)
- WMO → WOB (buildings with groups/portals/doodads)

The wowee project now has a complete suite of novel, open file
formats for creating and distributing custom WoW content without
any dependency on Blizzard proprietary file formats.
2026-05-05 10:28:24 -07:00
Kelsi
b4cb833108 feat: Wowee Open Model format (.wom) — novel M2 replacement
WOM format: binary model file with no Blizzard structures.
- WOM1 magic header + vertex/index counts + bounding box
- Vertices: position(vec3) + normal(vec3) + texCoord(vec2) = 32 bytes
- Indices: uint32 triangle list
- Texture paths: PNG references (not BLP)

WoweeModelLoader:
- load(): reads .wom binary back to WoweeModel struct
- save(): writes WoweeModel to .wom binary
- fromM2(): converts existing M2 models to WOM (static geometry,
  strips bone/animation data, converts BLP paths to PNG)
- exists(): checks for .wom file

Format replacement progress — 5 out of 6 done:
- DONE: ADT → WOT/WHM (terrain)
- DONE: WDT → zone.json (map definition)
- DONE: BLP → PNG (textures)
- DONE: DBC → JSON (data tables)
- DONE: M2 → WOM (static models)
- TODO: WMO → open building format
2026-05-05 10:24:46 -07:00