A hand-edited project.json could carry a 0 or massively-OOR mapId
(would break DBC indexing) or tile coords outside 0..63 (would
produce garbage ADT filenames). Defaults restore safe values.
A user-edited zone.json could carry NaN/inf or out-of-range volumes
which would silently corrupt terrain elevation or produce silent /
clipping audio. Now baseHeight defaults back to 100 if non-finite,
and music/ambience volumes clamp to [0,1] with NaN fallbacks.
Sanitize at write time too, not just on load. A mid-edit NaN spike
(e.g. brush operation that produced NaN before being committed) would
otherwise be persisted into the WHM and require the load-time guard
to clean it up forever after. AlphaSize is also capped at 64KB to
match the loader cap.
Three new sanity checks before stbi_write_png:
- dimensions <=0 or >8K rejected (matches PNG override loader cap)
- data buffer must be >= width * height * 4 bytes (corrupt BLP could
have mismatched dimensions vs data length, and stbi reads off the
end of the buffer otherwise)
Skips with warning rather than crashing the exporter mid-zone.
Two write-side guards mirroring the unpack-side ones:
- Path length truncated to 1KB (matches unpack cap; long paths would
silently wrap u16 and corrupt the pack)
- Files >4GB on disk skipped with a zero-length entry rather than
silently producing a truncated dataSize that overflows uint32
Texture paths come from M2/WMO files which a malicious zone author
could craft to include '..' or absolute paths. Without this check,
exporting such a zone would write PNGs outside outputDir/textures/
and clobber sibling export files.
Three guards on quest objective loading from JSON:
- type out of QuestObjectiveType range (0..5) -> defaults to 0 (Kill)
- targetCount of 0 -> 1 (no-op objectives are nonsense)
- targetCount > 1000 -> 1000 (typo guard, biggest legit WoW quest is ~100)
- >10 objectives per quest -> dropped (matches SQL slot capacity, also
bounds per-quest memory)
Path length cap (1KB) — uint16 can hold 64KB but no real zone path
should exceed 256 chars. Path traversal check extended to also catch:
- Windows backslash absolute paths ('\' at start)
- Windows drive-prefixed paths ('C:\...')
A WCP downloaded from a forum and unpacked on Windows would otherwise
have these vectors open.
Three security/robustness guards on unpackZone:
1. fileCount > 1M or infoSize > 16MB rejected upfront — would OOM on
the next allocation.
2. Per-file dataSize > 256MB rejected — single malicious entry could
exhaust memory mid-extraction.
3. Path traversal ('..' or absolute paths) rejected — would write
outside destDir/<zoneName>/ and clobber system files.
WCPs are user-shareable archives, so a hostile pack downloaded from a
forum should not be able to OOM the editor or write to /etc.
NaN positions in waypoints would teleport the creature to chaos coords
mid-patrol. Cap wait time at 600000ms (10 min) — prevents obvious typos
(e.g. 24h = 86400000) from producing a creature that effectively never
moves.
Mirrors the recent scale guards. NaN/inf positions or rotations make
the M2 renderer's matrix math produce chaos-shaped instances or crash
during normal computation. Now both fields are reset to zero on load
when any component is non-finite.
Orientation values from edited JSON could be negative or wrap multiple
revolutions; now normalised once at load. Scale was already clamped
on small-positive but didn't reject NaN/inf — now rejects both with the
same defensive check object_placer just got.
Same defensive check the WoB doodad load just got — guards against
corrupted/partial-write JSON where scale ends up 0/NaN/inf. Without
this an invisible (scale=0) or crashed (NaN matrix) placement could
silently bork a loaded zone.
validateChains warnings were only going to the log file. Most users
don't tail the log while editing, so a broken chain (quest pointing at
a deleted nextQuestId, circular dependency) would only be discovered
when testing in-game. Now also shows a 5s toast with the issue count.
Quest links (questGiverNpcId, turnInNpcId, KillCreature targetName) all
key off CreatureSpawn.id, but loadFromFile always assigned new ids via
nextId(). So saving and reloading would silently break every quest hook
in the zone. Now JSON stores id, the loader reads it back when present
(legacy files fall back to nextId()), and idCounter_ is bumped past
loaded values to prevent future collisions. Same fix as the recent
PlacedObject.uniqueId one.
When a user picks a creature preset, faction stays 0 (server treats this
as 'use template') unless the user already typed a value. Now defaults
based on the preset category:
Critter -> 250 (critter, indifferent to all)
hostile preset -> 14 (monster, hostile to all)
friendly preset -> 35 (friendly to all)
Means picking 'Wolf' from the preset list immediately produces a hostile
NPC that actually attacks players in-game without further configuration.
Stationary and Patrol creatures should have wander_distance=0; only
Wander behaviour uses it. Previously the editor's wanderRadius template
default of 10.0 was being written for every spawn, making stationary
guards drift around in-game.
WHM: clarify chunkCount=256, vertsPerChunk=145 are fixed, baseHeight is
a float, heights are 145 floats. WOM: spell out boundRadius+min+max as
separate fields (was 'bounds(28)'), nameLen prefix, indices uint32, and
texPath length-prefixing. WOM2 trailing block reorganized into a single
list with bone+anim sequence shown as one block. Reading the spec now
yields a working parser without source-diving.
The previous WOB section was loose ('bounds(4)' was actually boundRadius;
group size annotations missed the prefixed string lengths). Updated to
show every byte: 4-byte field sizes, 12-byte vec3s, 2-byte length
prefixes, 48-byte interleaved vertices. Reverse-engineering a WOB from
the spec is now possible without reading the source.
Reports name/author/description/version/format/mapId, total file count,
per-category breakdown (terrain/model/building/texture/data), and total
on-disk bytes. Useful for inspecting third-party WCPs before importing
or for sanity-checking your own exports.
packZone stores files relative to the zone subdirectory (e.g. just
'MyZone_32_32.adt'), so unpacking to 'custom_zones/' produced files at
'custom_zones/MyZone_32_32.adt' — without the zone subdir the loader
expects. Now reads the info JSON to extract the zone name and unpacks
to 'custom_zones/<zoneName>/' so imported zones load correctly.
The CreatureSpawn struct default of 3.0 made every newly placed NPC
appear as an oversized 3x-scale creature, very obviously not what users
wanted. Existing JSON spawn files load their stored scale unchanged
(only impacts newly placed templates).
uniqueId was always regenerated on load, so re-saving the same zone
produced a different uniqueId per placement each time. Since
syncToTerrain copies obj.uniqueId into MDDF/MODF, the ADT also rotated
uniqueIds across cycles. Now JSON stores uniqueId, the loader reads it
back when present (falling back to nextUniqueId() for legacy files),
and uniqueIdCounter_ is bumped past any loaded value so future
placements never collide.
The respawnTimeMs field was loaded/saved/exported but never editable
through the UI. Added a DragFloat showing seconds (range 5-86400) in
both the template and the selected-NPC editors. SQL export already
divides by 1000 for AzerothCore's spawntimesecs column.
Last batch of stat fields missing from the selected-NPC editor. Now any
property a user could set on the template can also be edited on an
already-placed NPC, without removing and re-placing.
Adds Hostile/Quest/Vendor/Inn/Train/Bank/Auc/Repair/Flight checkboxes
to the selected-NPC editor matching the template editor's set. Lets
users toggle these on already-placed NPCs without removing and
re-placing them.
The CreatureSpawn struct has a faction field that was already exported
to creature_template.faction but wasn't editable. Added an InputInt with
a tooltip listing the common AzerothCore FactionTemplate IDs (Stormwind,
Monster, Beast, Friendly, Critter, etc.) so users can pick the right
hostility/disposition without referencing the DBC manually.
EditorViewport gains setActiveMapName() so rebuildObjects can pass
per-zone prefixes (output/<map>/models|buildings/, custom_zones/<map>/...)
to tryLoadByGamePath. EditorApp wires it from loadADT, loadWMOInstance,
and createNewTerrain. Now the editor's preview mirrors the main game's
priority: per-zone WOM/WOB beats global custom_zones/, beats game data.
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.
CreatureSpawn struct gains four AzerothCore-standard NPC flag bits:
trainer -> npcflag 0x10
repair -> npcflag 0x1000
banker -> npcflag 0x20000
auctioneer -> npcflag 0x200000
Saved/loaded via the JSON spawn file, exported to creature_template.npcflag,
exposed as checkboxes in the NPC template panel. Lets users build full
city NPCs (city auctioneer, weapon trainer, etc.) without dropping to SQL.
The editor's M2 placement path tries WOM (custom_zones/models/, output/
models/) before falling back to game M2 files. WMO placement just went
straight to game files. Now mirrors the M2 path: probes
custom_zones/buildings/ + output/buildings/ for a .wob, converts via
toWMOModel, falls back to MPQ-extracted WMO only on miss. Lets exported
zones render their custom buildings without needing the original WMO.
WMORenderer::createInstance accepts a scale parameter (default 1.0),
but the editor's call site ignored obj.scale. So a WMO sized to 2.0 in
the panel still rendered at 1.0. Now passes obj.scale through, so the
loaded MODF scale + any user gizmo scaling work end-to-end.
Helps users understand the search base — 'Pool: 1234 M2 56 WMO' tells
them why their filter might be matching too many results, and gives a
quick view into how much asset variety the loaded data has.
Two related fixes:
1. loadADT now resets zoneManifest_ at the top so the previous zone's
mapId/displayName/flags/audio don't bleed into the new export.
2. When loading a zone that has a previously-exported zone.json on disk,
call manifest.load() to restore the user-customized metadata. Without
this every reload would reset Map ID back to 9000 etc.
Two related zone-manifest bugs:
1. displayName was always overwritten with the .adt prefix on every
export, throwing away whatever the user typed in the Zone Metadata
panel.
2. tiles vector was push_back'd to without clearing, so re-exporting the
same zone would accumulate duplicate tile entries.
Both fixed by checking displayName.empty() before assignment and calling
tiles.clear() before the rebuild loop.
Users who set a custom Map ID via the Zone Metadata panel saw it ignored
when exporting the WCP — the pack info would always say mapId=9000.
Now reads from zoneManifest_, falling back to 9000 only when the field
is unset (0).
The MODF scale field (u16 / 1024 = float) is now propagated in both
directions: load reads wp.scale -> obj.scale, syncToTerrain converts
obj.scale * 1024 -> wp.scale (clamped to u16). Combined with the prior
loader/writer changes this means non-1.0 WMO scales (used by some
WotLK content) survive a save/reload cycle.
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.
The editor stores quest hooks (questGiverNpcId, turnInNpcId, KillCreature
targetName) as the spawner's per-spawn .id sequence. The SQL exporter
writes creature_template entries as 'creatureStartEntry + index'. The
two number spaces are different, so quest links pointed at non-existent
creature entries. Added a spawn.id -> SQL entry map built from the
spawns vector and used it in:
- RequiredNpcOrGo[1..4] for KillCreature objectives
- creature_queststarter / creature_questender
Template was a SliderFloat 0.5-10 while the selected-NPC editor uses
DragFloat 0.1-50. Inconsistent ceilings made it surprising that an NPC
could be scaled higher after placement than during placement. Now both
use DragFloat with the same range.
The new persistent path->modelId map keeps models alive across rebuilds,
which is great for the common case of moving an instance, but means
models that lost all references stay in GPU memory forever. Added a
30s timer that calls m2Renderer->cleanupUnusedModels(), which has its
own 60s grace period before actual eviction — so models stick around
~60-90s after their last instance is removed and then get freed.