Compare commits

..

1324 commits

Author SHA1 Message Date
Kelsi
3e260453a5 feat(editor): add --info-quests-by-level + --info-quests-by-xp analytics
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Quest-side analytics paralleling --info-creatures-by-faction/-level.
Two distribution views for difficulty-curve and reward-pacing analysis:

  wowee_editor --info-quests-by-level $Z/quests.json

  Quests by required level: ... (47 total)
    range : 1 to 60 (avg 22.4)

    level   count  bar
        1       8  ████████████████████████████████████████
        5       6  ██████████████████████████████
        ...
       60       1  █████

  wowee_editor --info-quests-by-xp $Z/quests.json

  Quests by XP reward: ... (47 total)
    range : 100 to 5000 (avg 1462, 0 with 0 XP)

    bucket (≥XP)   count  bar
               0       8  ████████████████████████████████████████
             250       6  ██████████████████████████████
             500       4  ████████████████████
            5000       1  █████
    (bucket size: 250 XP)

--by-level: catches difficulty-curve gaps (every quest level 1 → no
mid-game; cluster at 60 → no early game) and outliers (level-30
quest dropped into a starter zone).

--by-xp: bucket size auto-grows with the max XP value so the
histogram stays readable for both starter zones (10-100 XP per bin)
and endgame (5000+ XP per bin). Surfaces no-reward quests
explicitly so designers spot ones they forgot to fill in.

JSON modes emit per-bucket records for dashboards. Verified on a
4-quest seed (xp 100/250/500/5000): bucket-size correctly auto-
selected as 250 XP, range and avg match.
2026-05-06 18:20:37 -07:00
Kelsi
d12ea8e23e feat(editor): add --for-each-tile for per-tile batch operations
Per-tile batch runner. Pairs with --for-each-zone (project-level
tile iteration is too coarse for tile-level commands like
--build-woc, --validate-whm, --info-whm).

  wowee_editor --for-each-tile custom_zones/MyZone -- \
    wowee_editor --build-woc {}

  [custom_zones/MyZone/MyZone_30_30 (30, 30)]
  WOC built: custom_zones/MyZone/MyZone_30_30.woc (32768 triangles, ...)
  [custom_zones/MyZone/MyZone_31_30 (31, 30)]
  WOC built: custom_zones/MyZone/MyZone_31_30.woc (32768 triangles, ...)

  for-each-tile: 2 tiles, 0 failed

The {} substitution receives the tile-base path (zoneDir/mapName_TX_TY)
which is the form most tile-level commands accept. Sorts tiles by
(tx, ty) so output ordering is deterministic.

Use cases:
- Build WOC for every tile in one shot after editing terrain
- Validate WHM headers across all tiles
- Export per-tile previews via --export-whm-obj
- Per-tile data dump via --info-whm

Same shell-escaping + cmd-substitution machinery as --for-each-zone
(safe against names with spaces and quotes). Returns failed-run
count as exit code (capped at 255).

Verified on a 2-tile zone running --build-woc per tile: both
tiles built correctly, exit 0.
2026-05-06 18:11:54 -07:00
Kelsi
aba0a63dd0 feat(editor): add --bench-validate-project for validation timing analysis
Times --validate-project per zone. Useful for catching unusually
slow zones (huge WHM/WOC pairs, lots of WOM batches) and tracking
validation overhead growth across releases:

  wowee_editor --bench-validate-project custom_zones

  Bench validate: custom_zones
    zones    : 12
    total    : 4731.20 ms
    per zone : avg=394.27 min=87.42 max=2103.55 ms
    slowest  : Stranglethorn (2103.55 ms)

    Per-zone timings:
      zone                       ms       files  ms/file
      AshenForest                  87.42      8   10.928
      BoneOasis                   245.10     17   14.418
      Stranglethorn              2103.55     94   22.378
      ...

Reports total + avg/min/max per zone + slowest zone callout. Per-
zone table with ms/file ratio surfaces formats that scale poorly
('this zone has only 5 files but takes 2 seconds — one is a 100MB
WHM that's slow to load').

Useful for:
- Pre-commit profiling: 'did my validator change make Stranglethorn
  3× slower?'
- CI cycle time budgeting
- Catching pathological inputs (a 10000-creature creatures.json that
  blows up validation cost)

JSON mode emits per-zone records + aggregate stats for dashboards.
Verified on a 2-zone project: per-zone timings make sense (Forest
with .woc takes 5× longer than Desert without).
2026-05-06 18:03:55 -07:00
Kelsi
33b231b8a2 feat(editor): add --info-zone-density for spawn distribution analysis
Per-tile content density. Catches sparse zones (5 mobs across 16
tiles → boring) and over-stuffed ones (200 mobs in 1 tile → frame-
rate bomb). Reports overall averages plus per-tile bucket counts:

  wowee_editor --info-zone-density custom_zones/MyZone

  Zone density: custom_zones/MyZone
    tiles      : 4
    totals     : 47 creatures, 23 objects, 8 quests
    per-tile   : 11.75 creatures, 5.75 objects, 2.00 quests

    Per-tile breakdown:
      tile        creatures  objects
      (28, 30)            12        7
      (29, 30)            18        4
      (29, 31)             9        8
      (30, 30)             8        4

Spawn-to-tile bucketing reverses the WoW grid transform from world
position back to tile (tx, ty) coords. Out-of-zone spawns silently
drop (they show up in --check-zone-refs / --check-zone-content as
their own warning class).

Useful for difficulty-curve work ('how packed is this hub vs this
zone-edge?'), perf budgeting ('which tile do I need to lod-out
first?'), and content-pacing reviews ('is the early game too empty?').

JSON mode emits per-tile records for dashboards. Verified on a
1-tile mvp-zone: 1 creature + 1 object + 1 quest, all bucketed
into the correct tile (28, 30).
2026-05-06 17:55:15 -07:00
Kelsi
f4e8623d58 feat(editor): add --info-objects-by-path + --info-objects-by-type analytics
Object-side counterparts to --info-creatures-by-faction/-level. Two
analytics commands for placement audits:

  wowee_editor --info-objects-by-path $Z/objects.json

  Objects by path: ... (47 total, 12 unique)
    count   share   path
       18    38.3%  World/Generic/Tree.m2
        9    19.1%  World/Generic/Lamp.m2
        4     8.5%  World/Building/Inn.wmo
        ...

  wowee_editor --info-objects-by-type $Z/objects.json

  Objects by type: ...
    M2  : 38  (scale 0.80-2.50, avg 1.12)
    WMO : 9   (scale 1.00-1.00, avg 1.00)

--info-objects-by-path: most-used model paths first. Catches
'this looks repetitive, diversify the doodads' design feedback
and surfaces texture-budget hot spots (one model used 50× pulls
its textures into the working set 50×).

--info-objects-by-type: M2 vs WMO split + per-type scale stats.
Catches scale outliers ('this WMO is at 0.001 scale, did you mean
1.0?') and gives composition sense (mostly props vs mostly
buildings).

JSON modes emit per-path / per-type records for dashboards.
Verified on a 4-object seed (3 M2 + 1 WMO with mixed scales):
correctly reports Tree.m2 used 2× (50%), M2 scale range 0.80-1.50
avg 1.10.
2026-05-06 17:46:51 -07:00
Kelsi
1797ffd280 feat(editor): add --info-pack-budget for per-extension WCP byte breakdown
Where --info-wcp shows file counts per category, this drills into
per-extension byte costs so users can spot what's bloating an
archive before shipping:

  wowee_editor --info-pack-budget custom_zones/MyZone.wcp

  WCP budget: custom_zones/MyZone.wcp
    total: 47 file(s), 2.34 MB

    ext           count        bytes      KB    share
    .whm              4      1683456  1644.0   70.3%
    .wob              3       451200   440.6   18.8%
    .wom             12       163840   160.0    6.8%
    .json             8        85120    83.1    3.6%
    .woc              1        12672    12.4    0.5%

Sorted by bytes descending so the heaviest contributors surface
first. Useful for:
- Spotting accidental .glb/.obj inclusion in shipping packs
  (`--pack-wcp` should run after `--strip-zone` to keep
  derived outputs out)
- Capacity budgeting when targeting a max-pack-size
- Comparing pre/post compression ratios

Pairs with --info-wcp (counts), --list-wcp (full file list),
--diff-wcp (compare two packs), --info-pack-budget (this one,
byte costs).

Verified on a freshly-mvp-zone packed WCP: 6 files / 0.17 MB
correctly broken down (whm 84%, wot 14.9%, json 1.1%).
2026-05-06 17:38:28 -07:00
Kelsi
baf54d5e47 feat(editor): add --info-zone-water for water-layer aggregation
Aggregates water-layer stats across every tile in a zone. Useful for
confirming a 'lake zone' actually has water, budgeting water-heavy
zones, or auditing what liquid types appear (water vs ocean vs magma
vs slime affects gameplay rules):

  wowee_editor --info-zone-water custom_zones/Z

  Zone water: custom_zones/Z
    loaded tiles : 1
    water chunks : 0 (out of 256 possible)
    total layers : 0
    (no water in this zone)

  wowee_editor --info-zone-water custom_zones/Stranglethorn

  Zone water: custom_zones/Stranglethorn
    loaded tiles : 4
    water chunks : 387 (out of 1024 possible)
    total layers : 412
    height range : 12.40 to 18.50

    By liquid type:
      water (0): 380 layer(s)
      ocean (1): 28 layer(s)
      magma (2): 4 layer(s)

Per-chunk water can have multiple layers (different liquid types or
height regions overlapping). Liquid types: 0=water, 1=ocean, 2=magma,
3=slime — different gameplay rules apply (oceans are swimable, magma
is damage-over-time, etc.).

JSON mode emits per-type layer counts + height range for programmatic
audit. Verified on a freshly-scaffolded zone: correctly reports 0
water chunks.
2026-05-06 17:30:05 -07:00
Kelsi
8257ae72db feat(editor): add --validate-project for whole-project validation gate
Multi-zone wrapper around --validate-all. Walks every zone in a
project and runs the per-format validators (WOM/WOB/WOC/WHM).
Aggregates pass/fail per zone with file-level breakdown:

  wowee_editor --validate-project custom_zones

  validate-project: custom_zones
    zones        : 12 (1 failed)

    zone                       files  failed  errors  status
    AshenForest                    47       0       0  PASS
    BoneOasis                      31       0       0  PASS
    CrystalCaverns                 28       2      14  FAIL
    ...

    1 zone(s) failed validation

Designed for CI gates before --pack-wcp (or before tagging a release):
one command checks the whole project's binary integrity, exits 1 if
anything's broken with a clear breakdown of which zone went wrong.

Pairs with --validate-all (single-zone, all formats) and the
per-format validators (--validate-wom etc.). Three levels of
granularity now:
  --validate-{wom,wob,woc,whm}  single file
  --validate-all                single zone (or any dir)
  --validate-project            entire project

JSON mode emits per-zone records (totalFiles + failedFiles +
totalErrors) for dashboard consumption. Verified on a 2-zone
project with one tile having .woc built and one without — both
zones PASS, exit 0.
2026-05-06 17:21:57 -07:00
Kelsi
e0ed2ab58e feat(editor): add --info-creatures-by-faction + --info-creatures-by-level
Two analytics commands for combat-balance work. Where --info-creatures
gives totals + behavior counts, these give the distributions:

  wowee_editor --info-creatures-by-faction $Z/creatures.json

  Creatures by faction: ... (47 total)
    faction    count   share
          7        12    25.5%
         14        29    61.7%
         35         6    12.8%
    (factions: 7=human, 14=monster, 35=neutral, etc.)

  wowee_editor --info-creatures-by-level $Z/creatures.json

  Creatures by level: ... (47 total)
    range : 5 to 32 (avg 14.2)

    level   count  bar
        5       4  ████████████████████████████████████████
        6       3  ██████████████████████████████
        ...
       30       1  ██████████

Faction histogram catches single-faction zones (one giant melee) and
mixed-faction tuning issues. Level histogram catches difficulty-curve
problems (cluster at 5, gap, cluster at 30) and outlier spawns
(level-60 boss accidentally placed in starter area).

ASCII bar chart for level distribution since gameplay tuning is
visual — '60% of mobs are levels 8-12 with a long tail' is more
intuitive as a bar than as numbers. Bars scale to longest bin so
small zones still get usable visualization.

JSON mode emits per-faction / per-level records for dashboards.
Verified on a 4-creature seed (3×faction-14 + 1×faction-35; levels
7/8/12/30): faction percentages and level range/avg both correct.
2026-05-06 17:13:44 -07:00
Kelsi
1eb8232bb8 feat(editor): add --validate-cli-help self-check for documentation drift
Self-check that every flag in kArgRequired (the master list of
commands needing positional args) appears in the help text emitted
by printUsage. Catches drift where a handler+arg-check pair gets
added but the help line is forgotten:

  wowee_editor --validate-cli-help

  CLI help self-check
    kArgRequired entries : 126
    PASSED — every kArgRequired flag is documented

If it ever fails, the missing flags are listed:

    FAILED — 2 flag(s) missing from help text:
      - --new-thing-i-forgot-to-document
      - --another-undocumented-flag

CI integration: add this to the test target so a PR that adds a
new command without docs can't merge. The --info-cli-stats command
gives the surface-size view; this gives the surface-completeness
view.

Verified: ran on the current binary (126 kArgRequired entries),
PASSED — every flag is documented.
2026-05-06 17:04:57 -07:00
Kelsi
d048beaa1e feat(editor): add --info-project-tree for multi-zone project overview
Project-level tree view: every zone with quick counts + bake/viewer
artifact status. --info-zone-tree drills into one zone; this gives
the bird's-eye view across the whole project:

  wowee_editor --info-project-tree custom_zones

  custom_zones/  (2 zones, 2 tiles, 1 creatures, 1 objects, 1 quests)
  ├─ Empty/  (tiles=1, creat=0, obj=0, quest=0)
  │  ├─ name      : Empty
  │  ├─ mapName   : Empty
  │  ├─ artifacts : (none)
  │  └─ status    : empty (only terrain)
  └─ Forest/  (tiles=1, creat=1, obj=1, quest=1)
     ├─ name      : Forest
     ├─ mapName   : Forest
     ├─ artifacts : .glb
     └─ status    : populated

Per-zone summary line shows counts; sub-tree shows display name,
map slug, which derived artifacts have been baked (.glb / .obj /
.stl / .html / ZONE.md), and a populated/empty status.

Project-header line aggregates totals across all zones for the
'how big is my project?' answer in one glance.

Pairs with --info-tilemap (spatial coverage) and --zone-stats
(quantitative aggregate) — three different lenses on the project:
spatial, quantitative, and structural.
2026-05-06 16:57:26 -07:00
Kelsi
2152b230c8 feat(editor): add --export-project-md for GitHub-renderable project README
Markdown counterpart to --export-project-html. Generates a README.md
indexing every zone with counts + bake/viewer/doc artifact status.
GitHub renders it natively at the project root:

  wowee_editor --export-project-md custom_zones

# Wowee Project — Zone Index

*Auto-generated. 2 zone(s) discovered in `custom_zones`.*

## Summary

| Metric | Total |
|---|---:|
| Zones      | 2 |
| Tiles      | 2 |
| Creatures  | 2 |
| ...

## Zones

| Zone | Tiles | Creatures | Objects | Quests | Bake | Viewer | Docs |
|---|---:|---:|---:|---:|:---:|:---:|:---:|
| Desert | 1 | 1 | 1 | 1 | — | — | — |
| [Forest](Forest/ZONE.md) | 1 | 1 | 1 | 1 | ✓ | [view](Forest/Forest.html) | [md](Forest/ZONE.md) |

Per-zone row links to its ZONE.md (if --export-zone-summary-md was
run) and its HTML viewer (if --export-zone-html was run). The Bake
column shows ✓ if .glb exists. Status columns make it instantly
visible which zones are bake-ready vs documentation-only.

Pairs with --export-project-html (interactive viewer index) — same
data, different presentation: HTML for browsers, Markdown for
GitHub Pages READMEs and PR descriptions.

Verified on a 2-zone project where one zone had been baked +
viewer-exported + doc-exported and the other hadn't: README.md
correctly shows ✓/links for the baked zone, em-dashes for the
unbaked one.
2026-05-06 16:48:46 -07:00
Kelsi
f3130bbd3d feat(editor): add --info-zone-extents for spatial bounding box analysis
Computes the zone's spatial bounding box: XY world coords from
manifest tile coords (each tile is 533.33 yards), Z height range
across all loaded chunks. Useful for sizing camera frustums,
planning where new tiles can fit contiguously, or quick sanity
checks ('this zone is 4km across? something's wrong'):

  wowee_editor --info-zone-extents custom_zones/MyZone

  Zone extents: custom_zones/MyZone
    tile count   : 3 (3 loaded, 0 missing on disk)
    tile range   : x=[30, 31]  y=[30, 31]
    world box    : (0.0, 0.0, 98.5) - (1066.7, 1066.7, 101.5) yards
    size         : 1066.7 x 1066.7 x 3.0 yards (975m x 975m x 2.7m)

WoW grid math: tile (32, 32) is at world origin; +X tile = -X world
(north convention), +Y tile = -Y world (west convention). The
displayed world coords use the same transform the renderer uses
so they line up with --bake-zone-glb output bounds.

Per-axis size in yards + meters (0.9144 conversion) since some
designers think in metric, others in WoW-canonical yards.

Tracks loaded vs missing tiles in case the manifest references a
tile whose .whm got deleted — surfaces silently bad zones early.

JSON mode emits full bounding box + tile range + size for
programmatic consumption (camera autofit, layout planning).
2026-05-06 16:41:07 -07:00
Kelsi
9b362fc825 feat(editor): add --diff-checksum for SHA256SUMS comparison
Compares two SHA256SUMS files (from --export-zone-checksum). Reports
added / removed / changed entries between two zone snapshots — much
faster than walking the filesystem to recompute hashes of unchanged
content:

  wowee_editor --export-zone-checksum custom_zones/Z /tmp/before.sha256
  ... edits happen ...
  wowee_editor --export-zone-checksum custom_zones/Z /tmp/after.sha256
  wowee_editor --diff-checksum /tmp/before.sha256 /tmp/after.sha256

  Diff: /tmp/before.sha256 vs /tmp/after.sha256
    added   : 1
    removed : 0
    changed : 0
    +  creatures.json

Standard diff-style markers (+/-/~) for added/removed/changed,
sorted within each category alphabetically.

Use cases:
- Audit what an editing session actually touched (snapshot before,
  snapshot after, diff)
- Verify a zone bundle re-extracts identically (transfer integrity
  beyond the per-file PASS/FAIL of sha256sum -c)
- CI gate: fail build if a refactor touches files it shouldn't

Diff family for content/integrity formats:
  --diff-zone      unpacked zone dir vs zone dir (high-level)
  --diff-extract   per-extension counts in two extracts
  --diff-checksum  per-file hash diff between two snapshots  <- new

Verified: scaffold + export checksum, add creature, re-export,
diff correctly reports '+ creatures.json' (added) and exit 1.
2026-05-06 16:32:55 -07:00
Kelsi
dbf973e29e feat(editor): add --mvp-zone for one-command demo zone setup
Quick-start: scaffold a zone AND populate one of each content type
(1 creature, 1 object, 1 quest with objective + XP reward) in a
single command. Goes from empty filesystem to 'something to look at'
without 7 chained --add-* commands:

  wowee_editor --mvp-zone 'Demo Land' 30 30

  Created demo zone: custom_zones/Demo_Land
    tile     : (30, 30)
    contents : 1 creature, 1 object, 1 quest (with objective + reward)
    next     : wowee_editor --info-zone-tree custom_zones/Demo_Land

  Demo Land/
  ├─ Manifest ...
  ├─ Tiles (1) — (30, 30)
  ├─ Creatures (1) — lvl 5 Demo Wolf
  ├─ Objects (1) — m2 World/Generic/Tree.m2
  ├─ Quests (1) — [1] Welcome to Demo Land (lvl 1, 100 XP)
  │     └─ kill ×1 Demo Wolf

Demo content is positioned roughly at tile center (533.33-yard
intervals from origin tile 32/32). Quest references the demo
creature's auto-id so --check-zone-refs passes immediately.

Use cases:
- Smoke-testing the bake/validate pipeline
- Screenshot bait for docs / blog posts
- Editor onboarding (open a zone in the GUI to see the format)
- CI sanity check (does our editor still produce a viewable zone?)

Verified end-to-end: --mvp-zone 'Demo Land' → --info-zone-tree
shows all 4 sections populated correctly, file list matches
expected 6 files.
2026-05-06 16:25:23 -07:00
Kelsi
9c7b6aebfc feat(editor): add --info-quest-graph-stats for chain topology analysis
Where --export-quest-graph visualizes the quest dependency graph,
this quantifies it. Useful for spotting authoring issues (orphan
quests that only appear as one-offs, broken chains via cycles)
and getting a sense of quest density:

  wowee_editor --info-quest-graph-stats $Z/quests.json

  Quest graph: $Z/quests.json
    total quests : 4
    roots        : 2 (no inbound chain — entry points)
    leaves       : 2 (no outbound chain — terminal)
    orphans      : 1 (root AND leaf — one-shot)
    cycles       : 0
    max depth    : 3
    avg depth    : 2.00 (chain length per root)

Definitions:
  roots    = quests no other quest chains TO (player entry points)
  leaves   = quests with no nextQuestId or nextQuestId pointing to
             a missing quest (terminal — chain ends here)
  orphans  = root AND leaf (one-shot quests with no neighbors)
  cycles   = number of roots whose forward walk hits a node twice
  maxDepth = longest path from any root forward through the chain
  avgDepth = mean path length across all roots

Cycle-guarded forward walk uses a visited-set per root, so the
cycle count is bounded even on intentionally-broken inputs.

Exit 1 if cycles > 0 so CI can gate before shipping a broken
chain. JSON mode emits all six stats for dashboard consumption.

Verified on 4-quest zone (Q1→Q2→Q3 chain + Loner orphan):
correctly reports 2 roots, 2 leaves, 1 orphan, 0 cycles, max
depth 3, avg depth 2.00.
2026-05-06 16:17:30 -07:00
Kelsi
cc91a1146f feat(editor): add --bake-project-stl + --bake-project-glb completing the project trio
Three project-bake formats now match the three zone-bake formats —
full project terrain reachable from every universal-3D ecosystem:

  wowee_editor --bake-project-obj  custom_zones    # DCC tools
  wowee_editor --bake-project-stl  custom_zones    # 3D printing
  wowee_editor --bake-project-glb  custom_zones    # web viewers

Shared per-zone walking pass collects vertex+index pools per zone,
then the format-specific tail emits:
  STL → per-triangle 'facet normal'+'outer loop'+vertex×3
  GLB → packed BIN chunk + JSON describing per-zone meshes

GLB output gives one mesh+node per zone (named 'zone_NAME') so
viewers can toggle zones independently — same pattern as
--bake-zone-glb but at project scope. STL is single-solid since
slicers don't have a useful concept of multi-part STL.

Coords align across all three exporters and across zone vs project
scope, so:
- A zone .obj overlaid with its containing project .obj lines up
- A project .glb opened in three.js shows zones at the same coords
  the renderer uses

Verified on a 2-zone project (Forest + Desert):
- project.stl: 2 zones, 2 tiles, 65536 facets
- project.glb: 2 zones, 2 tiles, 41472 verts, 65536 tris, 1.78MB BIN
- --validate-glb on project.glb: PASSED

Bake granularity matrix complete:
                     OBJ              STL              GLB
  single model   --export-obj    --export-stl     --export-glb
  single zone    --bake-zone-obj --bake-zone-stl  --bake-zone-glb
  whole project  --bake-project-obj --bake-project-stl --bake-project-glb
2026-05-06 16:08:44 -07:00
Kelsi
54c309a779 feat(editor): add --bake-project-obj for whole-project terrain export
Project-level OBJ bake — combines every zone's terrain into one
giant OBJ with one 'g zone_NAME' block per zone. Useful for
previewing an entire multi-zone project's terrain in MeshLab/
Blender at once, or for printing the full map:

  wowee_editor --bake-project-obj custom_zones

  Baked custom_zones -> custom_zones/project.obj
    2 zone(s), 3 tiles, 62208 verts, 98304 tris

Layout: single global vertex pool (so OBJ indexing stays valid),
per-zone face groups so designers can hide individual zones in
their viewer for area-by-area inspection. Hole bits respected.
Coords match WoweeCollisionBuilder's outer-grid layout exactly so
zones spatially line up at WoW grid boundaries — adjacent tiles
across zones connect seamlessly.

Pairs with the existing --bake-zone-* family (single zone) and
--export-project-html (web index of per-zone viewers). Three
levels of granularity now available:
  --export-glb / --export-obj / --export-stl     single model/file
  --bake-zone-glb / -obj / -stl                  single zone
  --bake-project-obj                             entire project  <- new

Verified: 2-zone project (Forest 2 tiles + Desert 1 tile) baked
to project.obj with 62208 verts (3 × 20736), 98304 tris (3 ×
32768), 2 'g' blocks correctly named (zone_Desert, zone_Forest).
2026-05-06 15:59:51 -07:00
Kelsi
b628535a91 feat(editor): add --export-zone-checksum for SHA-256 integrity manifests
Emits a SHA-256 manifest of every source file in a zone in the
standard sha256sum format. Lets users verify zone integrity after
download/transfer using the standard system tool — no custom
verifier needed:

  wowee_editor --export-zone-checksum custom_zones/MyZone

  3298c35a...  Z_30_30.whm
  f81e3d37...  Z_30_30.wot
  6a49519f...  creatures.json
  4625e30b...  zone.json

  sha256sum -c custom_zones/MyZone/SHA256SUMS
  Z_30_30.whm: OK
  Z_30_30.wot: OK
  ...

Source-only by design — derived outputs (.glb/.obj/.stl/.html/.png/
ZONE.md/DEPS.md/quests.dot/SHA256SUMS itself/Makefile) are excluded
since they're regeneratable and would invalidate the checksum on
every rebuild.

Includes a self-contained 90-LoC SHA-256 (FIPS 180-4 / RFC 6234)
in an internal namespace — no OpenSSL/Crypto++ dependency added.
Streaming hash (16KB chunks) so it scales to giant terrain WHMs
without holding the whole file in memory.

Verified end-to-end: scaffolded zone with 1 creature → checksum
manifest of 4 source files (zone.json, creatures.json, .whm, .wot)
in standard format → sha256sum -c reports all 4 OK.
2026-05-06 15:51:50 -07:00
Kelsi
4668140eed feat(editor): add --info-cli-help for substring searching the help text
130+ commands. 'Is there a thing for X?' is a common question.
Scrolling --help to find out is slow. This searches:

  wowee_editor --info-cli-help quest

  --add-quest <zoneDir> <title> [giverId] [turnInId] [xp] [level]
                         Append one quest to <zoneDir>/quests.json and exit
  --add-quest-objective <zoneDir> <questIdx> <kill|collect|...> ...
                         Append one objective to a quest by index
  --remove-quest-objective <zoneDir> <questIdx> <objIdx>
                         Remove the objective at given 0-based index from a quest
  --clone-quest <zoneDir> <questIdx> [newTitle]
                         Duplicate a quest (with all objectives + rewards)
  ...

  32 line(s) matched 'quest'

Case-insensitive substring match. Continuation lines (the indented
description right after a flag) are emitted along with their flag
line for context. Match-count summary on stderr so it doesn't
contaminate piped output.

Pairs with --list-commands (full list) and --info-cli-stats (counts
by category). Three meta commands now cover the discovery loop:
'how many?' (--info-cli-stats), 'which ones?' (--list-commands),
'what does X do?' (--info-cli-help X).
2026-05-06 15:43:02 -07:00
Kelsi
f5cb2adbda feat(editor): add --gen-project-makefile for top-level multi-zone builds
Pairs with --gen-makefile (per-zone) — generates a project-level
Makefile that delegates to each zone's per-zone Makefile, enabling
parallel rebuilds across zones:

  wowee_editor --gen-project-makefile custom_zones
  make -C custom_zones -j$(nproc)        # all zones in parallel

  make Forest-bake                       # one zone
  make clean                             # strip every zone
  make validate                          # validate every zone
  make index                             # rebuild project HTML index
  make stats / tilemap                   # project-level reports

Per-zone targets (one set per zone):
  ZONE-bake     -> ensures ZONE/Makefile exists then `make -C ZONE all`
  ZONE-clean    -> --strip-zone ZONE
  ZONE-validate -> --validate-all ZONE

Auto-generates the per-zone Makefile if missing (so the project
Makefile bootstraps a fresh project without an extra setup step).

Top-level utility targets reuse the existing project-level commands:
- index    -> --export-project-html (HTML zone index)
- stats    -> --zone-stats (aggregate counts)
- tilemap  -> --info-tilemap (ADT grid visualization)

Verified: 2-zone project (Forest + Desert) generates a Makefile
with 6 zone-level targets (3 per zone) + 5 top-level targets, sorted
alphabetically by zone name.
2026-05-06 15:37:29 -07:00
Kelsi
dadefab64e feat(editor): add --check-zone-content for content data-quality validation
Sanity-checks creature/object/quest fields for plausible values.
Where --check-zone-refs catches dangling references, this catches
data-quality issues that pass technical validation but break
gameplay:

  wowee_editor --check-zone-content custom_zones/MyZone

  Zone content: custom_zones/MyZone
    creature warnings: 1
    object warnings  : 0
    quest warnings   : 2
    FAILED — 3 total warning(s):
      - creature[0] 'Wolf' has displayId=0 (will render invisibly)
      - quest[0] 'Hunt' has no objectives (uncompletable)
      - quest[0] 'Hunt' has no reward at all

Per-type checks:

Creatures:
  - empty name
  - 0 health (dies on spawn)
  - level 0
  - minDamage > maxDamage (broken combat math)
  - non-positive or non-finite scale
  - displayId=0 (invisible at runtime)

Objects:
  - empty path
  - non-positive or non-finite scale
  - non-finite position

Quests:
  - empty title
  - no objectives (player can never complete)
  - no reward at all (XP=0, items=[], coins all 0)
  - requiredLevel=0

Both --check-zone-refs (link integrity) and --check-zone-content
(data quality) needed — a quest can have valid NPC IDs (refs OK)
AND no objectives (content broken). Run both before --pack-wcp.

Verified end-to-end: zone with displayId=0 creature + objective-
less + rewardless quest reports 3 warnings; after fixing all three,
PASSED.
2026-05-06 15:31:12 -07:00
Kelsi
c9e8ad9930 feat(editor): add --diff-extract for asset-extract directory comparison
Compares two extracted asset directories side-by-side per file
extension. Useful for diffing a fresh asset_extract run against
a previous baseline (did the new MPQ add files? did any get
dropped?), or comparing what each WoW expansion contributes:

  wowee_editor --diff-extract baseline/ new/

  Diff: baseline/ vs new/
    totals: 4 files / 0.0 MB    vs    4 files / 0.0 MB

    Per-extension (count then bytes):
    ext            a count   b count    a bytes      b bytes  status
    .blp                 2         2           0            0
    .dbc                 1         0           0            0  -A
    .m2                  1         2           0            0  DIFF

    2 extension(s) differ

Status column flags imbalance:
  -A   only in A (extension dropped going B-ward)
  +B   only in B (extension added)
  DIFF count differs but both sides have some

Recursive walk so subdirectories aggregate into the parent's
extension counts. JSON mode emits per-extension {count,bytes}
pairs for both sides plus union diff count for CI consumption.

Diff family for directory-shaped formats:
  --diff-zone     unpacked zone dir vs zone dir
  --diff-extract  extracted asset dir vs extract dir  <- new

Verified on synthesized 4-file dirs (a: 2 blp + 1 dbc + 1 m2;
b: 2 blp + 0 dbc + 2 m2): correctly flags -A on .dbc, DIFF on
.m2, exit 1.
2026-05-06 15:25:18 -07:00
Kelsi
0f275cfee7 feat(editor): add --info-cli-stats meta command for surface tracking
130 commands and counting — surface inspection has graduated from
'nice to have' to 'necessary to plan'. This drops a per-category
breakdown:

  wowee_editor --info-cli-stats

  CLI surface stats
    total commands : 130
    longest flag   : 24 chars

    Categories (by verb prefix, sorted by count):
      --info           33
      --export         15
      --list           12
      --validate       11
      --diff            8
      --add             6
      --remove          5
      --convert         5
      --bake            3
      --migrate         3
      --clone           3
      ...

Pulls the command list via the same printUsage-parser as
--list-commands so it auto-tracks new flags. Buckets by the verb
prefix (text between '--' and the next '-') so '--info-zone-tree'
counts under 'info'. JSON mode emits the histogram as an object
for dashboard consumption.

Useful for spotting category imbalances ('we have 33 inspectors
but only 5 add commands — should consider more authoring CRUD?'),
tracking growth over releases, and planning where to invest.
2026-05-06 15:21:24 -07:00
Kelsi
1f20d0c5a2 feat(editor): add --gen-makefile for incremental zone-output rebuilds
Generates a Makefile that rebuilds every derived output for a zone
with proper dependency tracking. Designers can `make` to refresh
glb/obj/stl/html/csv/md from sources after editing creatures.json
or terrain — without remembering which wowee_editor flag does what,
and without rebuilding outputs that haven't changed:

  wowee_editor --gen-makefile custom_zones/MyZone
  cd custom_zones/MyZone && make

  make all       # rebuild everything that's stale
  make glb       # just the glTF bake
  make clean     # nuke derived outputs (calls --strip-zone)
  make validate  # run --validate-all on the zone

Targets generated:
  glb obj stl html docs csv graph all clean validate

Dependency tracking:
  - terrain bakes (.glb/.obj/.stl) depend on zone.json + WHM tiles
  - HTML viewer depends on the .glb (forces glb rebuild first)
  - docs (ZONE.md/DEPS.md) depend on content JSONs
  - csv/graph use '-' prefix so missing-content failures don't
    block 'make all' (zone with no quests still bakes terrain)

Uses /proc/self/exe absolute path so the Makefile works from any
cwd (run via `make -C custom_zones/MyZone` from anywhere). Falls
back to PATH lookup if /proc not available.

Verified end-to-end: scaffolded zone, generated Makefile, ran
`make all` from inside the zone dir — all derived outputs (.glb,
.obj, .stl, .html, ZONE.md, DEPS.md) generated; csv+graph
gracefully skipped due to no content; make exit 0.
2026-05-06 15:17:53 -07:00
Kelsi
5ebd04a953 feat(editor): add --info-extract-tree for hierarchical extract-dir overview
After asset_extract finishes, '142k files across 17 dirs' is hard
to reason about. This groups them visually by top-level subdir +
file format with byte totals — instant orientation:

  wowee_editor --info-extract-tree Data

  Data/  (13 dirs, 284612 files, 31451.1 MB)
  ├─ expansions/  (85141 files, 12871.2 MB)
  │  ├─ .adt         5469 files  6226626.7 KB
  │  ├─ .wav        19517 files  3567936.9 KB
  │  ├─ .m2         26354 files  1477702.1 KB
  │  ├─ .wmo         5195 files   744940.1 KB
  │  ├─ .blp        26911 files   691248.5 KB
  │  ├─ .dbc          282 files    92373.6 KB
  │  ├─ ...
  ├─ terrain/    (...)
  ├─ character/  (...)

Top-level dirs sorted by total bytes descending (heaviest first
so the high-impact directories surface immediately). Per-dir
extension breakdown also sorted by bytes. Walks recursively into
each top-level dir so 'expansions/wotlk/world/...' rolls up into
the expansions/ row.

Companion to --info-extract (counts + sidecar coverage) — that
one's wide-format JSON-friendly; this one's a tall tree-style
quick-look. Verified on a real 31GB extract of Data/ — output
makes the size distribution immediately clear (ADT files are
6GB of 13GB total for expansions/, etc.).
2026-05-06 15:10:45 -07:00
Kelsi
d3b7a085c2 feat(editor): add --validate-blp for proprietary BLP structural check
Companion to --validate-png — validates BLP textures without paying
the DXT decompress cost. Useful for spot-checking thousands of BLPs
in an extract dir:

  wowee_editor --validate-blp Texture.blp

  BLP: Texture.blp
    magic      : BLP2
    size       : 128 x 128
    valid mips : 8
    file bytes : 23044
    PASSED

Checks (header-only, no pixel decode):
- 4-byte magic is 'BLP1' or 'BLP2'
- Width/height non-zero and within 8192 (texture exporter cap)
- mipOffsets[16] / mipSizes[16] tables: each non-zero pair refers
  to a byte range within the file; mismatched (off=0 with size!=0
  or vice versa) flagged
- Stops at first zero offset (BLP convention for unused slots)

Per-version layout differences (BLP1 has compression+alphaBits as
uint32; BLP2 has compression/alphaDepth/alphaEncoding/hasMips as
uint8) handled inline.

Verified on real BLP (pvp-banner-emblem-1.blp): 8 valid mips,
PASSED. Non-BLP file (manifest.json starting with '{'): correctly
flags 'magic is {' error and exits 1.

Format-validator lineup is now exhaustive across both proprietary
and open formats:
  Proprietary: BLP / DBC (via --convert-dbc-json round-trip)
  Open binary: WOM / WOB / WOC / WHM / GLB
  Open text:   JSON DBC / STL / PNG (BLP sidecar)
2026-05-06 15:07:46 -07:00
Kelsi
a98e6e79c4 feat(editor): add --export-project-html for multi-zone web index
Generates an index.html linking to every zone's HTML viewer in a
project. Pairs with --export-zone-html (per-zone) and --bake-zone-glb
(terrain bake). Designed for github-pages / static-hosting style
'all my zones' showcase:

  wowee_editor --export-project-html custom_zones
  # -> custom_zones/index.html

Each zone gets a card showing:
- Display name (or mapName fallback)
- Counts: tiles · creatures · objects · quests (singular/plural
  agreement)
- 'Open viewer →' link if HTML exists
- Helpful nudge if HTML missing ('No HTML viewer (run --export-zone-html)')
- Or 'No .glb (run --bake-zone-glb)' if not even baked yet

Self-contained CSS (dark theme matching --export-zone-html), no
external dependencies. Responsive grid layout (300px-min cards
auto-flowing across viewport).

Verified on a 2-zone project (Forest with .glb+.html + Desert
without): index lists both, Forest gets a working link, Desert
gets the 'run --bake-zone-glb' hint.
2026-05-06 15:04:43 -07:00
Kelsi
f73b377b4d feat(editor): add --info-tilemap for project-wide ADT grid visualization
Renders the WoW 64x64 ADT coordinate grid as ASCII art showing which
tiles are claimed by which zones. Useful for spotting tile-coord
collisions before two zones ship overlapping content, and for
'where am I working?' overview of multi-zone projects:

  wowee_editor --info-tilemap custom_zones

  Tilemap: custom_zones
    zones      : 2
    tiles used : 5
    collisions : 0 (multiple zones claiming same tile)
    legend     : D=Desert F=Forest

         0         1         2         3         4         5         6
         0123456789012345678901234567890123456789012345678901234567890123
    y=30 ..............................FFDD..............................
    y=31 ...............................F................................

Glyphs are first-letter-of-zone uppercased; collision tiles render
as '*'. Empty rows are skipped to keep output bounded for projects
in one corner of the map (skipping all 60+ blank rows of the full
64x64 grid).

Exit 1 on collisions so CI can gate before merging two PRs that
both add tiles in the same area. JSON mode emits per-tile claims
for programmatic consumption.

Verified on a 2-zone project: Forest (3 tiles) + Desert (2 tiles)
correctly rendered, no collisions detected, glyphs F/D placed at
the right grid coords.
2026-05-06 15:01:32 -07:00
Kelsi
1718c2333f feat(editor): add --repair-zone to auto-fix manifest/disk drift
When a zone is hand-edited, partially copied, or modified by tools
that don't re-write zone.json, the manifest can fall out of sync
with the on-disk reality. --repair-zone reconciles them:

  wowee_editor --repair-zone custom_zones/MyZone --dry-run

  would add tile (31, 30) to manifest
  would set hasCreatures: false -> true

  repair-zone: custom_zones/MyZone (dry-run)
    fixes    : 2
    warnings : 0 (manual decision needed)
    re-run without --dry-run to apply

Auto-fixes:
- WHM files on disk matching <mapName>_TX_TY.whm pattern but not
  in manifest tiles[] -> add to tiles
- hasCreatures flag mismatched against actual creatures.json
  presence + non-empty -> sync

Warns (no auto-fix — needs manual decision):
- Tiles in manifest but no .whm on disk (could be in-progress
  work or genuinely deleted; user decides)

--dry-run flag previews changes. Pairs with --strip-zone (cleanup
derived) and --validate (open-format coverage) for the trio of
zone-health-maintenance commands.

Verified: scaffolded zone, hand-copied an extra .whm/.wot pair to
simulate disk-without-manifest drift, added a creature then flipped
hasCreatures=false in zone.json. --repair-zone correctly identifies
2 fixes, dry-run lists them, real run applies them, manifest now
shows correct tiles array + hasCreatures=true.
2026-05-06 14:58:08 -07:00
Kelsi
b82f9827d4 feat(editor): add --info-zone-bytes for per-file size breakdown
Drills into one zone's contents with categorized + sorted file
sizes. --zone-stats aggregates across multiple zones; this answers
'which file is 80% of THIS zone?' and 'how much would --strip-zone
free?':

  wowee_editor --info-zone-bytes custom_zones/MyZone

  Zone bytes: custom_zones/MyZone
    total: 2282178 bytes (2228.7 KB) across 6 file(s)

    Per-file (largest first):
    path                                bytes  category
    Z_30_30.woc                       1212456  terrain
    Z.glb                              891736  3D export (derived)
    Z_30_30.whm                        150540  terrain
    Z_30_30.wot                         26680  terrain
    zone.json                              446  json (source)
    quests.json                            320  json (source)

    Per-category:
    3D export (derived)    1 files    891736 bytes  ( 39.1%)
    json (source)          2 files       766 bytes  (  0.0%)
    terrain                3 files   1389676 bytes  ( 60.9%)

Categories: terrain / model (open|proprietary) / building (open|
proprietary) / texture (open|proprietary) / DBC / json (source) /
3D export (derived) / doc (derived) / other.

Source vs derived split surfaces what --strip-zone would clean up
(any 'derived' category) so capacity planning shows both 'what's
mine' (source) and 'what's regeneratable' (derived).

Recursive walk so subdirs (data/) are included with relative paths.
JSON mode emits per-file records + per-category aggregate for
programmatic consumption.
2026-05-06 14:54:29 -07:00
Kelsi
7dfc0b6333 feat(editor): add --strip-zone for derived-output cleanup
Removes the derived outputs (everything --bake/--export-* generates)
leaving only source files in a zone directory:

  wowee_editor --strip-zone custom_zones/MyZone

  removed: MyZone.obj (1099177 bytes)
  removed: MyZone.glb (891736 bytes)
  removed: DEPS.md (357 bytes)
  removed: ZONE.md (534 bytes)
  removed: quests.dot (198 bytes)

  strip-zone: custom_zones/MyZone
    removed  : 5 file(s)
    freed    : 1945.3 KB

What gets deleted:
- .glb / .obj / .stl  (3D format exports)
- .html               (browser viewer)
- .dot                (Graphviz quest graph)
- .csv                (spreadsheet exports)
- ZONE.md / DEPS.md   (markdown documentation)
- .png at zone root   (heightmap previews — NOT inside data/, those
                       are source BLP→PNG sidecars)

What stays:
- zone.json + creatures.json + objects.json + quests.json
- *.whm / *.wot / *.woc (terrain + collision)
- *.wom / *.wob         (open binary models/buildings)
- data/*.json           (DBC sidecars — source, not derived)

Top-level only — does not recurse into subdirectories so source
sidecars under data/ are untouched.

--dry-run flag previews what would be removed without deleting.
Useful before committing to git so derived blobs don't bloat
history, or before --pack-wcp so the archive doesn't carry
redundant exports.

Verified: scaffolded zone, generated 5 derived files (glb/obj/
ZONE.md/DEPS.md/quests.dot), --dry-run lists all 5 with sizes,
real run deletes them and frees 1945 KB. Source files (whm/wot/
woc/zone.json/quests.json) all preserved.
2026-05-06 14:51:36 -07:00
Kelsi
8f5a3b3d95 feat(editor): add --info-glb-tree for hierarchical glTF structure view
--info-glb gives counts; --info-glb-tree shows the actual scene ->
node -> mesh -> primitive hierarchy with names and accessor refs.
Useful when debugging 'why is this imported model showing up empty
in three.js?' (often the scene's nodes[] points to the wrong node):

  wowee_editor --info-glb-tree Z.glb

  Z.glb
  ├─ asset (v2.0, wowee_editor --bake-zone-glb)
  ├─ buffers (1)
  │  └─ [0] 1781760 bytes
  ├─ bufferViews (3)
  │  ├─ [0] off=0 len=497664 (vertex)
  │  ├─ [1] off=497664 len=497664 (vertex)
  │  └─ [2] off=995328 len=786432 (index)
  ├─ accessors (4)
  │  ├─ [0] f32 VEC3 ×41472 (bv=0)
  │  ├─ [1] f32 VEC3 ×41472 (bv=1)
  │  ├─ [2] u32 SCALAR ×98304 (bv=2)
  │  └─ [3] u32 SCALAR ×98304 (bv=2)
  ├─ meshes (2)
  │  ├─ [0] (1 primitives)
  │  │  └─ [0] TRIANGLES indices=acc#2
  │  └─ [1] (1 primitives)
  │     └─ [0] TRIANGLES indices=acc#3
  ├─ nodes (2)
  │  ├─ [0] tile_30_30 -> mesh#0
  │  └─ [1] tile_31_30 -> mesh#1
  └─ scenes (1, default=0)
     └─ [0] nodes=[0,1] (2 nodes)

Decodes glTF componentTypes (5120-5126 -> i8/u8/i16/u16/u32/f32),
bufferView targets (34962=vertex, 34963=index), primitive modes
(0=POINTS / 1=LINES / 4=TRIANGLES). Node sub-line shows mesh
reference so the scene-graph wiring is visible at a glance.

Pairs with --info-zone-tree (zone content tree) — both use the
same UTF-8 box-drawing pattern for visual consistency.
2026-05-06 14:48:37 -07:00
Kelsi
2904fa0560 feat(editor): add --export-zone-deps-md for shareable dependency tables
Markdown counterpart to --list-zone-deps. PR reviewers see at a
glance whether every referenced model exists in either open or
proprietary form across the conventional asset roots:

  wowee_editor --export-zone-deps-md custom_zones/MyZone
  # -> custom_zones/MyZone/DEPS.md

# Dependencies — MyZone

*Auto-generated. Status is best-effort — checks zone-local, output/,
custom_zones/, Data/ roots in that order.*

## Direct M2 placements (12)

| Refs | Path | Status |
|---:|---|---|
| 8 | `World/Tree.m2`        | open + proprietary |
| 3 | `World/Lamp.m2`        | open only |
| 1 | `World/Banner.m2`      | MISSING |

Status column resolves each path against zone-local + output/ +
custom_zones/ + Data/ roots, trying both .wob/.wmo for buildings
and .wom/.m2 for models. Catches missing assets BEFORE pack-wcp
would silently include broken refs.

GitHub-Flavored Markdown — sortable by Refs column once rendered,
backtick-wrapped paths so URLs/spaces don't confuse the viewer.

Verified: scaffolded zone with 2 M2 placements (one duplicated) +
1 WMO placement → DEPS.md has 3 sections (one per category) with
correct ref counts (Tree.m2 ×2) and MISSING status for paths that
don't resolve in any root.
2026-05-06 14:44:53 -07:00
Kelsi
84f897f0ec feat(editor): add --diff-jsondbc completing the diff family for the last text format
Diff family is now exhaustively complete across every shippable
format:

  --diff-wcp       archive
  --diff-zone      unpacked zone dir
  --diff-glb       glTF binary
  --diff-wom       WOM model
  --diff-wob       WOB building
  --diff-whm       WHM/WOT terrain pair
  --diff-woc       WOC collision
  --diff-jsondbc   JSON DBC sidecar  <- new

Schema-level compare:
  - format tag
  - source filename
  - recordCount + fieldCount (header values)
  - actualRecs (records[] array length)

Useful for catching schema regressions when a sidecar is regenerated
by a different tool version, or for verifying a --migrate-jsondbc
pass actually changed what it claimed.

Verified: same file vs itself reports IDENTICAL exit 0;
2-record vs 3-record (with same format/source/fieldCount) reports
2 DIFFs (recordCount + actualRecs) with exit 1.
2026-05-06 14:41:32 -07:00
Kelsi
e531901de8 feat(editor): add --validate-png for full PNG structural + CRC validation
Goes beyond --info-png (header sniff only) to validate every chunk
and verify CRC32. Catches corruption that browsers silently skip:

  wowee_editor --validate-png Texture.png

  PNG: Texture.png
    size       : 256 x 256
    bit depth  : 8 (color type 6)
    chunks     : 3 (0 CRC mismatches)
    file bytes : 142336
    PASSED

  wowee_editor --validate-png corrupted.png
    chunks     : 1 (1 CRC mismatches)
    FAILED — 4 error(s):
      - chunk 'IHDR' at offset 8: CRC mismatch (stored=0xC33E61CB
        actual=0x8A716D80)
      - missing required IDAT chunk
      - missing required IEND chunk

Checks:
- 8-byte PNG signature (89 50 4E 47 0D 0A 1A 0A)
- Per-chunk length doesn't exceed file
- CRC32 of (chunk type + data) matches stored CRC (PNG spec)
- IHDR is the first chunk
- IHDR / IDAT / IEND all present
- No trailing bytes after IEND

CRC32 implementation uses the standard polynomial 0xEDB88320 with a
runtime-built lookup table — no zlib dependency since we already
have to compute it ourselves anyway.

Verified on real PNG (BLP→PNG conversion of pvp-banner emblem):
PASSED. Hand-corrupted IHDR byte: correctly flags the CRC mismatch
with stored vs actual hex values + cascading missing-chunk errors.
Format-validator lineup is now exhaustive across both proprietary
and open formats:
  Open binary: WOM / WOB / WOC / WHM / GLB
  Open text:   JSON DBC / STL
  Sidecar:     PNG (proprietary BLP -> PNG bridge)
2026-05-06 14:38:17 -07:00
Kelsi
f8f5735d9b feat(editor): add --diff-whm and --diff-woc completing the open-format diff suite
Last two missing entries in the diff family — terrain heightmap pairs
and collision meshes:

  wowee_editor --diff-whm Z1/Z1_30_30 Z2/Z2_31_30
  Diff: ...
                         a              b
    tile         : (  30,  30)    (  31,  30)  DIFF
    loadedChunks :          256          256
    doodadPlace  :            0            0
    wmoPlace     :            0            0
    heightRange  : [-1.50,1.50]  [-1.50,1.50]

  wowee_editor --diff-woc tile_a.woc tile_b.woc
  Diff: ...
                         a              b
    tile        : (  30,  30)    (  31,  30)  DIFF
    triangles   :        32768        32768
    walkable    :        32768        32768
    steep       :            0            0

--diff-whm: tile coord, loaded chunk count, doodad/WMO placement
counts, texture/name table sizes, height range (min/max with float
epsilon). Pointwise height compare intentionally not done — float
perturbation from format round-trips would false-flag.

--diff-woc: tile coord, total triangles, walkable + steep counts.
Catches whether a --regen-collision pass actually changed something.

Diff family is now exhaustively complete for every shippable open
format:
  --diff-wcp    archive vs archive
  --diff-zone   unpacked zone dir vs zone dir
  --diff-glb    glTF binary vs glTF binary
  --diff-wom    WOM model vs WOM model
  --diff-wob    WOB building vs WOB building
  --diff-whm    WHM/WOT terrain pair vs pair
  --diff-woc    WOC collision vs collision

Verified: tile (30,30) vs (31,30) reports tile DIFF + identical
counts (since both are flat scaffolds); same-vs-self reports
IDENTICAL with exit 0.
2026-05-06 14:35:03 -07:00
Kelsi
cc3e85be5a feat(editor): add --migrate-jsondbc to auto-fix common JSON DBC issues
Designers receive JSON DBCs from various tools (older asset_extract
versions, third-party converters) that may omit fields the runtime
now expects. --validate-jsondbc tells you what's wrong; this fixes
the auto-fixable ones:

  wowee_editor --migrate-jsondbc db/Spell.json

  added: format = 'wowee-dbc-json-1.0'
  added: source = 'Spell.dbc'
  fixed: recordCount 99 -> 47882 (matches actual)
  inferred: fieldCount = 234 (from first row)
  Migrated db/Spell.json -> db/Spell.json
    fixes applied: 4

Auto-fixes:
- Missing 'format' tag → add 'wowee-dbc-json-1.0'
- Missing 'source' field → derive from filename stem + '.dbc'
- Missing 'fieldCount' → infer from first row
- recordCount mismatch → recompute from actual records[] length

NOT auto-fixed (data loss risk — surfaced as warnings instead):
- Wrong-width rows (silently padding/truncating could mangle field
  values; the user needs to inspect and decide)

In-place by default (writes back to the input path); accepts an
optional output path for non-destructive migration.

Verified: a JSON missing format/source/fieldCount with mismatched
recordCount=99 (actual 2) → migrate applies 4 fixes →
--validate-jsondbc reports PASSED on the result.
2026-05-06 14:31:35 -07:00
Kelsi
84902316e2 feat(editor): add --export-zone-html for browser-viewable zone preview
Generates a single-file HTML viewer next to the zone .glb. Anyone
with a modern browser can open it — no installs, no Blender, no
node_modules:

  wowee_editor --bake-zone-glb custom_zones/MyZone     # bakes .glb
  wowee_editor --export-zone-html custom_zones/MyZone  # writes .html
  open custom_zones/MyZone/MyZone.html                 # any browser

Uses Google's <model-viewer> web component (loaded from unpkg with
^4.0.0 version pin so a unpkg 'latest' bump can't silently break
older HTML files). The HTML itself is ~1.1KB; the .glb sits beside
it for the viewer to load via relative URL.

Features baked into the page:
- Camera controls (orbit, zoom, pan)
- Auto-rotate at 15deg/sec for the headless preview case
- Shadow casting + neutral environment IBL for non-flat lighting
- Header strip showing display name, map slug, tile count, mapId

Refuses to run if the zone .glb doesn't exist yet (clear error
message points the user at --bake-zone-glb).

Verified: scaffolded zone -> --bake-zone-glb -> --export-zone-html.
1.1KB HTML opens cleanly in browsers, references the sibling .glb.
Missing-glb case correctly errors with exit 1 + helpful next-step
hint.
2026-05-06 14:28:42 -07:00
Kelsi
d618d6a517 feat(editor): add --diff-wob completing the binary-format diff suite
Companion to --diff-wom for buildings. Same count-based shape so
round-trips through OBJ/glTF can be validated without false
positives from float perturbation:

  wowee_editor --diff-wob orig back

  Diff: orig.wob vs back.wob
                         a              b
    groups      :            5            5
    portals     :            3            3
    doodads     :           12           12
    materials   :            8            8
    groupTex    :            7            7
    totalVerts  :         4609         4609
    totalIdx    :        10950        10950
    name        : Stormwind     Stormwind
    boundRadius : match
    IDENTICAL

Compares: groups, portals, doodads, aggregated materials count
(per-group materials summed), aggregated group-texture count,
total verts/indices across all groups, name, boundRadius (with
0.01 epsilon).

Diff family is now complete across every binary format that ships
in a content pack:
  --diff-wcp    archive vs archive
  --diff-zone   unpacked zone dir vs zone dir
  --diff-glb    glTF binary vs glTF binary
  --diff-wom    WOM model vs WOM model
  --diff-wob    WOB building vs WOB building

Verified: identical pair reports IDENTICAL exit 0; pair with extra
group + extra doodad + name change reports 6 DIFFs with exit 1.
2026-05-06 14:25:40 -07:00
Kelsi
a01b5e5e89 feat(editor): add --info-object completing the single-entity inspector trio
Symmetric to --info-creature and --info-quest. Every PlacedObject
field for one entry:

  wowee_editor --info-object $Z/objects.json 3

  Object [3]
    type      : wmo
    path      : World/StaticObject/Stormwind/HumanInn.wmo
    nameId    : 3497721408
    uniqueId  : 4
    position  : (-8412.300, 633.200, 110.500)
    rotation  : (0.00, 90.00, 0.00) deg
    scale     : 1.000

Useful when debugging a misplaced object — quickly see the exact
position/rotation/scale + uniqueId + nameId without having to
grep through objects.json or run --list-objects through awk.

JSON mode emits the structured record. Inspector lineup is now
symmetric across all three content types:
  --info-{creatures,objects,quests}    aggregate counts
  --list-{creatures,objects,quests}    per-entry table
  --info-{creature,object,quest}       single-entry deep dive
2026-05-06 14:22:08 -07:00
Kelsi
b7696d1aa9 feat(editor): add --info-creature and --info-quest single-entity inspectors
The --list-* commands give table views; the --info-* (creatures/objects/
quests) give summary counts. Neither shows every field of a single
entry. These fill that gap:

  wowee_editor --info-creature $Z/creatures.json 0

  Creature [0] 'Captain'
    id            : 1
    displayId     : 11430
    position      : (100.00, 200.00, 50.00)
    orientation   : 0.00 deg
    scale         : 1.00
    level         : 12
    health/mana   : 100 / 0
    damage        : 5-10
    armor         : 0
    faction       : 0
    behavior      : stationary
    wander rad    : 10.0
    aggro rad     : 20.0
    leash rad     : 40.0
    respawn ms    : 300000
    patrol points : 0
    flags         :

  wowee_editor --info-quest $Z/quests.json 0

  Quest [0] 'Hunt Wolves'
    id              : 1
    required level  : 5
    giver NPC id    : 100
    turn-in NPC id  : 100
    next quest id   : 0 (terminal)
    reward          : 250 XP, 0g 0s 0c, 1 item(s)
      item[0]      : Item:Sword
    objectives      : 1
      [0] kill    ×5  Wolf  — Slay 5 Wolf

Useful for digging into 'why is this NPC not behaving like I expect?'
or reviewing one quest's full design in one screen instead of running
3-4 list-* commands. JSON mode emits every field as a structured
record for programmatic consumption.

Inspector lineup is now complete from aggregate to per-entry:
  --info-{creatures,objects,quests}      (counts)
  --list-{creatures,objects,quests}      (table)
  --info-{creature,quest}                (single entry, all fields)
2026-05-06 14:19:17 -07:00
Kelsi
17b83858c1 feat(editor): add --export-zone-csv for spreadsheet design workflows
Designers often prefer spreadsheets over JSON for read-only analysis
('which 5 quests give the most XP?', 'how many lvl 10+ creatures?',
'pivot table by faction'). This emits creatures.csv / objects.csv /
quests.csv in standard CSV that LibreOffice / Excel / Numbers / Python
pandas all consume natively:

  wowee_editor --export-zone-csv custom_zones/MyZone

  wrote custom_zones/MyZone/creatures.csv (47 rows)
  wrote custom_zones/MyZone/objects.csv (12 rows)
  wrote custom_zones/MyZone/quests.csv (8 rows)

CSV columns chosen to be designer-actionable:
- creatures: index/id/name/displayId/level/health/mana/faction/
  position/orientation/scale + hostile/questgiver/vendor/trainer flags
- objects: index/type/path/position/rotation/scale
- quests: index/id/title/requiredLevel/giver/turnIn/reward fields/
  objectiveCount + objectives/itemRewards joined by '; ' for keep-
  one-row-per-quest sortability

RFC 4180 quoting: fields with comma/quote/newline get wrapped in
double quotes with internal quotes doubled. Verified on a creature
named 'Big, Bad Bear' — comes out as '"Big, Bad Bear"'.

Round-trip back into the editor isn't supported yet (would need
schema-aware CSV parsing); this is read-only-export for now.
2026-05-06 14:16:00 -07:00
Kelsi
8dc91adc52 feat(editor): add --diff-wom completing the diff suite for models
Structural compare of two WOM models. Useful for verifying that
--migrate-wom or a round-trip through OBJ/glTF/STL preserved the
right counts:

  wowee_editor --diff-wom orig back

  Diff: orig.wom vs back.wom
                         a              b
    version     :            1            1
    vertices    :            5            5
    indices     :           18           18
    triangles   :            6            6
    textures    :            0            0
    bones       :            0            0
    animations  :            0            0
    batches     :            0            0
    name        : Pyramid       Pyramid
    bounds      : match
    IDENTICAL

Compares sizes only (vertex / index / bone / animation / batch /
texture counts) plus name and bounds. Bounds match-with-epsilon
(0.01 unit slop, generous since positions are typically yards) so
text-format round-trips that perturb the last bit don't false-flag.
Pointwise vertex compare is intentionally not done — it would be
O(n²) and brittle to tiny float diffs from format conversions.

Diff family is now complete:
  --diff-wcp    (archive vs archive)
  --diff-zone   (unpacked zone dir vs zone dir)
  --diff-glb    (glTF binary vs glTF binary)
  --diff-wom    (WOM model vs WOM model)

Verified: identical pair reports IDENTICAL exit 0; pair with 1 extra
vertex + extra triangle + name change correctly reports 4 DIFFs
(verts/indices/triangles/name) with exit 1.
2026-05-06 14:13:07 -07:00
Kelsi
468a1b8ede feat(editor): add --validate-stl for STL structural sanity check
Pairs with --export-stl / --import-stl / --bake-zone-stl. Catches
the corruption modes that crash slicer mesh analyzers:

  wowee_editor --validate-stl Tree.stl

  STL: Tree.stl
    solid name : Tree
    facets     : 6
    vertices   : 18
    PASSED

  wowee_editor --validate-stl truncated.stl

  STL: truncated.stl
    solid name : Truncated
    facets     : 1
    vertices   : 2
    FAILED — 3 error(s):
      - missing 'endsolid' footer
      - 1 unclosed 'facet' (missing 'endfacet')
      - vertex count 2 != 3 * facet count 1

Checks:
- 'solid' header present
- 'endsolid' footer present
- Every 'facet' has matching 'endfacet' (no leaks)
- Every facet has exactly 3 vertices
- Total vertex count = 3 × facet count
- All facet normals + vertex coords are finite (no NaN/inf)
- 'facet normal' has 'normal' subtoken + 3 floats
- 'vertex' has 3 floats

Errors capped (30 listed) so a giant corrupt file with consistent
breakage doesn't drown the report. Exit 1 on any error so CI can
gate. Format-validator lineup is now complete:
  Open binary: WOM / WOB / WOC / WHM / GLB
  Open text:   JSON DBC / STL
Every shippable open format has a CLI validator.
2026-05-06 14:10:07 -07:00
Kelsi
a3333b7b4d feat(editor): add --bake-zone-obj completing the bake-zone trio
OBJ companion to --bake-zone-glb / --bake-zone-stl. Same multi-tile
WHM aggregation, this time as Wavefront OBJ — opens directly in
Blender / MeshLab / 3DS Max for hand-editing the terrain mesh:

  wowee_editor --bake-zone-obj custom_zones/MyZone
  # -> custom_zones/MyZone/MyZone.obj

  Baked custom_zones/MyZone -> custom_zones/MyZone/MyZone.obj
    2 tile(s), 41472 verts, 65536 tris

Each tile becomes its own 'g tile_TX_TY' block so designers can hide
tiles independently in Blender. Single global vertex pool with
per-tile vertex base indices for face emission (OBJ requires verts
before faces, so we collect per-tile face indices in memory then
emit them after all verts are streamed to disk).

Hole bits respected (cave-entrance quads dropped). Coords match
WoweeCollisionBuilder's outer-grid layout exactly so .obj/.glb/.stl
of the same source align spatially when overlaid.

Why three formats for full-zone export: glTF for on-screen 3D
viewers, STL for fabrication, OBJ for DCC editing. Three different
ecosystems, three different format sweet spots.

Verified: 2-tile zone (Z + added tile) baked correctly. 41472 verts
(2 × 20736), 65536 tris (2 × 32768), 2 'g' blocks (tile_30_30 +
tile_31_30) — matches what --bake-zone-glb reports for the same
input.
2026-05-06 14:07:22 -07:00
Kelsi
6113582a7d feat(editor): add --check-glb-bounds for stale-bounds detection
Cross-checks every position accessor's claimed min/max against the
actual data in the BIN chunk. glTF viewers use these bounds for
camera framing and frustum culling; stale values (e.g. from a tool
that edited geometry without recomputing) cause models to vanish
at certain angles or get framed wrong on load.

  wowee_editor --check-glb-bounds Tree.glb

  GLB bounds: Tree.glb
    position accessors checked : 1
    mismatched                 : 0
    PASSED

  wowee_editor --check-glb-bounds bad.glb

  GLB bounds: bad.glb
    position accessors checked : 1
    mismatched                 : 1
    FAILED — 1 error(s):
      - accessor 0 bounds mismatch: claimed [-9999,-9999,-9999]-[9999,
        9999,9999] vs actual [533.3,533.3,98.5]-[1066.7,1066.7,101.5]

Walks the meshes/primitives tree, dedups the POSITION attribute
accessors (multiple primitives can share one), then for each unique
accessor reads the BIN chunk via the bufferView+byteOffset chain
and recomputes the actual min/max. Compares with float epsilon
(1e-3) since perfect equality across float compilers isn't
guaranteed.

Also flags missing min/max — the glTF 2.0 spec REQUIRES position
accessors to declare bounds (validators like Khronos's reference
impl reject .glbs that omit them).

Verified: a fresh --export-whm-glb passes clean. After hand-editing
the JSON to claim bogus bounds (-9999 to 9999 for a 533-1067 range
mesh), --check-glb-bounds correctly reports the mismatch with full
claimed-vs-actual values, exit 1.
2026-05-06 14:04:48 -07:00
Kelsi
06b21884ad feat(editor): add --bake-zone-stl for full-zone 3D-printable terrain
STL companion to --bake-zone-glb. Designers can 3D-print a miniature
of an entire multi-tile zone in one slicer load — useful for tabletop
RPG props, physical playtest references, or just the satisfaction of
holding a zone in your hand:

  wowee_editor --bake-zone-stl custom_zones/MyZone
  # -> custom_zones/MyZone/MyZone.stl

  Baked custom_zones/MyZone -> custom_zones/MyZone/MyZone.stl
    2 tile(s), 65536 facets, 0 hole quads skipped

Streams ASCII STL directly to disk (no in-memory accumulation —
relevant for large multi-tile zones). Per-triangle face normal
computed from cross product since slicers use it for orientation.
Hole bits respected (cave-entrance quads dropped) and counted
separately so users see how much got skipped.

Why STL alongside the existing glTF zone bake: glTF targets
on-screen 3D viewers; STL targets fabrication. Different ecosystems,
different file formats, both now reachable from the same WHM source
with one command each.

Verified: 2-tile zone (Z + added tile) baked correctly. 65536 facets
(2 tiles × 32768 each), 12MB ASCII STL, well-formed solid/endsolid
framing, normals computed (e.g. '0.305 -0.399 -0.864' for the first
sloped facet).
2026-05-06 14:02:13 -07:00
Kelsi
b7b600c177 feat(editor): add --list-commands and --gen-completion for shell ergonomics
The CLI surface is now 103 commands. Tab completion is no longer a
nice-to-have:

  source <(wowee_editor --gen-completion bash)   # one-time setup
  wowee_editor --info-<TAB>                       # all info-* offered

Two complementary commands:

  --list-commands: parses printUsage's own output to extract every
  '--flag' it documents, dedupes, sorts. Auto-tracks new commands
  as they're added (no parallel list to maintain).

  --gen-completion bash|zsh: emits a completion script that re-execs
  --list-commands at completion time, so newly-added flags light up
  without regenerating the script. Bash version uses compgen with
  per-session caching in ; zsh version uses the
  _arguments + compdef framework.

Both completion scripts also fall back to file-path completion in
arg slots (the common case for --info-/--validate-/--export-
commands that take a path).

Verified:
- --list-commands: 103 unique flags emitted alphabetically
- --gen-completion bash: well-formed script using full binary path
  for the cached --list-commands invocation
- --gen-completion zsh: same shape, _arguments-based
- Unknown shell: clear error + exit 1
2026-05-06 13:59:54 -07:00
Kelsi
d82f90dd82 feat(editor): add --diff-glb completing the diff-* family
Structural compare of two glTF 2.0 binaries. Completes the diff
suite alongside --diff-wcp (archive-vs-archive) and --diff-zone
(unpacked-zone-vs-zone):

  wowee_editor --diff-glb a.glb b.glb

  Diff: a.glb vs b.glb
                         a      b
    meshes      :      1      2  DIFF
    primitives  :    256      2  DIFF
    accessors   :    258      4  DIFF
    bufferViews :      3      3
    buffers     :      1      1
    BIN bytes   : 890880 1781760  DIFF

Reports per-category counts side-by-side with a 'DIFF' marker on
mismatches. Compares structure (mesh/primitive/accessor counts +
BIN chunk size), NOT byte-level — JSON key ordering can vary
between tools so a byte diff would have false positives. JSON mode
emits the per-field {a, b} pair plus totalDiffs + identical bool
for CI consumption.

Useful for confirming alternate export paths produce equivalent
output (does --bake-zone-glb match concatenated --export-whm-glbs?
does a tool refactor preserve the same shape?).

Verified: same file vs itself reports IDENTICAL with exit 0.
Single-tile WHM .glb (1 mesh, 256 primitives, 890KB BIN) vs
2-tile bake (2 meshes, 2 primitives, 1.7MB BIN): correctly flags
4 DIFFs with exit 1.
2026-05-06 13:57:25 -07:00
Kelsi
901e48b659 feat(editor): add --info-zone-tree for hierarchical zone overview
At-a-glance comprehension of a zone's contents in a single `tree`-
style view. The other --info-* commands focus on one category each;
this composes them into a unified picture:

  wowee_editor --info-zone-tree custom_zones/MyZone

  MyZone/
  ├─ Manifest
  │  ├─ mapName     : MyZone
  │  ├─ mapId       : 9000
  │  ├─ baseHeight  : 100.0
  │  ├─ biome       : (unset)
  │  └─ flags       :
  ├─ Tiles (1)
  │  └─ (30, 30)
  ├─ Creatures (2)
  │  ├─ lvl 7  Wolf
  │  └─ lvl 12  Bear
  ├─ Objects (1)
  │  └─ m2   World/Tree.m2
  ├─ Quests (1)
  │  └─ [1] Hunt Wolves (lvl 5, 250 XP)
  │     ├─ kill ×5 Wolf
  │     └─ reward: Item:Sword
  └─ Files (6)
     ├─ Z_30_30.whm
     ├─ Z_30_30.wot
     ├─ creatures.json
     ├─ objects.json
     ├─ quests.json
     └─ zone.json

Quest sub-tree includes objectives + rewards bulleted underneath.
Files section shows what physically lives in the zone dir so users
spot orphan files (e.g. .obj exports) at a glance.

UTF-8 box-drawing characters for connectors. No --json mode by
design — the structured equivalent is just running --info-* per
category and concatenating, which already exists.
2026-05-06 13:54:48 -07:00
Kelsi
d65c315465 feat(editor): add --bake-zone-glb for whole-zone glTF export
Bakes every WHM tile in a multi-tile zone into ONE .glb so the
entire zone opens in three.js / model-viewer / Sketchfab with one
file. Each tile becomes its own mesh+node so they can be toggled
independently in the viewer:

  wowee_editor --bake-zone-glb custom_zones/MyZone
  # -> custom_zones/MyZone/MyZone.glb

  Baked custom_zones/MyZone -> custom_zones/MyZone/MyZone.glb
    3 tile(s), 62208 verts, 98304 tris, 3 meshes, 2672640-byte BIN

Implementation:
- Walks ZoneManifest::tiles, loads each tile's WHM/WOT pair via
  WoweeTerrainLoader.
- Same per-chunk 9x9 outer-grid + 8x8 quads layout as
  --export-whm-glb (so tiles align spatially with corresponding
  --export-woc-obj output).
- All tiles share one global vertex+index pool packed into a single
  BIN chunk; per-tile primitives slice their index range via
  byteOffset on the indices accessor.
- One node per tile, named 'tile_TX_TY', so viewers can hide
  individual tiles for area-by-area inspection of large zones.

v1 limitation: terrain only — object/WOB instances are a follow-up
that needs careful per-mesh bufferView slicing across many distinct
loaded models. Listed in --info-glb as 'meshes' so users see the
shape of the output before opening in a viewer.

Verified: 3-tile zone (Z + 2 added tiles) baked correctly. 62208
verts (3 × 20736), 98304 tris (3 × 32768), 3 meshes/primitives.
--validate-glb on the output: PASSED, all chunk types correct,
accessor bufferView refs in range.
2026-05-06 13:52:09 -07:00
Kelsi
a212479424 feat(editor): add --import-stl to round-trip STL back into WOM
Closes the WOM <-> STL bridge for the fabrication ecosystem (Cura,
PrusaSlicer, TinkerCAD, Meshmixer, SolidWorks, Fusion 360). Designers
can now edit prints in any CAD/3D-print tool and bring them back to
the engine:

  asset_extract     # M2 -> WOM (open binary)
  --export-stl      # WOM -> STL (universal fabrication format)
  ... edit in TinkerCAD / sculpt in Meshmixer ...
  --import-stl      # STL -> WOM (back to engine format)
  --validate-wom    # confirm consistency

Parser handles ASCII STL only (binary STL is a follow-up):
- 'solid <name>' header — preserves model name
- 'facet normal nx ny nz' — sets the face normal for following verts
- 'vertex x y z' — accumulates 3 per facet
- 'endfacet' — emits the triangle with the face normal applied to all
  3 verts (STL doesn't carry per-vertex normals, so the round trip is
  faceted-shading by design)
- Dedupes on (pos, normal) so shared vertices on the same face merge
  (a 4-vert square base with 2 tris collapses to 4 verts), but verts
  shared across faces with different normals stay distinct (correct
  for faceted geometry)
- Computes bounds from positions for renderer culling

Round-trip cost: a 5-vert/6-tri pyramid round-trips to 16 verts/6
tris. The vertex inflation is structural (STL faceted-shading
semantics) — geometry preservation is exact. Verified via
WOM -> STL -> WOM -> --validate-wom (PASSED, bounds and tri count
match exactly).
2026-05-06 13:49:02 -07:00
Kelsi
9b24e0be8a feat(editor): add --export-stl for 3D-printer-compatible WOM export
ASCII STL is the universal 3D printing format — Cura, PrusaSlicer,
Bambu Studio, Slic3r, OctoPrint, MakerBot Print, and basically every
slicer made in the last 25 years opens it natively. Lets WOM models
drive physical prints with no conversion friction beyond one command:

  wowee_editor --export-stl Tree         # -> Tree.stl
  wowee_editor --export-stl Tree out.stl

Per-spec STL ASCII output:
- 'solid <name>' header / 'endsolid <name>' footer (name sanitized
  to alphanum + underscore for slicers that strict-parse)
- Per-triangle 'facet normal nx ny nz' with normal computed from
  cross-product of edges 1 and 2 (most slicers use this for
  orientation hints; falls back to (0,0,1) for degenerate triangles)
- 'outer loop' with three vertex lines per facet
- No shared vertex pool — STL stores every triangle independently

Why STL alongside OBJ + glTF: OBJ targets DCC tools (Blender etc.),
glTF targets web 3D viewers (Sketchfab, three.js), and STL targets
fabrication. Three different ecosystems, three different format
needs — wowee open formats now bridge to all three.

Verified on a 5-vert/6-tri pyramid: STL has 6 facets with correctly
computed normals (0 -1 0 for the bottom faces, computed slopes for
the side triangles), proper solid/endsolid framing, name preserved
('solid Pyramid').
2026-05-06 13:46:25 -07:00
Kelsi
6d79963f24 feat(editor): add --migrate-zone for batch WOM upgrade across a zone
Companion to --migrate-wom (single-file upgrade). Walks a zone dir
recursively and upgrades every legacy WOM (v1/v2 with no batches[])
to WOM3 in-place:

  wowee_editor --migrate-zone custom_zones/MyZone

  upgraded: custom_zones/MyZone/models/v1_0.wom
  upgraded: custom_zones/MyZone/models/v1_1.wom

  migrate-zone: custom_zones/MyZone
    scanned   : 3 WOM file(s)
    upgraded  : 2 (added single-batch entry)
    already v3: 1 (no change needed)

Idempotent: already-migrated files become no-ops, so it's safe to
run inside --for-each-zone over a whole project. Useful when the
editor adds a new WOM3-only feature and legacy zones need a one-shot
upgrade pass.

Returns exit 1 if any file fails to write (separate from 'no upgrade
needed'), so CI can gate on it.

Verified: seeded a dir with 2 v1 WOMs + 1 v3 WOM. First run
upgraded 2, reported 1 as already-v3. Re-run reported all 3 as
already-v3 (no double-batching).
2026-05-06 13:44:16 -07:00
Kelsi
b3e34e0edf feat(editor): add --validate-jsondbc for strict JSON DBC schema check
--info-jsondbc only verifies recordCount matches the actual records[]
array length. This goes deeper, validating the full sidecar schema
that --convert-json-dbc consumes:

  wowee_editor --validate-jsondbc db/Spell.json

Checks:
- top-level value is a JSON object
- 'format' field exists, is a string, equals 'wowee-dbc-json-1.0'
- 'source' field present (so re-import knows the DBC slot)
- recordCount + fieldCount are non-negative integers
- 'records' is an array; recordCount matches actual length
- each record is an array exactly fieldCount cells wide
- each cell is string|number|bool|null (no nested objects/arrays)

Errors capped (3 per category) with '... and N more' tail so a
1000-row file with consistent breakage doesn't drown the report.
Exit 1 on any error so CI can gate.

Verified on a hand-rolled good JSON (passes clean) and a bad one
with: wrong format tag, missing source, wrong-width row, and an
object cell — all 4 issues reported with precise positions and
exit 1.

Format-validator lineup is now complete:
  Open binary: WOM / WOB / WOC / WHM / GLB
  Open text:   JSON DBC
Every shippable open format has a CLI validator that gates on
schema/structure errors.
2026-05-06 13:41:45 -07:00
Kelsi
fab6238e64 feat(editor): add --migrate-wom for WOM1/WOM2 -> WOM3 schema upgrade
WOM3 is a strict superset of WOM1/WOM2: adds a batches[] array that
lets a single mesh carry multiple materials. Older content saved as
WOM1 (static) or WOM2 (animated) is missing this metadata, so
batch-aware tooling (--info-batches, --export-glb per-primitive
splits, future material-aware renderers) treats it as one implicit
batch. This makes that explicit:

  wowee_editor --migrate-wom Tree            # in-place upgrade
  wowee_editor --migrate-wom Tree out/Tree   # write to a different base

What it does:
- Loads the old WOM, adds a single Batch covering the entire index
  range with textureIndex=0 and blend=opaque (matches the implicit
  treatment).
- Saves — WoweeModelLoader::save picks WOM3 magic automatically once
  batches.size() > 0, so the version field re-derives correctly.

Idempotent: running on an already-migrated v3 file is a no-op
('already had batches; no schema change') so it's safe to run
in for-each-zone batch passes.

Verified: built a v1 single-tri model, migrated to v3, --info reports
'version : 3 (multi-batch)', --info-batches shows '1 batches' with
the expected default fields. Re-running --migrate-wom on the v3 file
correctly says 'already had batches; no schema change'.
2026-05-06 13:39:18 -07:00
Kelsi
028a8011b0 feat(editor): add --info-bones and --list-zone-textures inspectors
Two more sub-inspectors filling gaps in the format-coverage matrix:

  wowee_editor --info-bones HumanMale.m2

  M2 bones: HumanMale.m2 (54)
    idx  parent  depth  keyBone  flags    pivot (x, y, z)
      0      -1      0        0    512    (  0.00,  0.00,  0.95)
      1       0      1        1    512    (  0.00,  0.00,  1.05)
      2       1      2       -1    512    (  0.00,  0.05,  1.18)
      ...

The depth column is computed by walking parents (cycle-guarded by
boneCount cap) so the tree shape is visible at a glance. Useful
when debugging skeleton structure ('why is this finger bone not
following its hand?').

  wowee_editor --list-zone-textures custom_zones/MyZone

  Zone textures: custom_zones/MyZone
    WOMs scanned    : 47
    unique textures : 23

    refs  path
       8  Character/Human/Male/HumanMaleSkin00_00.blp
       6  Character/Generic/Hair01.blp
       3  Tileset/Generic/Stone.blp
       ...

Pairs with --list-zone-deps (which lists model paths) — this lists
the textures those models pull in. Each WOM contributes at most
one count per unique texture (so a model with the same texture in
3 batches doesn't inflate the ref count). Useful for verifying
every BLP/PNG ships with the zone before --pack-wcp.

Verified on a real WoW M2 (nexusraid_skya.m2): 44 bones reported
with parent indices, depths, and pivot offsets in the expected
ranges.
2026-05-06 13:36:59 -07:00
Kelsi
eca43cfefa feat(editor): add --info-attachments / --info-particles / --info-sequences
Three M2 sub-inspectors covering data fields the proprietary M2 format
carries that the open WOM doesn't yet (or carries differently). Useful
for understanding what gets lost when running --convert-m2 vs what
ships with the open conversion:

  wowee_editor --info-attachments Character/Human/HumanMale.m2
    M2 attachments: ... (15)
      idx   id  bone  pos (x, y, z)
        0    0     5  (  0.00,   0.50,   1.20)   # head
        1    1    27  (  0.10,  -0.20,   0.30)   # right hand
        ...

  wowee_editor --info-particles Spells/Fireball.m2
    particles: 8, ribbons: 2
    Particles:
      idx   id  bone  tex  blend     type  pos (x, y, z)
        ...
    Ribbons:
      idx   id  bone  tex  mat  pos (x, y, z)
        ...

  wowee_editor --info-sequences Creature/Wolf/Wolf.m2
    M2 sequences: ... (12)
      idx   id  var  duration  flags    speed  blend
        0    0    0      1733     32    0.00    150   # Stand
        4    4    0       950      0    1.50    150   # Walk
        5    5    0       625      0    3.20    150   # Run
        ...

Three commands share one entry point — they all need the same
M2Loader::load + skin-merge dance, then differ only in which sub-
array they iterate. Reduces duplicate boilerplate. JSON mode
emits per-entry records with index for programmatic consumption.

Why these matter: M2 carries scene-graph metadata (where to mount
weapons, where particles spawn, which animation is which) that
gameplay code reads at runtime. Surfacing it in a CLI lets
designers verify content without spinning up the renderer.
2026-05-06 13:34:23 -07:00
Kelsi
749aa18f0d feat(editor): add --validate-glb and --info-glb for glTF inspection
Companion to --validate-{wom,wob,woc,whm,all}: now the .glb files
we emit (and any third-party .glbs we receive) get the same deep-
check treatment.

  wowee_editor --info-glb model.glb        # show metadata, exit 0
  wowee_editor --validate-glb model.glb    # show + exit 1 on errors

Shared parser, different verdict policy. Checks:
- 12-byte header: magic = 'glTF' (0x46546C67), version = 2,
  totalLength matches actual file size
- JSON chunk: type = 'JSON' (0x4E4F534A), length within file
- BIN chunk: type = 'BIN\0' (0x004E4942), length within file
- asset.version is '2.0' (per-spec required string)
- accessor.bufferView indices in range
- bufferView.byteOffset+byteLength fits within BIN chunk

Reports counts: meshes, primitives, accessors, bufferViews, buffers,
plus chunk sizes. JSON mode emits structured output for CI scripts.

Verified on a real --export-whm-glb output (256-primitive terrain,
929KB total): all checks pass clean. Synthesized a broken file
with corrupted magic bytes: --validate-glb correctly reports
"magic is not 'glTF'" and exits 1.
2026-05-06 13:31:31 -07:00
Kelsi
c1c1f1b2a8 feat(editor): add --export-quest-graph for Graphviz quest-chain visualization
Visualizing quest chains in plain text rapidly becomes unreadable
past ~10 quests. This emits a DOT file that renders to a labeled
DAG you can paste into a wiki or PR description:

  wowee_editor --export-quest-graph custom_zones/MyZone
  dot -Tpng custom_zones/MyZone/quests.dot -o quests.png

Node coloring conveys completion-readiness at a glance:
- lightgreen: has objectives + reward (will complete in-game)
- lightyellow: has objectives but no reward (uncommon)
- lightgray: no objectives (won't complete — common authoring bug)
- mistyrose dashed: synthetic node for a quest ID referenced by
  nextQuestId but missing from quests.json (broken chain)

Edges:
- solid black: valid chain link
- dashed red labeled 'missing': dangling nextQuestId

Labels include quest ID, title (DOT-escaped for safety), required
level, and XP reward — enough context that the graph stands alone
without needing to cross-reference quests.json.

Verified on a 3-quest zone with chain 1->2->3->999 (last broken):
DOT output has 3 colored nodes (lightgreen for the 2 quests with
objectives, lightgray for the third), 2 solid edges, 1 dashed-red
edge to a synthetic mistyrose <missing> node.
2026-05-06 13:29:23 -07:00
Kelsi
a183143002 feat(editor): add --check-zone-refs cross-reference validator
Catches dangling references that --validate (which only checks
open-format file presence) doesn't:

  wowee_editor --check-zone-refs custom_zones/MyZone

  Zone refs: custom_zones/MyZone
    objects checked  : 12 (2 missing)
    quests checked   : 5 (1 bad NPC refs)
    FAILED — 3 issue(s):
      - object[3] missing: World/Doodad/Tree.m2
      - object[7] missing: World/Building/Inn.wmo
      - quest[1] 'Hunt' giver 999 not in creatures.json

Two checks:
1. Every objects.json model path resolves on disk in any of the
   conventional roots (zone-local, output/, custom_zones/, Data/),
   trying both the open (.wom/.wob) and proprietary (.m2/.wmo)
   variants, with a case-fold fallback for case-sensitive Linux
   filesystems where extracted assets are usually lowercased.
2. Every quest's questGiverNpcId / turnInNpcId references a
   creature in creatures.json (when the zone has any). Filter:
   only flag IDs < 100000 since production wires upstream NPCs
   with 6-digit IDs that legitimately reference outside content.

Errors capped at 30 listed (with full counts in the summary) so
a misconfigured zone with 500 broken refs doesn't drown the
output. Exit 1 on any issue so CI can gate.

Verified: zone with 1 creature, 2 dead model refs, 1 bad NPC
giver — all 3 issues reported with precise indices and exit 1.
2026-05-06 13:27:18 -07:00
Kelsi
e0360ee314 feat(editor): add --list-zone-deps for asset packaging audit
Enumerates every external model path a zone references — both directly
placed (objects.json) and indirectly via WOB doodad placements. Useful
when packaging a content pack to confirm every asset will ship:

  wowee_editor --list-zone-deps custom_zones/MyZone

  Zone deps: custom_zones/MyZone
    WOBs scanned        : 3
    unique paths total  : 12

    Direct M2 placements (5 unique):
      World/Generic/Lamp.m2
      World/Generic/Tree.m2 ×8
      ...

    Direct WMO placements (2 unique):
      World/StaticObject/Stormwind/HumanInn.wmo

    WOB doodad M2 refs (5 unique):
      World/Furniture/Chair.wom
      World/Furniture/Table.wom
      ...

Aggregation:
- Counts duplicates so frequently-used assets show '×N' instead of
  cluttering the list with N identical lines.
- Walks WOBs in the zone dir recursively (catches buildings/ subdir).
- Also recurses into WOBs referenced by direct WMO placements so
  transitive doodad deps surface — matches the runtime's actual load
  chain.
- Sorted output (std::map) so diffs across versions are stable.

JSON mode emits per-category arrays for programmatic consumption
(e.g. piping into a script that checks each path exists in Data/).

Verified on a synthesized zone with 3 M2 placements (one duplicated)
+ 1 WMO placement + no WOBs: 3 unique total, Tree.m2 correctly
deduped with ×2 count, JSON arrays well-formed.
2026-05-06 13:24:27 -07:00
Kelsi
3eea4350af feat(editor): add --zone-stats multi-zone aggregator
Walks every zone in <projectDir> and emits totals across the project
plus a per-zone breakdown table. Useful for content-pack release
notes ('this update adds 47 quests across 12 zones'), capacity
planning ('terrain bytes are 73% of pack size'), and PR diffs that
touch many zones at once:

  wowee_editor --zone-stats custom_zones

  Zone stats: custom_zones
    zones      : 2
    tiles      : 3 total
    creatures  : 3 (0 hostile)
    objects    : 0
    quests     : 1 (0 chained, 250 total XP)
    bytes      : 1706.5 KB total
      whm/wot  : 441.0 KB / 78.2 KB
      woc      : 1184.0 KB
      wom/wob  : 0.0 KB / 0.0 KB
      png/json : 0.0 KB / 3.2 KB

    per-zone breakdown:
      name                tiles  creat  obj  quest    bytes
      Desert                  1      1    0      0    174.2 KB
      Forest                  2      2    0      1   1532.3 KB

Aggregates:
- zone count (every dir with a zone.json)
- tile count (sum across all manifests)
- creature count + hostile subset
- object count
- quest count + chained subset + total XP awarded
- on-disk bytes bucketed by extension (.whm/.wot/.woc/.wom/.wob/
  .png/.json + 'other')

JSON mode emits per-zone records for programmatic consumption
(release-note generators, dashboards, etc.). Verified on a 2-zone
project: counts and byte totals match what individual --info-*
commands report per-zone, summed correctly.
2026-05-06 13:21:54 -07:00
Kelsi
82987af460 feat(editor): add --info-textures and --info-doodads detailed inspectors
Two more drill-down inspectors that complement --info-batches:

  wowee_editor --info-textures HumanMale.wom
  WOM textures: HumanMale.wom (3 textures)
    idx  blp  png  path
      0   y    y   Character/Human/Male/HumanMaleHair.blp
      1   y    -   Character/Human/Male/HumanMaleBody.blp
      2   -    -   Character/Human/Male/HumanMaleEye.blp

  wowee_editor --info-doodads Stormwind.wob
  WOB doodads: Stormwind.wob (3 placements)
    idx  scale  pos (x, y, z)             rot (x, y, z)             model
      0   1.50  (   1.0,    0.0,    1.0)  (   0.0,   90.0,    0.0)  Furniture/Chair.wom
      ...

--info-textures: cross-checks each referenced texture against both
the proprietary BLP and the open PNG sidecar on disk (each lookup
tries the literal path AND falls back to Data/<lowercased path>
since WoW assets are case-sensitive on Linux but designers usually
type Mixed Case). The y/- columns let you spot which textures are
missing before --pack-wcp would fail at runtime.

--info-doodads: model path + position + rotation + scale per
instance. Companion to --info-textures (GPU resources) — this tracks
scene composition.

Both have --json mode that emits per-entry records with index for
programmatic consumption. Verified on synthesized model with 3
texture refs + building with 3 doodads; all fields render correctly.
2026-05-06 13:19:45 -07:00
Kelsi
ac096c5c86 feat(editor): add --export-whm-glb completing the .glb exporter trio
Third and final glTF 2.0 binary exporter — terrain heightmaps now
ship to the modern web 3D ecosystem alongside WOM models and WOB
buildings:

  wowee_editor --export-whm-glb custom_zones/MyZone/MyZone_30_30

Mesh layout matches --export-whm-obj exactly (9x9 outer vertex grid
per chunk, 8x8 quads -> 2 tris each, hole bits respected) so the
.glb and .obj of the same source align spatially when overlaid.

Per-chunk primitive scheme:
- One global vertex pool (positions + normals interleaved by section).
- One global index pool packed sequentially.
- Each loaded chunk gets its own primitive sliced from the shared
  index bufferView via byteOffset.
- Designers can hide chunks individually in three.js for area-by-area
  inspection of large terrains.

v1 simplifications:
- Normals synthesized as +Z (terrain Z-up). Real per-vertex normals
  with cross-chunk smoothing is a follow-up; viewers can compute
  their own from positions in the meantime.
- No UVs (no per-chunk material yet — that depends on splat alpha
  layers which are a separate format problem).

Verified on a 256-chunk scaffolded zone: 20736 verts, 32768 tris,
256 primitives. First chunk's primitive has 384 indices (128 tris
× 3). Position accessor bounds match WoW coord system for tile
(30, 30): min ~(533, 533), max ~(1067, 1067). Total .glb 929KB,
all spec-compliant.
2026-05-06 13:17:37 -07:00
Kelsi
0ad99e4033 feat(editor): add --clear-zone-content for templating prep
Pairs naturally with --copy-zone for the 'duplicate then re-populate'
templating workflow:

  wowee_editor --copy-zone custom_zones/Base 'Variant'
  wowee_editor --clear-zone-content custom_zones/Variant --all
  # ... fresh content can now be added without seeing the source's
  # creatures/objects/quests bleed through.

Selective wipe via flags so you can keep some content:

  --clear-zone-content ZONE --creatures              # wipe NPCs only
  --clear-zone-content ZONE --quests --objects       # quests + props
  --clear-zone-content ZONE --all                    # nuke everything

Implementation:
- Deletes (not blank-writes) so subsequent --info-* commands report
  'no content' rather than 'total: 0' which falsely implies the
  file existed. Missing files are the canonical 'no content' state.
- Refuses to run with no flags (avoids ambiguous 'do you mean
  everything?' silent foot-gun).
- Resets ZoneManifest::hasCreatures when wiping creatures so server
  module gen doesn't expect an NPC table that's no longer there.
- Reports per-file action (removed / skipped) plus total count.

Verified end-to-end: scaffolded zone, populated all 3 content
files, --creatures wipes one (others remain), --all wipes the
rest while reporting the already-removed one as 'skipped'.
2026-05-06 13:14:42 -07:00
Kelsi
3cebcab048 feat(editor): add --info-batches per-batch breakdown for WOM3 models
--info shows aggregate batch count; this drills into each one:

  wowee_editor --info-batches HumanMale
  WOM batches: HumanMale.wom (v3, 2 batches)
    idx  iStart  iCount  tris   blend       flags          texture
      0       0       3      1  opaque      -              Body.blp
      1       3       3      1  alpha       two-sided no-zwrite  Hair.blp

Useful for debugging:
- 'why is this submesh transparent?' -> check blendMode column
- 'why is the back face missing?' -> check flags for two-sided
- 'why is depth wrong?' -> check no-zwrite flag
- 'which batch has the bad UV?' -> indexStart/indexCount range
- 'why does this material show wrong texture?' -> textureIndex + path

Decodes:
- Blend modes: 0=opaque, 1=alpha-test, 2=alpha, 3=add
- Flags: 0x01=unlit, 0x02=two-sided, 0x04=no-zwrite

Falls back to a 'no batches (WOM1/WOM2 single-material model)' note
when called on an older-version model that doesn't have a batch
table.

Verified on a synthesized 2-batch model (opaque body + alpha hair
with two-sided + no-zwrite flags): all fields decoded correctly,
table aligns, JSON output enumerable.
2026-05-06 13:12:28 -07:00
Kelsi
febf779ea7 feat(editor): add --clone-object completing the CRUD-clone trio
Symmetric to --clone-creature and --clone-quest. Common workflow:
place one tree just right (rotation, scale dialed in), then clone N
copies along a path:

  for x in 100 110 120 130 140; do
    wowee_editor --clone-object $Z 0 $x 0 0
  done

Defaults match --clone-creature: 5-yard X offset prevents z-fighting.
Rotation and scale carry over verbatim (so a tilted barrel stays
tilted). uniqueId is reset so the new placement doesn't collide with
the source's identifier in any downstream system that dedups by it.

CRUD-clone surface is now complete:
  --clone-quest      <zone> <questIdx> [newTitle]
  --clone-creature   <zone> <idx> [newName] [dx dy dz]
  --clone-object     <zone> <idx> [dx dy dz]

Verified: scaffolded zone, added a tree at (100, 200, 50, scale=1.5).
Cloned twice (default offset, custom offset). Result: 3 trees with
correctly-shifted positions and preserved scale=1.50.
2026-05-06 13:10:25 -07:00
Kelsi
4df5a367f8 feat(editor): add --export-wob-glb for buildings -> glTF 2.0 binary
Mirrors --export-glb (WOM -> .glb) for the WOB format. Buildings now
also reach the modern web 3D viewer ecosystem with zero conversion:

  wowee_editor --export-wob-glb House          # -> House.glb
  wowee_editor --export-wob-glb House out.glb

Mapping for multi-group buildings:
- Per-group vertex arrays merged into a single global pool packed
  into the BIN chunk (positions, normals, UVs interleaved by section).
- Each group becomes one primitive in a single mesh.
- Per-group indices offset by the group's vertex base so the merged
  pool indexing still resolves to the right vertices.
- Per-group indices accessor sliced from a shared bufferView via
  byteOffset (no buffer duplication).
- mode=4 (TRIANGLES), uint32 indices, vec3 float positions/normals,
  vec2 float UVs — same layout as --export-glb.

Verified on a 2-group building (4-vert floor + 3-vert wall, 9
indices total): output .glb has 7 verts, 2 primitives with the
right per-group index counts (6 floor, 3 wall) sliced from the
shared 36-byte index bufferView. BIN = 7*32 + 9*4 = 260 bytes.
2026-05-06 13:08:31 -07:00
Kelsi
8375c47c4d feat(editor): add --export-glb for WOM -> glTF 2.0 binary
OBJ is universal but ancient (1992) — it can't carry skinning,
animations, or PBR materials. glTF 2.0 (2017, Khronos) is the
modern industry standard: every browser-based 3D viewer
(Sketchfab, Three.js, Babylon.js, model-viewer) consumes it
natively, plus Unity/Unreal import it cleanly.

  wowee_editor --export-glb Tree         # -> Tree.glb
  wowee_editor --export-glb Tree out.glb

Shipping WOM through .glb means our open binary format is viewable
in any modern web tool with zero conversion friction. Big win for
the open-format ecosystem reach.

Implementation (single-file binary .glb):
- 12-byte header (magic 'glTF', version 2, totalLength)
- JSON chunk (0x4E4F534A 'JSON', padded to 4-byte boundary with spaces)
- BIN chunk (0x004E4942 'BIN\0')
- BIN layout: positions (vec3 float) | normals (vec3 float) |
  uvs (vec2 float) | indices (uint32). 32 bytes/vert keeps the
  index region naturally 4-byte aligned for free.
- Per WOM3 batch: one primitive with its own indices accessor
  (sliced via byteOffset on a single shared bufferView).
- Position accessor includes min/max bounds for viewer auto-framing.

v1 limitations (deliberate):
- Bones / animations not yet emitted. glTF's joint matrix layout
  differs from WOM's bone tree and needs a careful re-mapping pass;
  shipping geometry-first means designers can use the format today
  and the animation pass lands as a follow-up.
- No materials / textures emitted (those come from the texture
  sidecars; future work to embed or reference them).

Verified: WOM(3 verts, 1 tri) -> .glb(108-byte BIN, 856-byte JSON,
1116-byte total). JSON is spec-compliant glTF 2.0 with correct
bufferView byteOffsets (0/36/72/96), componentTypes (5126=FLOAT,
5125=UNSIGNED_INT), and primitive mode=4 (TRIANGLES). Will open in
any glTF viewer without modification.
2026-05-06 13:05:29 -07:00
Kelsi
bbdbce2ec4 feat(editor): add --export-zone-summary-md for markdown documentation
Renders a human-readable markdown page summarizing a zone's manifest +
content. Useful for designers tracking changes between versions, PR
reviews, or generating GitHub Pages docs without cracking open the GUI:

  wowee_editor --export-zone-summary-md custom_zones/MyZone
  # -> custom_zones/MyZone/ZONE.md (default path)

  wowee_editor --export-zone-summary-md custom_zones/MyZone docs/Zone.md
  # -> custom Markdown destination

Sections rendered:
- Manifest (table): mapName, displayName, mapId, biome, baseHeight,
  tile count, gameplay flags (flying/PvP/indoor/sanctuary), audio
  (music/ambient day/ambient night), description.
- Tiles (table): every (tx, ty).
- Creatures (table): index, name, level, displayId, position, flags.
- Objects (table): index, m2/wmo, path, position, scale.
- Quests (sections): title, level, giver/turnIn IDs, XP/coin reward,
  objectives bulleted with type/target/count/description, item
  rewards bulleted.

Designed to be diff-friendly so PR review highlights actual changes
(adding 'Bear' creature shows up as one row added, not whole-file
churn). Output is GitHub-Flavored Markdown so it renders cleanly on
GitHub Pages and in PR previews.

Verified end-to-end: scaffolded zone, populated 2 creatures + 1
object + 1 quest with full objectives + item reward; ZONE.md
generated correctly with all tables and sections, quest details
including bulleted objective + item list.
2026-05-06 13:02:36 -07:00
Kelsi
605af53c98 feat(editor): add --for-each-zone batch runner
Shell-script-equivalent of looping over every zone in a project dir,
in a single editor invocation. Substitutes '{}' in the command with
each zone's path (find -exec convention):

  wowee_editor --for-each-zone custom_zones -- \
    wowee_editor --validate-all {}

  wowee_editor --for-each-zone custom_zones -- \
    wowee_editor --info-creatures {}/creatures.json

Implementation:
- Walks projectDir for child dirs containing zone.json (canonical
  'is this a zone?' test the rest of the editor uses).
- Sorts results so output is deterministic across runs.
- Each token after '--' is shell-escaped via single-quote wrapping
  with embedded ' replaced by '\'' — safe against names with spaces
  and quotes.
- {} replacement done per-token so paths with spaces don't get split.
- fflush(stdout) before std::system so the [zone] header lands above
  the child output instead of after (parent stdout buffered, child
  unbuffered to terminal).
- Returns failed-run count as exit code (capped at 255 for POSIX).

Verified: scaffolded 2 zones, ran --info-creatures across both via
for-each-zone. Output ordered correctly with [zone] headers + child
output, exit 0 when all succeed.
2026-05-06 13:00:15 -07:00
Kelsi
6927c19d72 feat(editor): add --clone-creature symmetric to --clone-quest
Designer workflow: archetype one 'patrol guard' with stats + faction
+ behavior + equipment dialed in, then stamp it across spawn points
around a town:

  for x in 100 110 120 130; do
    wowee_editor --clone-creature $Z 0 "Guard $x" $x 200 50
  done

Defaults give safe behavior:
- Position offset by (5, 0, 0) yards so the copy doesn't z-fight
  the original when no offset is passed.
- Name appended with ' (copy)' if no newName given.
- id reset (auto-assigned by NpcSpawner).
- Patrol path NOT offset — patrol points are world-space waypoints,
  not relative to the spawn; cloning that vector unchanged matches
  designer intent. Re-author the path on the clone if needed.

Verified: scaffolded zone, added Guard, cloned twice (default
offset, custom name + 3D offset). list-creatures shows 3 spawns
with correctly differentiated positions and names.
2026-05-06 12:57:31 -07:00
Kelsi
ff444e93f8 feat(editor): add --info-adt proprietary terrain inspector
Completes the proprietary-format inspector lineup. Pairs naturally
with --info-wot / --info-whm (the open WOT/WHM equivalents) so users
can verify the conversion preserves chunk counts, doodad placements,
and WMO references:

  wowee_editor --info-adt World/Maps/Azeroth/Azeroth_32_48.adt

  ADT: ...
    version          : 18
    file bytes       : 450192
    coord            : (0, 0)
    chunks loaded    : 256/256
    height range     : [-0.00, 0.00]
    hole chunks      : 0 (with cave/gap masks)
    water chunks     : 0
    textures         : 0
    doodad names     : 0 (0 placements)
    wmo names        : 1 (1 placements)

Reports loaded chunk count (out of fixed 256), height min/max across
all loaded chunks (with NaN guard so corrupted heights don't poison
the range), hole chunks (cave/gap masks), water chunks, texture and
doodad/WMO name table sizes, and placement counts.

Verified on a real WoW ADT (ahnqirajtemple_29_46.adt, 450KB):
correctly reports 256/256 chunks loaded, 1 WMO name + 1 placement.
JSON mode emits structured output for CI scripts.

The format-inspector lineup is now complete:
  Proprietary: BLP / DBC / M2 / WMO / ADT
  Open:        PNG / JSON DBC / WOM / WOB / WOC / WOT/WHM / WCP
Every format on both sides of the open-format bridge has an inspector.
2026-05-06 12:55:31 -07:00
Kelsi
2c41f7804b feat(editor): add --clone-quest for templating quests with shared shape
Common designer workflow: build a base quest with objectives + rewards
once, then make N variants ('Slay Wolves' / 'Slay Bears' / 'Slay
Tigers') with the same shape. Doing this through individual --add-*
commands means re-typing all objectives + items each time. --clone-quest
deep-copies an existing quest in one shot:

  wowee_editor --clone-quest $Z 0                  # 'Foo' -> 'Foo (copy)'
  wowee_editor --clone-quest $Z 0 'Slay Bears'     # custom title

Carries:
- All objectives (deep copy via vector value-copy)
- All item rewards
- XP / coin reward fields
- requiredLevel, giver, turnIn

Resets:
- id (set to 0; addQuest auto-assigns a fresh one)
- nextQuestId (cleared — chaining the clone to the same next quest
  would corrupt chain semantics; user can re-set it explicitly)

Verified: scaffolded zone, added quest with 2 objectives + 1 item +
xp=250. Cloned twice (default name, custom name). Result: 3 quests
in list, both clones have full objective list and reward intact.
2026-05-06 12:53:23 -07:00
Kelsi
b6ce6b4fe9 feat(editor): add --info-m2 and --info-wmo proprietary inspectors
Round out the format-inspector lineup. The wowee open formats had
inspectors (--info-wom, --info-wob); these are the proprietary
counterparts that pair with --convert-m2 / --convert-wmo so users
can verify what the conversion preserves vs drops:

  wowee_editor --info-m2  Character/Human/Male/HumanMale.m2
  wowee_editor --info-wmo World/wmo/Stormwind/Stormwind.wmo

--info-m2 reports verts/tris, bones, sequences (animations),
batches, textures, materials, attachments, particles, ribbons,
collision tris, and bound radius. Auto-merges <base>00.skin if
present (WotLK+ M2s store geometry there) so vertex/index counts
match what gets rendered.

--info-wmo reports group count + portals + lights + doodads +
materials + textures + total verts/tris across loaded groups.
Auto-merges matching <base>_NNN.wmo group files; pre-resizes the
groups vector so loadGroup populates the right slots.

Verified against real WoW assets:
  nexusraid_skya.m2: v264, 20917 verts, 22940 tris, 44 bones,
    1 sequence, 44 batches, 28 textures, 42 materials.
  ed_zd_ziggurat.wmo: v17, 1 group (1 loaded), 8 materials, 7
    textures, 4609 verts, 3650 tris from the group file.

Bug caught during testing: initial snprintf used an 8-byte buffer
for '_NNN.wmo' (which is 8 chars + NUL = 9), silently truncating
to '_000.wm' and failing every group lookup. Bumped to 16 bytes
with a comment so the trap doesn't get re-stepped.
2026-05-06 12:51:40 -07:00
Kelsi
03c700b030 feat(editor): add --info-blp inspector for proprietary BLP textures
The --info-* suite covers wowee open formats and their JSON/PNG
sidecars. BLP itself was the gap — the proprietary source format
that asset_extract converts FROM. Useful for verifying source
files before --convert-blp-png:

  wowee_editor --info-blp Texture.blp

  BLP: Texture.blp (BLP2)
    size       : 128 x 128
    channels   : 4
    format     : BLP2
    compression: DXT5
    mip levels : 16
    file bytes : 23044
    decoded RGBA bytes: 65536

Magic-checks the first 4 bytes (BLP1 or BLP2) before invoking the
loader so feeding a non-BLP gets a clear 'not a BLP1/BLP2 file'
message instead of a confusing 'invalid' from the decoder. Reports
both file size (compressed-on-disk) and decoded RGBA byte count
(width*height*4) so users can see the compression ratio at a glance.

JSON mode emits the same fields for CI/scripting.

Verified on a real WoW BLP (pvp-banner-emblem-1.blp): correctly
identifies as BLP2 / DXT5 / 128x128 / 16 mips. Total format-
inspector lineup is now complete (BLP/PNG/DBC-bin/DBC-json plus
all 5 wowee binary formats).
2026-05-06 12:44:09 -07:00
Kelsi
8fb3717995 feat(editor): add --list-quest-objectives and --list-quest-rewards
--list-quests shows the quest roster but doesn't drill into objectives
or item rewards (--info-quests just counts). These complete the
detail picture and unblock --remove-quest-objective by exposing the
0-based objIdx the user needs:

  wowee_editor --list-quest-objectives custom_zones/Z/quests.json 0

  Quest 0 ('Hunt'): 3 objective(s)
    idx  type     count  target              description
      0  kill         5  Wolf                Slay 5 Wolf
      1  collect      3  Pelt                Collect 3 Pelt
      2  talk         1  Mayor               Talk to Mayor

  wowee_editor --list-quest-rewards custom_zones/Z/quests.json 0

  Quest 0 ('Hunt') rewards:
    xp     : 999
    coin   : 5g 30s 99c
    items  : 2
      [0] Item:Sword
      [1] Item:Shield

Both have --json mode for CI/scripting. Range-check questIdx and
exit 1 on out-of-range with a precise message.

Verified: scaffolded zone, added quest with 3 objectives + 2 items
+ full coin reward; both listings show the right data, JSON output
is well-formed and enumerable.
2026-05-06 12:42:17 -07:00
Kelsi
d4b789b811 feat(editor): add --remove-tile and --list-tiles for zone tile management
Round out the tile lifecycle CLI. --add-tile shipped earlier; these
are the symmetric counterparts:

  wowee_editor --list-tiles custom_zones/MyZone
  wowee_editor --list-tiles custom_zones/MyZone --json
  wowee_editor --remove-tile custom_zones/MyZone 31 30

--list-tiles: shows tile coords AND on-disk presence of .whm/.wot/.woc
per tile, so you can spot manifest-vs-disk drift before pack-wcp would
complain. JSON mode emits per-tile records for programmatic checks.

--remove-tile: drops the manifest entry AND deletes the .whm/.wot/.woc
files for that tile so the zone stays consistent (no orphan sidecars).
Refuses to remove the last tile — server module gen and pack-wcp both
expect at least one, so a zero-tile zone would just be a footgun. Use
'rm -rf custom_zones/X/' for total removal.

Verified end-to-end: scaffolded zone, added 2 more tiles, built WOC
on one. --list-tiles shows 3 tiles with correct file presence (y/y/y
on the built one, y/y/- on the others). Removed (31, 30) — 2 files
deleted, 2 tiles remaining. Tried removing last tile after dropping
to 1: correctly refused with exit 1.
2026-05-06 12:40:19 -07:00
Kelsi
86978b55eb feat(editor): add --convert-blp-png standalone texture converter
Companion to --convert-dbc-json and --convert-json-dbc. Refresh one
PNG sidecar without re-running asset_extract --emit-open across the
whole tree:

  wowee_editor --convert-blp-png Texture.blp          # -> Texture.png
  wowee_editor --convert-blp-png Texture.blp out.png  # custom path

Same code path as the asset_extract emitter (BLPLoader::load +
stbi_write_png), with the same dimension/buffer guards (rejects
0x0 or >8192x8192, rejects images where data is shorter than
width*height*4).

Verified on a real WoW BLP (Data/interface/pvp-banner-emblem-1.blp,
128x128 RGBA): converted cleanly, --info-png on the result reports
'128 x 128 / 8-bit / rgba (4 channels) / 24136 file bytes' (PNG
zlib compression on a sparse banner texture).
2026-05-06 12:38:14 -07:00
Kelsi
1328998ec5 feat(editor): add --remove-quest-objective for symmetric CRUD
Symmetric counterpart to --add-quest-objective. Quest design is
iterative — when an objective gets reworked or trimmed, you need
to remove the wrong one without nuking the entire quest.

  wowee_editor --remove-quest-objective <zoneDir> <questIdx> <objIdx>

Both indices are 0-based and reported by --info-quests / --list-quests.
Range-checks both: questIdx against quest count, objIdx against the
selected quest's objective count. Each errors with a precise out-of-
range message.

Verified: scaffolded zone, added quest with 3 objectives (kill /
collect / talk), removed objective at index 1 (the collect),
--info-quests confirms '1 kill, 0 collect, 1 talk' afterward.
Bad quest and bad objective indices both rejected with exit 1.

Quest-authoring CLI is now fully symmetric:
  --add-quest                   --remove-quest
  --add-quest-objective         --remove-quest-objective
  --add-quest-reward-item       (additive only — items are dedup'd)
  --set-quest-reward            (idempotent field-level update)
2026-05-06 12:35:10 -07:00
Kelsi
9c46d3aeeb feat(editor): add --add-tile to extend a zone with another ADT tile
--scaffold-zone creates a zone with one tile; some zones (continent
fragments, large dungeons) span multiple ADT tiles. This extends an
existing zone:

  wowee_editor --add-tile custom_zones/MyZone 29 30           # default baseHeight=100
  wowee_editor --add-tile custom_zones/MyZone 28 31 250.5     # custom baseHeight

What it does:
- Generates a fresh blank-flat WHM/WOT pair via the same factory
  --scaffold-zone uses, so output is consistent.
- Appends (tx, ty) to ZoneManifest::tiles. Save() rebuilds the
  files-block from tiles, so the new adt_TX_TY entry appears
  automatically in zone.json.

Safety:
- Tile coord must be in WoW grid [0, 64) per axis; rejects 99,99.
- Refuses if the tile is already in the manifest (catches typos).
- Refuses if the .whm/.wot files exist on disk but aren't in the
  manifest (catches manifest-out-of-sync drift from hand edits).
- Optional baseHeight allows seeding flat terrain at a non-default
  elevation.

Verified end-to-end: scaffolded 1-tile zone, added 2 more tiles
(one with custom height). Result: 3 tiles in manifest, 6 files on
disk, files-block has all 3 adt_TX_TY entries. Duplicate and
out-of-range cases both rejected with exit 1.
2026-05-06 12:33:32 -07:00
Kelsi
b04a3ede99 feat(editor): add --convert-dbc-json and --convert-json-dbc
Standalone DBC <-> JSON converters. asset_extract --emit-open already
does this in bulk during extraction, but designers often need to
refresh ONE sidecar after a hand-edit, or push an edited JSON back to
binary DBC for private-server compat:

  wowee_editor --convert-dbc-json Spell.dbc          # -> Spell.json
  wowee_editor --convert-json-dbc Spell.json         # -> Spell.dbc
  wowee_editor --convert-dbc-json Spell.dbc out.json # custom output

DBC -> JSON: uses the wowee-dbc-json-1.0 schema (matches asset_extract
output exactly so the editor's runtime overlay loader picks it up).
Same string/float/uint heuristic the existing emitter uses.

JSON -> DBC: builds a Blizzard-format-compatible binary DBC with:
- 20-byte WDBC header (magic + recordCount + fieldCount + recordSize +
  stringBlockSize)
- Records as little-endian uint32 fields (always LE per spec)
- Deduped string block with leading NUL so offset=0 = empty string
- Tolerates JSON numeric types (string/float/int/bool/null) and
  derives fieldCount from row size if header omits it

Verified with a hand-built 3-record/2-field DBC: DBC->JSON->DBC->JSON
round-trips with identical records[] arrays, no data loss. Output
is byte-loadable by the existing DBC parser.
2026-05-06 12:31:33 -07:00
Kelsi
92ea41f1ae feat(editor): add --export-whm-obj for terrain heightmap visualization
Completes the open-format -> universal-text bridge for the last
binary geometry format. WHM was the missing one; designers now have
OBJ exports for all four (WOM models, WOB buildings, WOC collision,
WHM terrain).

  wowee_editor --export-whm-obj custom_zones/MyZone/MyZone_30_30

Mesh layout:
- 9x9 outer vertex grid per chunk (skips the 8x8 inner verts the
  engine uses for 4-tri fans). That's 81 verts and 128 tris per
  chunk; full ADT = 20736 verts + 32768 tris.
- One OBJ 'g chunk_X_Y' per MapChunk so designers can hide chunks
  individually in Blender (e.g. to inspect a single problem area).
- Hole bits respected — cave-entrance quads correctly disappear.
- Coords match WoweeCollisionBuilder's outer-grid layout exactly,
  so an --export-whm-obj and --export-woc-obj of the same source
  align spatially when overlaid in Blender. (Verified: first vertex
  of both is (1066.67, 1066.67, ~98.5) for a tile (30, 30) export.)
- UVs are simply row/8, col/8 in [0,1] per chunk so a checker
  texture renders at the canonical scale for size reference.

Verified: scaffolded zone -> WHM/WOT auto-built -> --export-whm-obj
produces 256 chunks loaded, 20736 verts, 32768 faces, 256 'g'
blocks. Counts exactly match the chunk × outer-grid math.
2026-05-06 12:27:46 -07:00
Kelsi
0f05759027 feat(editor): add --rename-zone for in-place zone renames
--copy-zone duplicates and renames; --rename-zone does it in place
without doubling disk usage. Useful when fixing typos or rebranding
a zone without touching its data:

  wowee_editor --rename-zone custom_zones/Old 'Brand New Name'

What changes:
- zone.json mapName: Old -> Brand_New_Name
- zone.json displayName: 'Old' -> 'Brand New Name'
- zone.json files block (adt_NN_NN, wdt): regenerated from new slug
- slug-prefixed files: Old_28_30.whm -> Brand_New_Name_28_30.whm
- the directory itself: custom_zones/Old/ -> custom_zones/Brand_New_Name/

Order of operations matters for crash safety: slug-prefixed files
get renamed first (atomic per-file via fs::rename), then the
manifest is rewritten in the still-named source dir, then the
directory is moved last. If the dir-move fails we surface the
manifest-already-updated state so the user can recover.

Refuses to run if target dir already exists (avoids silent merge),
or if both slug AND displayName already match the target (no-op).

Verified end-to-end: scaffolded 'Old' with 1 creature, renamed to
'Brand New Name'. Result: dir renamed, .whm/.wot files renamed,
zone.json fully updated, creatures.json preserved with 1 entry.
2026-05-06 12:24:36 -07:00
Kelsi
a20e795ddb feat(editor): add --export-woc-obj for collision-mesh visualization
WOC is the open collision format used for movement queries. When the
player gets stuck or walks somewhere they shouldn't, you need to SEE
the mesh — eyeballing flag bits in --info-woc only goes so far.

  wowee_editor --export-woc-obj custom_zones/Z/Z_30_30.woc

Each flag class gets its own OBJ 'g' block so designers can hide
categories independently in Blender to debug:
  - walkable / steep / water / indoor / nonwalkable
  - and combinations like 'walkable_water' for shallow swimable areas

Implementation notes:
- Triangle-soup topology preserved (no vertex dedupe). Adjacent
  triangles often have different flags; deduping would merge
  categories and lose the boundary.
- Bucket by exact flag value (uint8) so combination flags become
  their own group rather than counting twice.
- Index math: triangle t vertex k -> OBJ index (t*3 + k + 1).
- Header comment preserves source path, triangle counts, walkable/
  steep counts, and tile (x, y) for provenance.

Verified on a freshly-scaffolded zone's auto-built WOC (32768
triangles, all walkable on flat terrain): export reports
'1 flag class(es)', single 'g walkable' block in the OBJ, last
face index correctly = 32768*3 = 98304.
2026-05-06 12:22:31 -07:00
Kelsi
a9789b0154 feat(editor): add --import-wob-obj to round-trip OBJ back into WOB
Closes the WOB <-> universal-format round trip, mirroring what
--import-obj does for WOM. Workflow now works end-to-end for
buildings:

  asset_extract        # WMO -> WOB (open binary)
  --export-wob-obj     # WOB -> OBJ (universal text)
  ... edit in Blender / MeshLab / Maya ...
  --import-wob-obj     # OBJ -> WOB (back to engine format)
  --validate-wob       # confirm consistency

Mapping handles:
- Each OBJ 'g name' starts a new WOB group; faces under it become
  that group's indices. Default 'imported' group catches faces
  before any 'g' directive (raw OBJ files from non-DCC sources).
- Per-group dedupe table on (pos, uv, normal) triples — each WOB
  group has its own local vertex array, so the global OBJ index
  pool gets remapped per group.
- '_outdoor' suffix stripped + isOutdoor flag set, mirroring
  --export-wob-obj's naming convention.
- Doodad placements recovered from the # doodad ... comment lines
  --export-wob-obj writes; round-trip preserves them via re-parse.
- UV V flipped back (1.0 - v) so the round trip is exact.
- Per-group bounds + global bound radius computed from positions.

Verified: WOB(2 groups, 7 verts, 3 tris, 1 doodad) -> OBJ ->
WOB(2 groups, 7 verts, 3 tris, 1 doodad). validate-wob clean,
info-wob shows the same counts. Materials and portals don't
survive the OBJ trip (no semantic equivalent) — that's documented.
2026-05-06 12:20:37 -07:00
Kelsi
2048496aaf feat(editor): add --add-quest-reward-item and --set-quest-reward
Closes the quest-authoring CLI: --add-quest creates a quest, then
--add-quest-objective adds completion conditions, and now these add
the rewards.

  wowee_editor --add-quest-reward-item $Z 0 'Item:Sword' 'Item:Shield' 'Item:Potion'
  wowee_editor --set-quest-reward      $Z 0 --xp 999 --gold 5 --silver 30

--add-quest-reward-item greedy-consumes any trailing positional args
that don't start with '-', so a whole loot table can be added in one
invocation rather than N round-trips through load+save.

--set-quest-reward updates only the fields explicitly passed (--xp /
--gold / --silver / --copper) — avoids the round-trip-and-clobber
footgun of a 'replace whole reward' command. Field flags are
order-independent. At least one flag is required (else error).

Both validate questIdx against quest count and bail with exit 1 on
out-of-range / missing file / bad numeric value.

Verified: scaffolded zone, added quest with default xp=100, batch-
added 3 item rewards in one shot, then set xp=999/gold=5/silver=30
in one shot. --info-quests reports total XP=999, with-items=1.
2026-05-06 12:18:20 -07:00
Kelsi
705899d6a7 feat(editor): add --info-png and --info-jsondbc sidecar inspectors
PNG (BLP sidecar) and JSON DBC (DBC sidecar) didn't have inspectors —
the existing --info-* suite only covered the binary native formats
(WOM/WOB/WOC/WOT/WHM/WCP). These fill the gap so debugging the
asset_extract --emit-* output doesn't need GIMP / a JSON pretty-printer:

  wowee_editor --info-png Textures/Sky01.png
  PNG: Textures/Sky01.png
    size      : 256 x 256
    bit depth : 8
    color     : rgba (4 channels)
    file bytes: 142336

  wowee_editor --info-jsondbc db/Spell.json
  JSON DBC: db/Spell.json
    format    : wowee-jsondbc-1
    source    : Spell.dbc
    records   : 47882 (header) / 47882 (actual)
    fields    : 234

PNG inspector reads only the IHDR chunk (24 bytes total) — no pixel
decode — so it works instantly on huge files. Validates the
8-byte signature, parses big-endian width/height, derives channel
count from PNG color type (grayscale/rgb/palette/grayscale+alpha/
rgba per spec table 11.1).

JSON DBC inspector parses with nlohmann::json, reports the schema
fields (format/source/recordCount/fieldCount), and cross-checks
recordCount against actual records[] array length. Exits 1 on
count mismatch — catches truncated extracts where the header lies
about how much data follows. Verified with hand-rolled 2x3 RGBA
PNG (correct dims) and JSON DBC files (one matched, one mismatched
99 vs 1).
2026-05-06 12:16:01 -07:00
Kelsi
23a2233852 feat(editor): add --export-wob-obj for buildings -> Wavefront OBJ
WOM (the open M2 replacement) already round-trips through OBJ; this
extends the universal-format bridge to WOB (the open WMO replacement)
so buildings can also be edited in Blender / MeshLab / Maya / etc.

  wowee_editor --export-wob-obj House         # writes House.obj
  wowee_editor --export-wob-obj House out.obj # custom path

Mapping decisions:
- Each WOB group becomes one OBJ 'g' block (named after the group;
  outdoor groups get an '_outdoor' suffix). Preserves the room/floor
  structure for downstream selection and per-area editing.
- Single global vertex pool with per-group offsets (OBJ requires v
  indices to be globally 1-based; we track a running vertOffset).
- UV V flipped (1.0 - v) so texturing matches Blender bottom-left
  convention, same as --export-obj for WOM.
- Doodad placements written as # comment lines at the end. OBJ has
  no native concept for instanced models, but emitting them as
  structured comments keeps the placement data recoverable for
  tools that want to re-instance them.
- Portals and material flags drop on the floor — OBJ has no
  semantics for either. The native WOB always remains canonical.

Verified on a synthesized 2-group house (4-vert floor + 3-vert wall,
1 doodad): output OBJ has 7 verts / 7 vt / 7 vn entries, 2 'g'
blocks with proper index offsetting, doodad comment line preserved.
2026-05-06 12:14:04 -07:00
Kelsi
caa0df7e5e feat(editor): add --add-quest-objective for headless objective creation
--add-quest creates a quest shell but quests with no objectives never
complete in-game. This fills the gap:

  wowee_editor --add-quest-objective <zoneDir> <questIdx> \
    <kill|collect|talk|explore|escort|use> <targetName> [count]

Workflow:

  Z=custom_zones/MyZone
  wowee_editor --add-quest $Z 'Hunt Wolves' 100 100 250 5
  wowee_editor --add-quest-objective $Z 0 kill    'Wolf'      5
  wowee_editor --add-quest-objective $Z 0 collect 'Wolf Pelt' 3

Auto-generates a description from type+count+name so addons and
tooltips show something sensible ('Slay 5 Wolf', 'Collect 3 Wolf
Pelt', 'Talk to Mayor'). Designers can hand-edit quests.json after
the fact for bespoke prose; the auto-text is the floor, not the
ceiling.

Quest index matches --list-quests output (0-based). Out-of-range
index, unknown type, or missing quests.json all error with clear
messages and exit 1.

Verified: scaffolded zone, added 2 quests + 3 objectives across
them; --info-quests reports '1 kill, 1 collect, 1 talk'. Bad type
and bad index both rejected.
2026-05-06 12:12:06 -07:00
Kelsi
fdc7ca7ee7 feat(editor): add --diff-zone for comparing two unpacked zone dirs
The natural counterpart to --diff-wcp (which compares two .wcp
archives) — operates on the unpacked side of the workflow. Shows
exactly what changed across zone.json fields, creature roster,
object placements, and quest list:

  wowee_editor --diff-zone custom_zones/Base custom_zones/Variant

  Diff: custom_zones/Base vs custom_zones/Variant
    manifest  : 2 field diff(s)
      ~ mapName: 'Base' -> 'Variant'
      ~ displayName: 'Base' -> 'Variant'
    creatures : 2 vs 2
      - Bear
      + Tiger
    quests    : 1 vs 2
      + Defeat the Tiger

Pairs naturally with --copy-zone: template a base zone, fork a
variant, then diff to see exactly what was customized. Useful for
PR review when a designer modifies a zone — diff against the
upstream version to scope the change.

Comparison strategy: sorted set diff on stable identifying fields
(creature.name, object.path, quest.title). This intentionally hides
position/orientation changes since those are continuous and would
flag every pixel-perfect tweak as a diff — content-level changes
are the signal here.

Exit 0 if identical, 1 otherwise (so CI can gate). JSON mode emits
per-category onlyA/onlyB arrays + manifestDiffs list + totalDiffs
count for programmatic consumption.

Verified: diffed two zones forked from a common base (one with
creature swap + new quest); reported 5 diffs across manifest +
creatures + quests with exit 1. Same zone vs itself reports
IDENTICAL with exit 0.
2026-05-06 12:10:22 -07:00
Kelsi
df4e0a30a7 feat(editor): add --import-obj to round-trip Wavefront OBJ back into WOM
Closes the open-format authoring loop:

  asset_extract     # M2 -> WOM (open binary)
  --export-obj      # WOM -> OBJ (universal text)
  ... edit in Blender / MeshLab / ZBrush / Maya ...
  --import-obj      # OBJ -> WOM (back to engine format)

The same WOM file ships in custom zones, gets validated by
--validate-wom, and renders identically through the existing pipeline
— no proprietary M2 ever needs to touch the authoring path.

Parser handles:
- v / vt / vn pools, deduped on (pos, uv, normal) triples so the
  resulting WOM vertex buffer stays compact
- 1-based AND negative (relative) face indices
- f tokens in v, v/t, v//n, and v/t/n forms
- Triangles, quads, and convex n-gons (fan-triangulated)
- CRLF line endings
- Reverses --export-obj's V flip (1.0 - v) so UVs round-trip exactly
- Auto-computes boundMin/Max/Radius from positions (renderer culls
  by these — wrong values make the model disappear)
- Output WOM is WOM1 (static); bones/anims/material flags don't
  exist in OBJ and stay empty by design

Verified end-to-end: WOM -> OBJ -> WOM -> validate-wom yields a
5-vert, 6-tri pyramid back identical to the input. Bounds, vertex
count, index count, and name all preserved.
2026-05-06 12:07:55 -07:00
Kelsi
5067432bae feat(editor): add --export-obj for WOM -> Wavefront OBJ conversion
WOM is our open M2 replacement, but it's still a custom binary format
no DCC tool understands out of the box. OBJ is the universally
supported text format that opens directly in Blender, MeshLab,
ZBrush, Maya — basically every 3D tool ever made:

  wowee_editor --export-obj Tree         # writes Tree.obj
  wowee_editor --export-obj Tree out.obj # custom output path

This closes the loop for content authors:
  asset_extract -> WOM (open binary) -> OBJ (universal text) ->
  edit in Blender -> back to WOM via a future --import-obj.

Layout details that matter for downstream tools:
- 1-based face indices (OBJ standard)
- UV V flipped (1.0 - v) so texturing matches between Vulkan
  top-left and Blender bottom-left conventions
- Per-batch groups when WOM3 batches exist, named with the
  texture basename so material assignment carries through
- Single 'mesh' group for WOM1/WOM2 models
- Header comment preserves provenance (source, version, counts)

Verified on a synthesized 5-vert pyramid (4 base + apex, 6 tris):
output OBJ has 5 v / 5 vt / 5 vn entries, 6 f lines, opens cleanly
in MeshLab. Build green, ctest 31/31.
2026-05-06 12:05:41 -07:00
Kelsi
78a2624159 feat(editor): add --list-{creatures,objects,quests} for indexed enumeration
Verbose enumeration of every entry, complementing the existing
--info-* (summary counts) and unblocking --remove-* (which takes a
0-based index).

  wowee_editor --list-creatures custom_zones/Z/creatures.json

  creatures.json: custom_zones/Z/creatures.json (2 total)
    idx  name                            lvl  display  pos (x, y, z)
      0  Wolf                             7    11430  (100.0, 200.0, 50.0)
      1  Black Bear                      12    11445  (110.0, 210.0, 55.0)

The 'idx' column is exactly what --remove-creature/object/quest
takes, closing the workflow loop:

  wowee_editor --list-creatures $Z/creatures.json |
    grep -i 'wolf' | awk '{print $1}' |
    xargs -I{} wowee_editor --remove-creature $Z {}

Each command has matching --json mode that emits per-entry records
with index, key fields, and position. Verified on a synthesized
zone with 2 creatures + 2 objects + 2 quests; tables align, indices
match, JSON is well-formed and round-trips through jq cleanly.
2026-05-06 12:03:55 -07:00
Kelsi
aa499b7462 feat(editor): add --remove-{creature,object,quest} CRUD-delete commands
Symmetric counterparts to --add-{creature,object,quest} from earlier
batches:

  wowee_editor --remove-creature <zoneDir> <index>
  wowee_editor --remove-object   <zoneDir> <index>
  wowee_editor --remove-quest    <zoneDir> <index>

Index is 0-based and is reported in the corresponding --info-* output;
nothing identifies entries reliably across reloads, so name-based
removal would silently delete the wrong row when duplicates exist.
Pair them in scripts:

  idx=$(wowee_editor --info-creatures $Z/creatures.json --json |
        jq '.spawns | map(.name) | index("Wolf")')
  wowee_editor --remove-creature $Z $idx

Each prints the removed entry's name/path and the new total so
scripts can verify the right row went away. Out-of-range indices
exit 1 with a clear message.

Verified end-to-end: scaffolded zone, added 3 creatures + 2 objects
+ 2 quests, removed one of each by index, totals correctly went
3->2/2->1/2->1. Bad index 99 properly errors out.
2026-05-06 12:01:52 -07:00
Kelsi
270fcd8e55 feat(editor): add --list-missing-sidecars for actionable open-format triage
--info-extract reports sidecar coverage % per format, but doesn't say
which files are missing. This makes it actionable:

  wowee_editor --list-missing-sidecars Data/

Output: one path per line, prefixed with the missing extension so
shell tools can filter:

  png   Data/Textures/Skybox/Sky01.blp
  json  Data/DBFilesClient/SoundEntries.dbc
  wom   Data/Character/Human/Male/HumanMale.m2

Pipe into xargs to drive a targeted re-extract:

  wowee_editor --list-missing-sidecars Data/ |
    awk '/^png/ {print $2}' |
    xargs asset_extract --emit-png-only

Skips WMO group files (Foo_NNN.wmo) since only the parent file gets
a .wob sidecar — they would otherwise inflate the missing list with
hundreds of false positives per WMO.

Exit 1 when anything is missing (so CI can gate). JSON mode emits
arrays per format type for programmatic consumption. Verified
against synthetic dir with 5 files (4 lacking sidecars + 1 with
.png present): all 4 reported, the one with sidecar correctly
omitted.
2026-05-06 12:00:12 -07:00
Kelsi
8f6315f155 feat(editor): add --validate-woc + --validate-whm and roll into validate-all
Round out the per-format validator suite. Open-format zone validation
now covers all four binary formats:

  --validate-wom Tree
  --validate-wob House
  --validate-woc terrain.woc
  --validate-whm Zone_28_30
  --validate-all custom_zones/Zone1   # runs everything

WOC checks: finite vertex coords on every triangle, no degenerate
triangles (two verts identical), known flag bits only (0x0F mask),
tile coords within WoW grid (< 64), bounds.min <= bounds.max.

WHM/WOT checks: finite heights across all 145 verts/chunk, finite
chunk position vectors, tile coord in [0, 64), reasonable height
envelope ([-10000, 10000] is a generous outer bound — beyond that
suggests units confusion), placements have finite positions and
nameId within doodadNames/wmoNames table size.

validate-all now reports all four format counts (WOM/WOB/WOC/WHM)
and aggregates errors. Verified end-to-end: a fresh scaffolded zone
with --build-woc yields 256/256 chunks loaded, 32768 walkable
triangles, validate-all PASSED. Synthesized WOC with 0xFF flags
correctly fails with 'unknown flag bits 0xFF' and exit 1.
2026-05-06 11:58:20 -07:00
Kelsi
67b719a2d9 feat(editor): add --validate-all + extract validator helpers
Walks a directory recursively and runs the deep validators on every
.wom and .wob it finds. Single CI gate for an entire zone tree:

  wowee_editor --validate-all custom_zones/MyZone --json

Reports per-file failures (capped at first 20 to keep output bounded)
plus aggregate counts so you know which file to drill into with
--validate-wom or --validate-wob individually.

Refactor: pulled the validation bodies out of --validate-wom and
--validate-wob into static helpers (validateWomErrors / validateWobErrors)
returning vector<string>. The per-file commands now share the same
logic as --validate-all — fix one, fix all three. ~200 lines of
duplicate validation code consolidated.

Verified end-to-end: seeded /tmp dir with 2 WOMs (1 with DAG bone
violation) + 1 valid WOB, --validate-all reports 'WOM: 2 total, 1
failed' / 'WOB: 1 total, 0 failed' with the bad file's full path and
error printed below. JSON mode emits per-file failure list for CI.
2026-05-06 11:54:54 -07:00
Kelsi
542a3217f7 feat(editor): add --validate-wob for deep building consistency checks
Companion to --validate-wom; same load-is-lenient story for buildings:

  wowee_editor --validate-wob House [--json]

Cross-references checked per group:
- indices.size() divisible by 3
- every index < vertices.size()
- material textures non-empty
- group boundMin <= boundMax per axis

Building-level:
- portal groupA/groupB references real groups (or -1 = exterior)
- portal polygon has >= 3 vertices
- doodad modelPath non-empty
- doodad scale finite and > 0
- boundRadius >= 0

Errors batched (caps to 3 listed plus '... and N more') so a thousand
bad indices in one group don't drown the report. JSON mode emits the
same error list for CI consumption. Exit 1 on any failure so shell
scripts can gate on it.

Verified against a synthesized 2-group building with intentional
flaws across every category — caught all 4 (group indices count,
empty material texture, short portal polygon, empty doodad model)
and exited 1.
2026-05-06 11:51:26 -07:00
Kelsi
15f9cbb50c feat(editor): add --validate-wom for deep WOM consistency checks
The WOM loader is intentionally lenient — it clamps out-of-range
indices to 0, resets bad bone parents to -1, and skips invalid
batches. That keeps broken files from crashing the renderer, but
also hides corruption that authoring scripts should catch BEFORE
the file ships.

  wowee_editor --validate-wom Tree [--json]

Cross-references checked (none auto-fixed by load()):
- bone DAG order: parent must be strictly less than self index
- animation boneKeyframes count == bone count when both nonzero
- batch indexStart+Count <= total indexCount
- batch indexCount divisible by 3
- batch textureIndex < texturePaths size
- boundMin <= boundMax per axis, boundRadius >= 0
- header version in [1,3], indices count divisible by 3

Verified against a synthesized 3-bone model with parent=self+1
(invalid DAG order) — load() preserves it as written, validator
reports 'bone 1 parent=2 not strictly less (DAG order)' and
exits 1. JSON mode emits errorCount + errors[] + passed boolean
for CI scripts to gate on.
2026-05-06 11:49:30 -07:00
Kelsi
1c4c5a97fa feat(editor): add --copy-zone CLI for templating zones
Duplicate an existing zone to a new slug:

  wowee_editor --copy-zone custom_zones/Original "My New Zone"

Workflow this enables: scaffold one base zone, populate it with
creatures/objects/quests, then copy-zone N times to create variants
without re-scaffolding each. Designers can template a 'forest base'
zone and stamp it into Dark Forest, Frozen Forest, etc.

What it does:
- Recursive copy preserves any subdirs (e.g. data/ for DBC sidecars)
- Reads source slug from zone.json (not the dir name) to know what
  prefix to rewrite — handles users who renamed dirs without
  touching the manifest
- Renames slug-prefixed files (Original_28_30.whm -> NewSlug_28_30.whm,
  matches both _-suffixed and .-suffixed forms)
- Saves a fresh zone.json via ZoneManifest::save which rebuilds the
  files-block from mapName, so the manifest references the renamed
  files correctly

Verified end-to-end: scaffolded Original, added creature + quest,
copied to 'My New Zone'. Result: 2 files renamed, zone.json
mapName/displayName/files all updated, creatures.json + quests.json
copied verbatim.
2026-05-06 11:44:31 -07:00
Kelsi
070919ef51 feat(editor): add --add-quest CLI for headless quest creation
Third headless authoring command, finishing the trio:

  wowee_editor --add-quest <zoneDir> <title> [giverId] [turnInId] [xp] [level]

Optional positional fields are read in order; omit from the right.
Bare 'wowee_editor --add-quest zone Title' produces a valid quest
with default values, matching the editor GUI's New Quest behavior.

Verified: appended 3 quests (250+100+0 XP) to a scaffolded zone,
--info-quests --json reports total=3, totalXp=450, withReward=3.
2026-05-06 11:41:11 -07:00
Kelsi
1017ab4751 feat(editor): add --add-object CLI for headless M2/WMO placement
Mirrors --add-creature for the object placer:

  wowee_editor --add-object <zoneDir> <m2|wmo> <gamePath> <x> <y> <z> [scale]

Lets shell scripts populate objects/buildings without the GUI:

  for tree in 100,200 150,250 200,300; do
    x=${tree%,*}; y=${tree#*,}
    wowee_editor --add-object "$zone" m2 'World/Doodad/Tree.m2' $x $y 0
  done

Loads existing objects.json first then appends, so multiple
invocations build up. Optional scale slot lets the caller set
non-default size (clamped to >0 by the load-time guard).
Verified end-to-end: scaffolded zone → added m2 + wmo → info-objects
reports total=2 with the correct scale range.
2026-05-06 11:37:10 -07:00
Kelsi
83d20180c3 feat(editor): add --add-creature CLI for headless creature placement
Appends a single creature spawn to a zone's creatures.json. First
real authoring tool that doesn't need the GUI placement system —
useful for batch-populating zones via shell script:

  for npc in goblin spider wolf; do
    wowee_editor --add-creature "$zone" "$npc" 100 200 50
  done

Args: <zoneDir> <name> <x> <y> <z> [displayId] [level]
- displayId 0 → SQL exporter substitutes 11707 (generic humanoid)
- level defaults to 1
- Coordinates are render-space (renderX=wowY, renderY=wowX)

Loads any existing creatures.json first then appends, so multiple
invocations build up the file. The standard NPC spawner caps
(50k creatures) protect against runaway scripts.
2026-05-06 11:35:07 -07:00
Kelsi
57b81a2344 feat(extract): --purge-proprietary --json for machine-readable purge report
Schema:

  asset_extract --purge-proprietary Data --json
  {
    "dir":         "Data",
    "confirmed":   false,
    "candidates":  21570,
    "removed":     0,
    "totalBytes":  17175328768
  }

"candidates" = files where a sidecar exists and is at least as
new (would be deleted on --confirm-purge). "removed" reflects the
actual delete count when --confirm-purge is set; 0 in dry-run.

Lets CI scripts gate on the dry-run candidate count before actually
purging:

  count=$(asset_extract --purge-proprietary Data --json | jq .candidates)
  [ "$count" -gt 1000 ] && asset_extract --purge-proprietary Data --confirm-purge

The asset_extract --json flag now applies to both upgrade and purge
modes.
2026-05-06 11:32:42 -07:00
Kelsi
47f3f6c55b feat(extract): --upgrade-extract --json for machine-readable upgrade summary
Mirrors the wowee_editor --info-extract --json schema so CI scripts
can use a single jq filter for both pre-check and post-upgrade:

  asset_extract --upgrade-extract Data --json
  {
    "dir": "Data",
    "elapsedSeconds": 47.2,
    "skipped": 0,
    "png":     { "ok": 12340, "failed": 0 },
    "jsonDbc": { "ok": 240,   "failed": 0 },
    "wom":     { "ok": 8500,  "failed": 0 },
    "wob":     { "ok": 730,   "failed": 0 },
    "terrain": { "ok": 5660,  "failed": 0 }
  }

Now both wowee_editor and asset_extract have JSON-mode summaries
covering every command CI is likely to gate on. The extraction
pipeline is fully scriptable end-to-end.
2026-05-06 11:31:01 -07:00
Kelsi
65d867c035 feat(editor): --json mode on --info-wot and --info-zone
Last two inspectors to gain --json mode. Schemas show off the
nested structure these commands surface (chunk counts, audio
sub-object, tile array):

  --info-wot  → { base, tileX, tileY,
                  chunks: { withHeightmap, withLayers, withWater },
                  textures, doodads, wmos, heightMin, heightMax }
  --info-zone → { file, mapName, displayName, mapId, biome, baseHeight,
                  hasCreatures, description, tiles: [[x,y], ...],
                  flags: { allowFlying, pvpEnabled, isIndoor, isSanctuary },
                  audio?: { music, musicVolume, ambienceDay,
                            ambienceVolume, ambienceNight } }

Every CLI inspector in the wowee_editor binary now supports --json.
Total: 14 commands with machine-readable output (extract/zone/wcp
inspectors, validate, diff-wcp, list-zones, info-wom/wob/woc/wot/
zone/creatures/objects/quests, zone-summary).
2026-05-06 11:27:53 -07:00
Kelsi
02227d209e feat(editor): --json mode on remaining binary inspectors
Adds --json to --info (WOM), --info-wob, --info-woc. Each emits a
structured object matching the labelled fields from the human-
readable output:

  --info <wom>   → { wom, version, name, vertices, indices, triangles,
                     textures, bones, animations, batches, boundRadius }
  --info-wob     → { wob, name, groups, portals, doodads, boundRadius,
                     totalVerts, totalTris, totalMats }
  --info-woc     → { woc, tileX, tileY, triangles, walkable, steep,
                     boundsMin: [x,y,z], boundsMax: [x,y,z] }

Twelve inspectors now support --json; only --info-wot and --info-zone
left without (they have nested structures and are less commonly
gated by CI).
2026-05-06 11:25:31 -07:00
Kelsi
dbb9879a6e feat(editor): --json mode on remaining content inspectors
Adds --json to --info-creatures, --info-objects, --info-quests so
the per-content inspectors match the per-zone aggregator. Schemas
match the existing --zone-summary --json sub-objects.

Now 9 inspectors support --json:
  --info-extract     --info-wcp        --validate
  --info-creatures   --info-objects    --info-quests
  --zone-summary     --list-zones      --diff-wcp

CI can now drill into per-content reports the same way as the
top-level summary, e.g. fail a build if a zone's quests have any
chain errors:

  wowee_editor --info-quests "$zone/quests.json" --json \
    | jq -e '.chainErrors | length == 0'
2026-05-06 11:23:02 -07:00
Kelsi
39a9f224a0 feat(editor): --diff-wcp --json for machine-readable archive diff
Sixth and last commonly-used inspector to gain --json mode. Schema:

  {
    "a": "path/to/A.wcp", "b": "path/to/B.wcp",
    "identical": 1, "changed": 0, "onlyA": 2, "onlyB": 2,
    "changedFiles": [
      {"path": "foo.dbc", "aSize": 1024, "bSize": 1280}
    ],
    "onlyAFiles": ["Da_30_30.whm", "Da_30_30.wot"],
    "onlyBFiles": ["Db_31_31.whm", "Db_31_31.wot"]
  }

Exit code matches the human path: 0 if identical, 1 otherwise.
Lets CI verify two builds produce byte-identical archives:

  if ! wowee_editor --diff-wcp old.wcp new.wcp --json | jq -e \
    '.changed == 0 and .onlyA == 0 and .onlyB == 0'; then
    echo "WCP layout drift detected"; exit 1
  fi
2026-05-06 11:19:29 -07:00
Kelsi
987dc81f13 feat(editor): --list-zones --json for machine-readable zone discovery
Adds JSON mode to the zone discovery scanner. Returns an array of
zone objects, each with name/dir/mapId/author/description/tiles/
hasCreatures/hasQuests.

Lets CI scripts iterate every available zone and run a per-zone
gate, e.g.:

  for zone in $(wowee_editor --list-zones --json | jq -r '.[].directory'); do
    wowee_editor --validate "$zone" --json | jq -e '.score == 7'
  done

Fifth and last commonly-used inspector to gain --json mode (after
--info-extract, --validate, --info-wcp, --zone-summary).
2026-05-06 11:16:08 -07:00
Kelsi
81cc146d58 feat(editor): --zone-summary --json for unified machine-readable report
Adds --json output to the one-shot zone-summary aggregator. Refactor
also moves creature/object/quest data reads to a shared step before
either branch so both human and JSON outputs use the same numbers.

Schema:

  {
    "zone": "custom_zones/Foo",
    "score": 3, "maxScore": 7,
    "formats": "WOT WHM zone.json ",
    "counts": { "wot":1, "whm":1, "wom":0, "wob":0, "woc":0, "png":0 },
    "creatures": { "total":N, "hostile":N, "questgiver":N, "vendor":N },
    "objects":   { "total":N, "m2":N, "wmo":N },
    "quests":    { "total":N, "chainWarnings":N }
  }

Now CI can gate on any combination — open-format coverage, NPC
counts, quest chain health — from a single command. Fourth and
last commonly-CI'd inspector to gain --json mode (after
--info-extract, --validate, --info-wcp).
2026-05-06 11:14:41 -07:00
Kelsi
89f4b57e99 feat(editor): --info-wcp --json for machine-readable WCP metadata
Mirrors --info-extract --json and --validate --json. Schema:

  {
    "wcp": "/path/to/zone.wcp",
    "name": "Wj",
    "author": "...",
    "description": "...",
    "version": "1.0",
    "format": "wcp-1.0",
    "mapId": 9000,
    "fileCount": 3,
    "totalBytes": 177671,
    "categories": { "terrain": 2, "data": 1 }
  }

Lets CI scripts inspect packed zones — e.g. fail a release if a
zone's WCP doesn't include a creature category, or auto-tag a
release with the totalBytes field.
2026-05-06 11:12:18 -07:00
Kelsi
bed7e4b892 feat(editor): --validate --json for machine-readable zone scoring
Mirrors --info-extract --json. Schema:

  {
    "zone": "custom_zones/Foo",
    "score": 3, "maxScore": 7,
    "formats": "WOT WHM zone.json ",
    "wot": { "present": true,  "count": 1, "valid": true },
    "whm": { "present": true,  "count": 1, "valid": true },
    "wom": { "present": false, "count": 0, "valid": false },
    "wob": { ... },
    "woc": { ... },
    "png": { "present": true,  "count": 12 },
    "zoneJson": true,
    "creatures": false, "quests": false, "objects": false
  }

Exit code is still 0 if score == 7 (full open coverage), 1 otherwise,
so CI gates work the same way:

  if ! wowee_editor --validate "$zone" --json | jq -e '.score == 7'; then
    echo "zone incomplete"; exit 1
  fi
2026-05-06 11:09:59 -07:00
Kelsi
25d68d5a6a feat(editor): --info-extract --json for machine-readable coverage output
Adds an optional --json flag that emits a structured nlohmann JSON
object instead of the human-readable text. Schema:

  {
    "dir": "...",
    "totalBytes": N, "proprietaryBytes": N, "openBytes": N,
    "overallCoverage": 100.0,
    "blp_png":  { "proprietary": N, "sidecar": N, "coverage": % },
    "dbc_json": { ... },
    "m2_wom":   { ... },
    "wmo_wob":  { ... },
    "adt_whm":  { ... }
  }

Lets CI scripts gate on coverage:

  cov=$(wowee_editor --info-extract Data --json | jq .overallCoverage)
  if [ "$cov" != "100" ]; then asset_extract --upgrade-extract Data; fi
2026-05-06 11:07:11 -07:00
Kelsi
e547b4b82b feat(extract): add --purge-proprietary mode to free disk after open conversion
Walks an extracted tree and removes every BLP/DBC/M2/skin/WMO/ADT
that has a confirmed open-format sidecar at least as new. Dry-run
by default — requires --confirm-purge to actually delete:

  asset_extract --purge-proprietary Data/expansions/wotlk
  Dry-run: would purge proprietary files...
    would remove: 21570 files (16380.4 MB)
    (re-run with --confirm-purge to actually delete)

  asset_extract --purge-proprietary Data/expansions/wotlk --confirm-purge
  Purging...
    removed: 21570 files (16380.4 MB)

Pairing rules:
  .blp  → .png
  .dbc  → .json
  .m2   → .wom
  .skin → matching .m2's .wom (foo00.skin pairs with foo.wom)
  .wmo  → .wob (root)
  .wmo group sub-files (foo_NNN.wmo) → parent foo.wob
  .adt  → .whm

Servers can't run without proprietary files, so this only makes
sense for wowee-runtime-only setups. Files without a sidecar are
left untouched.
2026-05-06 11:05:10 -07:00
Kelsi
5799b5f88f feat(editor): --info-extract reports proprietary vs open-format byte totals
Adds two summary lines so users can see how big a 'purge proprietary
after open conversion' workflow would shrink their tree (or how
much extra a dual-format extraction costs):

  proprietary bytes: 18432.4 MB
  open-format bytes: 21340.7 MB (115.8% of proprietary)

Counts every BLP/DBC/M2/WMO/ADT into the proprietary bucket and
every PNG/JSON/WOM/WOB/WHM/WOT/WOC into the open bucket. The
ratio surfaces things like 'PNG is bigger than DXT-compressed BLP'
or 'JSON DBC is much smaller than the binary' without the user
having to run du themselves.
2026-05-06 11:03:23 -07:00
Kelsi
397034a750 feat(extract): incremental --upgrade-extract skips up-to-date sidecars
Compares the source file's mtime against the sidecar's; if the
sidecar is newer, the conversion is skipped and counted into
stats.skipped. Re-running --upgrade-extract on a fully-converted
tree is now nearly free (just an mtime check per file).

  asset_extract --upgrade-extract Data/expansions/wotlk
  Walking ... (first run)
    JSON (DBC→JSON)   : 240 ok
  asset_extract --upgrade-extract Data/expansions/wotlk
  Walking ... (second run, all sidecars up to date)
    up-to-date (skip) : 240
    JSON (DBC→JSON)   : 0 ok

emitOpenFormats() takes a new optional 'incremental' flag (default
false to preserve the asset_extract main-loop's overwrite behavior
since fresh extraction always wants new sidecars).

Verified end-to-end with a hand-built DBC: first run converts,
second run reports 'up-to-date (skip): 1'.
2026-05-06 11:00:20 -07:00
Kelsi
463a8cd751 feat(extract): expose --threads to upgrade-extract + report elapsed time
emitOpenFormats now takes an optional threadCount parameter (0 =
auto). The asset_extract --upgrade-extract path forwards opts.threads
so users can override the auto-detect when running on a CI machine
with limited cores or wanting deterministic timing.

Also wraps the upgrade pass with a chrono timer and prints elapsed
seconds so the parallelization payoff is visible at a glance:

  asset_extract --upgrade-extract Data/expansions/wotlk --threads 8
  Walking Data/expansions/wotlk for open-format upgrades...
    elapsed           : 47.2 s
    PNG (BLP→PNG)     : 12340 ok
    ...

Verified end-to-end: --threads 2 on 5 hand-built DBCs converts all
5 in well under a second.
2026-05-06 10:57:18 -07:00
Kelsi
cab1912441 perf(extract): parallelize open-format emit pass
Conversions are CPU-bound (BLP decode, M2/WMO parse, WOM/WOB
serialize) so the serial walk leaves cores idle. Now collects
every job into a vector during the directory walk, then dispatches
across hardware_concurrency() workers via an atomic next-index
queue. Stats use atomics to avoid the per-job mutex.

Expected ~5-8x speedup for full-tree --upgrade-extract on a
modern desktop. Existing test_open_format_emitter still passes
(it exercises both single-file emit*From* helpers and the parallel
emitOpenFormats walker).
2026-05-06 10:55:05 -07:00
Kelsi
30b15554a9 feat(extract): add --upgrade-extract for in-place sidecar generation
Standalone post-extract pass: walks an existing extracted asset
tree and writes open-format sidecars in place, without re-running
MPQ extraction.

  asset_extract --upgrade-extract Data/expansions/wotlk

Lets users with old extractions opt into the open-format pipeline
without losing the extracted state. Implies --emit-open if no
individual --emit-* flag is set.

Verified end-to-end: created a hand-built DBC in a temp dir, ran
--upgrade-extract, observed test.json appear with correct
metadata. Servers continue to read .dbc from manifest; the
runtime client picks up the new .json sidecar via the existing
pickup path.
2026-05-06 10:52:55 -07:00
Kelsi
5a816f104c feat(editor): add --info-extract CLI for extraction coverage report
Walks an extracted asset tree and reports per-format counts plus
how many proprietary files have a wowee open-format sidecar.
Lets users (or CI) see at a glance whether asset_extract was run
with --emit-open and how complete the open-format coverage is:

  BLP textures : 12340  (12340 PNG sidecar = 100.0% open)
  DBC tables   : 240    (240 JSON sidecar = 100.0% open)
  M2 models    : 8500   (0 WOM sidecar = 0.0% open)
  ...
  overall open-format coverage: 41.2%
  (run `asset_extract --emit-open` to fill missing sidecars)

Skips _NNN group sub-files when counting WMOs (only the root WMO
ships with a WOB sidecar). The headless CLI is now at 22 commands.
2026-05-06 10:50:17 -07:00
Kelsi
ea745005ce feat(runtime): pick up WHM/WOT/WOC sidecars from asset tree
Closes the loop on the asset_extract --emit-terrain pipeline. The
runtime terrain loader now probes for a .whm/.wot/.woc trio in the
same directory as the resolved ADT (e.g. <data>/world/maps/foo/
foo_30_30.{whm,wot,woc}) before falling back to ADTLoader.

Hits the open-format path when:
  custom_zones/<map>/<map>_X_Y.{whm,wot} exists  (zone author override)
  output/<map>/<map>_X_Y.{whm,wot} exists        (editor export)
  <data>/world/maps/<map>/<map>_X_Y.{whm,wot}    (asset extractor)
  -- otherwise falls through to ADTLoader::load(adtData)

Promotes AssetManager::resolveFile to public so callers (terrain
sidecar probe here, anything else later) can locate an extracted
file's directory without reading the bytes.

Servers/private servers continue to read .adt via manifest paths
unchanged. Runtime sidecar coverage now matches the extractor's
emit set across all five binary open formats.
2026-05-06 10:48:40 -07:00
Kelsi
b5ff9eb2a2 feat(runtime): pick up WOM/WOB sidecars from asset tree at load time
terrain_manager already attempts WOM/WOB via tryLoadByGamePath but
its prefix list only included custom_zones/ and output/. The asset
extractor's --emit-wom/--emit-wob writes sidecars next to the M2/WMO
in the asset tree itself (e.g. <data>/world/maps/foo/foo.wom).

Pass the AssetManager's data path as an extra prefix so the runtime
picks the open-format sidecar up there before falling back to the
proprietary M2/WMO load path. Completes the runtime side of the
dual-format extraction:

  AssetManager::loadTexture → tries .png sidecar first, then BLP.
  AssetManager::loadDBC     → tries .json sidecar first, then DBC.
  TerrainManager M2/WMO     → tries .wom/.wob sidecar first, then m2/wmo.

Servers/private servers see no change — they read from the proprietary
files via manifest paths and don't touch the sidecars.
2026-05-06 10:45:43 -07:00
Kelsi
1995ed9824 feat(runtime): pick up JSON DBC sidecars from --emit-json-dbc
AssetManager::loadDBC now probes for a .json sidecar in the same
directory as the binary DBC the manifest resolves to. asset_extract
--emit-json-dbc writes that sidecar on extraction, so the runtime
client transparently falls back to it when the binary DBC is absent
(e.g. open-format-only extraction for end-to-end format testing).

Order: binary DBC > JSON sidecar > custom_zones JSON > CSV > error.
Servers (AzerothCore/TrinityCore) only read the binary DBC, so this
change is invisible to them — the wowee runtime is the only consumer
that tries the JSON path.

The PNG sidecar pickup (tryLoadPngOverride) was already in place from
prior work — this completes the symmetric runtime-side wiring for
both BLP→PNG and DBC→JSON open-format outputs.
2026-05-06 10:43:13 -07:00
Kelsi
6872ba2bcb test(extract): lock in DBC→JSON emission round-trip
4 tests covering the open_format_emitter:
- emitJsonFromDbc round-trips a hand-built 2-record DBC through
  DBCFile::load (which auto-detects JSON via the '{' prefix) and
  recovers identical record/field/value data.
- Missing input file → graceful failure (no JSON written).
- Bad DBC magic → graceful failure.
- emitOpenFormats walks a subdirectory and writes the side-file
  in the right place (matches the extractor's recursive walk).

Brings ctest target count to 31.
2026-05-06 10:40:33 -07:00
Kelsi
d4c69a2b46 feat(extract): emit WHM+WOT+WOC for ADT terrain tiles
Final piece of the open-format emit pipeline:
  --emit-terrain  foo.adt → foo.whm + foo.wot + foo.woc

With this, --emit-open now produces a fully open-format zone
alongside every Blizzard MPQ extraction:
  BLP  → PNG       (textures)
  DBC  → JSON      (data tables)
  M2   → WOM       (models, with skin merge)
  WMO  → WOB       (buildings, with group merge)
  ADT  → WHM/WOT   (terrain heights + metadata)
       → WOC       (collision mesh derived from heights)

Originals stay on disk and indexed by manifest.json so private
servers continue to load proprietary formats; wowee runtime/editor
read the open formats directly. One extraction now feeds both
audiences with no separate conversion pass.

Implementation:
- Inline WHM+WOT writer in open_format_emitter.cpp (mirrors the
  editor's WoweeTerrain::exportOpen but without the PNG-preview /
  normal-map deps so the extractor stays editor-independent).
- Tile coords (x,y) parsed from <map>_<x>_<y>.adt filename.
- Collision mesh derived via WoweeCollisionBuilder::fromTerrain
  (terrain triangles only — WMO collision overlays would need
  asset manager and aren't worth the extractor complexity).
2026-05-06 10:36:14 -07:00
Kelsi
e6ace7cce5 feat(extract): emit WOM and WOB side-files (M2/WMO → open formats)
Extends asset_extract with two more open-format emitters:
  --emit-wom  foo.m2 (+ foo00.skin) → foo.wom
  --emit-wob  foo.wmo (+ foo_NNN.wmo groups) → foo.wob
  --emit-open now also turns these on

Originals are preserved so private servers still load .m2/.wmo
through the manifest path; the wowee runtime/editor pick up the
.wom/.wob next to them via the existing open-format search rules.

Implementation:
- New WoweeModelLoader::fromM2Bytes(m2Data, skinData) shares the
  conversion body with fromM2(path, am) via a static helper
  (convertM2ToWom). Lets the extractor convert without standing
  up an AssetManager.
- fromM2(path, am) moved to a separate translation unit
  (wowee_model_fromm2.cpp) so asset_extract doesn't have to
  link the AssetManager dependency.
- WoweeBuildingLoader::fromWMO already takes a WMOModel directly,
  so emitWobFromWmo just needs to read root + group files and
  call save().
- Group sub-files (<base>_NNN.wmo) are skipped during the walk
  since they're merged into the root WMO.
2026-05-06 10:32:17 -07:00
Kelsi
5ed2008621 feat(extract): emit open-format side-files (BLP→PNG, DBC→JSON)
The asset_extract tool now optionally writes wowee open-format
copies next to each extracted proprietary file:
  --emit-png      foo.blp → foo.png
  --emit-json-dbc foo.dbc → foo.json
  --emit-open     shortcut for both

Originals are left untouched, so private servers (AzerothCore,
TrinityCore) that load from the manifest's .blp/.dbc paths
continue to work unchanged. The wowee runtime / editor can now
consume the open formats directly without an extra conversion pass.

Implementation:
- New tools/asset_extract/open_format_emitter.{hpp,cpp} encapsulates
  the post-extract walk + per-file conversion.
- BLP→PNG uses BLPLoader::load + stbi_write_png with the same
  dimension/buffer-size sanity guards the editor's texture exporter
  applies.
- DBC→JSON mirrors the editor's DBCExporter::exportAsJson schema
  (string/float/uint heuristic) so the runtime DBC overlay loader
  can consume the output drop-in.
2026-05-06 10:23:32 -07:00
Kelsi
9e801f93b6 fix(brush): NaN-guard EditorBrush::setPosition
applyBrush already early-outs on NaN brush position, but the stored
worldPos_ would still capture NaN and surface it to UI panels and
the brush-circle indicator (which renders a NaN ring). Reject NaN
at the setter so the editor state itself stays sane.
2026-05-06 10:15:00 -07:00
Kelsi
4c0f8dd5c0 fix(history): bounds-check chunkIndex in captureChunk/restoreChunk
ADTTerrain.chunks is std::array<MapChunk, 256> — out-of-range
indexing is undefined behaviour. Reject indices outside [0, 255]
and return empty / no-op rather than crashing on a stale undo
record from a future-version terrain layout.
2026-05-06 10:13:56 -07:00
Kelsi
17ca42b70b fix(camera): validate setSpeed input against wheel-clamp range
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
EditorCamera::setSpeed accepted any float, including NaN/inf and
values outside the wheel-zoom clamp range [10, 2000]. NaN speed
would propagate into the per-tick position update (NaN * dt =
NaN) and corrupt the camera view matrix. Match the wheel clamp
so external setters can't bypass the UI's bounds.
2026-05-06 10:12:45 -07:00
Kelsi
e02f2baf9d feat(editor): add --fix-zone CLI to re-apply load-time scrubs/caps
Loads + immediately re-saves zone.json, creatures.json,
objects.json, and quests.json. The load-time scrubs (NaN,
out-of-range, oversize) and save-time caps fire on the round-trip,
producing a cleaned-up zone without ever opening the GUI.

Useful when an old zone was created before recent hardening
batches — running this once normalizes the on-disk state to match
what the current loaders expect.
2026-05-06 10:08:49 -07:00
Kelsi
a531f70890 fix(dbc): skip non-array rows in loadJSON instead of failing
A JSON DBC with a malformed record (object instead of array, or
a string entry) would call row[col] which throws on non-arrays —
the outer try-catch treated this as a hard failure for the whole
DBC. Skip the row (stays zero-initialized) so a single malformed
record doesn't lose all the rest.
2026-05-06 10:07:49 -07:00
Kelsi
8e80f97bbc fix(wot): cap doodadNames/wmoNames at 65536 + guard non-string entries
Both name lists used n.get<std::string> which throws on non-string
entries (would abort the entire WOT load). Real zones use ~5k names
max; cap at 65536 (uint16 nameId range upper bound) so the cap is
generous but bounded. Guard with is_string so a single bad entry
just gets skipped instead of failing the file.
2026-05-06 10:06:20 -07:00
Kelsi
fc895ab564 fix(wot): cap textures/per-chunk layers + range-check int reads
Three issues:
- textures vector was unbounded (cap at 1024).
- Per-chunk layers vector was unbounded (cap at 8 — WoW ADT
  format supports 4, doubling for headroom).
- texId.get<uint32_t> and holes.get<uint16_t> would throw
  json::type_error on negative or oversize values, aborting the
  entire WOT load. Read as int64, clamp to the target range.
2026-05-06 10:04:45 -07:00
Kelsi
e7462efaf6 fix(wot): cap doodad/WMO placement count at 100k on load
Same defense pattern as the editor JSON loaders. Real ADTs cap at
~64k MDDF entries and ~5k in practice; 100k matches the editor
ObjectPlacer cap so an extreme WOT can't bloat the in-memory
terrain past what the editor itself would accept.
2026-05-06 10:03:08 -07:00
Kelsi
fc284c7460 fix(project): cap zones at 1024 on project load
Same defense pattern as the other editor JSON loaders. WoW only
supports 65535 maps total and the editor loads one tile at a
time; 1024 zones per project is plenty. Stale autosave or hand-
edit could otherwise grow zones unbounded and slow the project
picker UI.
2026-05-06 10:01:36 -07:00
Kelsi
ffc0862977 fix(wcp): cap readInfo file-list parse at 1M entries
readInfo iterated the info JSON's files array without bounding;
a malicious WCP could declare more entries than the header
fileCount allows and grow info.files unbounded. Cap to 1M
matching the header check so both readInfo callers and
--list-wcp/--info-wcp stay bounded.
2026-05-06 09:57:37 -07:00
Kelsi
bd97470929 fix(npc): cap spawn count at 50k on load
Same defense pattern as QuestEditor (4096) and ObjectPlacer (100k).
A stale autosave or scatter-runaway could carry millions of NPCs;
each emits creature_template + creature + optional addon/waypoint
rows, drowning the SQL output and the M2 marker mesh.

Every editor JSON loader now has a matched-to-cost upper bound
(NPCs 50k, quests 4k, objects 100k, waypoints 256).
2026-05-06 09:56:55 -07:00
Kelsi
49a2907bc5 fix(editor): cap quest count at 4096 and object count at 100k on load
A stale autosave or hand-edited JSON could carry an unbounded list:
- 100k quests would emit 100k quest_template + queststarter/ender
  INSERTs (huge SQL, slow validate, slow chain walks).
- 1M+ objects bloats the M2 instance SSBO and drags editor framerate
  to single digits.

Caps mirror the 256-waypoint cap added in the previous batch — log
a warning and drop the rest so the editor stays responsive.
2026-05-06 09:56:03 -07:00
Kelsi
8039dff51f fix(npc): cap patrol path length at 256 waypoints on load
A stale autosave or hand-edited creature.json could carry an
unbounded patrolPath. The SQL exporter would emit one waypoint_data
INSERT per entry and produce huge SQL files. 256 waypoints covers
any realistic route.
2026-05-06 09:54:25 -07:00
Kelsi
58e0069404 fix(sql): downgrade Wander to stationary when wanderRadius == 0
Same defect as the empty-Patrol case: Wander behavior with 0
radius would spawn a creature that pretends to wander but never
moves. Downgrade to stationary so the export reflects the actual
in-game behavior, matching the Patrol-without-waypoints fix.
2026-05-06 09:53:07 -07:00
Kelsi
0736b27ec7 fix(sql): downgrade Patrol behavior to stationary when path is empty
A creature with behavior=Patrol but an empty patrolPath would emit
movement_type=2 (waypoint) without any waypoint_data rows.
AzerothCore would log 'creature X has no waypoints' on every spawn
and the NPC would behave erratically. Fall back to stationary so
the spawn appears cleanly; user can fix the missing path after.
2026-05-06 09:52:16 -07:00
Kelsi
d07748398f feat(sql): warn about unsupported quest objective types in export
Pre-scans the quest list and emits a single header note when any
quest uses ExploreArea / EscortNPC / UseObject — those have no
direct quest_template column and need AzerothCore script_quest
hooks. Prevents silent dropping of objectives leaving an unfinished
quest in-game; the user sees the warning once at the top of
02_spawns.sql instead of having to grep through editor logs.
2026-05-06 09:51:38 -07:00
Kelsi
6b82196b7d fix(wcp): skip out-of-tree files at pack time
fs::relative can return '../foo' when the pack source is a symlink
that resolves outside the pack root. The unpacker rejects '..' or
absolute paths wholesale, so a single rogue symlink would ruin the
whole archive. Skip the offending file at pack with a warning so
the rest of the zone still ships.
2026-05-06 09:47:49 -07:00
Kelsi
2f56941ad2 feat(sql): export quest chain link to NextQuestInChain column
Quest.nextQuestId was captured by the editor and used by
validateChains for cycle detection, but never made it into the
AzerothCore quest_template SQL. Now resolves the editor-relative
ID to the matching SQL entry (startEntry + nextQuestId) and
writes it to the NextQuestInChain column. Players can now
auto-progress through quest chains in-game.
2026-05-06 09:46:26 -07:00
Kelsi
afd8e69a41 feat(sql): export quest reward items to RewardItem1-4 columns
Quest.reward.itemRewards entries were captured in the editor JSON
but never made it into the AzerothCore SQL export. Parse each
entry as a numeric item ID and emit RewardItem1-4 + count columns;
unparseable entries become 0 (skipped at quest grant time). 4 slot
limit matches AzerothCore's quest_template schema.
2026-05-06 09:45:23 -07:00
Kelsi
9309e62ab9 feat(sql): emit RequiredNpcOrGo for TalkToNPC quest objectives
Previously only KillCreature and CollectItem objectives translated
to SQL. AzerothCore reuses RequiredNpcOrGo for talk objectives
(count=1 indicates an interaction rather than a kill), so wire that
through and add a comment about which objective types need server
scripts (ExploreArea/EscortNPC/UseObject).
2026-05-06 09:44:23 -07:00
Kelsi
a0480dc3a2 feat(editor): add --info-zone CLI for printing zone.json fields
Mirrors the other --info-* family inspectors. Accepts either a
zone directory or the zone.json path directly. Prints every
manifest field: name, mapId, biome, baseHeight, tiles, flags,
audio config. Useful when diffing two zones or auditing the
audio/flag setup before packing.
2026-05-06 09:43:13 -07:00
Kelsi
7552880ca1 fix(npc): range-check waypoint waitTimeMs per-cell on load
Same per-cell range-check pattern as the JSON DBC fix: if a
waypoint's waitTime field is negative or > UINT32_MAX, the
.get<uint32_t> throws json::type_error and the outer try-catch
aborts the entire NPC file load on a single bad waypoint. Read
as int64, clamp to [0, 600000ms = 10-min cap].
2026-05-06 09:41:11 -07:00
Kelsi
f76aebc4b9 test(objects): add load-time clamp + uniqueId round-trip tests
- ObjectPlacer load: scale=0 clamped to 1.0, missing scale field
  defaults to 1.0 (locks in the load-side guard at line 346).
- ObjectPlacer save→load: uniqueId values survive a full round
  trip (locks in the explicit uniqueId preservation behavior the
  ADT round-trip relies on).

Adds object_placer.cpp to test_editor_units sources.
2026-05-06 09:40:06 -07:00
Kelsi
e6b0a84f3a fix(wcp): cap pack file count at unpack limit (1M)
Pack previously trusted recursive_directory_iterator to terminate
naturally — fine on most zones but a hostile symlink loop or a
giant accidental subdirectory would produce an archive with > 1M
files, which the unpack header check rejects wholesale. Cap at the
unpack limit and log a warning so the resulting WCP is at least
loadable, even if incomplete.
2026-05-06 09:36:54 -07:00
Kelsi
377cfb32d3 fix(wcp): cap per-file size at pack to match unpack limit (256MB)
Pack previously accepted any file < 4GB and wrote it raw. Unpack
caps at 256MB and rejects the whole archive on overflow — so a
huge file in the source dir would silently produce an unpackable
WCP. Cap at pack and skip the body (size=0 entry) so the rest of
the pack remains usable.
2026-05-06 09:35:49 -07:00
Kelsi
d85073241e test(dbc): add hardening tests for absurd recordCount/fieldCount
Locks in the recent DBC overflow guards:
- recordCount=1B + recordSize=1024 (would overflow uint32 product)
- fieldCount=65535 (would multiply to 256KB record size)

Both load() calls return false instead of allocating tiny buffers
that get memcpy'd from TB of memory.
2026-05-06 09:34:39 -07:00
Kelsi
269e0a02ef fix(dbc): range-check JSON DBC integer fields per-cell
val.get<uint32_t>() throws on negative or > UINT32_MAX. The
outer try-catch would then abort the entire JSON DBC load on a
single bad cell. Read as int64_t, clamp to [0, UINT32_MAX], and
zero out anything out of range — matches the per-field NaN scrub
applied to floats one branch up.
2026-05-06 09:33:17 -07:00
Kelsi
64b85ff9ff fix(whm): reject load on overlong per-chunk alphaSize
Same load-desync pattern as elsewhere — alphaSize > 65536 silently
skipped the read but the actual alpha bytes were still on disk, so
the next chunk's baseHeight float read would parse alpha bytes.
Now rejects the load with LOG_ERROR.
2026-05-06 09:31:36 -07:00
Kelsi
9facea14a7 fix(dbc): reject absurd header values + use 64-bit size math
DBCFile::load multiplied recordCount * recordSize as uint32 (line
108), so a header with recordCount=1B and recordSize=1024 would
wrap to a tiny size — resize allocates ~tiny, memcpy reads ~TB
of memory and crashes.

Reject impossible header values up front (10M records / 1024
fields / 16KB record / 256MB string block) and use uint64_t for
the file-size sanity check + size_t for the resize/memcpy product
so the bounds-check is the only path that allows large counts.
2026-05-06 09:29:24 -07:00
Kelsi
9f2e8e1979 test(wob): add 2 string-length reject tests
Locks in the recent silent-corruption fix:
- Overlong building name (5000 > 1024) → load returns invalid
  instead of silently zeroing the length and reading 5000 stale
  bytes as the next group's name+counts.
- Overlong group name (9999 > 1024) → same.

Catches regression of the silent-desync defect that affected 7
length fields across the WoB and WOM loaders.
2026-05-06 09:26:08 -07:00
Kelsi
bbd2e0502b fix(wom): reject load on out-of-range string lengths
Same silent-corruption pattern as WoB: model.name had no length
check at all (would happily allocate 64KB), and texture paths
silently zeroed pathLen on overflow leaving the actual bytes on
disk to shift the rest of the file. Now reject with LOG_ERROR.
2026-05-06 09:24:55 -07:00
Kelsi
d818ff382c fix(wob): reject load on out-of-range string lengths
Building name, group name, group texture path, material texture
path, and doodad model path all had the same defect: when the
length field exceeded 1024 the loader silently set the local
counter to 0 and skipped the read — but the actual string bytes
were still on disk, so the next read interpreted them as the next
length+data pair and the whole rest of the file desynced.

Now reject the whole load on each oversize length with an explicit
LOG_ERROR. Save caps at 1024 so this only triggers on hand-crafted
or future-version files, but the failure mode was severe enough
(silent zone corruption, not a clean error) to warrant the fix.
2026-05-06 09:23:19 -07:00
Kelsi
17f67e3ec8 fix(wob): reject load on out-of-range material count
Previously load silently skipped the materials block when mc > 256,
leaving the file pointer right after the count — the next group's
name would then read material bytes as garbage and the rest of the
file would shift. Save now caps at 256 (so the asymmetry shouldn't
trigger from our own writer), but a hand-crafted or future-version
WoB could still hit it.
2026-05-06 09:19:24 -07:00
Kelsi
0bf7c8ac3f test(open-formats): add 2 round-trip cap tests
Locks in the recent save-side count caps:
- WoB save with 1500 texture paths → load reads exactly 1024,
  first/last entries match what was written before the cap fired.
- WOC save with tileX/tileY=200 → load reads tileX/tileY=32
  (clamped at write, no warning on the second reload).

Catches the asymmetry that would silently drop everything past
the load limit.
2026-05-06 09:17:48 -07:00
Kelsi
63dbfe74b0 fix(wom): cap top-level vert/index/tex counts on save
Top-level WOM save was writing raw model.vertices/.indices/.texturePaths
sizes; load enforces 1M / 4M / 1024 limits. A pathological model would
emit a header rejected on load, leaking the rest of the file body.

Cap each count at the load limit and iterate the WOM1 vertex block +
texture-path block by index so the body matches the header.
2026-05-06 09:16:43 -07:00
Kelsi
b85734e311 fix(wom): cap WOM3 batch count at load limit (4096) on save
Same per-section cap pattern. The loader caps batchCount at 4096;
save iterated all validBatches without checking. A model with
>4096 batches would write a header rejected on round-trip.
2026-05-06 09:15:02 -07:00
Kelsi
0547ab882a fix(wob): cap per-portal vertex count on save to match load limit
Same per-section cap pattern. Real portals carry 4-12 verts; the
load enforces 4096 max. Save previously wrote raw size() so a
huge portal would write a header the loader rejects.
2026-05-06 09:13:34 -07:00
Kelsi
30d1acbeee fix(wob): cap per-group vertex/index/texture counts on save
Per-group counts were uncapped on save while load enforced 1M
vertices, 4M indices, 1024 texture paths. A single huge group
exceeded any cap would write a header the loader rejects, leaking
the rest of the file body into a misread chain.

Cap counts at the load limits and iterate the texture-path block
by index so the body matches the header on round-trip.
2026-05-06 09:12:17 -07:00
Kelsi
2cd69d677a fix(wob): cap group/portal/doodad header counts on save
WoB load enforces 4096 groups / 8192 portals / 65536 doodads. Save
previously wrote raw size() and iterated all entries — a build
exceeding any cap would be rejected wholesale on round-trip.

Cap each count at the load limit and use indexed loops so the
written body matches the header count even if the in-memory data
goes over.
2026-05-06 09:10:14 -07:00
Kelsi
c5008750ce fix(woc): cap triangle count and clamp tile coords on save
WOC load caps tris at 2M and clamps tile coords to 0..63. Save
previously wrote raw size() and tileX/Y — a >2M-tri collision
would be silently rejected on round-trip, and OOR tile coords
would log a warning every reload. Cap at save and reuse the
load-side clamp so the on-disk file is round-trip clean.
2026-05-06 09:08:07 -07:00
Kelsi
241722feaa fix(wom): cap bone/animation counts at save (matches load limits)
WOM load caps bones at 512 and animations at 1024. Save previously
wrote raw size() and iterated all entries — a model with >512 bones
would write fine but truncate on round-trip, and the post-truncation
keyframe data would be misread as the next animation.

Cap both counts at save and iterate using the capped value so the
per-bone keyframe block stays aligned with what load expects.
2026-05-06 09:06:46 -07:00
Kelsi
4a534c24e8 fix(wob): cap material count at 256 on save (matches load limit)
Save previously wrote raw materials.size() as the count, then iterated
all materials. Load caps at 256, so a build with >256 materials would
write fine but truncate on round-trip and the post-truncation block
would be misread as the next group's data. Cap at save and only write
the first 256.
2026-05-06 09:04:18 -07:00
Kelsi
4d6c65ab73 test(wob): add save-side scrub tests for NaN portals and vertices
Two new round-trip tests verify the save-side hardening:
- NaN portal vertices and out-of-range groupA/groupB indices are
  cleaned by save → load reads back finite verts and groupA/B = -1.
- NaN bld.boundRadius, group bounds, and vertex position/normal
  are scrubbed to safe defaults (1.0 boundRadius, zero pos, +Z up).

Locks in the recent WoB scrub work and ensures the on-disk format
stays self-consistent.
2026-05-06 09:00:03 -07:00
Kelsi
1d1cbafa95 test(camera): add 6 setter/basis-degeneracy tests
Locks in the recent Camera setter NaN/range guards and the
getRight/getUp fallback when forward is parallel to world up:
- setPosition rejects NaN/inf
- setRotation rejects NaN
- setFov rejects NaN/0/negative/>=180
- setAspectRatio rejects NaN/<=0
- getRight/getUp return finite at +/-89 pitch (clamped path)
- getRight/getUp degrade safely at exactly +/-90 (crosses to zero)

Brings ctest target count to 30.
2026-05-06 08:58:55 -07:00
Kelsi
500ad2e711 fix(camera): NaN/range guards on Camera setters
setPosition/setRotation/setAspectRatio/setFov now reject:
- NaN/inf inputs (would produce NaN view/proj matrix → frozen GPU
  on some drivers, garbage frustum culling everywhere)
- aspectRatio <= 0 (degenerate perspective)
- fov <= 0 or >= 180 (degenerate perspective)

Camera is constructed and set from many code paths; pushing the
guards into the setters means none of them need to remember.
2026-05-06 08:57:31 -07:00
Kelsi
39f4a433ff fix(camera): NaN-safe getRight/getUp when forward ~= world up
If forward is parallel to (0,0,1) — camera staring straight up or
down — the cross product is zero and glm::normalize returned NaN.
That NaN flowed into glm::lookAt and produced a NaN view matrix.

The editor camera clamps pitch to +/-89 so it doesn't trigger,
but other call sites or scripted test paths could construct a
Camera at +/-90 and immediately blow up. Length-check the cross
and fall back to world +X / +Z.
2026-05-06 08:55:49 -07:00
Kelsi
eba6b941e5 docs(formats): document the complete headless CLI surface
The CLI grew from 6 to 19 commands across recent batches —
catalogue them in FORMAT_SPEC so users can discover the headless
workflow without grepping --help. Grouped by purpose: inspection,
validation, authoring, packaging, discovery.
2026-05-06 08:53:45 -07:00
Kelsi
341c07d412 feat(editor): add --regen-collision CLI for batch WOC rebuild
Walks a zone directory recursively, finds every WHM file, and
rebuilds the matching WOC. Useful after batch terrain edits when
you want to refresh collision for many tiles in one shot. Reports
per-tile triangle counts and exits 1 if any rebuild failed.
2026-05-06 08:53:12 -07:00
Kelsi
cfd257aa78 fix(m2): skip NaN vertices when computing tight model bounds
glm::min/max on NaN is implementation-defined, so a single bad
vertex would propagate NaN into the camera-occlusion and culling
AABB used by the runtime. WOM/M2 loaders already scrub but defense
in depth catches anything they miss. Falls back to a unit box if
every vertex is bad.
2026-05-06 08:51:31 -07:00
Kelsi
61fb486b9e fix(m2): degenerate-normal guard during walkable-floor raycast
The matrix-transformed normal could be near-zero if the M2 instance
has a degenerate scale; glm::normalize then returns NaN that
contaminates the slope check (NaN < 0.35 is false → no early-out)
and bestNormalZ goes NaN, breaking the walkable-floor heuristic.

Length-check the transformed normal and fall back to the (0,0,1)
flat default — same pattern as the WMO renderer.
2026-05-06 08:49:26 -07:00
Kelsi
ae20fbf621 fix(wmo): degenerate-normal guard during tangent generation
A WMO vertex with zero-length or NaN normal would produce a NaN
normalized normal, contaminating the Gram-Schmidt tangent for the
whole vertex and producing visibly broken normal mapping for the
affected face. Length-check before normalize and fall back to
(0,0,1) when degenerate.
2026-05-06 08:47:31 -07:00
Kelsi
86c544b841 fix(wmo): degenerate-portal guard during portal plane build
A portal whose first three vertices are coincident or collinear
produces a zero cross product and glm::normalize returns NaN. The
NaN propagates into the portal-frustum cull (every interior group
either always-visible or never-visible depending on plane orientation).

Use the same length-check pattern as the editor's spline/path code:
zero cross → fall back to (0,0,1) up-axis.
2026-05-06 08:46:20 -07:00
Kelsi
b88c555830 feat(editor): add --zone-summary CLI for one-shot zone overview
Combines validate + creature/object/quest counts in a single
output. Useful for CI reports and quick sanity checks. Exits 0
if open-format score is 7/7 (full coverage), 1 otherwise.
2026-05-06 08:39:38 -07:00
Kelsi
eb251639cf fix(editor): NaN-safe baseHeight propagation in addAdjacentTile
Source tile's chunks[0].position[2] could be NaN if mid-edit
terrain hadn't run stitchEdges yet. Fall back to 100.0 so the
adjacent tile doesn't start with poisoned base.
2026-05-06 08:37:19 -07:00
Kelsi
b7e3266c7a fix(m2): NaN guards on createInstanceWithMatrix boundary
Mirrors the createInstance guard. position drives the dedup hash
key (std::round of NaN is implementation-defined) and the matrix
flows into the GPU UBO.
2026-05-06 08:36:11 -07:00
Kelsi
61116e94a5 fix(m2): NaN guards on setInstancePosition and setInstanceTransform
Same boundary-rejection pattern as createInstance. NaN in either
function would corrupt the spatial grid (stale cells pointing at
NaN-bounded instances) and the GPU model-matrix UBO.
2026-05-06 08:34:53 -07:00
Kelsi
f96ea12fe7 fix(m2): reject NaN inputs at M2Renderer::createInstance
Even with all the upstream guards I've been adding, internal callers
or addon-style scripted spawns could pass NaN. Reject at the API
boundary so we never hash-key with NaN coords (std::round of NaN
is implementation-defined) or push a NaN instance into the model-
matrix uniform buffer (GPU crash / origin render).
2026-05-06 08:32:48 -07:00
Kelsi
4cbffe17d5 feat(editor): add --diff-wcp CLI for archive comparison
Compares two WCP archives file-by-file from their info JSON: lists
added (+), removed (-), and size-changed (~) entries. Useful for
verifying that an authoring tweak changed only what it claimed to
change, and for editor-version regression detection. Exit code 0
if identical, 1 otherwise.
2026-05-06 08:29:21 -07:00
Kelsi
07f4043343 fix(viewport): clear ghost preview on NaN/non-positive inputs
Without this guard, NaN cursor position from a degenerate raycast
would feed directly into the M2 renderer instance transform and
either crash on GPU or silently render at the origin.
2026-05-06 08:24:51 -07:00
Kelsi
303eeb9107 fix(validate): require minimum body bytes when checking format magic
A 4-byte file with just the right magic and no body would pass
the previous magic-only check but fail any actual loader. Require
at least 8 bytes (magic + 1 field) for a file to count as 'valid'
in the score.
2026-05-06 08:24:08 -07:00
Kelsi
8fb7690ea1 feat(editor): add --export-png CLI for terrain preview rendering
Renders heightmap, normal-map, and zone-map PNGs alongside a
WHM/WOT terrain pair. Useful for portfolio screenshots, ground-
truth map comparison, and quick visual validation without
launching the GUI.
2026-05-06 08:22:26 -07:00
Kelsi
21078f8806 feat(editor): add --build-woc CLI for headless collision generation
Loads a WHM/WOT terrain pair and writes a .woc collision mesh
alongside it. Terrain triangles only (no WMO overlays — those need
the asset manager) but enough for first-pass walkability while
authoring.

Verified end-to-end: scaffold-zone → build-woc → info-woc reports
32k triangles for a flat 256-chunk tile.
2026-05-06 08:21:14 -07:00
Kelsi
81832ea676 feat(editor): add --pack-wcp CLI for headless zone packaging
Mirrors --unpack-wcp. Accepts either a zone name (auto-resolved
under custom_zones/ then output/) or a directory path. Default
output is <name>.wcp in the current directory. Combined with
--scaffold-zone and --unpack-wcp, the editor can do the full
zone authoring round-trip from the command line.
2026-05-06 08:19:42 -07:00
Kelsi
b2fa4cd509 feat(editor): add --unpack-wcp CLI for headless extraction
Mirrors --info-wcp / --list-wcp. Default destination is
custom_zones/ (matches the GUI's preferred location). With both
this and --scaffold-zone, the editor binary can fully bootstrap
a zone install without launching the GUI.
2026-05-06 08:18:21 -07:00
Kelsi
ab5d574758 feat(editor): add --scaffold-zone CLI for empty zone bootstrap
Creates custom_zones/<slug>/ with a flat-terrain WHM, default WOT,
and minimal zone.json — score 3/7 on --validate, ready to open in
the GUI for further authoring. Saves the round-trip of launching
the GUI just to make a starter directory.
2026-05-06 08:17:33 -07:00
Kelsi
6657f08252 fix(wob): sanitize portal vertices/group indices on load and save
Three issues addressed:
- NaN portal vertices break the WMO portal-frustum cull, defeating
  the indoor optimization and forcing the whole interior to draw.
- Out-of-range portal.groupA/groupB indices walk past wmo.groups
  during cull; clamp to -1 (invalid) on load.
- Symmetric save-side scrub so we don't persist bad in-memory data.
2026-05-06 08:15:31 -07:00
Kelsi
9d37da45d8 test(wcp): add truncation detection test
Locks in the recent unpack truncation guard. Hand-writes a WCP
that declares dataSize=1000 but only ships 50 bytes; verifies
the unpack returns false AND that no partial file landed on disk.
2026-05-06 08:13:43 -07:00
Kelsi
aa2a70de8d fix(wcp): detect truncated WCP files on unpack
Previously a short read (truncated WCP, partial download, etc.)
would silently write the partial bytes that were read and report
success — leaving the consumer with a half-extracted zone that
would fail in confusing ways at runtime. Check gcount and return
false so the caller can refuse the broken pack.
2026-05-06 08:13:04 -07:00
Kelsi
44777c7d58 fix(mesh): clamp NaN terrain heights to 0 in vertex generation
WHM load already scrubs, but mid-edit terrain can briefly carry
NaN before stitchEdges runs. A single NaN vertex propagates into
normal computations and the chunk's frustum cull, crashing both.
2026-05-06 08:11:43 -07:00
Kelsi
19a4716ec1 fix(sql): scrub NaN coords/orientation when emitting INSERTs
ostream prints NaN as 'nan' which AzerothCore's SQL import rejects
with a syntax error — would silently break the entire export from
a single bad spawn. Defensive scrub at write time, mirroring the
load-side guard pattern used everywhere else.
2026-05-06 08:09:06 -07:00
Kelsi
a0876bbe3d fix(cli): error on non-GUI options that are missing their argument
Previously '--info-wcp' (no path) silently dropped into the GUI
because the option-parse loop's i+1<argc guard hid the typo. Pre-
scan and bail out with a helpful message before trying to start
the editor, so users get fast feedback on bad invocations.
2026-05-06 08:08:05 -07:00
Kelsi
2400271a4f test(brush): add 6 EditorBrush::getInfluence tests
Locks in the recent NaN/zero-radius/zero-falloff guards:
- inner radius returns full influence (1.0)
- at/beyond outer radius returns 0
- rim falls off as 1 - t² (smooth)
- NaN/inf distance rejected (returns 0, not 1 from short-circuit)
- zero/negative/NaN radius rejected
- zero falloff produces hard-edge brush
2026-05-06 08:05:43 -07:00
Kelsi
c9c4665642 fix(water): skip chunks with NaN water height in mesh build
Belt-and-braces — WOT load already scrubs the height to 0 if non-
finite, but if a downstream caller mutates terrain.waterData
in-memory the bad value would leak into the water mesh and Vulkan
would drop the entire batch on a single bad chunk.
2026-05-06 08:04:44 -07:00
Kelsi
55f9616aa6 fix(camera): NaN guards on pivot orbit, setPosition, setYawPitch
Three issues:
- processMiddleMouseMotion: NaN pivot poisons camera position
  permanently; the next frame produces NaN view/proj matrices.
- setPosition: no input validation — used by bookmark restore and
  fly-to-target which could be passed a NaN target.
- setYawPitch: same; also clamp pitch to [-89, 89] to match the
  mouse-motion path so a saved bookmark with bad pitch doesn't
  roll the camera upside down.
2026-05-06 08:03:57 -07:00
Kelsi
d03c96e3bd test(wcp): add unpack security tests
6 tests covering ContentPacker::unpackZone defenses:
- absurd fileCount header rejected
- absurd infoSize header rejected (16MB cap)
- relative path traversal ('../../etc/passwd_clone') rejected
- absolute paths ('/tmp/...') rejected
- malicious zone name slugified instead of escaping destDir
- bad magic rejected

Each test confirms the defense fires AND that no escape file
landed outside the test dir.
2026-05-06 08:03:02 -07:00
Kelsi
98f2a6c3bf fix(gizmo): hide on NaN target instead of building NaN geometry
setTarget previously stored the position raw, then updateBuffers
ran glm::normalize on axis offsets. NaN target → NaN normalized
axes → NaN gizmo vertices → Vulkan validation drops the whole
draw and the gizmo is invisible regardless of target value.

Hide the gizmo upfront so the user sees no gizmo (which is the
intent of the NaN handling) without leaking garbage into the
vertex buffer.
2026-05-06 08:01:23 -07:00
Kelsi
cdc9bb94ee fix(npc): NaN guards in NpcSpawner::selectAt distance test
Same NaN-comparison short-circuit pattern: NaN worldPos or NaN
spawn position would short-circuit dist < bestDist (NaN < x is
false), so it never updates bestIdx — but if every entry had NaN
the bestIdx stays -1 (correct) only because the first comparison
also fails. Belt and braces: skip NaN entries explicitly.
2026-05-06 08:00:36 -07:00
Kelsi
94469592f2 fix(objects): NaN guards in selectAt ray-sphere test
Without these, NaN ray or NaN object position would short-circuit
the disc < 0 early-out (NaN comparisons return false) and select
the object at a garbage t — silently 'picking' arbitrary objects.
2026-05-06 07:59:33 -07:00
Kelsi
0f15d0f3a0 fix(terrain): reject NaN rays in raycastTerrain
Without this, the AABB tests divide by ray.direction components and
NaN propagates through tmin/tmax into the triangle intersection,
returning undefined behavior at the hit position.
2026-05-06 07:58:56 -07:00
Kelsi
493cb68ddc fix(viewport): defensive NaN checks in patrol path ribbon
len < 0.001f returns false for NaN — same short-circuit class of
bug as elsewhere. Reject non-finite endpoints upfront and double-
check the computed length before dividing.
2026-05-06 07:57:54 -07:00
Kelsi
ba017193db fix(viewport): skip NPCs with NaN position in marker update
Same defensive pattern as updateObjectMarkers — non-finite NPC
position would produce NaN vertex positions and Vulkan would drop
the entire NPC marker batch, hiding every NPC marker in the zone.
2026-05-06 07:57:19 -07:00
Kelsi
b1cfef8264 fix(markers): skip objects with NaN position/scale on update
A non-finite object transform would produce NaN vertex positions
in the marker mesh — Vulkan validation flags it and dropping the
entire batch leaves all markers invisible. Skip the bad object
instead so the rest of the markers still render.
2026-05-06 07:56:26 -07:00
Kelsi
45dabaff44 fix(wcp): normalize separators before traversal check on unpack
Older WCP files packed on Windows (before pack-side normalization
was added) carry backslash separators. Normalize to '/' first so
the unpack works on any platform — and so the traversal check sees
a consistent canonical form (no more '\' special case).
2026-05-06 07:54:54 -07:00
Kelsi
439d1381f0 fix(editor): catch NaN-from-normalize when camera flies to a target
When the camera looks straight up/down, projecting forward onto XY
gives a zero vector — glm::normalize then returns NaN. The original
length<0.001 fallback ran AFTER the divide-by-zero, and NaN length
< 0.001 is false (NaN comparisons return false), so the fallback
never fired. Length-check the source before normalizing.
2026-05-06 07:53:41 -07:00
Kelsi
130aa34d73 fix(viewport): hide path preview on zero-length input
start == end would call glm::normalize on a zero vector, producing
NaN dir/perp and NaN ribbon vertex positions. Vulkan would either
drop the draw silently or trip a validation error. Hide the preview
when the segment is degenerate.
2026-05-06 07:53:07 -07:00
Kelsi
4babaebf86 fix(stamp): scrub NaN samples at save time
Symmetric with the load-side scrub. Without this, a stamp captured
on terrain that had a NaN mid-edit would throw on serialize and
abort the whole save.
2026-05-06 07:51:56 -07:00
Kelsi
ebb7e0f831 fix(wcp): normalize path separators on pack for cross-platform reads
WCP packs created on Windows would store paths with backslashes;
unpack on Linux/macOS would either fail the path-traversal check
('\' treated as absolute prefix) or land each file as a single
opaque filename rather than a directory tree. Normalize to '/' on
write so the format is portable in both directions.
2026-05-06 07:50:10 -07:00
Kelsi
ad65b2ad36 test(editor): add 4 quest validateChains tests + rename to test_editor_units
Tests cover:
- non-existent nextQuestId is flagged
- orphan quests (no questgiver, no turn-in) are flagged
- turn-in only quest is accepted (auto-completed quest pattern)
- circular chain is detected

Renamed test_sql_escape → test_editor_units since the file now
houses both SQL escape and quest validation tests.
2026-05-06 07:49:26 -07:00
Kelsi
ea713ae994 test(sql): add escape unit tests
5 tests covering: doubled single quotes (King's Land case),
backslash escaping, ordinary text passthrough, control characters
(NUL drop, CR/LF/tab/Ctrl-Z escape sequences), and combined
escapes. Locks in the recent escape expansion that fixed the
multi-line INSERT bug.
2026-05-06 07:47:58 -07:00
Kelsi
5366c53734 fix(objects): NaN guards on transform deltas
A NaN move/rotate/scale delta would poison every selected object's
transform permanently and produce NaN model matrices in the
renderer. Reject upfront.
2026-05-06 07:45:26 -07:00
Kelsi
2c5710b910 fix(terrain): NaN guards + zero-length checks on river/road/ridge generators
Same defensive pattern as paintAlongPath. carveRiver, flattenRoad,
and createRidge all called glm::normalize on a possibly-zero
direction vector, then divided by lineLen later. NaN endpoints
short-circuited dist comparisons and applied the height delta
to every vertex on every chunk.
2026-05-06 07:43:39 -07:00
Kelsi
869cee70b1 fix(painter): reject NaN endpoints and zero-length lines in paintAlongPath
Two bugs:
1. NaN start/end produced NaN distances that the chunk-skip check
   (dist > width + 40) treated as 'always within range', so every
   chunk got painted.
2. Zero-length line caused glm::normalize to return NaN; same
   downstream effect.

Compute lineDir manually after the length check so we never hit
the divide-by-zero path.
2026-05-06 07:41:58 -07:00
Kelsi
7e48658ab1 fix(terrain): NaN guards on createCrater and createMesa
Same defensive pattern as createHill — NaN center/radius would
short-circuit dist comparisons and apply the height delta to every
vertex on every chunk. Reject upfront.
2026-05-06 07:40:29 -07:00
Kelsi
f484b742db fix(editor): NaN guards on flattenAroundSelected and createHill
Same NaN-comparison short-circuit pattern: dist >= radius is false
when dist is NaN, so the loop body would run for every vertex and
write garbage heights / bend the terrain unbounded.
2026-05-06 07:39:35 -07:00
Kelsi
12bcd0ef8c fix(brush): defensive guards in EditorBrush::getInfluence
Three issues:
1. NaN distance returned 1.0 (full influence) because distance >=
   radius is false for NaN; the inner-radius check then returned 1.
2. Non-positive radius would divide by zero in the t computation.
3. falloff = 0 produces division by zero in the outer falloff path.

Also clamps falloff to [0,1] so a slider extreme can't break the math.
2026-05-06 07:38:50 -07:00
Kelsi
bd6e5fe3de fix(brush): reject NaN/non-positive brush settings before sculpt apply
Same NaN-comparison short-circuit bug as the texture painter — a
brush with a NaN cursor position would mark every vertex in every
'affected' chunk as full influence and silently rewrite huge
swaths of terrain. Reject upfront in applyBrush.
2026-05-06 07:37:54 -07:00
Kelsi
9207d54f20 fix(painter): reject NaN brush center / non-positive radius
NaN comparisons return false, so the dist >= radius early-out
would never fire and the falloff path would skip its inner check
too — the brush would paint full strength on every texel in the
chunk. Reject upfront.
2026-05-06 07:37:11 -07:00
Kelsi
891bb711a0 fix(editor): bound tile coords + NaN-guard baseHeight in createNewTerrain
Same defensive validation as loadADT — out-of-range tile coords
would generate broken save paths. Also guards against a NaN
baseHeight slider (would propagate into every terrain vertex).
2026-05-06 07:36:32 -07:00
Kelsi
d9d0797b7f fix(wcp): cap info JSON string lengths at pack time
A stray gigantic name/description/author field would inflate the
info JSON past the 16MB unpack cap and make the pack unreadable
via readInfo/unpackZone. Caps mirror the zone manifest limits.
2026-05-06 07:35:44 -07:00
Kelsi
efd0a6de29 fix(editor): reject out-of-range tile coords in loadADT
A tileX/tileY outside 0..63 would generate ADT paths the asset
manager refuses, then poison the manifest.tiles entries on save.
Reject upfront with a log message.
2026-05-06 07:34:58 -07:00
Kelsi
237cc67b24 fix(wcp): sanitize zone name before using it as a directory path
The unpacker used info.name verbatim as the destination subdirectory.
A malicious WCP could carry a name like '../etc' or '/usr/bin' to
write extracted files outside destDir. Now slugified to alphanumeric
+ underscore/dash, matching the server module slug rule.
2026-05-06 07:34:09 -07:00
Kelsi
6b06bd07f9 feat(quest): detect orphan quests + speed up chain validation
validateChains now also flags quests with no questgiver and no
turn-in NPC — those are unreachable in-game and a common authoring
mistake. Also replaced the O(n²) inner lookup with an O(1)
unordered_map of id → nextId so circular detection scales.
2026-05-06 07:33:31 -07:00
Kelsi
778f4aca3e fix(wob): warn + clamp uint32 indices on WMO conversion
WoB allows uint32 indices but WMO format is uint16. The previous
static_cast would silently wrap a >65k index into a wrong-but-
valid value — producing visible mis-stitched triangles in the
renderer. Now log a warning once per group and clamp to 0
(degenerate triangle) so the bug is visible.
2026-05-06 07:32:07 -07:00
Kelsi
9d944ed2f9 feat(editor): add --list-wcp CLI to print every file in a WCP archive
--info-wcp gives counts and totals; --list-wcp gives the full file
listing sorted by path. Useful for spotting missing texture/model
entries before unpacking and shipping a zone.
2026-05-06 07:30:26 -07:00
Kelsi
9cb8b160ef fix(wob): clamp out-of-range indices at save time
Symmetric with the load-side index clamp.
2026-05-06 07:29:29 -07:00
Kelsi
dbb3be86f2 fix(wom): clamp out-of-range indices at save time
Symmetric with the load-side index clamp. A WoM whose indices
reference past the vertex buffer would crash the GPU vertex shader;
the save side now clamps to 0 (degenerate triangle) so the file
matches what the load guard would produce.
2026-05-06 07:28:31 -07:00
Kelsi
a0895fabdf fix(wom): drop invalid batches at save time
Symmetric with the load-side validation. A WOM3 batch whose
indexStart+indexCount exceeds the index buffer, or whose texture
index points past the texture array, would otherwise emit an
invalid file that the load-time guard then has to drop.

Filter at save instead so the on-disk file stays compact and
self-consistent.
2026-05-06 07:27:33 -07:00
Kelsi
c00bfab1a5 fix(wom): scrub NaN bone pivots and clamp parent indices at save time
Symmetric with the existing load-side guards. A bone with a NaN
pivot poisons its child bones' world matrices; an out-of-range
parent index would walk past the bones array during evaluation.
2026-05-06 07:26:26 -07:00
Kelsi
3b1fad7be9 fix(wob): scrub NaN/inf group vertices at save time
Symmetric scrub on the WoB save path matching the existing load
guard. A manually-constructed WoweeBuilding with NaN vertices
would otherwise persist them and force the load-time scrub to
re-clean the same data on every reload.
2026-05-06 07:24:51 -07:00
Kelsi
6347e78d72 fix(wom): scrub NaN/inf bone keyframes at save time
The load side already scrubs keyframe translation/rotation/scale
floats, but fromM2 → save → load is the typical path: a corrupt
M2 source would write NaN keyframes that the load-time guard would
have to clean up on every subsequent load. Symmetric scrub here
ensures the file is clean from the start.

movingSpeed also defaults to 0 if non-finite (matches load).
2026-05-06 07:23:35 -07:00
Kelsi
7a03011625 fix(sql): expand string escape to handle NUL, CR/LF/tab, Ctrl-Z
The previous escape only doubled quotes and backslashes. A quest
description containing a literal newline would emit a multi-line
INSERT that breaks per-line execution scripts; a NUL byte could
prematurely terminate the string in non-length-prefixed clients;
Ctrl-Z is the historical MySQL string terminator on Windows.

Now full MySQL/MariaDB string-literal escape: NUL drops, CR/LF/tab
become \r/\n/\t, Ctrl-Z becomes \Z.
2026-05-06 07:22:04 -07:00
Kelsi
19e479a8ff test(open-formats): add WOB doodad NaN scrub test
Verifies the recent WOB doodad-transform sanitize. A WOB with NaN
position/rotation/scale on a doodad should load with the doodad
zeroed out (position/rotation 0, scale 1). Prevents regressing
the GPU crash that NaN model matrices would cause.
2026-05-06 07:21:20 -07:00
Kelsi
a16689f7fa test(open-formats): add hardening tests + fix existing liquidType assertion
The existing WOT round-trip test asserted liquidType==5; the recent
commit clamped >3 to 0, so the test would have failed once rebuilt.
Updated the test data to use type=3 (slime, in valid range).

Adds 5 new hardening test cases:
- WOT clamps OOR tileX/tileY to 32
- WOT clamps OOR water liquidType to 0
- WOC load skips degenerate triangles
- WOC rejects > 2M triangle headers
- WOC clamps OOR tileX/tileY to 32

Catches regressions in the defensive bounds added across recent
commits.
2026-05-06 07:19:40 -07:00
Kelsi
a2a554dff7 feat(editor): add --info-quests CLI for quests.json summary
Completes the --info-* family. Reports total/chained quest counts,
reward/item counts, total XP awarded, objective-type breakdown
(kill/collect/talk), and any quest-chain validation errors. Lets
zone authors spot broken chains, missing rewards, and lopsided XP
balance from the command line.
2026-05-06 07:16:27 -07:00
Kelsi
df2027463a feat(editor): add --info-objects CLI for objects.json summary
Mirrors --info-creatures and the other format inspectors. Reports
total placement count, M2/WMO breakdown, unique source paths, and
scale range. Useful for spotting empty zones, accidental scale
extremes, or duplicated placements before packing.
2026-05-06 07:15:40 -07:00
Kelsi
93e67ae31b feat(editor): add --info-creatures CLI for creature.json summary
Mirrors the existing --info-wom/wob/woc/wot/wcp inspectors. Reports
total spawn count, hostile/questgiver/vendor/trainer flag counts,
behavior breakdown (stationary/wander/patrol), and unique displayId
count. Useful for triaging zone NPC content from the command line.
2026-05-06 07:14:42 -07:00
Kelsi
d1f347a9c1 fix(wob): sanitize doodad transform during fromWMO conversion
A WMO with a NaN doodad quaternion would produce NaN euler angles
through glm::eulerAngles() and persist them into the WoB. Identity
quaternion fallback for a non-finite source, plus NaN scrub on
position/rotation/scale separately so the converted WOB is always
load-safe.
2026-05-06 07:13:49 -07:00
Kelsi
5af4bba556 feat(validate): report file counts and per-format invalid totals
The previous --validate output told you whether *some* file of each
type existed, which was hard to act on for partially-valid zones.
Now reports the per-format file count and how many failed magic
validation, e.g. 'WOM (12 invalid: 2)' so a zone author can spot
missing or corrupted models without grepping through file listings.
2026-05-06 07:12:04 -07:00
Kelsi
7e2dc4ec1d fix(wcp): apply unpack-side header bounds to readInfo + categorize .woc
readInfo previously trusted fileCount/infoSize blindly, so a malicious
or corrupted WCP could allocate a 4GB string just to print metadata
via --info-wcp. Same 1M file / 16MB info caps now applied. Also
categorizes .woc collision files (was bucketed under 'other').
2026-05-06 07:10:53 -07:00
Kelsi
ed749b9afa fix(wot): clamp liquid type to known range on load
WoW liquid types are 0=water/1=ocean/2=magma/3=slime. A user-edited
WOT could carry an out-of-range value that the editor renderer
silently maps to plain water but the server treats as undefined.
2026-05-06 07:09:48 -07:00
Kelsi
1c1250a37c fix(wot): scrub NaN water height on load
A WOT water entry with non-finite height would push NaN through
the water mesh builder and produce a degenerate Vulkan draw
(invisible water at best, GPU hang at worst).
2026-05-06 07:08:50 -07:00
Kelsi
b8e2d08b17 fix(wob): scrub NaN/inf doodad transforms at save time
Same scrub now applied symmetrically on the save side so a
corrupted in-memory doodad transform can't be persisted into a
WOB and then have to be cleaned up on every subsequent load.
2026-05-06 07:07:21 -07:00
Kelsi
5d78cbb81d fix(wob): scrub NaN/inf doodad position+rotation on load
Already had a guard for scale; extending to position/rotation too.
A WoB with non-finite doodad transforms produces NaN model matrices
that propagate into the M2 instance SSBO and crash the GPU.
2026-05-06 07:06:25 -07:00
Kelsi
1135e499d7 fix(zone): scrub NaN/inf manifest floats at save time
Mirrors the load-side sanitize. nlohmann throws on NaN/inf, so
a corrupted in-memory baseHeight or volume would abort the manifest
save and lose all zone-level settings (flags, audio config, etc.).
2026-05-06 07:05:13 -07:00
Kelsi
49feb8b9f6 fix(objects): scrub NaN/inf floats at save time
Same json-serialization-safety fix as for NPC and WOT saves.
Object position/rotation NaN would abort the entire objects.json
save and lose all placement work in the session.
2026-05-06 07:04:27 -07:00
Kelsi
999388b805 fix(npc): scrub NaN/inf floats at save time
nlohmann::json throws on non-finite serialization, which would
abort the entire creatures.json save and lose every spawn change
in the session. Scrub position/orientation/scale/radii/patrol
floats on the way out so a single corrupt NPC can't kill the
batch save.
2026-05-06 07:03:51 -07:00
Kelsi
9ef04414d1 fix(wot): scrub NaN/inf in doodad/WMO placements at save time
nlohmann::json serialization throws on NaN/inf floats, which would
abort the entire WOT save and leave the user with an unsaved zone
state. Scrub on the way out so a single corrupted placement can't
take down the whole save.
2026-05-06 07:03:02 -07:00
Kelsi
826d218226 fix(zone): cap manifest strings at AzerothCore SQL limits
map_dbc.MapName / area_table_dbc.AreaName are varchar(100). Edited
zone.json could carry longer strings that would either fail the
INSERT or silently truncate. Cap mapName/displayName at 100,
biome at 64, description at 4096.
2026-05-06 07:02:09 -07:00
Kelsi
2119546a7a fix(npc): truncate over-long NPC name/modelPath at SQL limits
creature_template.name is varchar(100) in AzerothCore. Edited
creature JSON could carry longer names that would either fail
the INSERT or silently truncate. Cap name at 100 and modelPath
at 1024 on load.
2026-05-06 07:01:25 -07:00
Kelsi
280fe1e6e8 fix(quest): truncate over-long title/desc/completion to SQL limits
quest_template.LogTitle is varchar(200) in AzerothCore. Edited
quest JSON could carry longer strings that would either fail the
INSERT or silently truncate at the server. Cap title at 200 chars
and the longer text fields at 8KB on load.
2026-05-06 07:01:00 -07:00
Kelsi
3614a7dcd5 fix(zones): clamp discovery mapId and tile coords on scan
Mirrors the editor-side ZoneManifest sanitize on the discovery
scanner used by the launcher and asset manager. A custom_zones/
zone with bad mapId or out-of-range tile coords would otherwise
appear in the picker and silently fail when the user selects it.
2026-05-06 07:00:04 -07:00
Kelsi
6651eccf3b feat(editor): add --info-wot CLI for inspecting terrain metadata
Mirrors the existing --info-wom/--info-wob/--info-woc/--info-wcp
inspectors. Reports the tile coord, populated chunks, layer count,
water count, texture/doodad/WMO counts, and computed height range
across all chunks. Useful for triaging zones from the command line
without opening the GUI.
2026-05-06 06:58:34 -07:00
Kelsi
d127053e21 fix(objects): bound populateBiome density and asset scale ranges
Same defensive guards as scatter: cap per-asset count at 50k to
prevent editor freeze under a high-density biome, and ensure scale
range preconditions (a<b, both positive) so distScale construction
doesn't undermine validity.
2026-05-06 06:56:32 -07:00
Kelsi
c1af157587 fix(objects): guard ObjectPlacer::scatter against bad inputs
Same defensive guards now applied to NPC scatter: reject NaN/inf
center/radius, cap count at 100k (prevents editor freeze on huge
inputs), and ensure minScale < maxScale so uniform_real_distribution
preconditions hold.
2026-05-06 06:55:43 -07:00
Kelsi
10e77f1c2e fix(npc): guard NpcSpawner::scatter against zero radius and bad inputs
A radius of 0 would either throw from the uniform distribution
constructor (uniform_real_distribution requires a < b) or divide
by zero in the sqrt-based area-uniform sampler. Also reject NaN
center, non-positive radius, and absurdly large counts (>10k)
which would freeze the editor on placement.
2026-05-06 06:54:48 -07:00
Kelsi
d96d040831 fix(sql): substitute generic displayId for NPCs with displayId=0
A creature_template row with modelid1=0 spawns an invisible NPC
in-game. Fall back to 11707 (a generic humanoid) on export so
"Creature"-named placeholder spawns are at least usable; the user
can edit the displayId after.
2026-05-06 06:53:47 -07:00
Kelsi
1e378fb4ce fix(wot): clamp tile coords and scrub NaN doodad/WMO placements
A WOT JSON could carry tile coords outside 0..63 (would compute
chunk world positions tens of thousands of units off-grid) or NaN
position/rotation values on doodad/WMO placements (would propagate
into rendering matrices and produce invisible geometry).
2026-05-06 06:51:51 -07:00
Kelsi
aa9ef6f2ca fix(adt): scrub NaN/inf floats at write time
ADTWriter::writeFloat now coerces non-finite values to 0 before
emitting bits, so a stray NaN in a chunk position, MDDF rotation,
or MODF extent can't leak into the saved ADT and produce invisible
terrain or off-map placements after reload.
2026-05-06 06:49:51 -07:00
Kelsi
bc1839d9ab fix(zone): drop out-of-range tile coords on manifest load
WoW's tile grid is 64x64. A tile (200,200) entry would generate an
ADT filename the loader rejects and silently leave the zone with no
terrain. Drop bad entries instead.
2026-05-06 06:48:28 -07:00
Kelsi
4a98dd7a2c fix(editor): slugify mapName for module dir + conf keys
A zone named with spaces or punctuation (e.g. "My Zone!") used to
produce a module directory path "mod_wowee_My Zone!/" and conf
keys like "Wowee.My Zone!.Enabled" which AzerothCore's config
parser rejects. The slug is now stripped to [A-Za-z0-9_-], with
spaces/slashes mapped to underscores. SQL VALUES still use the
raw display text via SQLExporter::escape.
2026-05-06 06:47:43 -07:00
Kelsi
a8464fc367 fix(editor): escape user strings in server module map/zone/tele SQL
The map_dbc, area_table_dbc, and game_tele INSERTs previously
embedded mapName/displayName/manifest.mapName as raw strings — a
zone called "King's Land" or anything containing a single quote
would emit malformed SQL that AzerothCore would reject. Promotes
the existing escapeSql helper to a public SQLExporter::escape and
uses it in all three INSERTs.
2026-05-06 06:46:58 -07:00
Kelsi
cc1e1cb7fa fix(editor): cap stamp vertex count and skip NaN samples on load
A malformed stamp JSON could carry millions of entries (would OOM)
or NaN dx/dy/height (would propagate through brush blends and leave
permanent holes in the heightmap).
2026-05-06 06:45:41 -07:00
Kelsi
0a5583310c fix(editor): clamp project mapId and zone tile coords on load
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.
2026-05-06 06:45:09 -07:00
Kelsi
721efb2ecb fix(zone): clamp manifest audio volumes and reject NaN baseHeight on load
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.
2026-05-06 06:44:30 -07:00
Kelsi
185b7b522d fix(wob): sanitize boundRadius + per-group boundMin/Max at save time
Same float-NaN scrub for WoB save matching the WHM/WOC/WOM saves.
Building boundRadius defaults to 1.0 if non-finite; per-group bounds
zero out non-finite components (would otherwise corrupt the cull
frustum).
2026-05-06 06:39:49 -07:00
Kelsi
d25654d11a fix(wom): sanitize boundRadius/min/max floats at save time
Mirrors the WHM and WOC save sanitize. boundRadius defaults to 1.0 if
non-finite (matches load-time default); boundMin/boundMax components
zero out non-finite values. Prevents an in-memory model with a NaN
spike (e.g. mid-edit) from being persisted into the WOM and requiring
load-time cleanup forever after.
2026-05-06 06:36:46 -07:00
Kelsi
663d34af0d fix(woc): sanitize triangle vertices + bounds at save time
In-memory collision can be polluted by addMesh on bad input (e.g. a
WMO with NaN vertex positions). Without this, the save would persist
that NaN into the WOC and the load-time guards would have to clean
it up forever. Now scrubs vertices and bounds at write time, matching
the WHM save sanitize.
2026-05-06 06:35:05 -07:00
Kelsi
71378c20ff fix(whm): exportOpen sanitizes heights + caps alphaSize at 64KB on save
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.
2026-05-06 06:32:24 -07:00
Kelsi
2df49c725f fix(editor): texture exporter validates BLP image before passing to stbi
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.
2026-05-06 06:29:26 -07:00
Kelsi
d3a85776f8 fix(content-pack): packZone truncates path length + skips files >4GB
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
2026-05-06 06:27:04 -07:00
Kelsi
5019e21787 fix(wom): writeStr helper truncates length-prefixed strings to fit u16 length
Same fix as WoB save just got. Without truncation a model name or
texture path over 65535 chars would silently get a wrap-around length
and corrupt the file.
2026-05-06 06:25:33 -07:00
Kelsi
361ff712d7 fix(wob): writeStr helper truncates length-prefixed strings to fit u16 length
Without truncation, names over 65535 chars would silently get a wrap-
around length value and produce a corrupt file (the actual string would
be longer than the saved length, shifting everything after it). Single
shared writeStr lambda replaces five copy-pasted (uint16 length + bytes)
write blocks.
2026-05-06 06:23:35 -07:00
Kelsi
be64298218 fix(dbc): cap JSON DBC string sizes (4KB/string, 64MB total) + NaN floats
JSON DBC values are mostly small integers, but a malicious file could
stuff 100MB strings into every cell to OOM the stringBlock (which has
no per-string or total cap). Added 4KB per-string + 64MB total caps —
fields exceeding either are zeroed. Float fields also get NaN scrub
(same pattern as the earlier vertex/keyframe guards).
2026-05-06 06:19:01 -07:00
Kelsi
719951976d fix(wom+wob): reject path traversal in WOM texture paths + WOB material/group texPaths
Same defensive check as the WoB doodad path guard. Texture paths from
hostile WOM/WoB are passed to the asset manager; '..' or absolute paths
could probe outside the assets/ tree. Now cleared on detection — slot
survives but loads no texture (renderer falls back to white).

Single shared rejectTraversal lambda in WoB to avoid copy-paste.
2026-05-06 06:16:54 -07:00
Kelsi
c4463ba96e fix(wob): reject doodad paths with traversal/absolute components
Doodad model paths from a WoB are passed to the asset manager via
outModel.doodadNames. The asset manager only reads files, but '..' or
absolute paths from a hostile WoB could probe for files outside the
expected assets/ tree. Now clears the modelPath on traversal — the
doodad slot survives but loads no model.
2026-05-06 06:14:22 -07:00
Kelsi
bbfc364119 fix(editor): texture exporter rejects path-traversal in source M2/WMO texture paths
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.
2026-05-06 06:12:11 -07:00
Kelsi
b5a9ce7816 fix(assets): cap PNG override texture dimensions at 8K to prevent OOM
stbi_load happily decodes any PNG up to 32K x 32K — at 4 bytes/pixel
that's 4GB which OOMs the editor before the override even returns.
WoW textures top out at 4K; 8K cap leaves headroom for HD upgrades
without enabling abuse. Also widens the wxh multiplication to size_t
to defeat int overflow on 8K x 8K images.
2026-05-06 06:09:13 -07:00
Kelsi
2d8c843704 fix(dbc): cap JSON DBC fieldCount/recordCount to prevent OOM on hostile file
Real DBCs cap at ~250 fields and a few million records (Spell.dbc is
the biggest at ~50K rows). A malicious JSON DBC declaring fieldCount=
1G or recordCount * recordSize > 256MB would OOM the recordData
allocation. Now rejects upfront — JSON DBCs are user-shareable so a
zone export downloaded from a forum should not be able to OOM the
client by including a bad data table.
2026-05-06 06:07:09 -07:00
Kelsi
5b6f59bbbd fix(adt): scrub NaN/inf in MDDF + MODF placement floats
ADT placement positions and rotations are loaded as raw floats from the
binary chunks. A corrupted MDDF/MODF entry could feed NaN into the
M2/WMO instance transform and crash render. Now position/rotation are
scrubbed for both MDDF doodads and MODF buildings; MODF also scrubs the
extentLower/extentUpper bounding box used for cull tests.
2026-05-06 06:05:17 -07:00
Kelsi
15bf77c616 fix(editor): NPC stat field bounds across the board
Across-the-board NPC stat sanity on JSON load:
  - level: 0 -> 1, >255 -> 255
  - health: 0 -> 1
  - maxDmg < minDmg -> bumped to minDmg
  - behavior enum: clamp to 0..3 range
  - wander/aggro/leash radius: NaN/inf -> default; wander capped at 1000
  - respawnTimeMs: <1s -> 1s, >24h -> 24h
2026-05-06 06:02:10 -07:00
Kelsi
f5fc23e003 fix(editor): quest level + reward sanity bounds + item slot cap
Rounds out the quest load guards:
  - requiredLevel: 0 -> 1 (no level-0 quests), >255 -> 80 (typo guard)
  - reward.xp: cap 1M
  - reward.gold: cap 10000
  - reward.silver/copper: cap 99 (server overflows otherwise)
  - reward.itemRewards: cap 6 entries (matches WoW quest_template
    RewardItemId[1..6] slot capacity)
2026-05-06 05:59:05 -07:00
Kelsi
c4c8d9e7ed fix(editor): quest objective load clamps type, count, and per-quest size
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)
2026-05-06 05:56:11 -07:00
Kelsi
62b668e898 fix(content-pack): cap WCP per-entry path length + catch backslash/drive traversal
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.
2026-05-06 05:54:31 -07:00
Kelsi
c0c1be1c9e fix(wob): cap material/doodad path lengths + portal vertex count
Three more per-record sanity bounds that the per-group sweep didn't
cover:
  - material.texturePath length cap (1KB) — was unbounded
  - doodad.modelPath length cap (1KB) — was unbounded
  - portal.vertexCount cap (4096) — real portals are 4-12 verts;
    >4K is corrupt and would OOM the resize
2026-05-06 05:52:58 -07:00
Kelsi
93edf1bd55 fix(wob): per-group count sanity bounds + group name length cap
The WoB top-level header sanity bounds catch obviously-bad totals, but
each group's vc/ic/tc was still unbounded. A corrupted group could
declare 4G vertices and OOM the resize before the next group even
started. Now per-group: vc<=1M, ic<=4M, tc<=1K, name<=1KB.
2026-05-06 05:49:15 -07:00
Kelsi
cb7f11f2ea fix(wom): cap total keyframes at 10M to prevent OOM on hostile model
Per-bone-anim cap of 10K keyframes still let a malicious file allocate
up to 1024 anims × 512 bones × 10K keys = 5.24B keyframes — multi-GB
pre-OOM allocation. Now tracks total across the whole model and stops
allocating after 10M (real models stay well under 100K). When the cap
is hit we still seek past the remaining payload to keep file alignment
intact for whatever follows.
2026-05-06 05:46:55 -07:00
Kelsi
a7d62d1af9 fix(woc): reject corrupted triCount + clamp out-of-range tile coords
Header sanity bounds for WOC matching the WOM/WoB ones. A whole-tile
collision mesh maxes at ~32K terrain tris + a few thousand building
overlay tris; triCount > 2M is corrupted and would OOM. Tile coords
are 0..63 in WoW; clamp to 32 with warning when bogus.
2026-05-06 05:44:37 -07:00
Kelsi
90289ba48b fix(wob+wom): reject corrupted header counts before allocating
Adds upfront sanity bounds to both WoB and WOM load:
  WOM: vert<=1M, index<=4M, tex<=1K
  WOB: groups<=4K, portals<=8K, doodads<=64K
Real WoW models stay well under these limits (M2 vert is uint16 anyway).
Without these checks a corrupted header could trigger a multi-GB
allocation and OOM the process before we finish reading the body. Also
caps name length to 1KB on WoB load (already done on WOM).
2026-05-06 05:42:50 -07:00
Kelsi
c05d421c29 fix(content-pack): reject malicious WCP headers (oversize counts + path traversal)
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.
2026-05-06 05:38:53 -07:00
Kelsi
53780de6da fix(wob): clamp out-of-range group indices + reject absurd texture path lengths
Mirrors the WOM index-clamp + texture-path-length guards. Out-of-range
indices into the WMO group's vertex buffer would crash the GPU draw.
Texture path length over 1KB indicates a corrupted/truncated WoB; clamp
to 0 to prevent allocating 65KB-string buffers per bad entry.
2026-05-06 05:36:20 -07:00
Kelsi
a2eaf3965a fix(wom): clamp out-of-range indices + reject absurd texture path lengths
Out-of-range indices were a silent vector overrun on the GPU side that
could crash the vertex shader on some drivers. Replace with 0 rather
than dropping so triangle counts stay aligned (a degenerate triangle is
harmless, an off-by-one indexing the wrong vertex is silent corruption).

Texture path length over 1KB is almost certainly a corrupted or
truncated file — was previously read into a 65KB-string allocation per
entry which could exhaust memory on a malicious file.
2026-05-06 05:34:41 -07:00
Kelsi
fd4354c17d fix(wom): fromM2 sanitizes vertex floats during conversion (matches WOB)
Same NaN scrub during fromM2 conversion that fromWMO got. Ensures a
corrupt source M2 (mangled MPQ block, partial extraction) doesn't
silently produce a NaN-laced WOM. Also feeds the cleaned positions
into boundMin/boundMax so the saved WOM bounds are clean too.
2026-05-06 05:32:47 -07:00
Kelsi
7acde76025 fix(wob): fromWMO sanitizes vertex floats during conversion
Sanitize at conversion time too, not just on WoB load. Avoids the
case where a corrupt source WMO (extracted from a partially-decoded
MPQ) silently poisons the WOB the editor exports — and the WOB
load-time guard from the previous commit only catches it on later
reload, not during the first conversion. Also catches the boundRadius
calculation which would otherwise inherit a NaN from one bad vertex.
2026-05-06 05:29:30 -07:00
Kelsi
2b5f69187e fix(woc): fromTerrain skips degenerate triangles before normalize
The walkability classifier did glm::normalize(cross(...)) without
guarding for zero-length cross. A flat-on-itself triangle (e.g. all
three vertices at the same height in a hole-edge case) produces NaN
normal, NaN walkability flag, and crashes the downstream nz check.
Now the cross-length is computed once and the triangle is skipped if
it's effectively zero.
2026-05-06 05:26:46 -07:00
Kelsi
70bbed4222 fix(woc): scrub NaN triangle vertices + skip degenerate triangles on load
NaN vertices in collision triangles produce NaNs in ray-triangle
intersection (used by movement collision queries), making the player
phase through walls or fall through the floor. Degenerate triangles
(zero-area, collinear) similarly produce NaN normals. Now both are
sanitized/skipped on load and the count is reported in the log.
2026-05-06 05:24:46 -07:00
Kelsi
a3fb267e0b fix(wom): sanitize per-keyframe translation/rotation/scale + movingSpeed NaN
Bone interpolation returns NaN for any NaN input. A single bad keyframe
in any animation would corrupt the entire skeleton during playback —
even bones that weren't being keyed in that animation got NaN final
matrices via parent-chain multiplication. Also catches movingSpeed which
leaks into the engine's displacement maths.
2026-05-06 05:22:32 -07:00
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
2b02ca6b58 fix(editor): patrol waypoint NaN guard + 10-minute wait-time cap
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.
2026-05-06 05:05:39 -07:00
Kelsi
604d29d375 fix(editor): NPC + object position/rotation NaN guards on JSON load
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.
2026-05-06 05:04:25 -07:00
Kelsi
d525318e9c fix(editor): normalise NPC orientation to [0,360) and guard scale against NaN
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.
2026-05-06 05:02:24 -07:00
Kelsi
1979d921a7 fix(editor): clamp PlacedObject scale to 1.0 when JSON value is invalid
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.
2026-05-06 04:59:28 -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
70e48640d8 feat(editor): surface quest-chain validation issues to user via toast
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.
2026-05-06 04:54:51 -07:00
Kelsi
df1eed1c42 fix(editor): preserve CreatureSpawn.id across JSON save/load
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.
2026-05-06 04:53:09 -07:00
Kelsi
00717543a8 feat(editor): preset selection auto-fills sensible AzerothCore faction
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.
2026-05-06 04:48:30 -07:00
Kelsi
70366dc5f6 fix(sql): wander_distance is 0 for non-Wander NPCs
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.
2026-05-06 04:46:57 -07:00
Kelsi
2d41e560f2 docs(format-spec): add quick-reference table mapping formats to replaced Blizzard formats 2026-05-06 04:44:24 -07:00
Kelsi
b1c89823d4 docs(format-spec): tighten WHM + WOM layouts with exact field sizes
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.
2026-05-06 04:42:04 -07:00
Kelsi
15630f7723 docs(format-spec): tighten WOB layout description with exact field sizes/types
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.
2026-05-06 04:38:15 -07:00
Kelsi
079ff5bfb5 feat(editor): --info-wcp CLI prints content pack metadata
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.
2026-05-06 04:36:40 -07:00
Kelsi
8d78b5f8c6 fix(content-pack): unpackZone now creates the zone subdirectory
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.
2026-05-06 04:32:28 -07:00
Kelsi
a0e363f706 feat(editor): WCP export toast reports file size in MB 2026-05-06 04:30:13 -07:00
Kelsi
c0ae924fc7 fix(editor): NPC default scale 1.0 (was 3.0) to match AzerothCore defaults
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).
2026-05-06 04:27:22 -07:00
Kelsi
4d11949048 fix(editor): preserve PlacedObject uniqueId across JSON save/load
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.
2026-05-06 04:23:39 -07:00
Kelsi
882321863a feat(editor): NPC template + selected-NPC editor expose Respawn (s) input
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.
2026-05-06 04:21:42 -07:00
Kelsi
8d006b6b86 feat(editor): selected-NPC editor gains Mana / Min Dmg / Max Dmg / Armor inputs
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.
2026-05-06 04:18:25 -07:00
Kelsi
a7ab2756d6 feat(editor): selected-NPC editor gains role flag checkboxes
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.
2026-05-06 04:17:31 -07:00
Kelsi
9625201952 feat(editor): selected-NPC editor gains Faction input (parity with template) 2026-05-06 04:15:39 -07:00
Kelsi
4e01dd5553 feat(editor): NPC template gains Faction ID input with common-value tooltip
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.
2026-05-06 04:14:33 -07:00
Kelsi
da2e7a4133 feat(editor): viewport WOM/WOB lookups also probe per-zone roots
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.
2026-05-06 04:13:03 -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
99aaab3aa8 feat(editor): add Trainer/Banker/Auctioneer/Repair NPC flags + SQL export
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.
2026-05-06 04:03:23 -07:00
Kelsi
bc6e60c6e9 polish(editor): placement scale slider matches selected-object range (0.1-50) 2026-05-06 04:00:07 -07:00
Kelsi
a156b6246e polish(editor): NPC selected-editor Facing slider shows 'deg' unit (matches template) 2026-05-06 03:59:10 -07:00
Kelsi
597c6547ac feat(editor): WMO objects also try WOB open format first like M2->WOM does
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.
2026-05-06 03:56:52 -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
cbf1d4638f fix(editor): WMO instance scale actually applied to renderer
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.
2026-05-06 03:52:40 -07:00
Kelsi
32ff80f177 feat(editor): texture panel shows total pool count + dir count 2026-05-06 03:48:37 -07:00
Kelsi
f4805b8e69 feat(editor): object panel shows total pool count for M2/WMO assets
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.
2026-05-06 03:47:54 -07:00
Kelsi
ebf90eba9f fix(editor): zone manifest reset on load + auto-load existing zone.json
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.
2026-05-06 03:46:40 -07:00
Kelsi
f9187ef58a fix(editor): exportZone preserves user displayName + dedupes tile list
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.
2026-05-06 03:42:54 -07:00
Kelsi
28c63cb6d9 fix(editor): exportContentPack uses zoneManifest.mapId instead of hardcoded 9000
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).
2026-05-06 03:41:29 -07:00
Kelsi
f856a90281 feat(editor): preserve WMO instance scale across ADT load/save
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.
2026-05-06 03:40:03 -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
446b0970dc fix(sql): translate spawn.id to creature SQL entry for quest links
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
2026-05-06 03:33:36 -07:00
Kelsi
d258144df4 docs(format-spec): document WOB->WMO restoration details under WOB section 2026-05-06 03:31:04 -07:00
Kelsi
f022459971 fix(editor): NPC template scale slider matches selected-NPC editor range (0.1-50)
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.
2026-05-06 03:30:30 -07:00
Kelsi
d5bbc28fe1 test(wob): cover toWMOModel material dedupe / portal / doodad / doodadSet
New TEST_CASE exercises the full toWMOModel restore path: materials
deduped across groups, outdoor flag preserved per group, portal vertex
+ groupA/groupB refs reconstructed, doodad path .wom->.m2 conversion,
default doodadSet emission. 113 assertions across 16 test cases now.
2026-05-06 03:28:40 -07:00
Kelsi
3abe47adc6 perf(editor): periodic M2 model GPU cache cleanup every 30s
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.
2026-05-06 03:26:39 -07:00
Kelsi
c1b6c9f621 fix(editor): exportZone clears autoSavePendingChanges_ flag
quickSave was the only path that cleared the flag, but exportZone is
also reachable through 'Export Open Format' and exportContentPack
without going through quickSave. Now any successful zone export clears
the dirty state so the asterisk and quit-confirm dialog reset properly.
2026-05-06 03:26:01 -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
f18976ced9 feat(editor): --info-woc CLI prints collision mesh metadata
Completes the --info* CLI family. Reports tile coords, triangle count,
walkable/steep classification breakdown, and world-space bounds — useful
for verifying that collision exports cover the expected area.
2026-05-06 03:17:10 -07:00
Kelsi
8787b13dc1 feat(editor): --info-wob CLI prints WOB building metadata
Companion to --info for WOM. Reports name, group/portal/doodad counts,
total vertex/triangle/material counts. Useful for verifying converted
WMOs and debugging building rendering issues without launching the GUI.
2026-05-06 03:15:43 -07:00
Kelsi
683d703fbc feat(editor): --info <wom> CLI prints model metadata for inspection
Useful for verifying WOM exports and debugging conversion issues without
loading the GUI. Accepts either /path/to/file.wom or /path/to/file
(loader expects no extension). Reports version, name, geometry counts,
texture/bone/animation/batch counts, and bound radius.
2026-05-06 03:14:12 -07:00
Kelsi
248dcd4eb4 feat(editor): quest objective limit raised to 10 (matches SQL slot capacity)
UI was capped at 4 but the SQL exporter writes RequiredNpcOrGo[1..4] +
RequiredItemId[1..6] = 10 total slots. Allowing 10 lets users define
mixed kill+collect quests fully.
2026-05-06 03:13:26 -07:00
Kelsi
ce778ed674 feat(editor): patrol waypoint reorder (up/dn) + insert-after-cursor (+after)
Previously waypoints could only be appended or removed; reordering meant
clearing and re-adding the whole path. Now each waypoint row has up/dn
swap buttons and a +after that inserts a new waypoint at the current
brush cursor right after this index — slicing long segments doesn't
require redoing the rest of the path.
2026-05-06 03:12:45 -07:00
Kelsi
0be537e73d feat(editor): --validate <zoneDir> CLI scores zone open-format completeness
New CLI option that runs ContentPacker::validateZone on a zone directory
and prints the open-format score (0-7) with per-file breakdown including
magic-byte validity. Exits 0 if 7/7, 1 otherwise — useful for CI checks
on exported zones.
2026-05-06 03:09:56 -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
7822790c60 feat(editor): status bar asterisk also reflects unsaved object/NPC/quest changes 2026-05-06 03:01:09 -07:00
Kelsi
848947604e fix(editor): quit-confirm dialog also triggers for unsaved object/NPC/quest changes
Previously only terrain edits would trigger the 'unsaved changes' prompt
on quit, so a user who only added NPCs or quests could lose their work
by closing the window. Now checks autoSavePendingChanges_ alongside
terrain dirty state.
2026-05-06 03:00:10 -07:00
Kelsi
11f0580ccb docs(format-spec): bump to v1.2, document WOC mesh-append + SQL export
- WOC: add note that addMesh() also appends placed WMO group geometry
- New section on SQL server export covering coord/orientation conversion
  rules, table list, and how quest objectives map to RequiredNpcOrGo/
  RequiredItem slots.
2026-05-06 02:57:07 -07:00
Kelsi
7b2cbcfc92 feat(editor): status bar shows cursor world position alongside camera
Cam in dim yellow, cursor (when brush is on terrain) in cyan. Useful for
quickly noting positions where to drop spawns/objects without flying over
to read the brush coords manually.
2026-05-06 02:56:10 -07:00
Kelsi
6610d950cb fix(editor): flyToSelected uses atan2(to.y, to.x) for correct camera yaw
Camera::getForward = (cos(yaw), sin(yaw), sin(pitch)) — to make it
parallel to a direction vector we need atan2(y, x). The implementation
had x and y swapped, causing Fly To to point the camera 90deg off from
the target so the user often saw nothing.
2026-05-06 02:55:05 -07:00
Kelsi
fa631a45d6 refactor(sql): use core::coords::renderToCanonical for render->WoW swap
Replaces hand-written x/y swap with the canonical helper from
include/core/coordinates.hpp so the conversion stays in sync if the
coord convention changes.
2026-05-06 02:52:46 -07:00
Kelsi
0f42ebab3d feat(editor): quest objectives now have a Target ID field + spawn picker
The SQL exporter writes objective targets to RequiredNpcOrGo/RequiredItem
slots based on the objective's targetName field, but the UI never let
the user fill that field. Added an InputText for Target ID and, for Kill
objectives, a dropdown that auto-fills with the entry of any placed NPC.
2026-05-06 02:51:12 -07:00
Kelsi
d44a8a48ce feat(editor): patrol path waypoints color-coded by traversal order
Path direction was ambiguous from a static screenshot: ribbon and waypoints
were uniform orange/white. Now ribbons fade from bright at the start to
dim at the end, and waypoints go green (NPC home) -> yellow/orange
(intermediate) -> red (last) so direction of travel reads at a glance.
2026-05-06 02:50:04 -07:00
Kelsi
e041ae7ac7 fix(sql): convert editor yaw (from +renderX/west) to WoW yaw (from +X/north)
Editor's orientation is measured from +renderX (which maps to west in WoW
canonical), but AzerothCore creature.orientation is the WoW yaw measured
from +X (north). Without conversion an editor 0deg NPC ended up facing
west in-game, off by 90deg even after the radians fix. Now applies
`wowYaw = π/2 - editorYaw` and normalizes to [0, 2π).
2026-05-06 02:49:02 -07:00
Kelsi
edce3abf41 fix(sql): swap render-coord x/y to WoW canonical for creature/waypoint export
Editor stores positions in render coords (renderX=wowY=west, renderY=wowX
=north, renderZ=wowZ=up) but AzerothCore creature.position_x/y are in
WoW canonical space (X=north-south, Y=west-east). Without the swap every
exported creature appeared on the wrong end of the map. Same swap now
applied to creature spawns AND waypoint_data path points.
2026-05-06 02:47:03 -07:00
Kelsi
b30b44ab7e chore(cli): --convert-m2 reports WOM version + batch count in success message 2026-05-06 02:45:23 -07:00
Kelsi
ee686051a5 test(woc): cover addMesh slope classification + extra-flag preservation
Two new TEST_CASEs verify WoweeCollisionBuilder::addMesh marks flat
triangles walkable and steep ones not, and that caller-supplied flags
(e.g. indoor 0x08) are OR'd onto the slope-derived flags. 98 assertions
across 15 test cases now.
2026-05-06 02:44:47 -07:00
Kelsi
469f046db5 feat(editor): NPC list shows hostile/friendly/questgiver/vendor counts
Helps users see at-a-glance the makeup of their zone's creature pop
without having to scroll the list looking at each entry's flags.
2026-05-06 02:41:23 -07:00
Kelsi
273c2fe10c feat(sql): export quest objectives to RequiredNpcOrGo/RequiredItem slots
quest_template now writes up to 4 KillCreature objectives into the
RequiredNpcOrGo/RequiredNpcOrGoCount slots and up to 6 CollectItem
objectives into the RequiredItemId/RequiredItemCount slots. The numeric
target ID is parsed from the objective's targetName field — empty/non-
numeric targets emit 0 (objective hooked up but unwired).
2026-05-06 02:38:05 -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
b7d9d54b29 feat(sql): emit creature_queststarter/questender for quest-NPC links
quest_template alone tells the server about the quest but not who hands
it out. Without creature_queststarter/questender entries, players can't
acquire or turn in the quest. Now writes both tables when the quest has
questGiverNpcId / turnInNpcId set.
2026-05-06 02:35:29 -07:00
Kelsi
22c9bc354c feat(editor): placed-object list also has name filter (parity with NPC list) 2026-05-06 02:34:41 -07:00
Kelsi
590ec6b3a3 feat(editor): NPC spawn list has name filter for fast lookup
A zone with 50+ creatures made finding a specific spawn tedious. Added
a case-insensitive name-filter input above the list, capped at 200
visible entries with a 'refine filter' hint when exceeded.
2026-05-06 02:34:05 -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
fdd527b373 fix(build): asset_extract tool needs extern/ in include path for nlohmann/json
asset_extract pulls in src/pipeline/dbc_loader.cpp which #includes
<nlohmann/json.hpp>, but the tool's include directories didn't list
extern/ where the header lives. Build succeeded if asset_extract was
disabled (no StormLib) but failed otherwise. Added the extern/ system
include so the tool builds wherever StormLib is found.
2026-05-06 02:30:04 -07:00
Kelsi
88c105103b fix(content-pack): validateZone accepts WOM2/WOM3 as valid WOM files
ContentPacker.validateZone only matched WOM1 magic (0x314D4F57). Any zone
exported with animated (WOM2) or multi-batch (WOM3) models was scored as
having invalid WOM files, lowering the open-format score from 7/7 to 6/7
even though everything is correct. Now accepts the WOM family.
2026-05-06 02:18:37 -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
49b7268dc9 feat(editor): show camera (x,y,z) in status bar
Helpful for navigation and noting positions for spawn placement, plus
diagnostic value when debugging coordinate issues. Shown only after a
terrain is loaded.
2026-05-06 02:10:50 -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
7c506f582a feat(editor): flyToSelected places camera with proper aim and offset
Previously just teleported camera 30u directly above the target — the user
still had to manually look down to see anything. Now positions the camera
back along the current view direction, slightly elevated, and aims it at
the selection so the target is visible immediately. Removes the round-trip
through manual rotation after every Fly To.
2026-05-06 02:04:50 -07:00
Kelsi
f1223cfc69 chore(editor): cap texture-not-found warnings at 5 with suppression count
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Character body/skin textures live in CharSections-composed paths that
don't exist as standalone BLPs. Exporting a zone with many character
NPCs would spam hundreds of warnings. Now logs the first 5, suppresses
the rest, and reports the total skipped count in the summary line.
2026-05-06 02:03:30 -07:00
Kelsi
bbdd48a78a fix(adt): MODF entry was 60 bytes but parser expects 64 — write nameSet+scale tail
Each MODF entry in ADT files is 64 bytes (per MODF spec). The writer was
emitting only 60 bytes, missing the trailing nameSet(u16) + scale(u16).
The loader uses entrySize=64 to advance per record, so any saved ADT with
more than one WMO placement misaligned every subsequent record on reload.
Now pads with nameSet=0 and scale=1024 (=1.0 fixed-point).
2026-05-06 02:02:01 -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
f6187e7f9a feat(editor): expose Display ID field in selected-NPC editor
SQL export uses CreatureSpawn.displayId for creature_template.modelid1.
Without it AzerothCore can't render the creature in-game. Added an
InputInt + warning text so users can set the displayId manually until
auto-discovery from CreatureDisplayInfo.dbc lands.
2026-05-06 01:57:49 -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
ac6038fe46 fix(editor): syncToTerrain inverts the load-time rotation transform too
ADT loading transforms placement rotations:
  obj.rot = (-dp.rot[2], -dp.rot[0], dp.rot[1] + 180)
syncToTerrain previously wrote them back unchanged, so save->load would
flip and shift orientation. Now applies the inverse:
  dp.rot = (-obj.rot.y, obj.rot.z - 180, -obj.rot.x)
which round-trips identically.
2026-05-06 01:54:10 -07:00
Kelsi
1f97cb957e fix(editor): syncToTerrain converts world coords back to ADT placement coords
ADT loading converts MDDF/MODF positions from ADT space to render/world
space via core::coords::adtToWorld, but syncToTerrain wrote object
positions back as raw render coords. So saving and reloading would
displace every placed object/WMO, accumulating each save cycle. Now
calls worldToAdt() to round-trip cleanly.
2026-05-06 01:53:32 -07:00
Kelsi
63fe5da04e feat(editor): selected NPC marker is larger and yellow-cyan tinted
Marker geometry now reacts to npc.selected: 2.5x base radius (vs 1.5x),
saturated yellow with cyan tinge, and full alpha. Marker rebuild also
fires on selection change so the highlight appears immediately rather
than only after the next placement.
2026-05-06 01:51:59 -07:00
Kelsi
b491ecb435 fix(editor): call WMORenderer::prepareRender before render
Mirror the M2 fix: the editor was skipping the WMO renderer's per-frame
state advance, so material UBO updates and frame ID tracking were stale
relative to the main game's render flow. Most visible effect is that
material setting toggles wouldn't propagate to the GPU.
2026-05-06 01:49:32 -07:00
Kelsi
2a2c217ae3 feat(editor): auto-save fires for any unsaved change (objects, NPCs, quests)
Auto-save was gated on terrainEditor.hasUnsavedChanges() so a session
where the user only edited NPCs or quests would lose data on crash.
Added autoSavePendingChanges_ flag flipped by every objectsDirty_ = true
site (and markObjectsDirty), cleared by quickSave. Auto-save now fires
on either dirty signal.
2026-05-06 01:48:34 -07:00
Kelsi
552e0d22e2 feat(editor): export creature/faction/item DBCs alongside zone DBCs
Spawned NPCs reference CreatureDisplayInfo, CreatureModelData, faction
templates, etc. Without exporting these the JSON DBC pack only covered
terrain data and exported zones couldn't resolve creature display IDs
on a clean install. Added: CreatureDisplayInfo, CreatureModelData,
CreatureType, CreatureFamily, FactionTemplate, Faction, ItemDisplayInfo.
2026-05-06 01:41:53 -07:00
Kelsi
4fd285b5c4 feat(editor): collectWMOTextures recurses into WMO doodad M2 textures
WMO buildings reference M2 doodads (chairs, candles, banners) via the
MODD chunk. Their textures live in those M2 files, not the WMO root.
Now collectWMOTextures walks every doodad name and collects M2 textures
recursively so exported buildings include all their interior decoration
textures.
2026-05-06 01:40:44 -07:00
Kelsi
f1d332825e feat(editor): export WMO textures with the zone
Placed WMO buildings reference textures (walls, floors, decorations) that
were not being exported alongside the WOB files. Added collectWMOTextures()
which loads the root WMO + all group files and gathers every texture path,
then folds these into the same PNG export pass that handles terrain and M2
textures. Exported zones now have every texture they need across all model
types.
2026-05-06 01:40:05 -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
9eae67d574 feat(editor): NPC scatter snaps each spawn to ground when enabled
The scatter tool spawned creatures all at the cursor's z-height which
made them hover when scattered over uneven terrain. Added a Snap to
Ground checkbox (defaults to on) that raycasts each scattered NPC and
places it on the actual surface.
2026-05-06 01:35:26 -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
8daa81eecb feat(editor): transform gizmo works on selected NPCs
Selected NPCs now display the move/rotate/scale gizmo and respond to
drag operations. Rotate uses only the yaw axis (NPCs have a single
orientation field, not full euler rotation). Move/scale work the same
way as objects.
2026-05-06 01:31:34 -07:00
Kelsi
b49cb344ac feat(editor): NPC context menu adds Snap to Ground and Face Camera
Right-click context menu on a selected NPC now mirrors the object menu:
Snap to Ground drops it to terrain elevation, Face Camera rotates the NPC
to face the current camera position. Also annotates Duplicate with the
Ctrl+D shortcut hint.
2026-05-06 01:30:27 -07:00
Kelsi
ec50c41044 fix(sql): convert NPC orientation from degrees to radians for AzerothCore
The editor's orientation field is stored in degrees (matches the UI slider
and the M2 renderer's glm::radians() call), but AzerothCore's creature.
orientation column expects radians. Without conversion every exported NPC
faces the wrong direction in-game (off by 57x).
2026-05-06 01:28:46 -07:00
Kelsi
09f573c6ee feat(editor): patrol UI shows loop length and per-point wait time editor
The patrol list now shows total loop distance and gives each waypoint a
draggable wait-time field (0-60s). Helps tune patrol pacing without re-saving
the JSON manually.
2026-05-06 01:27:50 -07:00
Kelsi
59a6c22625 test(wom): add WOM3 magic-distinctness check 2026-05-06 01:27:14 -07:00
Kelsi
700416b2c7 feat(editor): scale slider in selected-NPC editor
Once an NPC is placed there was no way to resize it without removing and
re-placing. Added a Scale DragFloat in the selected-creature editor with
the same 0.1-50 range used for placed objects.
2026-05-06 01:23:42 -07:00
Kelsi
7c4afdbca4 feat(editor): double-click in NPC/object list flies camera to entry
Single-click selects, double-click also flies to. Removes the trip back to
the side panel to hit the Fly To button when navigating long lists.
2026-05-06 01:22:44 -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
f39869ef6a docs(editor): document new shortcuts in F1 Keyboard Shortcuts dialog
Adds Ctrl+D duplicate, Ctrl+Wheel rotation, NPC click-to-select, and
W-for-waypoint to the help text so users discover the new bindings.
2026-05-06 01:17:57 -07:00
Kelsi
7d88c5f538 feat(editor): Ctrl+D duplicates the selected object or NPC at (10,10) offset 2026-05-06 01:17:25 -07:00
Kelsi
e342f2ba55 chore(wmo): demote 'cleared all WMO models' message to INFO level 2026-05-06 01:16:18 -07:00
Kelsi
53b2fc78fa feat(editor): patrol path visualization closes the loop back to start 2026-05-06 01:14:58 -07:00
Kelsi
f097763875 feat(editor): click on existing NPC marker selects it instead of placing duplicate
Plain left-click within 4u of an existing NPC now selects that NPC rather
than dropping a new spawn on top. Shift+click forces placement at the cursor
even if it overlaps an existing NPC, preserving the rapid-placement workflow.
2026-05-06 01:13:43 -07:00
Kelsi
7fcc923d2e feat(editor): W hotkey adds patrol waypoint at cursor in NPC mode
When an NPC with Patrol behavior is selected in NPC mode, pressing W
appends a waypoint at the current cursor position. Faster than clicking
the panel button for laying out long routes.
2026-05-06 01:12:51 -07:00
Kelsi
4b3375ac44 feat(editor): export NPC/M2 model textures as PNG with the zone
TextureExporter::collectUsedTextures only picked up terrain textures, so
exported zones were missing every texture referenced by NPC creature models
and placed M2 doodads. Added collectM2Textures() and unified the export
collection to include terrain + all referenced M2 paths, so the rendered
zone is fully self-contained in the PNG/WOM open formats.
2026-05-06 01:11:47 -07:00
Kelsi
732e58355a feat(editor): convert NPC creature models to WOM during zone export
Previously only placed M2 objects were converted to the WOM open format.
NPC creature models stayed as references to game M2/skin files, which
meant exported zones still depended on proprietary Blizzard assets to
render their NPCs. Now the exporter walks both placed objects and NPC
spawns and emits a WOM for every unique M2 path, making zones fully
self-contained.
2026-05-06 01:09:38 -07:00
Kelsi
35ad340ccc feat(editor): facing arrow on NPC markers + Ctrl+Wheel hint in object panel
NPC markers now show a yellow ground-triangle pointing in the orientation
direction so users can see facing without selecting. Object panel gained
the Ctrl+Wheel rotation hint to match the NPC panel.
2026-05-06 01:08:32 -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
00c078a9af feat(editor): snapSelectedToGround now also snaps NPCs and their patrol waypoints 2026-05-06 01:03:50 -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
eb8f5a09b1 feat(editor): visualize patrol path of selected NPC as ribbon with waypoints
Adds setPatrolPath() that draws a multi-segment orange ribbon between the
NPC's spawn position and each waypoint, plus diamond markers at each point
(green for start = NPC home, white for waypoints). Renders only while the
NPC is selected and has a patrol path defined.
2026-05-06 00:58:30 -07:00
Kelsi
191ff9ec16 feat(editor): NPC orientation control + Ctrl+Wheel rotates placement preview
Added orientation slider in NPC panel with random button. Ctrl+Wheel now
rotates the placement preview (objects and NPCs) instead of zooming —
Shift makes the step finer (5 deg vs 15 deg). Ghost preview now shows
the actual orientation that the placed NPC will have.
2026-05-06 00:56:19 -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
687923c885 fix(editor): call M2Renderer::prepareRender to allocate mega bone slots before render
Without prepareRender(), animated NPC creature instances had megaBoneOffset=0
which caused the render loop to skip them (filtered out at the bones check).
This is why all loaded NPC M2s produced 0 draw calls despite valid GPU buffers,
material sets, and instance creation. Matches the main game's render flow.
2026-05-06 00:47:21 -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
e97d6d0c23 fix(editor): remove nested upload batch, add instance creation diagnostics 2026-05-06 00:16:25 -07:00
Kelsi
a845723635 fix(editor): create M2 instances AFTER upload flush (isValid fix)
ROOT CAUSE: createInstance() checks mdlRef.isValid() which requires
vertexBuffer != VK_NULL_HANDLE. But vertex buffers are uploaded via
staging and only finalized by waitAllUploads(). Instances were being
created BEFORE the upload flush, so vertexBuffer was still null,
cachedIsValid was set to false, and all instances were skipped during
render (0 draws despite loaded models).

Fix: split rebuildObjects into two phases:
1. Load all models (upload geometry to staging)
2. waitAllUploads + pollUploadBatches (finalize GPU buffers)
3. Create all instances (vertexBuffer is now valid, isValid() = true)

This matches the client's terrain_manager pattern where models are
loaded on background threads and instances created after finalization.
2026-05-06 00:11:44 -07:00
Kelsi
c9c4a15e9a fix(editor): remove per-model upload waits, single batch flush
Log showed: models=2, instances=3, draws=0 — models loaded and
instances created but zero draw calls. The M2 renderer skips
instances where cachedIsValid is false, which depends on the GPU
vertex buffer being valid after upload.

The per-model waitAllUploads/pollUploadBatches calls inside the
loading loop may have corrupted the upload batch context (started
at beginUploadBatch but flushed mid-loop). Now all models upload
in a single batch with one final waitAllUploads+pollUploadBatches
at the end of rebuildObjects.
2026-05-06 00:06:40 -07:00
Kelsi
c077ee2cee fix(editor): add per-frame M2 render diagnostics (models/instances/draws) 2026-05-06 00:02:02 -07:00
Kelsi
6f066dee48 fix(editor): debounce M2 rebuild to prevent clear+reload loop
ROOT CAUSE of NPC models not rendering: every NPC placement triggered
an immediate full clear+rebuild of ALL M2 models. During rapid clicking,
this created a destroy-reload cycle where models were cleared faster
than they could render — the log showed rebuild firing every ~200ms
with models loading OK but being destroyed before the next frame.

Fix: debounce rebuilds with a 0.5s timer. Multiple rapid placements
reset the timer, so the rebuild only fires once after the user stops
clicking. Models stay loaded and visible between placements.

Before: click → clear all → reload all → click → clear all → reload...
After:  click → click → click → (0.5s pause) → single rebuild
2026-05-05 23:59:51 -07:00
Kelsi
09e867eb07 fix(editor): move rebuildObjects AFTER beginFrame (instance SSBO fix)
ROOT CAUSE of NPCs not rendering: rebuildObjects() called createInstance()
BEFORE beginFrame(), causing instance SSBO writes to use the wrong frame
index. The M2 renderer writes instance transforms to a per-frame buffer
indexed by getCurrentFrame(), but the frame index isn't valid until after
beginFrame() returns.

This is the same bug documented in the project memory:
"M2 models not rendering (draws=0): update() was called before
beginFrame(), causing frame index mismatch in instance SSBO"

Models loaded correctly (log confirmed 2192v 7926i 8b) but instances
were invisible because their transform data was written to the wrong
frame buffer.

Fix: move the rebuild block after beginFrame(), alongside update().
2026-05-05 23:53:52 -07:00
Kelsi
f1133cdfa7 fix(editor): NPC diagnostics use WARNING level (INFO invisible in Release)
Release builds set default log level to WARNING — all the NPC loading
diagnostic messages were at LOG_INFO/LOG_DEBUG and completely invisible.
This is why the log showed zero NPC messages despite NPCs being placed.

All NPC loading messages now use LOG_WARNING:
- "NPC rebuild: N creatures to load" — confirms rebuild loop runs
- "NPC M2 OK: path (Nv Ni Nb)" — model loaded successfully
- "NPC loaded from WOM: path" — WOM format used
- "NPC model file not found: path" — file missing
- "NPC model invalid: path" — parse failed
- "NPC M2 loadModel failed: path" — GPU upload failed
- "NPC has empty modelPath: name" — no model selected
2026-05-05 23:49:46 -07:00
Kelsi
5506fd155b fix(editor): all NPC loading messages now WARNING level (were DEBUG)
NPC model loading diagnostics were at LOG_DEBUG level which doesn't
appear in the default log output. Changed all NPC model loading
messages to LOG_WARNING/LOG_INFO:
- "NPC model file not found" now WARNING (was DEBUG, invisible)
- "NPC has empty modelPath" new WARNING for missing model selection
- "Loading N NPC models..." at loop entry to confirm rebuild runs
- "NPC M2 loaded" at INFO level shows successful loads

This will reveal exactly where the NPC rendering pipeline fails.
2026-05-05 23:25:57 -07:00
Kelsi
ef04159b46 fix(editor): upgrade NPC model loading diagnostics to WARNING level
Changed NPC model invalid and load success messages from LOG_DEBUG to
LOG_WARNING/LOG_INFO so they appear in the log output. This helps
diagnose why specific creature models fail to render — the log will
show vertex/index/batch counts for each load attempt.
2026-05-05 23:06:34 -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
d773109b50 feat(editor): WMO-only instance loading (Dire Maul, dungeons, raids)
Many WoW instances (Dire Maul, Blackrock Depths, etc.) are WMO-only
maps with no ADT terrain tiles. The editor now handles these:

- loadWMOInstance(): reads WDT, detects WDTF_GLOBAL_WMO flag, loads
  the root WMO path from MWMO chunk, places it as an object with
  correct position/rotation from MODF chunk
- Automatic fallback: when loadADT fails to find tiles, tries
  WMO-only loading before showing error
- Load dialog: "Find Tile" detects WMO-only instances and shows
  "WMO-only instance — click Load to open" instead of "not found"
- Camera positioned near the WMO for immediate editing
- Blank terrain floor generated as ground reference
2026-05-05 22:44:38 -07:00
Kelsi
072d0f78c2 fix(editor): begin upload batch before M2 model loading in rebuild
rebuildObjects() calls clearObjects() which clears the M2 renderer,
but didn't start a new upload batch before loading models. The M2
renderer needs an active upload batch context to upload vertex/index
buffers to the GPU. Without it, loadModel may silently fail.

Now calls vkCtx_->beginUploadBatch() after clear and before the
model loading loop. Also adds diagnostic logging when loadModel
fails for NPCs (shows vertex/index/batch counts).
2026-05-05 22:40:58 -07:00
Kelsi
98223995fc fix(editor): placed objects also use WOM format + always load skin
Applied same two fixes from NPC renderer to placed object renderer:
1. Check WOM open format before M2 fallback (custom_zones/output dirs)
2. Always load skin file regardless of initial isValid state

Both placed objects (M2 doodads from ADT import or manual placement)
and NPC creatures now have consistent WOM→M2 fallback pipeline with
proper skin file loading.
2026-05-05 22:29:31 -07:00
Kelsi
3f5b030a4a feat(editor): NPC models load from WOM open format before M2 fallback
NPC creature renderer now checks for WOM open format models in
custom_zones/models/ and output/models/ before falling back to M2
game data files. This completes the WOM integration — both placed
objects (via terrain_manager) and NPC creatures (via editor viewport)
can render from the novel open format.

WOM→M2Model conversion includes bone weights, textures, render batch,
and material setup — same pipeline as the client-side WOM loader.

Loading priority: WOM (open format) → M2 + skin (game data)
2026-05-05 22:27:35 -07:00
Kelsi
47bfe35b26 fix(editor): always load M2 skin file for NPC models
WotLK M2 models store geometry in separate .skin files. The NPC
renderer was only loading skin files when M2Loader::load() returned
invalid (empty vertices). But some M2 files have vertices in the
header yet need the skin file for indices, batches, and submeshes.

Now always attempts to load the skin file regardless of initial
isValid() state. This fixes creature models not rendering even
when the M2 and skin files exist on disk.

Also improved debug logging to show vertex/index counts when models
fail to load, making it easier to diagnose remaining issues.
2026-05-05 22:26:10 -07:00
Kelsi
92787901d8 feat(editor): tile count button in ADT load dialog
"Count" button next to "Find Tile" scans all 64x64 tile coordinates
and shows how many ADT tiles exist for the selected map. Helps users
understand the scope of a map before loading individual tiles.
2026-05-05 22:23:18 -07:00
Kelsi
cc7ec8e497 fix(editor): clear previous state on ADT load, fix model orientations
Two bugs fixed:
- Loading a new ADT tile now clears all previous objects, NPCs, quests,
  path state, and terrain before loading. Was accumulating old state
  across multiple loads
- ADT doodad/WMO rotation conversion now matches client's transform:
  renderRotX = -adtRotZ, renderRotY = -adtRotX, renderRotZ = adtRotY+180
  Was copying raw ADT rotations without coordinate system conversion,
  causing models to appear at wrong orientations
2026-05-05 22:19:18 -07:00
Kelsi
e287cc9a78 fix(editor): shrink NPC markers, add show/hide toggle
- NPC markers reduced from 30-unit poles to 8-unit poles (was
  overwhelming and obscuring terrain). Base 1.5u, diamond top 1u
- "Show Position Markers" checkbox in NPC panel to toggle visibility
- Markers hidden when checkbox unchecked — useful when M2 creature
  models are rendering and markers are redundant
- Marker alpha reduced for less visual noise
2026-05-05 22:11:53 -07:00
Kelsi
d84ad82e26 fix(editor): recompute chunk positions after ADT coord override
When loading ADT tiles, the editor overrides terrain_.coord with the
filename-derived tile coordinates (instanced maps have arbitrary
internal values). But it wasn't recomputing the per-chunk world
positions to match, causing terrain to render at wrong coordinates.

Now recalculates all 256 chunk positions from the corrected tile
coordinates using the standard WoW formula:
  chunkX = (32 - tileX) * 533.33 - cx * 33.33
  chunkY = (32 - tileY) * 533.33 - cy * 33.33

This fixes terrain appearing at the wrong location in the editor
when loading instanced maps or tiles with mismatched internal coords.
2026-05-05 22:05:43 -07:00
Kelsi
9d14bc19cb fix(editor): log when NPC model files not found (helps debug rendering) 2026-05-05 21:59:51 -07:00
Kelsi
1db1166bec fix(editor): NPC markers now always render (pipeline binding bug)
NPC position markers were silently not rendering when no object was
selected because they relied on the gizmo's pipeline being bound, but
the gizmo only binds its pipeline when active. Now explicitly binds
the water pipeline (same pos+color vertex format with alpha blend)
before drawing NPC markers, ensuring they always appear regardless of
gizmo state.

Note: M2 creature models still require extracted game data files to
render. When model files aren't found, the colored markers (poles with
diamond tops) provide reliable visual feedback for NPC positions.
2026-05-05 21:58:32 -07:00
Kelsi
fde1fa8129 fix(editor): batch convert CLI has visible output and proper exit codes
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
- --convert-m2 and --convert-wmo now print progress and results to
  stdout (was LOG_INFO to log file only, invisible to user)
- Failures return exit code 1 (was always 0, breaking scripting)
- Success output shows vertex/bone counts (M2) or group count (WMO)
- Error messages go to stderr for proper pipe handling
2026-05-05 16:57:06 -07:00
Kelsi
1337a963a0 fix(editor): CLI output visible on stdout (was silent LOG_INFO)
--help, --version, and --list-zones now print to stdout via printf
instead of LOG_INFO which writes to log file only. Users running
CLI commands would see no output at all — critical first-impression
bug for new users.

Also updated --version format list to include WOC.
2026-05-05 16:54:03 -07:00
Kelsi
c7f6a81cdb docs: add biome vegetation and line count to CHANGELOG 2026-05-05 16:43:47 -07:00
Kelsi
110f250150 feat(editor): biome vegetation auto-population system
One-click procedural object placement based on biome rules:

- 10 biome vegetation rulesets with density, scale range, and slope
  constraints per asset type (trees, bushes, rocks, ferns, etc.)
- Grassland: pine trees + bushes + rocks
- Forest: ashenvale trees + ferns + forest rocks (dense canopy)
- Jungle: palm trees + ferns + vines (high density)
- Desert: cacti + desert rocks + bones (sparse)
- Barrens: scattered trees + dry bushes + rocks
- Snow: snow pines + snowdrifts + rocks
- Swamp: dark trees + mushrooms + logs
- Rocky: rock formations + rock piles
- Beach: palm trees + beach rocks
- Volcanic: lava rocks + charred trees

Objects panel > Auto-Populate Biome: select biome, set seed, click
"Populate Zone" to fill the entire tile with biome-appropriate
vegetation at rule-defined densities.
2026-05-05 16:42:41 -07:00
Kelsi
0d401c3eb8 docs: document server module generator in CHANGELOG, README, help panel
- CHANGELOG: add server module generator description
- README: add AzerothCore integration paragraph with feature summary
- Help panel (F1): add "File → Generate Server Module" to export section
2026-05-05 16:34:40 -07:00
Kelsi
cceff622b4 feat(editor): AzerothCore server module generator
One-click generation of a complete AzerothCore/TrinityCore server module
from editor zone data. File > Generate Server Module creates:

- sql/01_map.sql: map_dbc + area_table_dbc registration
- sql/02_spawns.sql: creature_template + creature + waypoint_data + quests
- sql/03_teleport.sql: game_tele entry for .tele command
- sql/04_zone_flags.sql: sanctuary/PvP area flags
- conf/mod_wowee.conf.dist: worldserver.conf snippet with zone settings
- README.md: step-by-step server admin installation guide
- module.json: machine-readable module manifest

Server admins can import the SQL files, add the config snippet, and
restart their server to have the custom zone fully operational with
NPC spawns, patrol paths, quests, teleport commands, and zone flags.
2026-05-05 16:31:13 -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
109b288573 docs: add SQL export and zone map to CHANGELOG, update line count 2026-05-05 16:08:18 -07:00
Kelsi
b19627d82c feat(editor): SQL spawn export for AzerothCore/TrinityCore
Private server integration: export creature spawns, patrol waypoints,
and quest definitions as ready-to-import SQL for AzerothCore/TrinityCore.

- creature_template: name, level, health, mana, damage, armor, faction,
  npcflag (questgiver/vendor/flightmaster/innkeeper), displayId, scale
- creature: spawn position, orientation, respawn time, wander distance,
  movement type (stationary/wander/patrol)
- creature_addon + waypoint_data: patrol path waypoints with delays
- quest_template: title, description, completion text, level, XP, money
- All use ON DUPLICATE KEY UPDATE for safe re-imports
- Auto-exported as spawns.sql alongside other assets on zone save
- File > Export Server SQL menu item for standalone export
- Map ID from zone metadata panel used in spawn table
2026-05-05 16:06:34 -07:00
Kelsi
84a431880e feat(editor): zone map image export (colored top-down PNG)
- exportZoneMap(): renders terrain as colored top-down image with
  height-based coloring (blue lowlands → green plains → brown hills →
  white peaks), water overlay, hole visualization, doodad markers
- Configurable resolution (128-2048px, default 512)
- Auto-exported as zone_map.png alongside other assets on save
- File > Export Zone Map menu with resolution slider
- Useful for documentation, server admin tools, custom map websites
2026-05-05 16:01:29 -07:00
Kelsi
998d09e119 docs: update CHANGELOG with latest features
- Zone metadata (mapId, displayName, gameplay flags)
- Zone audio configuration
- PNG/JPG heightmap import
- Collision slope overlay
- Client WOC loading
- Line count updated to 13.8k+
2026-05-05 15:55:19 -07:00
Kelsi
9db5cced2c feat(editor): zone metadata panel with mapId, displayName, gameplay flags
- Map ID: configurable integer input (0-65535) for private server
  integration. Custom zones default to 9000+ to avoid Blizzard conflicts
- Display Name: editable text field for in-game world map/loading screen
- Description: multi-line text field for zone documentation
- Zone Flags: Allow Flying, PvP Enabled, Indoor, Sanctuary checkboxes
- All fields serialized to zone.json under "flags" key
- Info panel shows quest count alongside objects/NPCs
2026-05-05 15:52:59 -07:00
Kelsi
2136727c68 feat(editor): zone audio configuration panel
- Zone manifest gains audio fields: musicTrack, ambienceDay,
  ambienceNight, musicVolume, ambienceVolume
- Serialized to/from zone.json under "audio" key
- Info panel: collapsable "Zone Audio" section with text inputs for
  music/ambience paths and volume sliders
- Preset selector: Elwynn Forest, Durotar, Darkshore, Dungeon, None
- ZoneManifest stored persistently on EditorApp so audio settings
  survive between exports (was recreated each save)
- Custom zones can now specify their own background music and ambient
  soundscapes via zone.json
2026-05-05 15:48:49 -07:00
Kelsi
36dc9ddef7 feat(editor): PNG/JPG/BMP heightmap image import, undo for all imports
- importHeightmapImage(): loads any resolution PNG/JPG/BMP/TGA via
  stb_image, supports both 8-bit and 16-bit precision, maps to terrain
  vertices with bilinear coordinate mapping
- Both image import and RAW import now wrapped with undo
  (recordGeneratorUndo/commitGeneratorUndo)
- UI: File > Import Heightmap now offers "Import Image" (any format)
  and "Import RAW" (binary) as separate options
- Enables professional terrain workflows: paint in Photoshop/GIMP,
  generate in World Machine/Gaea, import directly as terrain
2026-05-05 15:42:35 -07:00
Kelsi
33042c3a47 feat(editor): collision slope overlay, 7/7 format references
- Minimap slope overlay: toggle "Show Slopes (collision)" to visualize
  steep terrain chunks with red intensity overlay (50 deg threshold)
- Lets zone creators preview walkability before exporting WOC
- About dialog updated to show 7/7 format replacements including WOC
- Help panel documents collision preview and export includes collision
2026-05-05 15:37:08 -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
a25c4cfe1f docs: update README and CHANGELOG for 7/7 formats including WOC
- README: "7 novel open format replacements" with WOC collision listed
- CHANGELOG: 7/7 format count, WOC entry, validation score 0-7,
  328 test assertions across 84 test cases
2026-05-05 15:27:07 -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
961d863f82 fix: shell injection in gitCommit projectDir, test directory cleanup
- gitCommit() now uses double quotes for projectDir consistently with
  gitPush/gitPull/gitStatus (was single quotes, breaking on paths with
  apostrophes like "John's Project")
- Test suite auto-cleans test_output_formats/ directory via Catch2
  event listener after all tests complete (was leaving empty dir)
2026-05-05 15:16:20 -07:00
Kelsi
90142cb0df docs: CHANGELOG, README editor section, fix dbc_to_csv build
- CHANGELOG: add World Editor section (12.5k+ lines, 6 modes, 30+ tools)
  and Novel Open Formats section (6/6 replacements, 309 test assertions)
- README: add World Editor section with build/run/CLI examples, format
  summary, and reference to FORMAT_SPEC.md
- Fix dbc_to_csv build: add extern/ to include path for nlohmann/json
  (broke when dbc_loader.cpp gained JSON DBC loading support)
2026-05-05 15:12:19 -07:00
Kelsi
22fb509416 test: WOT metadata round-trip with doodad/WMO placements
Comprehensive test for WOT JSON metadata parsing: verifies tile coords,
texture list, chunk layers with holes, water data, doodad names and
placements (nameId, uniqueId, position, rotation, scale, flags), and
WMO names and placements (nameId, uniqueId, position, rotation, flags,
doodadSet) all survive the export→load cycle.

31 new assertions. Total: 309 assertions across 81 test cases.
All open format round-trips now have test coverage.
2026-05-05 15:06:39 -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
9e33ad645c test: add unit tests for WOB and WHM open format round-trips
- WOB save/load round-trip: verifies groups, vertices, indices,
  portals, doodad placements with rotation and scale preservation
- WOB toWMOModel conversion: verifies WOB→WMO geometry bridge works
- WHM heightmap save/load: verifies 256-chunk terrain with per-vertex
  height data round-trips correctly through binary format
- WHM invalid magic rejection: verifies corrupt files are rejected
- WOB missing file rejection: verifies graceful failure
- 29 new assertions across 5 test cases
2026-05-05 14:49:05 -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
d00ddd1c73 feat(editor): WCP shortcut, help panel overhaul, active path restore
- Ctrl+Shift+E keyboard shortcut for WCP content pack export
- Help panel expanded: object tools (snap/align/flatten/scatter/select
  by type), all terrain generators, stamp save/load, export shortcuts
- ObjectPlacer::loadFromFile restores activePath_ from loaded objects
  so users can continue placing the same type after loading
- WCP export menu item now shows Ctrl+Shift+E hint
2026-05-05 14:38:01 -07:00
Kelsi
7473728360 feat(editor): stamp persistence, WCP file preview, enriched stats
- Terrain stamps save/load to JSON: reuse terrain features across zones
  and sessions. Save/Load buttons in Sculpt > Stamp/Clone panel
- WCP Inspect now shows full file breakdown: terrain/model/building/
  texture/data counts with total size. Powered by readInfo file list
  parsing with auto-categorization by extension
- stats.json now includes chunk count, triangle count, tile count, and
  editor version alongside existing object/NPC/quest/texture counts
- Fix unprotected std::stoi in custom zone WOT filename parser
2026-05-05 14:33:52 -07:00
Kelsi
d3e8f999c7 feat(editor): flatten around object, scatter auto-align, manifest batch
- Flatten Ground: flattens terrain to object height with smooth falloff
  around placed buildings/structures (undoable). Button in object panel
- Scatter auto-align: checkbox enables terrain snapping + slope alignment
  for scattered objects (trees snap to ground and lean with hillsides)
- Batch convert now falls back to asset manifest when filesystem dir is
  empty — converts M2/WMO from game data without filesystem extraction
- Public terrain editor wrappers: beginGeneratorUndo, endGeneratorUndo,
  markDirty, stitchChunkEdges, getChunkVertexWorldPos
2026-05-05 14:28:14 -07:00
Kelsi
115fe8436f feat(editor): terrain-aligned objects, batch convert, WCP import+load
New features:
- Align to Slope: rotates objects to match terrain surface normal at
  their position (trees on hillsides lean naturally). Works with
  multi-select. Available in object panel and right-click context menu
- Batch Convert Assets: File menu option to recursively convert all
  M2→WOM and WMO→WOB files in a data directory to open format
- Import & Load: one-click WCP unpack + auto-open the imported zone
- sampleTerrainNormal() for slope detection via height differencing
- Zone load error toasts for missing/corrupt files
2026-05-05 14:22:21 -07:00
Kelsi
acb519a243 fix(editor): object ID reset on load, zone name validation
- ObjectPlacer::loadFromFile() now resets uniqueIdCounter_ and clears
  selectedIndices_ to prevent ID collisions on repeated loads
- Zone rename validates against path traversal and special characters
  (rejects slashes, dots, colons, control chars, empty strings)
- UI shows error toast for invalid zone names
2026-05-05 14:15:28 -07:00
Kelsi
16a34afbf6 fix(editor): ID counter resets, quest panel guard, zone rename, load paths
Bug fixes:
- ObjectPlacer::clearAll() now resets uniqueIdCounter_ to 1
- NpcSpawner::clearAll() added with idCounter_ reset (was manual clear)
- clearAllObjects() uses NpcSpawner::clearAll() instead of manual clear
- Quest panel has terrain-loaded guard (prevents crash before loading)

Features:
- Zone rename: editable map name field in Info panel (press Enter)
- Load objects/creatures/quests from both output/ and custom_zones/
  directories (WOT zones from custom_zones now get their NPCs loaded)
2026-05-05 14:10:47 -07:00
Kelsi
10a63f0581 fix(editor): version string, NPC ID reset, author attribution + UX
Bug fixes:
- Fix README.txt version from v0.8.0 to v1.0.0
- Reset NPC idCounter_ on loadFromFile to prevent ID collisions
- WCP content pack uses project author/description if loaded instead
  of hardcoded "Kelsi Davis"

New features:
- Select by Type: context menu items for "Select All M2 Models" and
  "Select All WMO Buildings" for batch type-based selection
- selectByType(PlaceableType) added to ObjectPlacer
- Export summary toast now shows object/NPC/quest counts alongside
  file count and open format score (5s duration)
2026-05-05 14:05:22 -07:00
Kelsi
28d63addc4 feat(editor): brush size presets/hotkeys, export dialog update
- Brush size presets: S(15)/M(50)/L(100)/XL(180) buttons in sculpt panel
- [ / ] bracket keys adjust brush radius by 10 units (clamped 5-200)
- Export dialog now lists all output formats (ADT+WDT, WOT+WHM, WOM,
  WOB, PNG, JSON) instead of just ADT/WDT
- Document bracket keys in help panel
2026-05-05 14:00:49 -07:00
Kelsi
97da4c38f0 feat(editor): auto-save settings UI, multi-tile zone.json, toast notify
- Auto-save toast notification when auto-save fires
- Edit > Auto-Save Settings: enable/disable toggle, interval slider
  (60-900s), countdown timer display
- Zone manifest now scans output directory for all exported ADT tiles
  and includes them in zone.json (adjacent tiles no longer orphaned)
- Auto-save interval and enabled state exposed via EditorApp accessors
2026-05-05 13:58:07 -07:00
Kelsi
a7e34ad102 feat(editor): adjacent tile edge stitching, Escape clears all state
- Adjacent tile export now stitches border heights from current tile
  for seamless edges, exports both ADT and WOT/WHM open format
- Escape key now clears NPC selection and path capture state in
  addition to object selection and gizmo mode
2026-05-05 13:53:30 -07:00
Kelsi
533c218983 feat(editor): select all, recent zones, minimap selection highlights
- Ctrl+A selects all placed objects, context menu has Select All item
- selectAll() added to ObjectPlacer, works with multi-select transforms
- Recent Zones submenu in File menu (last 8 loaded zones, deduplicated)
- Minimap: selected objects shown as white dots with gold ring outline
  vs yellow dots for unselected objects
- Help panel updated with Ctrl+A and Ctrl+Shift+Click documentation
2026-05-05 13:52:02 -07:00
Kelsi
ddf97e9b8a feat(editor): multi-select objects, time-of-day lighting, WOT loading
- Multi-select: Ctrl+Shift+Click adds objects to selection, transforms
  (move/rotate/scale/delete) operate on all selected objects at once
- Time-of-day slider (0-24h) with automatic sun angle, light color,
  ambient, fog, and sky color transitions (dawn/day/dusk/night)
- View > Sky/Lighting menu: color pickers for light/ambient/fog, fog
  distance sliders, preset buttons (Dawn/Noon/Dusk/Night)
- loadADT prefers WOT/WHM open format from custom_zones/output dirs
- Selection count display when multiple objects selected
- setSkyPreset now delegates to setTimeOfDay for consistency
2026-05-05 13:47:23 -07:00
Kelsi
d44eaec487 feat(editor): enhanced info panel with height stats and active texture
- Show active texture name in info panel when in paint mode
- Display terrain height range (min/max/avg) in properties panel
- Document Alt+Click eyedropper shortcut in help panel
2026-05-05 13:41:05 -07:00
Kelsi
d2acdc7620 feat(editor): texture eyedropper, WOT/WHM preference loading
- Texture eyedropper: pickTextureAt() samples dominant texture at world
  position by reading chunk alpha maps. Alt+Click in paint mode or
  click the Eyedropper button to activate
- loadADT now checks custom_zones/ and output/ for WOT/WHM open format
  files first, falling back to ADT binary only if not found
- Fix unused variable warning in scatterPatches
- Document Alt+Click in help panel
2026-05-05 13:39:53 -07:00
Kelsi
acfbf19144 feat(editor): path preview line, transform undo, complete undo coverage
- River/road tool now shows translucent blue path preview ribbon with
  edge lines between start and end points before applying
- Preview follows cursor when waiting for end point, locks when set
- Undo support for all remaining operations: rotateTerrain90, mirrorX,
  mirrorY, scaleHeights, offsetHeights, invertHeights, smoothBeaches
- Every terrain-modifying operation in the editor is now undoable
2026-05-05 13:33:28 -07:00
Kelsi
7e02db73df feat(editor): generator undo, quit confirmation, state cleanup
- All terrain generators now undoable: crater, mesa, hill, voronoi,
  dunes, detail noise, thermal erosion, canyon, island, ridge, road,
  river, perlin noise — all wrapped with recordGeneratorUndo/commit
- Unsaved changes warning on quit: Save & Quit / Quit / Cancel dialog
- createNewTerrain clears quest editor and path capture state
- recordGeneratorUndo/commitGeneratorUndo helper methods snapshot all
  256 chunks before/after any generator operation
2026-05-05 13:26:38 -07:00
Kelsi
86f1a7d109 feat(editor): enhanced About dialog, validation menu, status bar
- About dialog: shows all 6 format replacements, feature counts, tech
- File menu: "Validate Open Formats" shows live per-format status with
  color-coded [OK]/[!!]/[--] indicators and score out of 6
- Status bar: shows quest count alongside objects/NPCs, undo/redo depth
2026-05-05 13:18:13 -07:00
Kelsi
97e7a4c71a feat(editor): complete importOpen, keyboard shortcuts, DBC exporter
- importOpen now loads WHM alpha maps + full WOT metadata (textures,
  layers, holes, water) — was height-only stub
- DBC exporter migrated to nlohmann/json (last naive JSON file)
- Add Z-axis gizmo constraint (Z key) alongside X/Y
- Add Ctrl+Y as alternate redo binding
- Add F1 keyboard shortcut for help panel toggle
- Update help panel: document Z-axis, Ctrl+Y, all shortcuts
2026-05-05 13:15:00 -07:00
Kelsi
5b180c5579 docs: update FORMAT_SPEC with WHM alpha maps, JSON DBC, PNG textures
- WHM spec now documents per-chunk alpha map data with backward compat
- Added JSON DBC format section (replaces binary DBC)
- Added PNG texture section (replaces BLP)
- Added open format scoring criteria (0-6 scale)
2026-05-05 13:10:48 -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
6b3cdd325a feat(editor): click-to-place path points for river/road carver
- River/Road tool now uses click-capture mode instead of button-based
  cursor position — click terrain directly to set start and end points
- 3-step flow: Click Start → Click End → Apply Path (with preview text)
- Cancel button available at each step
- Path state tracked on EditorUI with setPathPoint()/clearPath()
- Intercepts terrain clicks before mode-specific handling when active
2026-05-05 13:00:23 -07:00
Kelsi
4e2f704124 fix(editor): undo now covers texture painting, fix stale buffer bug
- Extend undo/redo to snapshot alpha maps and texture layers alongside
  heights — texture painting operations are now fully undoable
- Bracket paint mode with beginStroke/endStroke like sculpt mode
- Fix stale static char buffer in quest objective loop (showed wrong
  objective's description when editing multiple objectives)
- Zero-initialize all quest UI text buffers for null termination safety
- Fix unused variable warnings in terrain_editor.cpp
2026-05-05 12:58:11 -07:00
Kelsi
617228559a feat(editor): quest chain UI with editing, loading, and validation display
- Add chain link combo to quest creation (select next quest in chain)
- Add completion text field to quest creation form
- Quest list shows chain arrows (->chain) for linked quests
- Click quest in list to edit title, level, XP inline
- Delete button for individual quests
- Live chain validation with error display (broken refs, circular chains)
- Load Quests button alongside Save Quests
2026-05-05 12:49:59 -07:00
Kelsi
2eec089ef5 fix(editor): harden JSON handling, quest loading, and content validation
- Quest editor: add loadFromFile() with nlohmann/json, chain validation
  with circular reference detection, wire into ADT load and save pipeline
- Project: replace naive substring JSON parsing with nlohmann/json for
  both save() and load(), fix shell injection in gitCommit()
- Content pack: replace manual JSON with nlohmann/json, validate binary
  format magic numbers (WHM1/WOM1/WOB1), add WOB to openFormatScore
  (now scores 0-6), mark invalid files with (!) in summary
2026-05-05 12:48:50 -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
d8f2388635 milestone(editor): commit #200 — v1.0.0 complete, 11.5k lines, all formats open
200 commits building a complete world editor with novel open file
formats from scratch in a single development session.

Final stats:
- 11,532 lines across 55 files
- 6 editor modes, 30+ terrain tools, 3 noise types
- 5 novel binary formats: WHM1, WOM1, WOB1, WCP1, WOT
- All 6 Blizzard formats replaced with open alternatives
- 4/6 formats fully loading in client (terrain, textures, models, buildings)
- CLI: --help, --version, --list-zones, --convert-m2
- Project system with git collaboration
- Full content pipeline: create → export → pack → share → load

By Kelsi Davis
2026-05-05 12:21:00 -07:00
Kelsi
0fce340aa0 docs(editor): add --list-zones and --version to CLI help output 2026-05-05 12:19:53 -07:00
Kelsi
8ac36e2f05 feat(editor): --list-zones CLI flag to discover custom zones without GUI 2026-05-05 12:18:31 -07:00
Kelsi
c75337ed2c fix(editor): sync --version CLI output to v1.0.0 2026-05-05 12:16:21 -07:00
Kelsi
f20e602d32 fix(editor): minor README formatting improvement in export 2026-05-05 12:14:31 -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
71b06826b1 milestone(editor): v1.0.0 — complete world editor with full open format pipeline
191 commits, 11.5k lines, 55 files. Both targets clean, zero warnings.

World Editor v1.0.0 feature-complete:
- 6 modes: Sculpt, Paint, Objects, Water, NPCs, Quests
- 30+ terrain generators and tools
- Full painting, object placement, NPC, and quest pipelines
- Project management with git collaboration
- Content pack import/export with validation

All 6 Blizzard formats replaced with novel open alternatives:
- ADT → WOT/WHM (WHM1 magic)
- WDT → zone.json
- BLP → PNG (auto-converted)
- DBC → JSON (auto-converted)
- M2 → WOM (WOM1 magic, auto-converted)
- WMO → WOB (WOB1 magic)

Client integration: auto-loads WOT/WHM terrain + WOM models
Content pipeline: Editor → Export → WCP Pack → Share → Client loads

By Kelsi Davis
2026-05-05 11:54:58 -07:00
Kelsi
8f7a70e8cd feat(editor): --version CLI flag shows editor version and format info 2026-05-05 11:53:12 -07:00
Kelsi
84bb31012f docs(editor): add WOM version note and future animation support plan to FORMAT_SPEC 2026-05-05 11:50:22 -07:00
Kelsi
8bf02b5880 feat(editor): zone stats.json export with open format score and content summary 2026-05-05 11:48:57 -07:00
Kelsi
5c229e9603 feat(editor): auto-export hole mask PNG alongside terrain on zone save 2026-05-05 11:44:52 -07:00
Kelsi
1a95ec9d0a feat(editor): hole mask PNG export (16x16 chunk grid, white=holes) 2026-05-05 11:43:23 -07:00
Kelsi
b65b5a758d feat(editor): zone manifest includes ISO export timestamp for tracking 2026-05-05 11:40:56 -07:00
Kelsi
053278da7c fix(editor): accurate file count in export toast (includes all PNGs and textures) 2026-05-05 11:38:56 -07:00
Kelsi
196e67dddb feat(editor): auto-export water mask PNG alongside terrain on zone save 2026-05-05 11:37:31 -07:00
Kelsi
7ce49ebe96 feat(editor): water mask PNG export (16x16 chunk grid, white=water) 2026-05-05 11:36:04 -07:00
Kelsi
235eccad88 docs(editor): expand FORMAT_SPEC with zone.json fields and IP disclaimer 2026-05-05 11:34:09 -07:00
Kelsi
e73cb466d5 feat(editor): WOT metadata includes doodad/WMO placement counts 2026-05-05 11:31:26 -07:00
Kelsi
b5a798e53a chore(editor): bump version to 0.9.0 — 180 commits, 11.4k lines 2026-05-05 11:28:22 -07:00
Kelsi
324970b866 feat(editor): WOT metadata includes editor version for compatibility tracking 2026-05-05 11:26:54 -07:00
Kelsi
8ef151a07e feat(editor): include tileSize/chunkSize constants in WOT metadata 2026-05-05 11:24:29 -07:00
Kelsi
02c2d62a02 feat(editor): comprehensive export summary log with format breakdown 2026-05-05 11:22:55 -07:00
Kelsi
ae7942ef39 feat(editor): zone thumbnail included in export for content pack browsing 2026-05-05 11:21:11 -07:00
Kelsi
6747456f48 fix(editor): improved WCP inspect toast with version and format info 2026-05-05 11:19:35 -07:00
Kelsi
539de3f5b0 docs(editor): add WHM version info and total size to FORMAT_SPEC 2026-05-05 11:18:08 -07:00
Kelsi
60048c0ee4 docs(editor): export README now lists all open format types 2026-05-05 11:16:38 -07:00
Kelsi
77a91de9f1 feat(editor): heightmap preview PNG export for zone documentation
- Exports 129x129 grayscale PNG showing terrain elevation
- Auto-normalizes to 0-255 based on actual height range
- Useful for zone documentation, thumbnails, and previews
- Auto-exported alongside WOT/WHM/normals on every save
2026-05-05 11:14:58 -07:00
Kelsi
36dd4bf141 feat(editor): WOT metadata includes alpha map presence flag per chunk 2026-05-05 11:12:36 -07:00
Kelsi
d990b2819d feat(editor): auto-export alpha maps alongside terrain on zone save 2026-05-05 11:10:08 -07:00
Kelsi
4ea09b0b8b feat(editor): alpha map export as individual 64x64 grayscale PNGs 2026-05-05 11:08:40 -07:00
Kelsi
25f1893b49 feat(editor): auto-export terrain normal map PNG alongside WOT/WHM 2026-05-05 11:03:23 -07:00
Kelsi
54b7949dd1 feat(editor): terrain normal map export as PNG (129x129 RGB) 2026-05-05 11:01:37 -07:00
Kelsi
a3b6653e15 feat(editor): quest count and open format save indicator in Info panel 2026-05-05 10:58:11 -07:00
Kelsi
126567a1d0 feat(editor): show scale and rotation info for selected objects 2026-05-05 10:56:48 -07:00
Kelsi
05d7dcd927 feat(editor): show open format indicator in placed objects count 2026-05-05 10:55:07 -07:00
Kelsi
b29aee062a chore(editor): note all 6 Blizzard formats replaced in About dialog 2026-05-05 10:53:49 -07:00
Kelsi
e7cf125fda docs(editor): improved CLI help with format info and all options 2026-05-05 10:52:17 -07:00
Kelsi
4b94640cae feat(editor): CLI batch convert mode (--convert-m2) for M2→WOM conversion
- wowee_editor --convert-m2 <path> --data <datadir> converts a single
  M2 model to WOM open format without launching the GUI
- Output goes to output/models/ with same path structure
- Useful for batch scripts to convert entire asset directories
- Example: wowee_editor --data Data --convert-m2 creature\\bear\\bear.m2
2026-05-05 10:50:36 -07:00
Kelsi
bf83da61d2 chore(editor): update About dialog with open format stats (11k+ lines, 6 formats) 2026-05-05 10:48:31 -07:00
Kelsi
e191f35ed0 feat(editor): content pack inspector and improved import UI 2026-05-05 10:47:18 -07:00
Kelsi
6acde0290d docs(editor): FORMAT_SPEC.md documenting all 5 novel open file formats 2026-05-05 10:45:50 -07:00
Kelsi
4148c890dc feat(editor): auto-export WMO buildings as WOB placeholders on zone save
- Zone export now creates WOB placeholder files for all placed WMO
  buildings in output/MapName/buildings/
- Full WMO→WOB conversion (with geometry) requires group file loading
  which is complex — placeholders reserve the path structure for now
- All 6 format conversions now auto-run on every zone export:
  ADT→WOT/WHM, BLP→PNG, DBC→JSON, M2→WOM, WMO→WOB, WDT→zone.json
2026-05-05 10:44:38 -07:00
Kelsi
c532a1a787 chore(editor): bump version to 0.8.0 — full open format pipeline 2026-05-05 10:41:43 -07:00
Kelsi
adfa1d7086 feat(editor): export toast shows open format completeness score (X/5) 2026-05-05 10:40:15 -07:00
Kelsi
a54ce494be feat(editor): zone validation for open format completeness scoring
- ContentPacker::validateZone() scans a zone directory and checks
  for all open format files (WOT, WHM, PNG, WOM, zone.json, etc.)
- openFormatScore(): returns 0-5 based on how many open formats present
- summary(): human-readable list of found formats
- Foundation for quality gate on WCP export: warn if zone uses
  Blizzard formats that could be converted to open versions
2026-05-05 10:38:57 -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
5e2cb6fb3c feat(editor): auto-convert placed M2 objects to WOM on zone export
- Zone export now converts all placed M2 models to WOM open format
- Deduplicates: each unique M2 path converted only once
- WOM files saved to output/MapName/models/ with original path structure
- Uses WoweeModelLoader::fromM2() for M2→WOM conversion
- Combined with PNG texture export, zones can now be fully distributed
  without any Blizzard proprietary files

Full open format export pipeline:
  Terrain: ADT→WOT/WHM | Textures: BLP→PNG | Data: DBC→JSON
  Models: M2→WOM | Buildings: WMO→WOB (manual) | Map: WDT→zone.json
2026-05-05 10:31:51 -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
Kelsi
176115f279 feat(editor): DBC→JSON export for open format zone data tables
- DBCExporter: converts Blizzard DBC binary data tables to JSON
- Exports zone-relevant DBCs: AreaTable, Map, Light, LightParams,
  ZoneMusic, SoundAmbience, GroundEffectTexture/Doodad, LiquidType
- Auto-detects string vs numeric vs float fields
- Zone export now includes data/ directory with JSON DBCs
- Client can load these via existing CSV/JSON fallback paths

Format replacement progress:
- DONE: ADT → WOT/WHM (terrain)
- DONE: WDT → zone.json (map definition)
- DONE: BLP → PNG (textures)
- DONE: DBC → JSON (data tables)
- TODO: M2 → open model format
- TODO: WMO → open building format
2026-05-05 10:21:14 -07:00
Kelsi
cb3de59b5c feat(editor): BLP→PNG texture export for open format zones
- TextureExporter: converts Blizzard BLP textures to standard PNG
  for fully open redistribution of custom zones
- collectUsedTextures(): finds all texture paths referenced by terrain
- exportTexturesAsPng(): loads BLP via asset manager, writes RGBA PNG
  using stb_image_write to output/MapName/textures/
- Zone export now automatically converts all used textures to PNG
- Client's PNG override system already loads these automatically
  (checks for .png alongside .blp before loading)

Format replacement progress:
- DONE: ADT→WOT/WHM (terrain)
- DONE: WDT→zone.json (map definition)
- DONE: BLP→PNG (textures — auto-exported on zone save)
- TODO: DBC→JSON, M2→open model, WMO→open building
2026-05-05 10:17:03 -07:00
Kelsi
5adb6cb364 feat(editor): export toast shows file count for better feedback 2026-05-05 10:11:50 -07:00
Kelsi
40561fbe7a fix(editor): correct noise type count in About dialog (3 types: value, voronoi, detail) 2026-05-05 10:10:17 -07:00
Kelsi
b4023e13bf chore(editor): bump version to 0.7.0 — full custom content pipeline 2026-05-05 10:08:49 -07:00
Kelsi
a2b69e8e09 feat(editor): Import Content Pack button for loading shared WCP archives 2026-05-05 10:06:57 -07:00
Kelsi
c05c15f6c9 feat(editor): Load Custom Zone menu discovers and loads WOT/WHM zones
- File > Load Custom Zone scans output/ and custom_zones/ for zone.json
- Lists all discovered custom zones with name and quest indicator [Q]
- Tooltip shows description and author on hover
- Click to load the zone's WOT/WHM terrain into the editor
- Parses tile coords from WOT filename for correct positioning
- Full round-trip: export → discover → reload for iterative editing
2026-05-05 10:04:45 -07:00
Kelsi
d10d962e31 feat: custom zone discovery system for client auto-detection
- CustomZoneDiscovery scans directories for zone.json manifest files
- Discovers custom zones in custom_zones/ and output/ directories
- Reports: name, author, description, creature/quest availability
- Client can list all available custom expansions at startup
- Foundation for a zone selection menu in the client UI
2026-05-05 10:01:05 -07:00
Kelsi
954894460e feat: integrate Wowee Open Terrain loader into client terrain pipeline
The wowee client can now load custom zones exported from the editor
using the novel WOT/WHM format — no Blizzard files needed.

Loading priority in TerrainManager::prepareTile():
1. Check custom_zones/{mapName}/{mapName}_{x}_{y}.wot/.whm
2. Check output/{mapName}/{mapName}_{x}_{y}.wot/.whm (editor output)
3. Fall back to World\Maps\...\*.adt (standard extracted data)

Pipeline:
- WoweeTerrainLoader in src/pipeline/ (shared between client + editor)
- Loads .whm binary heightmap (WHM1 magic, 256 chunks × 145 floats)
- Loads .wot JSON metadata (textures, layers, holes, water)
- Populates the same ADTTerrain struct the mesh generator uses
- obj0 merge only runs for ADT-loaded tiles (custom zones have no obj0)

To use: export zone from editor → files appear in output/ → client
loads them automatically on next terrain request for that map name.
2026-05-05 09:56:24 -07:00
Kelsi
94e6d5276e feat(editor): git integration for collaborative expansion development
- File > Project > Git submenu: Init, Commit, Push, Pull operations
- Init Git Repo: initializes git in the project directory with initial commit
- Commit Changes: auto-saves zone then commits all changes
- Push/Pull: sync with remote repositories for team collaboration
- Git Status: shows current repo state directly in the menu
- Teams can collaborate on custom expansions using standard git workflows
2026-05-05 09:45:00 -07:00
Kelsi
6f35081013 feat(editor): switch between project zones via File > Project > Switch Zone
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
2026-05-05 09:42:58 -07:00
Kelsi
8c648dff85 chore(editor): bump version to 0.6.0 — 10k lines, projects, open formats 2026-05-05 09:41:08 -07:00
Kelsi
18939af73d feat(editor): project management UI in File menu
- File > Project submenu: New/Save/Load project, add zones
- "Add Current Zone to Project" captures loaded map/tile info
- Project path editable in the menu
- Zone count shown for quick reference
- Foundation for multi-zone custom expansion workflow:
  create project → add zones → edit each → export all as WCP
2026-05-05 09:39:01 -07:00
Kelsi
1d2f25f169 feat(editor): project system for multi-zone custom expansions
- EditorProject: manages multiple zones in a single project file
  (wowee-project-1.0 JSON format)
- Project stores: name, author, description, version, zone list
- Each zone has: mapName, tileX/Y, biome, description
- Save/load project files (.json) with full round-trip
- Foundation for creating custom expansions with multiple maps,
  quest chains spanning zones, and campaign progression
- getZoneOutputDir() resolves per-zone output paths
2026-05-05 09:36:44 -07:00
Kelsi
c28d3f8a99 feat(editor): open terrain format integrated into export workflow
- File > Export Open Format (.wot/.whm) menu item for standalone export
- Every zone export (Ctrl+S, Export Zone) now also writes .wot/.whm
  alongside the ADT/WDT — dual format output
- Export package now contains: ADT + WDT + WOT + WHM + JSON files
- Both Blizzard-compatible (for existing servers) and open format
  (for wowee-native loading and redistribution)
2026-05-05 09:34:49 -07:00
Kelsi
7177463df1 feat(editor): Wowee Open Terrain format (.wot/.whm) — no Blizzard formats
Novel open terrain format unique to wowee:

.wot (Wowee Open Terrain) — JSON metadata:
- Tile coordinates, chunk grid dimensions, texture list
- Per-chunk layer assignments and hole bitmasks
- Water data per chunk (type, height)
- Format version "wot-1.0"

.whm (Wowee HeightMap) — binary heightmap:
- "WHM1" magic header
- 256 chunks × (baseHeight + 145 float heights) = 148KB
- Direct float storage, no compression, fully portable

Both formats are entirely novel — no ADT, no WDT, no Blizzard
structures. The WCP content pack can bundle .wot/.whm instead of
ADT/WDT for fully open redistribution.

Import/export functions: WoweeTerrain::exportOpen() / importOpen()
2026-05-05 09:32:13 -07:00
Kelsi
be18ceea73 feat(editor): Export Content Pack button in File menu
- File > Export Content Pack (.wcp) saves zone + packs into WCP archive
- Auto-saves zone first, then bundles into the novel WCP format
- WCP is a custom format unique to wowee (not MPQ, not CASC)
- Output: output/MapName.wcp ready for distribution
- Toast notification on success/failure
2026-05-05 09:29:57 -07:00
Kelsi
79ae91a6d5 feat(editor): Wowee Content Pack (.wcp) format for zone distribution
- WCP format: simple binary archive with magic header, JSON manifest,
  and concatenated file data. Not tied to any proprietary format.
- ContentPacker::packZone(): bundles all zone files from output dir
  into a single .wcp file (terrain, objects, creatures, quests, manifest)
- ContentPacker::unpackZone(): extracts .wcp to a directory
- ContentPacker::readInfo(): reads pack metadata without extracting
- Format: "WCP1" magic + fileCount + infoJSON + file table + data
- Foundation for distributing custom zones to other wowee users
  and private servers

Note: currently bundles ADT/WDT files as-is. Future: convert terrain
to open format (heightmap + JSON) for fully open redistribution.
2026-05-05 09:27:00 -07:00
Kelsi
f5d8dcf75a chore(editor): bump version to 0.5.0 (130 commits) 2026-05-05 09:24:21 -07:00
Kelsi
dce51d9de6 fix(editor): cleaner cursor info display in Sculpt panel header 2026-05-05 09:21:44 -07:00
Kelsi
da79244fc5 feat(editor): show cursor height and position at top of Sculpt panel 2026-05-05 09:20:28 -07:00
Kelsi
ec8e3a41b5 fix(editor): Clear All shows toast when nothing to clear 2026-05-05 09:19:02 -07:00
Kelsi
d36631d242 fix(editor): Create + Generate button now actually runs generation pipeline
- "Create + Generate" was setting a flag but never calling generateCompleteZone()
- Now properly chains: create terrain → run full procedural pipeline
  (noise → smooth → normals → height paint → slope paint → detail → water → beaches)
- Uses generateAfterCreate_ flag to defer generation until after terrain is created
2026-05-05 09:17:23 -07:00
Kelsi
bdfadc7e76 fix(editor): remove all remaining brush.isActive() checks from UI buttons
- Pick height, Punch Hole, Fill Hole, Delete All in Radius buttons
  no longer require cursor to be actively on terrain
- All buttons now use last known brush position consistently
- Fixes the fundamental UX issue where clicking a UI button moves
  the cursor off terrain, making the button do nothing
2026-05-05 09:14:31 -07:00
Kelsi
560c4a40c0 fix(editor): About dialog now works, moved outside menu bar scope
- About dialog was broken because ImGui::OpenPopup inside BeginMenu
  has wrong ID scope — popup never appeared
- Changed to a proper ImGui::Begin window with showAbout_ flag
- Closeable with X button or clicking About again
- Shows version, author, feature summary, and tech stack
2026-05-05 09:11:46 -07:00
Kelsi
bab1318ec9 fix(editor): ghost preview logs warning when model file not found 2026-05-05 09:08:03 -07:00
Kelsi
c1f74d9c9c fix(editor): all generator buttons work without cursor on terrain
- Removed isActive() requirement from all "Create at Cursor" buttons
  (Mesa, Crater, Hill, Valley, Stamp, Ridge, Flatten Platform, Holes)
- Buttons now use the last known brush position which persists when
  cursor moves to the UI panel to click the button
- Fixes: "Create Mesa at Cursor can't work if cursor is clicking
  that button" — now it uses wherever the cursor was last on terrain
2026-05-05 09:06:08 -07:00
Kelsi
63f4711eb3 fix(editor): river/road path selection uses last brush pos, edge ramp note
- Set Start/End buttons for river/road no longer require brush to be
  actively on terrain at click time — uses last known brush position
  which persists when cursor moves to the UI panel
- Shows cursor coordinates above the buttons for visual confirmation
- Edge ramp confirmed to only affect terrain heights, not water levels
  (water has its own height independent of terrain)
2026-05-05 09:04:30 -07:00
Kelsi
f6d30fcf9b fix(editor): ghost preview survives object rebuilds, clearObjects resets ghost state
- Ghost model state (ID, path, active flag) properly reset when
  clearObjects() wipes the M2 renderer — prevents stale ghost
  references causing crashes on subsequent hover
- Ghost model ID uses 59999 to avoid collision with placed object IDs
- Ghost loadModel failure now handled gracefully (returns early)
2026-05-05 09:03:02 -07:00
Kelsi
34c06ede4e fix(editor): redo works in all modes, center uses real chunk positions
- Ctrl+Shift+Z redo now works globally (was only in Sculpt mode)
- Ctrl+Z undo works in all modes with toast confirmation
- centerOnTerrain() uses actual mesh chunk positions instead of formula
- Toast shows "Undo" / "Redo" / "Undo placement" for user feedback
2026-05-05 09:00:53 -07:00
Kelsi
66debe9cec fix(editor): camera auto-centers on actual terrain chunks after ADT load
- Camera now positions at the center of actual loaded terrain chunks
  instead of using formula-based coordinates that can be wrong for
  instanced maps
- Falls back to formula if no valid chunks loaded
- Fixes camera starting far from terrain on some maps
2026-05-05 08:58:07 -07:00
Kelsi
84e9146522 feat(editor): toast notification when importing ADT doodads/WMOs 2026-05-05 08:56:52 -07:00
Kelsi
61f0f39611 fix(editor): ADT doodad/WMO coordinate conversion, auto-find tile on map select
- ADT doodad/WMO positions now converted from ADT space to render coords
  via core::coords::adtToWorld() — fixes objects appearing at wrong positions
  when loading existing WoW maps
- Selecting a map in the Load dialog now auto-finds the first valid tile
  (no more clicking "Find Tile" manually — it's automatic)
- Better error messages with toast for failed terrain loads
- Validates mesh has valid chunks before attempting GPU upload
2026-05-05 08:55:09 -07:00
Kelsi
9c0bc5fbd1 fix(editor): override ADT internal coords with filename coords on load
Root cause of ADT loading failures: instanced maps (dungeons, raids)
have arbitrary internal coord values (e.g. tile 62,0 for a file named
_28_27.adt). The terrain mesh generator uses these coords to compute
world positions, resulting in chunks placed thousands of units away
from the camera → "Failed to upload terrain to GPU".

Fix: override terrain_.coord with the tileX/tileY from the filename
before mesh generation. This ensures chunks are positioned correctly
regardless of what the ADT file's internal header says.
2026-05-05 08:49:26 -07:00
Kelsi
79db38219f feat(editor): sand dune generator for desert terrain
- Dune Generator: creates rolling sand dune patterns with primary
  and secondary sine waves plus hash-based variation
- Configurable wavelength, amplitude, wind direction, and seed
- Directional waves create realistic parallel dune ridges
- Secondary wave adds natural irregularity at 2.3x wavelength
- Perpendicular variation breaks up uniform dune lines
- Pair with Desert biome paint for instant Tanaris-style zones
2026-05-05 08:47:44 -07:00
Kelsi
8255cda9a8 chore(editor): bump version to 0.4.0 2026-05-05 08:45:24 -07:00
Kelsi
29fe4bd062 feat(editor): quick biome paint presets (6 biomes, one-click full texture)
- Quick Biome Paint: select a biome (Grassland/Forest/Desert/Snow/Swamp/Barrens)
  and apply its full texture set in one click
- Sets base texture for all chunks + scatters variation patches
- Each biome has a primary ground texture + secondary variation
- Much faster than manual painting for initial zone texturing
2026-05-05 08:42:47 -07:00
Kelsi
b00143e8f7 feat(editor): scatter texture patches for natural surface variety
- Scatter Patches: randomly places texture circles across the terrain
  for natural-looking surface variation (dirt patches, rock outcrops)
- Configurable count, min/max radius, and seed
- Preset buttons for dirt and rock patches
- Uses the existing paint() method with random positions
- Adds visual variety to otherwise uniform terrain texturing
2026-05-05 08:40:43 -07:00
Kelsi
16a096b25d fix(editor): include quest count in export README, version tag 2026-05-05 08:37:51 -07:00
Kelsi
c8446fb782 feat(editor): seed step buttons (<< >>) for quick terrain seed cycling 2026-05-05 08:36:09 -07:00
Kelsi
14d305a477 feat(editor): rotate terrain 90 degrees clockwise
- Rotate 90 CW button in Mirror/Rotate section
- Snapshots outer vertex heights into 129x129 grid, rotates in-place,
  writes back with inner vertices averaged from surrounding outers
- Useful for reorienting terrain features or creating rotational
  symmetry (rotate + mirror for 4-way symmetric arenas)
2026-05-05 08:32:59 -07:00
Kelsi
66b6404d25 fix(editor): recalculate normals after detail noise in Generate pipeline 2026-05-05 08:30:07 -07:00
Kelsi
0a6e54e8a2 feat(editor): gradient texture blend for biome transitions
- Gradient Blend: smoothly transitions base texture from one biome
  to another across the entire tile (horizontal or vertical)
- Preset buttons: Grass→Sand and Grass→Snow gradients
- Creates natural biome transition zones without manual painting
- Alpha blending from 0% to 100% across 16 chunks
2026-05-05 08:28:09 -07:00
Kelsi
0907bb5ca2 feat(editor): undo/redo status in Info panel 2026-05-05 08:25:46 -07:00
Kelsi
d8c249e08f chore(editor): update About dialog tech stack info 2026-05-05 08:24:07 -07:00
Kelsi
571ea38efd feat(editor): Generate Complete Zone now adds detail roughness step 2026-05-05 08:22:09 -07:00
Kelsi
3504e57f75 feat(editor): detail noise for adding small-scale terrain roughness
- Detail Noise: adds high-frequency roughness to existing terrain
  without destroying the overall shape (amplitude 0.5-10, freq 0.01-0.5)
- Useful after smooth/generate to break up unnaturally smooth surfaces
- Workflow: generate → smooth → detail noise for natural look
- Separate seed from main noise generator for independent control
2026-05-05 08:20:54 -07:00
Kelsi
fab77952a6 feat(editor): river carver now auto-paints sand banks alongside carved channel 2026-05-05 08:18:07 -07:00
Kelsi
6de6da766d feat(editor): auto-texture roads with cobblestone on flatten
- paintAlongPath(): paints a texture along a line segment with
  configurable width and quadratic edge falloff
- Road mode now automatically applies Elwynn cobblestone texture
  when flattening a road path (flatten + texture in one operation)
- Quick distance check skips chunks far from the path for performance
- Alpha blending uses max() so overlapping paths don't wash out
2026-05-05 08:16:40 -07:00
Kelsi
8974cef9c0 milestone(editor): commit #100 — v0.3.0, 9.1k lines, complete world editor
Wowee World Editor milestone: 100 commits, 9,067 lines, 39 files.

Complete standalone zone creation tool with:
- 6 editor modes (Sculpt/Paint/Objects/Water/NPCs/Quests)
- 30+ terrain tools: noise (value + voronoi), hill, valley, mesa,
  crater, canyon, island, ridge, river, road, terrace, thermal erosion,
  stamp/clone, mirror, flatten platform, holes, smooth beaches,
  edge ramp, height scale/clamp/offset/invert/reset
- 6 brush modes: raise/lower/smooth/flatten/level/erode
- Auto-paint by height bands and slope
- 1285 tileset textures, 11k M2 models, 2k WMO buildings
- 631 NPC creature presets with stats/behavior/patrol paths
- Quest editor with objectives, rewards, and NPC linking
- Full export: zone.json + WDT + ADT + objects/creatures/quests JSON
- Map browser with tile availability checker
- Minimap with height/objects/NPCs/camera/brush/holes
- One-click Generate Complete Zone pipeline
- Persistent layout, auto-save, camera bookmarks, toast notifications
2026-05-05 08:14:14 -07:00
Kelsi
6277800773 feat(editor): edge ramp tool for seamless multi-tile connections
- Edge Ramp: smoothly transitions tile borders to a target height
  so adjacent tiles can connect seamlessly
- Configurable target height and ramp width (how far in from the edge)
- Quadratic blend for smooth start from edge → interior
- Essential for multi-tile zone creation: ramp each tile's edges to
  match its neighbor's border height
2026-05-05 08:13:04 -07:00
Kelsi
d56ea9ae64 feat(editor): Randomize All button for instant terrain exploration 2026-05-05 08:10:21 -07:00
Kelsi
312a7e03e0 fix(editor): update minimap legend with hole indicator 2026-05-05 08:09:13 -07:00
Kelsi
aed6e00aac feat(editor): Generate Complete Zone now includes water fill + beach smoothing 2026-05-05 08:07:49 -07:00
Kelsi
426863f19a feat(editor): smooth beaches tool for natural water-land transitions
- Smooth Beaches button in Water panel: creates gentle beach slopes
  near water level by blending terrain toward waterline height
- Configurable beach width (15 units default)
- Quadratic falloff creates natural concave beach profile
- Use after filling water to soften harsh land-water edges
- Workflow: sculpt → fill water → smooth beaches → paint sand
2026-05-05 08:06:20 -07:00
Kelsi
c8897eca83 feat(editor): Create + Generate button in New Terrain dialog 2026-05-05 08:04:05 -07:00
Kelsi
171fff3843 chore(editor): include editor version in zone manifest export 2026-05-05 08:02:38 -07:00
Kelsi
e7ce94448a chore(editor): update About dialog with current stats 2026-05-05 08:01:09 -07:00
Kelsi
b113c218bd feat(editor): voronoi cell noise for organic terrain patterns
- Voronoi noise generator: creates cell-like terrain patterns with
  ridge features at cell boundaries (F2-F1 distance field)
- Configurable cell count (5-100) and amplitude
- Toggle between Value noise and Voronoi in the noise generator section
- Creates interesting mesa/plateau formations and organic shapes
- Random cell centers with per-cell height variation
2026-05-05 07:59:10 -07:00
Kelsi
e65fc7caa2 chore(editor): bump version to 0.2.0 (89 commits, 8.9k lines) 2026-05-05 07:57:09 -07:00
Kelsi
d247aee728 fix(editor): Generate Complete Zone resets terrain first for clean slate 2026-05-05 07:54:26 -07:00
Kelsi
5f9bf5c924 feat(editor): height offset tool for shifting entire terrain up/down
- Offset Heights: shifts all terrain heights by a constant amount
  (-100 to +100 range with Apply button)
- Useful for raising terrain above water level or sinking below
- Slider + Apply button in the Noise Generator section
2026-05-05 07:53:01 -07:00
Kelsi
d50e4b3f78 feat(editor): invert button in sculpt panel alongside reset 2026-05-05 07:51:21 -07:00
Kelsi
825939db3b feat(editor): invert heights, fill entire tile with water, remove all water
- Invert Heights: flips terrain upside-down around midpoint (mountains
  become valleys and vice versa)
- Fill Entire Tile with Water: one-click fills all 256 chunks at the
  configured water height and liquid type
- Remove ALL Water: clears water from every chunk instantly
- Water panel now has three water operations: fill tile, remove under
  brush, remove all
- fillWater() and invertHeights() methods on TerrainEditor
2026-05-05 07:49:48 -07:00
Kelsi
bd1356bd08 fix(editor): enlarge sculpt panel default size for all generators 2026-05-05 07:46:46 -07:00
Kelsi
2d5692d5ad feat(editor): thermal erosion simulation for natural terrain aging
- Thermal Erosion: physically-based material transfer where steep
  slopes shed material to neighbors based on angle of repose
- Configurable iterations (1-50) and talus angle (10-80 degrees)
- Lower talus angle = more aggressive erosion (sandy terrain)
- Higher angle = less erosion (rocky terrain holds steep slopes)
- Creates natural talus fans at cliff bases and rounded hilltops
- Workflow: sculpt/generate → thermal erosion → smooth → auto-paint
2026-05-05 07:45:16 -07:00
Kelsi
9be32a6634 feat(editor): terrain terrace/step generator for layered landscapes
- Terrace tool: quantizes terrain heights into N flat shelves
  (like rice paddies, cliff shelves, or stepped pyramids)
- Configurable step count (2-20)
- Finds actual height range and divides evenly
- Auto-stitches chunk edges after terracing
- Useful for creating tiered arenas, agricultural zones, or
  stylized Meso-American terrain
2026-05-05 07:43:10 -07:00
Kelsi
f3846919a4 feat(editor): winding canyon generator with seeded sine-wave path
- Canyon Generator: carves a winding canyon across the entire tile
  using layered sine waves for natural serpentine shape
- Configurable width, depth, and seed for different canyon shapes
- Quadratic falloff at edges for smooth cliff walls
- Random seed button for quick shape exploration
- Fill with Water mode for instant river canyon
2026-05-05 07:37:27 -07:00
Kelsi
7971fd7989 feat(editor): island terrain generator with beach falloff
- Island Generator: creates raised center terrain dropping to edges
  with configurable center height and edge drop depth
- Flat interior plateau → gradual beach slope → steep underwater drop
- Preserves existing noise detail at 30% blend for natural variation
- Add water around the island for instant ocean environment
- Workflow: New Terrain → Island → Noise → Smooth → Auto-paint
2026-05-05 07:35:18 -07:00
Kelsi
f15fbf508f feat(editor): Reset + Apply noise button for quick terrain iteration 2026-05-05 07:32:41 -07:00
Kelsi
623aeff417 feat(editor): Ctrl+N and Ctrl+O shortcuts for New/Load dialogs 2026-05-05 07:31:20 -07:00
Kelsi
97f1d9c003 docs(editor): add quick actions to help overlay (Ctrl+N/O, middle-drag) 2026-05-05 07:29:04 -07:00
Kelsi
47bf2d662b feat(editor): random seed button for noise generator 2026-05-05 07:28:14 -07:00
Kelsi
629bfd6377 feat(editor): hole indicators on minimap (H marks on chunks with terrain holes) 2026-05-05 07:26:54 -07:00
Kelsi
9a3b253b14 feat(editor): frustum culling toggle in View menu 2026-05-05 07:25:32 -07:00
Kelsi
e516c3c71f feat(editor): one-click Generate Complete Zone pipeline
- File > Generate Complete Zone: runs the full procedural pipeline
  in one click: noise → smooth (3 passes) → recalc normals →
  height-based auto-paint (sand/grass/rock/snow) → slope-based
  cliff paint (rock on steep faces)
- Creates a fully textured, natural-looking zone from flat terrain
- Removed stale quickGenerate checkbox from New Terrain dialog
2026-05-05 07:21:59 -07:00
Kelsi
59c6dab2b3 feat(editor): auto-paint by slope for natural cliff texturing
- Auto-Paint by Slope: paints rock texture on steep terrain surfaces
  with configurable slope threshold (0.1 - 0.9)
- Uses per-vertex normal Z component to detect steepness
- Alpha blending based on slope gradient for smooth transitions
- Workflow: sculpt terrain → recalc normals → auto-paint slope → rock
  appears naturally on cliffs while flat areas keep their biome texture
2026-05-05 07:19:05 -07:00
Kelsi
8aee357a34 feat(editor): selected object name labels in viewport 2026-05-05 07:16:52 -07:00
Kelsi
2ed521a8f7 feat(editor): NPC name labels floating above markers in viewport
- Creature names rendered as screen-space text above each NPC marker
- Red text for hostile, green for friendly
- Labels project from 3D world position to screen coordinates
- Only visible when NPC is in front of camera (clip.w > 0)
- Much easier to identify placed creatures at a glance
2026-05-05 07:15:12 -07:00
Kelsi
0742abfe94 fix(editor): separate NPC marker updates from M2 rebuild cycle
- NPC placement now only updates cheap marker geometry (no M2 reload)
- Full M2 rebuild only triggers when PLACED OBJECT count changes
  (not NPC count — NPCs use markers, not M2 instances for now)
- Split lastObjectCount_ into lastObjCount_ + lastNpcCount_ to track
  objects and NPCs independently
- Prevents the destructive clear+reload cycle that caused GPU crashes
  when rapidly placing multiple NPCs
2026-05-05 07:13:24 -07:00
Kelsi
16308011ee fix(editor): filter bad M2 models, toast on tile not found, robustness
- Filter known effect/particle models from NPC presets (alliancebomb,
  blackhole, etc.) that cause vertex explosions — these are particle
  effect models misclassified as creatures
- "Find Tile" button now shows toast when no ADT exists for a map
- Vertex validation catches NaN/infinite/extreme positions before GPU
- These models are now skipped entirely from the creature browser
2026-05-05 07:10:29 -07:00
Kelsi
c60ddcfed4 fix(editor): stop destructive M2 rebuild on every NPC click, fix Clear All
Root cause of GPU crashes (VK_ERROR_DEVICE_LOST): every NPC placement
triggered a full clear+reload of ALL M2 models. After several cycles
the GPU state corrupted, causing vertex explosions and device lost.

Fixes:
- NPC placement now only updates cheap marker geometry (no M2 reload)
- Full M2 rebuild only happens when object COUNT changes (not every click)
- clearAllObjects() properly resets viewport, placer, spawner, markers,
  and history in one call with vkDeviceWaitIdle fence
- New Terrain uses clearAllObjects() for consistent reset
- Clear All menu item calls clearAllObjects()
- M2 vertex validation: rejects models with NaN/infinite/extreme
  vertex positions before GPU upload (prevents vertex explosions)
- NPC marker building extracted to updateNpcMarkers() method
  (can be called independently without M2 rebuild)
2026-05-05 07:07:33 -07:00
Kelsi
1c58911da0 feat(editor): ridge/mountain range generator between two points
- Ridge Generator: creates mountain ranges by setting start and end
  points, with configurable width and height
- Cross-section uses quadratic falloff, along-axis uses sqrt taper
  for natural mountain range silhouette (wider at center, tapering
  at both ends)
- Same start/end workflow as river/road tools
2026-05-05 07:00:05 -07:00
Kelsi
d6c58b5dc9 feat(editor): hill/valley generator with smooth bell curve shape
- Hill Generator: creates smooth bell-curve hills at cursor position
  with configurable radius and height
- Valley mode: same shape inverted, creates natural depressions
- Uses (1-t^2)^2 falloff for very smooth natural-looking slopes
- Two buttons: "Create Hill" and "Create Valley" for quick terrain
  shaping without switching brush modes
2026-05-05 06:57:18 -07:00
Kelsi
88416bbb1d fix(editor): NPC markers always on top, mesa generator, terrain tools
- NPC markers now render with NO depth test (via gizmo pipeline) so
  they're always visible even on sloped/rough terrain
- Mesa/Plateau generator: creates raised flat areas with steep cliff
  edges — configurable radius, height, and edge steepness
- NPC markers drawn after gizmo in the render pipeline to guarantee
  they appear on top of everything
- Fixes NPC visibility on non-flat terrain
2026-05-05 06:55:04 -07:00
Kelsi
1502c2ed85 feat(editor): crater generator for lakes, arenas, impact sites
- Crater Generator in Sculpt panel: creates a bowl-shaped depression
  with configurable radius, depth, and raised rim height
- Parabolic bowl interior with sinusoidal rim and smooth outer falloff
- Perfect for creating lakes (fill with Water mode), arenas, or
  impact craters for volcanic zones
- One click at cursor position, uses brush position for center
2026-05-05 06:50:24 -07:00
Kelsi
496f97f9db feat(editor): middle mouse orbit camera around terrain point
- Middle mouse drag orbits camera around the terrain point under cursor
  (or 100 units ahead if no terrain hit)
- Maintains distance from pivot while rotating yaw/pitch
- Much more intuitive for inspecting terrain features, placed objects,
  and NPC positions from different angles
- Works alongside right-drag (free look) and WASD (fly)
2026-05-05 06:48:05 -07:00
Kelsi
62cfb92c38 feat(editor): persistent window layout between sessions (ImGui ini) 2026-05-05 06:45:13 -07:00
Kelsi
6e62cab8bc docs(editor): add terrain tools workflow to help overlay 2026-05-05 06:43:45 -07:00
Kelsi
5d50a29d44 feat(editor): brush radius indicator on minimap
- Yellow circle on minimap shows current brush position and radius
- Scales proportionally with terrain tile size
- Visible in all brush-based modes (Sculpt/Paint/Water)
- Helps orient where you're working relative to the full tile
2026-05-05 06:42:16 -07:00
Kelsi
f593606251 feat(editor): reset-to-flat button for terrain, consolidation
- "Reset to Flat" button in Noise Generator section: zeroes all heights
  across entire tile for starting over without creating a new terrain
- Useful workflow: reset → noise → smooth → scale → clamp → auto-paint
2026-05-05 06:40:26 -07:00
Kelsi
3ac40d27ad feat(editor): road flattener tool alongside river carver
- River/Road Carver section now has two modes:
  - River: carves a channel below terrain (existing)
  - Road: flattens terrain to interpolated height between start/end
- Road mode smoothly transitions height along the path with quadratic
  falloff at edges for natural embankment shape
- Set Start → Set End + Apply workflow works for both modes
- Roads follow terrain slope by interpolating between start/end heights
- Pair with Paint mode to add cobblestone/dirt texture on the road
2026-05-05 06:37:54 -07:00
Kelsi
d253aed635 feat(editor): Find Valid Tile button, improved ADT loading workflow
- "Find Valid Tile" button in Load dialog: auto-scans manifest for the
  first available ADT tile of the selected map (checks center range
  25-45 first for open world, then full 0-63 for dungeons)
- Fixes "wrong coords" issue — dungeons use different tile coords than
  open world maps, now auto-detected
- Map name lowercased for manifest lookup consistency
- Tile check shows green "Tile found" or red "Tile not found" indicator
2026-05-05 06:35:37 -07:00
Kelsi
79a091526e fix(editor): much larger NPC markers, river carver tool
- NPC markers now 30 units tall with octagonal base, colored pole,
  and yellow diamond at top — visible from any camera altitude
- Red pole = hostile, green pole = friendly, yellow top = all NPCs
- River/Path Carver: set start point, set end point, carves a channel
  between them with configurable width and depth
- Smooth quadratic falloff at edges for natural riverbank shape
- Pair with Water mode to fill carved channels
2026-05-05 06:32:39 -07:00
Kelsi
d573f3a678 feat(editor): river/path carver tool for terrain channels
- River Carver in Sculpt panel: carves a channel between two points
  with configurable width and depth
- Set Start at cursor, then Set End + Carve to create the channel
- Smooth quadratic falloff at edges for natural riverbank shape
- Works by projecting each terrain vertex onto the line segment and
  lowering height based on distance from center
- Auto-stitches chunk edges after carving
- Pair with Water mode to fill the carved channel with liquid
2026-05-05 06:30:26 -07:00
Kelsi
14bb2cf7de feat(editor): one-click flatten platform for building sites
- "Create Flat Platform at Cursor" button in Sculpt panel
- Instantly flattens terrain under brush radius to cursor height
- Perfect for creating building sites, road beds, camp grounds
- Applies 30 iterations of flatten for a thorough result
- Restores previous brush mode after completion
2026-05-05 06:28:05 -07:00
Kelsi
9555a4a91a feat(editor): quest NPC linking via spawn list dropdowns
- Quest giver and turn-in NPC now selected from a dropdown of placed
  creatures instead of typing raw IDs — shows name + id for each
- Links quests directly to NPCs you've already placed on the map
- "None" option to unset NPC link
- Much more intuitive quest→NPC workflow
2026-05-05 06:26:15 -07:00
Kelsi
dd2b9294b5 feat(editor): terrain mirror X/Y for symmetric zone design
- Mirror X: copies left half of terrain to right half (mirrored)
- Mirror Y: copies top half to bottom half (mirrored)
- Useful for creating symmetric zones, arenas, or balanced landscapes
- Auto-stitches all chunk edges after mirror for seamless results
- UI buttons in Sculpt panel under "Mirror Terrain" section
2026-05-05 06:24:28 -07:00
Kelsi
ac88aed250 feat(editor): quick generate checkbox in New Terrain dialog 2026-05-05 06:22:44 -07:00
Kelsi
df4cc809f4 feat(editor): quest link hint in NPC panel for workflow guidance 2026-05-05 06:21:00 -07:00
Kelsi
673cfa6368 feat(editor): show all biome texture paths in New Terrain dialog 2026-05-05 06:19:49 -07:00
Kelsi
506fcc29f3 feat(editor): auto-scatter checkbox for height-based object placement 2026-05-05 06:18:47 -07:00
Kelsi
aa9a6a87a8 feat(editor): auto-paint terrain by height bands
- Auto-Paint by Height: automatically sets base texture per chunk based
  on average height — configurable thresholds for sand/grass/rock/snow
- Uses Tanaris sand, Elwynn grass, Barrens rock, Dragonblight snow
- One-click to texture an entire procedurally generated terrain
- Adjustable height thresholds via drag floats
- Workflow: noise → smooth → clamp → auto-paint for instant biome
2026-05-05 06:17:37 -07:00
Kelsi
1ba1a50112 feat(editor): terrain stamp/clone tool for replicating terrain features
- Copy Stamp: captures all vertex heights within brush radius at cursor
  position, storing relative offsets from center
- Paste Stamp: applies the copied height pattern at a new location,
  finding nearest vertices and setting their heights
- Stamp status shown in panel ("Stamp ready" / "No stamp copied")
- Auto-stitches chunk edges after paste for seamless results
- Useful for replicating hills, craters, or other terrain features
2026-05-05 06:15:33 -07:00
Kelsi
9a547f66d2 chore(editor): add multi-tile WDT placeholder comment for future 2026-05-05 06:13:02 -07:00
Kelsi
00fd1249d8 feat(editor): key 6 for Quest mode, update help overlay 2026-05-05 06:11:54 -07:00
Kelsi
f59d79537a feat(editor): quest editor with objectives, rewards, and quest chains
- New Quest mode (key 6) with full quest creation panel:
  - Title, description, required level
  - Quest giver / turn-in NPC ID linkage
  - Up to 4 objectives: Kill, Collect, Talk, Explore, Escort, Use Object
  - Rewards: XP and gold
  - Quest chain support via nextQuestId linking
- Quest list showing all created quests with level and objective count
- Save quests to JSON (included in Export Zone package)
- Foundation for campaign system: create quest chains across NPCs,
  link objectives to placed creatures, build storylines
2026-05-05 06:10:14 -07:00
Kelsi
124ff5a54a feat(editor): tile availability checker, NPC marker diagnostics
- Load dialog shows green "Tile found" / red "Tile not found" indicator
  by checking the manifest before you attempt to load
- NPC marker build/render diagnostic logging to trace rendering issues
- Map browser and tile checker work together for easy existing zone loading
2026-05-05 06:05:33 -07:00
Kelsi
a6b8cd75f6 feat(editor): map browser for loading existing WoW zones
- Load Map Tile dialog now shows a searchable list of all available
  maps from the manifest (Azeroth, Kalimdor, Outland, dungeons, etc.)
- Click a map name to select it, then pick tile X/Y coordinates
- Helpful tile range hints shown below coordinates
- Maps indexed from WDT files in the manifest on startup
- Foundation for loading and modifying sections of existing WoW maps
2026-05-05 06:01:42 -07:00
Kelsi
5ccc61f144 fix(editor): NPC position markers always visible regardless of M2 rendering
- Colored diamond markers rendered at every NPC position on terrain:
  red for hostile, green for friendly, with vertical pillar for height
- Renders through water pipeline (alpha-blended, depth-tested)
- Always visible regardless of whether M2 creature model renders
- Scales with NPC scale setting for consistent visual size
- This guarantees you can always see where NPCs are placed
2026-05-05 05:59:06 -07:00
Kelsi
b25f7fa4bf fix(editor): add M2 render diagnostics to Info panel for NPC debugging
- Info panel now shows M2 renderer stats: model count, instance count,
  draw call count — visible in real-time to diagnose NPC rendering issues
- When placing NPCs: if models=0/instances=0, the model failed to load
  If models>0 but draws=0, it's a culling/frame-sync issue
  If draws>0, the model IS rendering but may be too small to see
2026-05-05 05:56:49 -07:00
Kelsi
9322d37b81 feat(editor): height scale tool for terrain relief control
- Scale Heights: multiply all terrain heights by a factor (0.1x - 5.0x)
- >1.0 exaggerates relief (deeper valleys, taller peaks)
- <1.0 flattens terrain toward base height
- Re-stitches all chunk edges after scaling for seamless results
- Workflow: noise → smooth → scale → clamp for precise control
2026-05-05 05:55:05 -07:00
Kelsi
ff33babb1d feat(editor): fog toggle in View menu 2026-05-05 05:52:23 -07:00
Kelsi
d59d69b0c5 feat(editor): height clamp tool for controlled terrain range
- Clamp Heights: sets min/max height bounds across entire tile
  (DragFloatRange2 slider for min/max, -500 to 2000 range)
- Useful workflow: Generate noise → Smooth → Clamp to desired range
- Prevents terrain from going underground or too high
- All affected chunks marked dirty for mesh regeneration
2026-05-05 05:51:03 -07:00
Kelsi
c93a997424 docs(editor): add Home/scroll/speed shortcuts to help overlay 2026-05-05 05:48:56 -07:00
Kelsi
434fdf6c7f feat(editor): center on terrain (Home key), navigation improvements
- "Center on Terrain" (Home key or View menu): resets camera to center
  of loaded tile at 300 units altitude with 45-degree downward pitch.
  Essential for recovering when camera gets lost in empty space.
- Toast confirmation on center action
2026-05-05 05:48:00 -07:00
Kelsi
2b0a81fd9a fix(editor): update speed hint to show Shift+scroll 2026-05-05 05:45:17 -07:00
Kelsi
12acbfb2d5 feat(editor): scroll wheel zoom, clickable minimap navigation
- Scroll wheel now zooms camera (moves along look direction) instead
  of adjusting speed. Much more intuitive for terrain editing.
- Shift+scroll adjusts camera speed (old behavior preserved)
- Click on minimap to teleport camera to that location on the terrain
- Zoom speed scales with current camera speed for consistent feel
2026-05-05 05:44:24 -07:00
Kelsi
f891bd02a5 feat(editor): import ADT doodad/WMO placements on load
- Loading an existing ADT now imports its MDDF (doodad) and MODF (WMO)
  placements into the object placer with correct position/rotation/scale
- Allows editing zones that already have objects placed in them
- Mutable getObjects() accessor for bulk import operations
- Log shows imported doodad + WMO count on load
2026-05-05 05:42:14 -07:00
Kelsi
17fda37813 docs(editor): add mode shortcuts to help overlay, update memory 2026-05-05 05:39:48 -07:00
Kelsi
e1776620d5 feat(editor): brush mode tooltips, chunk texture inspector in Paint panel
- Brush mode tooltips: hover over the mode combo to see what each does
  (Raise/Lower/Smooth/Flatten/Level/Erode descriptions)
- Chunk texture inspector: Paint panel shows which texture layers are
  on the chunk under the cursor (base + blended layers with filenames)
- Helps identify what textures you're painting over before blending
2026-05-05 05:37:35 -07:00
Kelsi
dc9b085e38 feat(editor): add author credit to About dialog 2026-05-05 05:36:01 -07:00
Kelsi
f1168cf6b7 feat(editor): about dialog, terrain stats, enhanced status bar
- About dialog: Help > About shows editor version, capabilities, and
  supported format (WoW 3.3.5a compatible)
- Info panel now shows texture count, water chunk count, hole chunk count
- Status bar shows object/NPC counts alongside map name
- Better at-a-glance overview of zone composition without opening panels
2026-05-05 05:34:43 -07:00
Kelsi
864415d246 feat(editor): auto-load objects/NPCs, mode buttons, number key shortcuts
- Loading an ADT now auto-loads objects.json and creatures.json from
  the output directory if they exist (full session persistence)
- Toolbar buttons now highlight active mode in blue (clearer visual)
- Number keys 1-5 switch modes: 1=Sculpt 2=Paint 3=Objects 4=Water 5=NPCs
- Toast shows loaded object/NPC count on zone open
2026-05-05 05:29:30 -07:00
Kelsi
e18c2cf009 feat(editor): minimap camera indicator, bulk operations, snap all
- White crosshair on minimap shows camera position in real-time
- Bulk Operations section in Object panel:
  - "Delete All in Radius": removes all objects within brush radius
  - "Snap All to Ground": raycasts every object downward to terrain
    (fixes all floating objects in one click)
- Minimap legend updated with camera indicator
- Useful for cleaning up scattered objects or fixing placement height
2026-05-05 05:25:33 -07:00
Kelsi
fe91fda421 fix(editor): NPC default scale 3x, right-side panels track window resize
- NPC default scale changed from 1.0 to 3.0 so creatures are visible
  from typical editing altitude (WoW creature models are very small at
  scale 1.0)
- Properties/Info panel uses ImGuiCond_Always for position so it stays
  pinned to the right edge when the window is resized (was getting lost
  off-screen before)
2026-05-05 05:23:09 -07:00
Kelsi
befa12f9e6 fix(editor): Clear All, New Terrain reset, right-click menu, gizmo drag
- Clear All now actually removes all objects and NPCs (was only clearing
  selections before). Uses new ObjectPlacer::clearAll() method.
- New Terrain clears all objects/NPCs and resets viewport before creating
  fresh terrain. Fixes stale state from previous session.
- Right-click context menu works on both objects AND NPCs with
  appropriate options for each (Move/Rotate/Scale for objects,
  Fly To/Duplicate for NPCs)
- Gizmo drag: left-click now confirms the transform (ends drag) instead
  of requiring mouse-up. Right-click cancels. Camera no longer steals
  mouse events while gizmo is active.
- Right-click on unselected area passes through to camera correctly
2026-05-05 05:20:53 -07:00
Kelsi
d9ed7be36c feat(editor): fly-to-object, export README, quality of life
- "Fly To" button on selected objects and NPCs: moves camera 30 units
  above the selected item for quick navigation on large zones
- Export now generates README.txt with zone summary: map name, tile
  coords, object/NPC counts, and file listing
- Complete export package: zone.json + WDT + ADT + objects.json +
  creatures.json + README.txt
2026-05-05 05:16:43 -07:00
Kelsi
8c9407e0f5 feat(editor): object save/load JSON, working duplicate, export objects
- Object placer save/load: objects.json persists placed M2/WMO objects
  across sessions (path, position, rotation, scale, type)
- Fixed Duplicate button in Object panel: now actually creates a copy
  with correct path/type/scale instead of being a no-op stub
- Export Zone now saves objects.json alongside ADT/WDT/creatures/manifest
- Object JSON loader parses all fields for full round-trip
2026-05-05 05:14:03 -07:00
Kelsi
8341fb6dc9 feat(editor): minimap objects/NPCs, NPC duplicate, legend
- Minimap now shows placed objects (yellow dots) and NPCs (red=hostile,
  green=friendly) at their world positions on the height grid
- NPC Duplicate button: copies selected creature with 10-unit offset
  for quick population of similar spawns
- Minimap legend: colored dots showing Object/Hostile/Friendly markers
- All positions correctly mapped from world coords to minimap UV space
2026-05-05 05:11:33 -07:00
Kelsi
e0d14de5d2 feat(editor): zone manifest for client loading, export workflow complete
- Zone manifest (zone.json): generated on export with map name, map ID,
  tile list, biome, creature flag, and file paths. This is what the
  wowee client will read to discover and load custom zones.
- Export workflow now produces a complete loadable zone package:
  zone.json + MapName.wdt + MapName_X_Y.adt + creatures.json
- ZoneManifest class with save/load (JSON format)
- Custom map IDs start at 9000+ to avoid conflicting with retail maps
- New Terrain dialog shows helper text for map name format
2026-05-05 05:06:41 -07:00
Kelsi
3d6c508491 refactor(editor): remove dead marker renderer, clean up stale fields
- Remove markerRenderer_ initialization, shutdown, update, and clear
  calls from EditorViewport (markers replaced by actual M2 rendering)
- Remove unused saveAdtRequested_/saveWdtRequested_ fields and their
  void casts (replaced by unified exportZone workflow)
- Zero warnings across both wowee and wowee_editor targets
2026-05-05 05:04:14 -07:00
Kelsi
9bc05fae87 feat(editor): smooth entire tile, snap-to-ground toggle, object list improvements
- Smooth Entire Tile: global smoothing pass with configurable iterations
  (1-10). Smooths across chunk boundaries for seamless results. Updates
  inner vertices from smoothed outer grid. Great after noise generation.
- Snap to Ground checkbox: on by default, objects placed at terrain
  surface. Disable for floating/airborne objects.
- Random Rotation + Snap Ground checkboxes side-by-side for fast setup
- Toast notifications on noise apply and smooth operations
- Smooth pass uses cross-chunk neighbor averaging for edge continuity
2026-05-05 05:00:31 -07:00
Kelsi
5df007b7b9 feat(editor): random rotation, placed object list, quality of life
- Random Rotation checkbox: each placed object gets a random Y rotation
  (great for natural-looking tree/rock placement without manual tweaking)
- Placed Object List: collapsible list in Object panel showing all placed
  objects with name and position, click to select
- Both features reduce repetitive manual work when building dense zones
2026-05-05 04:57:42 -07:00
Kelsi
89312120f4 feat(editor): heightmap export, help overlay, keyboard reference
- Export Heightmap: File > Export Heightmap saves terrain as 16-bit
  RAW grayscale (129x129) for use in external terrain editors or
  as a backup. Configurable max height scale.
- Help overlay (F1 or Help menu): lists all keyboard shortcuts
  organized by category (navigation, editing, object transform, view)
- Round-trip heightmap workflow: import → edit → export
2026-05-05 04:52:36 -07:00
Kelsi
2f96f112bd feat(editor): heightmap import, toast notifications, workflow polish
- Import Heightmap: File > Import Heightmap loads RAW 8/16-bit grayscale
  files (129x129 or 257x257) and maps to terrain heights with configurable
  scale. Supports standard terrain editor heightmap formats.
- Toast notifications: non-intrusive green popup at bottom center for
  user feedback (save confirmations, import results, errors)
- Toasts fade out after 3 seconds with alpha animation
- Auto-save now shows toast on save
- Quick-save (Ctrl+S) shows toast confirmation
2026-05-05 04:49:43 -07:00
Kelsi
a91233a6ec feat(editor): erosion brush, NPC load, auto-save
- Erode brush mode: simulates water erosion by moving height downhill
  based on slope, creating natural drainage patterns and gullies
- NPC JSON loader: File > Load NPCs parses saved creatures.json back
  into the spawn list (round-trip save/load now works)
- Auto-save: every 5 minutes when unsaved changes exist, exports the
  full zone (ADT + WDT + creatures) to the output directory
- Sculpt mode now has 6 brush types: Raise/Lower/Smooth/Flatten/Level/Erode
2026-05-05 04:44:54 -07:00
Kelsi
42749e9b58 feat(editor): procedural noise generator, sky presets, viewport lighting
- Noise Generator in Sculpt panel: applies procedural value noise
  with configurable frequency, amplitude, octaves, and seed
  to create hills/valleys across entire tile instantly
- Sky/Lighting presets: View > Sky menu with Day (blue sky, high sun),
  Dusk (orange, low sun), Night (dark blue, moonlight)
- Viewport clear color and light direction now configurable at runtime
- Noise uses smoothstep interpolation with octave fractal layering
2026-05-05 04:40:37 -07:00
Kelsi
f5fe9a0101 feat(editor): terrain holes, recent textures, sculpt panel polish
- Punch Hole / Fill Hole buttons in Sculpt panel: creates terrain
  holes (4x4 bitmask) for cave entrances, mine shafts, etc.
  Uses brush radius to determine affected area.
- Recent Textures: paint panel shows last 6 used textures as quick-
  select buttons (no need to re-search the full list)
- Holes saved in ADT format (MCNK holes field) and respected by
  the mesh generator (triangles skipped at hole positions)
2026-05-05 04:34:03 -07:00
Kelsi
cc6a72e7b2 feat(editor): minimap, patrol path editing, flatten height picker
- Minimap window: 16x16 chunk grid colored by average height
  (blue=low, green=mid, brown=high, blue overlay=water)
- NPC patrol path UI: add waypoints at cursor, view path list,
  delete individual points or clear entire path
- Sculpt flatten "Pick" button: click to set target height from
  cursor position instead of typing manually
- Height range displayed in minimap footer
2026-05-05 04:28:44 -07:00
Kelsi
ba96de7138 fix(editor): normal recalculation after sculpt, mode switch cleanup
- Terrain normals recalculated after height changes (smooth lighting
  on sculpted terrain instead of flat-shaded appearance)
- Ghost preview and brush indicator cleared when switching modes
  (prevents stale model instances or circles persisting)
- File > Clear All resets undo history and selections
- Normal computation uses finite differences from neighbor heights,
  handles both outer (9x9) and inner (8x8) vertex grid positions
2026-05-05 04:24:20 -07:00
Kelsi
5daa359e74 feat(editor): object scatter, camera bookmarks, shortcut hints
- Object scatter tool: place N copies of selected M2/WMO in a radius
  with random rotation and scale range (Min/Max Scale slider)
- Camera bookmarks: F5 saves current position, View > Load Bookmark
  to jump back — useful for working on different parts of a large zone
- Shortcut hints shown at bottom of Object panel
  (G=move, R=rotate, T=scale, Del=remove)
- DragFloatRange2 for min/max scale in scatter UI
2026-05-05 04:20:26 -07:00
Kelsi
48026421c9 feat(editor): NPC scatter tool, adjacent tile creation, multi-tile prep
- Scatter tool: place N creatures in a radius around cursor position
  with random rotation and uniform disk distribution
- File > Add Adjacent Tile: creates and exports a blank tile N/S/E/W
  of current (foundation for multi-tile zone editing)
- Scatter UI: count slider (1-30), radius slider (10-200)
- Scatter places all copies with same stats/behavior as template
2026-05-05 04:14:29 -07:00
Kelsi
6e24e08818 feat(editor): brush radius circle indicator on terrain
- Yellow circle renders at cursor position showing brush radius
- Visible in Sculpt, Paint, and Water modes
- Built from 48-segment quad strip slightly above terrain surface
- Renders through the water pipeline (alpha-blended, depth-tested)
- Disappears when cursor leaves terrain or in Object/NPC modes
- Brush VB cleaned up properly on shutdown
2026-05-05 04:10:46 -07:00
Kelsi
d28c5ec842 feat(editor): unified Export Zone, quick-save, cursor world coords
- Ctrl+S quick-saves entire zone (ADT + WDT + creatures.json) to output/
- File > Export Zone dialog saves all data to chosen directory
- exportZone() bundles ADT, WDT, and NPC spawns in one operation
- Info panel shows cursor world coordinates for placement debugging
- Info panel shows NPC count alongside object count
- Save state indicator: "Saved" (green) vs "* Unsaved (Ctrl+S)" (yellow)
- File menu reorganized: Quick Save + Export Zone replaces separate ADT/WDT
2026-05-05 04:06:19 -07:00
Kelsi
88abbfb564 feat(editor): undo object placement, snap to ground, keyboard shortcuts
- Ctrl+Z in Object/NPC mode undoes last placement (50-deep stack)
- "Snap Ground" button raycasts straight down to place object on terrain
- Useful for objects placed too high or moved off terrain surface
- Undo stack adjusts indices when objects are removed mid-stack
2026-05-05 04:01:06 -07:00
Kelsi
ace6173401 fix(editor): terrain raycast on rough terrain, keyboard shortcuts, context menu
- Raycast AABB now uses actual min/max vertex heights per chunk
  instead of fixed ±200 padding (fixes misses on sculpted terrain)
- Right-click context menu opens correctly (deferred popup via flag
  since ImGui::OpenPopup must be called within ImGui frame)
- Keyboard shortcuts: G=Move, R=Rotate, T=Scale, X/Y=axis lock,
  Escape=deselect, Delete works in any mode for objects/NPCs
- Delete key now removes selected NPC in NPC mode too
2026-05-05 03:55:53 -07:00
Kelsi
f38884856f fix(editor): NPC ghost preview, scale control, frame sync for M2 rendering
- Ghost preview now shows in NPC mode (follows cursor with creature model)
- Added scale field to CreatureSpawn (default 1.0, slider 0.5-10x)
- NPC instances render at their configured scale
- Scale included in JSON save format
- M2Renderer::update() now runs AFTER beginFrame() so getCurrentFrame()
  returns the correct frame index — fixes instance SSBO mismatch that
  caused draws=0 despite loaded models
2026-05-05 03:52:43 -07:00
Kelsi
2980ca83e7 feat(editor): add standalone world editor (rough/WIP)
Standalone wowee_editor tool for creating custom WoW zones.
This is a rough initial implementation — many features work but
M2/WMO rendering still has issues (frame sync, texture layout
transitions) and needs further polish.

Terrain:
- Create new blank terrain with 10 biome types (Grassland, Forest,
  Jungle, Desert, Barrens, Snow, Swamp, Rocky, Beach, Volcanic)
- Load existing ADT tiles from extracted game data
- Sculpt brushes: Raise, Lower, Smooth, Flatten, Level
- Chunk edge stitching prevents seams between tiles
- Undo/redo (100-deep stack, Ctrl+Z/Ctrl+Shift+Z)
- Save to WoW ADT/WDT format

Texture Painting:
- Paint/Erase/Replace Base modes
- Full tileset texture browser (1285 textures from manifest)
- Per-zone directory filtering and search
- Alpha map editing with 4-layer limit (auto-replaces weakest)

Object Placement:
- M2 and WMO model placement with full manifest browser (11k M2s, 2k WMOs)
- M2Renderer + WMORenderer integrated (loads .skin files for WotLK)
- Ghost preview follows cursor before placing
- Ctrl+click selection, right-click context menu
- Transform gizmo (Move/Rotate/Scale with axis constraints)
- Position/rotation/scale editing in properties panel

NPC/Monster System:
- 631 creature presets scanned from manifest, categorized
  (Critters, Beasts, Humanoids, Undead, Demons, etc.)
- Stats editor: level, health, mana, damage, armor, faction
- Behavior: Stationary, Patrol, Wander, Scripted
- Aggro/leash radius, respawn time, flags (hostile/vendor/etc.)
- Save creature spawns to JSON

Water:
- Place water at configurable height per chunk
- Liquid types: Water, Ocean, Magma, Slime
- Rendered as translucent colored quads
- Saved in ADT MH2O format

Infrastructure:
- Free-fly camera (WASD/QE, right-drag look, scroll speed)
- 5-mode toolbar: Sculpt | Paint | Objects | Water | NPCs
- Asset browser indexes full manifest on startup
- Editor water/marker shaders (pos+color vertex format)
- forceNoCull added to M2Renderer for editor use
- AssetManifest::getEntries() and AssetManager::getManifest() exposed

Known issues:
- M2/WMO rendering may not display on first placement (frame index
  sync between update/render was misaligned — now fixed but untested
  end-to-end)
- Validation layer errors on shutdown (resource cleanup ordering)
- Object placement on steep terrain can miss raycast
- No undo for texture painting or object placement yet
2026-05-05 03:47:03 -07:00
Kelsi
d138269a35 fix(movement): reject server teleports to corrupted near-origin positions
Some checks failed
Build / Build (arm64) (push) Has been cancelled
Build / Build (x86-64) (push) Has been cancelled
Build / Build (macOS arm64) (push) Has been cancelled
Build / Build (windows-arm64) (push) Has been cancelled
Build / Build (windows-x86-64) (push) Has been cancelled
Security / CodeQL (C/C++) (push) Has been cancelled
Security / Semgrep (push) Has been cancelled
Security / Sanitizer Build (ASan/UBSan) (push) Has been cancelled
The server can persist a corrupted near-origin position on map 0 (from a
faulty area-trigger destination) across sessions. On re-login it sends the
bad position via LOGIN_VERIFY_WORLD; if the player walks into the offending
trigger again the server re-teleports there, and our heartbeats reinforce
the bad save — creating a permanent teleport loop.

Defenses added:
- handleTeleportAck rejects MSG_MOVE_TELEPORT to near-origin on map 0
  (no position update, no ACK, no world reload)
- applyPlayerTransportState rejects player UPDATE_OBJECT MOVEMENT blocks
  pushing the same bad position
- sendMovement blocks heartbeats originating from near-origin so the
  server cannot persist the bad save
- 10-second area-trigger cooldown after teleport / world entry / login
  (replaces the one-shot suppress flag that re-fired on jitter)
- Immediate STOP+HEARTBEAT after teleport ACK / WORLDPORT ACK / login
  to sync the real position with the server promptly
- CMSG_AREATRIGGER firing now logged at WARNING level for diagnosis
2026-04-24 17:48:49 -07:00
Kelsi
f9f02569d6 fix(vulkan): re-allocate megaBoneSet_ after descriptor pool reset and fix PlayerFrame ImGui crash
Some checks failed
Build / Build (arm64) (push) Has been cancelled
Build / Build (x86-64) (push) Has been cancelled
Build / Build (macOS arm64) (push) Has been cancelled
Build / Build (windows-arm64) (push) Has been cancelled
Build / Build (windows-x86-64) (push) Has been cancelled
Security / CodeQL (C/C++) (push) Has been cancelled
Security / Semgrep (push) Has been cancelled
Security / Sanitizer Build (ASan/UBSan) (push) Has been cancelled
Re-allocate megaBoneSet_[0..1] in M2Renderer::clear() after vkResetDescriptorPool invalidates all
sets from boneDescPool_. Stale handles were bound to command buffers during rendering, causing
cascading validation errors. Also add ImGui::Dummy() after SetCursorScreenPos in the shaman totem
bar to satisfy ImGui's window boundary extension assertion.
2026-04-15 13:22:30 -07:00
Kelsi
01fecbf3e0 fix(parsing): correct UPDATE_OBJECT PackedGuid, cape textures, and missing asset guards
Some checks failed
Build / Build (arm64) (push) Has been cancelled
Build / Build (x86-64) (push) Has been cancelled
Build / Build (macOS arm64) (push) Has been cancelled
Build / Build (windows-arm64) (push) Has been cancelled
Build / Build (windows-x86-64) (push) Has been cancelled
Security / CodeQL (C/C++) (push) Has been cancelled
Security / Semgrep (push) Has been cancelled
Security / Sanitizer Build (ASan/UBSan) (push) Has been cancelled
- Fix MOVEMENT update type to use readPackedGuid() instead of readUInt64() (WotLK 3.3.5a)
- Add desync diagnostic logging to UPDATE_OBJECT parser for future debugging
- Register MSG_MOVE_SET_COLLISION_HGT (0x518) as skip handler
- Fix cape texture lookup to only try .blp extension variants (4 files)
- Add fileExists() guards for underwear textures referencing missing BLP files (4 files)
- Add spell visual impact→cast M2 path fallback
- Skip WMO doodad instance creation when model load fails
- Demote spell caster position warning to debug level
2026-04-14 06:06:50 -07:00
Kelsi
83eef878fb fix: validate displayId range and skip missing equipment textures
Reject displayId values >100k (corrupted update-field data) to avoid
pointless DBC lookups and log spam. Add fileExists() guard before
using base texture path in 4 equipment compositing code paths that
were falling through without checking, causing excessive "Texture not
found" warnings when users have incomplete MPQ extractions.
2026-04-14 04:20:28 -07:00
Kelsi
2f3a973444 docs: update documentation for PRs #59-63 refactors
- architecture.md: add chat system modules (src/ui/chat/), world map
  modules (src/rendering/world_map/), CatmullRomSpline (src/math/),
  transport decomposition, and updated namespace list
- status.md: update timestamp to 2026-04-14, add recent refactors
  section and world map known gaps
- CHANGELOG.md: add detailed entries for PRs #58-63 covering
  architecture, features, bug fixes, and 19 new test files
- TESTING.md: expand test suite layout from 8 to 27 files organized
  by category (core, animation, transport, world map, chat)
- CONTRIBUTING.md: update namespace table, testing section, and key
  files list to reflect new module directories
- README.md: update status timestamp to 2026-04-14
2026-04-14 03:42:46 -07:00
Kelsi
3be40c3b69 fix: resolve 7 code quality issues across PRs #59-63
- Remove stale kVOffset (-0.15) from zone_highlight_layer hover detection;
  the offset was removed from rendering but left in the hit-test path,
  shifting hover ~15% vertically
- Add null guard for cachedGameHandler_ in ChatPanel::inputTextCallback
  to prevent dereference before first render frame
- Zero WindowBorderSize in world map ImGui window to eliminate gap
  between window edge and map content
- Replace hardcoded cosmic highlight multipliers with displayH×displayH
  square rendering, preserving 1:1 aspect ratio at any resolution
- Skip transport waypoints where serverToCanonical zeroes nonzero input
  instead of silently building paths with broken (0,0,0) coordinates
- Use length-squared check (posLenSq > 1.0) for spline endpoint
  validation instead of per-component != 0 comparison, so entities
  near the world origin are no longer skipped
- Fix off-by-one in ChatPanel::insertChatLink buffer capacity check
2026-04-14 02:41:55 -07:00
Kelsi Rae Davis
9547feabf9
Merge pull request #63 from ldmonster/fix/map-fixes
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
[chore] fix: World Map: ZMP pixel-accurate hover
2026-04-13 21:51:03 -07:00
Kelsi Rae Davis
3c8736449a
Merge pull request #62 from ldmonster/chore/chat-system
[chore] refactor(chat): decompose ChatPanel into modular architecture
2026-04-13 21:50:32 -07:00
Pavel Okhlopkov
97c95941f4 feat(world-map): remove kVOffset hack, ZMP hover, textured player arrow
- Remove the -0.15 vertical offset (kVOffset) from coordinate_projection,
  coordinate_display, and zone_highlight_layer; continent UV math is now
  identical to zone UV math
- Switch world_map_facade aspect ratio to MAP_W/MAP_H (1002×668) and crop
  the FBO image with MAP_U_MAX/MAP_V_MAX instead of stretching the full
  1024×768 FBO
- Account for ImGui title bar height (GetFrameHeight) in window sizing and
  zone highlight screen-space rect coordinates
- Add ZMP 128×128 grid pixel-accurate hover detection in zone_highlight_layer;
  falls back to AABB when ZMP data is unavailable
- Upgrade PlayerMarkerLayer with full Vulkan lifecycle (initialize,
  clearTexture, destructor); loads MinimapArrow.blp and renders a rotated
  32×32 textured quad via AddImageQuad; red triangle retained as fallback
- Expose arrowRotation_ / arrowDS_ accessors on Minimap; clean up arrow DS
  and texture in Minimap::shutdown()
- Wire PlayerMarkerLayer::initialize() into WorldMapFacade::initialize()
- Update coordinate-projection test: continent and zone UV are now equal

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-12 20:02:50 +03:00
Pavel Okhlopkov
ada019e0d4 refactor(chat): extract ItemTooltipRenderer, slim render(), consolidate utils
- Extract renderItemTooltip() (510 LOC) from ChatMarkupRenderer into
  dedicated ItemTooltipRenderer class; chat_markup_renderer.cpp 766→192 LOC
- Extract formatChatMessage(), detectChannelPrefix(), inputTextCallback()
  from render(); render() 711→376 LOC
- Consolidate replaceGenderPlaceholders() from 3 copies into
  chat_utils::replaceGenderPlaceholders(); remove 118 LOC duplicate from
  quest_log_screen.cpp, update 8 call sites in window_manager.cpp
- Delete chat_panel_commands.cpp (359 LOC) — absorb sendChatMessage,
  executeMacroText, PortBot helpers into chat_panel.cpp; move
  evaluateMacroConditionals to macro_eval_convenience.cpp
- Delete chat_panel_utils.cpp (229 LOC) — absorb small utilities into
  chat_panel.cpp
- Replace 3 forward declarations of evaluateMacroConditionals with
  #include "ui/chat/macro_evaluator.hpp"

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-12 15:46:03 +03:00
Pavel Okhlopkov
42f1bb98ea refactor(chat): decompose into modular architecture, add GM commands, fix protocol
- Extract ChatPanel monolith into 15+ focused modules under ui/chat/
  (ChatInput, ChatTabManager, ChatTabCompleter, ChatMarkupParser,
  ChatMarkupRenderer, ChatCommandRegistry, ChatBubbleManager,
  ChatSettings, MacroEvaluator, GameStateAdapter, InputModifierAdapter)
- Split 2700-line chat_panel_commands.cpp into 11 command modules
- Add GM command handling: 190-command data table, dot-prefix interception,
  tab-completion, /gmhelp with category filter
- Fix ChatType enum to match WoW wire protocol (SAY=0x01 not 0x00);
  values 0x00-0x1B shared across Vanilla/TBC/WotLK
- Fix BG_SYSTEM_* values from 82-84 (UB in bitmask shifts) to 0x24-0x26
- Fix infinite Enter key loop after teleport (disable TOGGLE_CHAT repeat,
  add 2-frame input cooldown)
- Add tests: chat_markup_parser, chat_tab_completer, gm_commands,
  macro_evaluator

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-12 14:59:56 +03:00
Kelsi Rae Davis
09c4a9a04a
Merge pull request #61 from ldmonster/feat/map-system
Some checks failed
Build / Build (arm64) (push) Has been cancelled
Build / Build (x86-64) (push) Has been cancelled
Build / Build (macOS arm64) (push) Has been cancelled
Build / Build (windows-arm64) (push) Has been cancelled
Build / Build (windows-x86-64) (push) Has been cancelled
Security / CodeQL (C/C++) (push) Has been cancelled
Security / Semgrep (push) Has been cancelled
Security / Sanitizer Build (ASan/UBSan) (push) Has been cancelled
[chore] refactor: Decompose World Map into Modular Component Architecture
2026-04-12 00:10:01 -07:00
Pavel Okhlopkov
fff06fc932 refactor: decompose world map into modular component architecture
Break the monolithic 1360-line world_map.cpp into 16 focused modules
under src/rendering/world_map/:

Architecture:
- world_map_facade: public API composing all components (PIMPL)
- world_map_types: Vulkan-free domain types (Zone, ViewLevel, etc.)
- data_repository: DBC zone loading, ZMP pixel map, POI/overlay storage
- coordinate_projection: UV projection, zone/continent lookups
- composite_renderer: Vulkan tile pipeline + off-screen compositing
- exploration_state: server mask + local exploration tracking
- view_state_machine: COSMIC→WORLD→CONTINENT→ZONE navigation
- input_handler: keyboard/mouse input → InputAction mapping
- overlay_renderer: layer-based ImGui overlay system (OCP)
- map_resolver: cross-map navigation (Outland, Northrend, etc.)
- zone_metadata: level ranges and faction data

Overlay layers (each an IOverlayLayer):
- player_marker, party_dot, taxi_node, poi_marker, quest_poi,
  corpse_marker, zone_highlight, coordinate_display, subzone_tooltip

Fixes:
- Player marker no longer bleeds across continents (only shown when
  player is in a zone belonging to the displayed continent)
- Zone hover uses DBC-projected AABB rectangles (restored from
  original working behavior)
- Exploration overlay rendering for zone view subzones

Tests:
- 6 new test files covering coordinate projection, exploration state,
  map resolver, view state machine, zone metadata, and integration

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-12 09:52:51 +03:00
Kelsi Rae Davis
db3f65a87e
Merge pull request #60 from ldmonster/chore/transport-manager
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
[chore] refactor: extract spline math, decompose TransportManager
2026-04-11 03:48:42 -07:00
Kelsi Rae Davis
5e82464658
Merge pull request #59 from ldmonster/fix/minor-bugs
[fix] minor-bugs: UI, animation, quest, rendering & movement fixes
2026-04-11 03:47:02 -07:00
Pavel Okhlopkov
f156876f46 bump
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-11 10:11:47 +03:00
Pavel Okhlopkov
39719cac82 refactor: decompose TransportManager and upgrade Entity to CatmullRom splines
TransportManager decomposition:
- Extract TransportClockSync: server clock offset, yaw flip detection,
  velocity bootstrap, client/server mode switching
- Extract TransportAnimator: spline evaluation, Z clamping, orientation
  from server yaw or spline tangent
- Slim TransportManager to thin orchestrator delegating to ClockSync and
  Animator; add pushTransform() helper to deduplicate WMO/M2 renderer calls
- Remove legacy orientationFromSplineTangent (now uses
  CatmullRomSpline::orientationFromTangent)

Entity path following upgrade:
- Replace pathPoints_/pathSegDists_ linear lerp with
  std::optional<CatmullRomSpline> activeSpline_
- startMoveAlongPath builds SplineKeys with distance-proportional timing
- updateMovement evaluates CatmullRomSpline for smooth Catmull-Rom
  interpolation matching server-side creature movement
- Reset activeSpline_ on setPosition/startMoveTo to prevent stale state

Tests:
- Add test_transport_components (9 cases): ClockSync client/server/reverse
  modes, yaw flip detection, Animator position eval, server yaw, Z clamping
- Link spline.cpp into test_entity for CatmullRomSpline dependency

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-11 09:50:38 +03:00
Pavel Okhlopkov
de0383aa6b refactor: extract spline math, consolidate packet parsing, decompose TransportManager
Extract CatmullRomSpline (include/math/spline.hpp, src/math/spline.cpp) as a
standalone, immutable, thread-safe spline module with O(log n) binary segment
search and fused position+tangent evaluation — replacing the duplicated O(n)
evalTimedCatmullRom/orientationFromTangent pair in TransportManager.

Consolidate 7 copies of spline packet parsing into shared functions in
game/spline_packet.{hpp,cpp}: parseMonsterMoveSplineBody (WotLK/TBC),
parseMonsterMoveSplineBodyVanilla, parseClassicMoveUpdateSpline,
parseWotlkMoveUpdateSpline, and decodePackedDelta. Named SplineFlag constants
replace magic hex literals throughout.

Extract TransportPathRepository (game/transport_path_repository.{hpp,cpp}) from
TransportManager — owns path data, DBC loading, and path inference. Paths stored
as PathEntry wrapping CatmullRomSpline + metadata (zOnly, fromDBC, worldCoords).
TransportManager reduced from ~1200 to ~500 lines, focused on transport lifecycle
and server sync.

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-11 08:30:28 +03:00
Pavel Okhlopkov
535cc20afe fix state gate races and robust spline
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-10 23:30:55 +03:00
Pavel Okhlopkov
6ba0edc2fb change weapon for ranged skills
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-10 23:01:16 +03:00
Pavel Okhlopkov
fe1dc5e02b make a user friendly delete message
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-10 22:22:14 +03:00
Pavel Okhlopkov
5b47d034c5 fix(movement): multi-segment path interpolation, waypoint parsing & terrain Z clamping
Add proper waypoint support to entity movement:

- Parse intermediate waypoints from MonsterMove packets in both WotLK
  and Vanilla paths. Uncompressed paths store absolute float3 waypoints;
  compressed paths decode TrinityCore's packed uint32 deltas (11-bit
  signed x/y, 10-bit signed z, ×0.25 scale, waypoint = midpoint − delta)
  with correct 2's-complement sign extension.

- Entity::startMoveAlongPath() interpolates along cumulative-distance-
  proportional segments instead of a single straight line.

- MovementHandler builds the full path (start → waypoints → destination)
  in canonical coords and dispatches to startMoveAlongPath() when
  waypoints are present.

- Snap entity x/y/z to moveEnd in the dead-reckoning overrun phase
  before starting a new movement, preventing visible teleports when the
  renderer was showing the entity at its destination.

- Clamp creature and player entity Z to the terrain surface via
  TerrainManager::getHeightAt() during active movement. Idle entities
  keep their server-authoritative Z to avoid breaking flight masters,
  elevator riders, etc.

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-10 20:35:18 +03:00
Pavel Okhlopkov
e07983b7f6 fix(rendering): crash on window resize due to stale swapchain
- Mark swapchain dirty in Application's SDL resize handler (was only done
  in Window::pollEvents which is never called)
- Skip swapchain recreation when window is minimized (0×0 extent violates
  Vulkan spec and crashes vmaCreateImage)
- Guard aspect ratio division by zero when height is 0

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-10 19:51:13 +03:00
Pavel Okhlopkov
759d6046bb fix(quest): quest log population, NPC marker updates on accept/abandon
- Delegate GameHandler::getQuestGiverStatus() to QuestHandler instead of
  reading from GameHandler's own empty npcQuestStatus_ map
- Immediately add quest to local log in acceptQuest() instead of waiting
  for field updates, fixing quests not appearing after accept
- Handle duplicate accept path (server already has quest) by also adding
  to local log
- Remove early return on empty questLog_ in applyQuestStateFromFields()
- Re-query nearby quest giver NPC statuses on abandon so markers refresh

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-10 19:50:56 +03:00
Pavel Okhlopkov
9c1ffae140 fix(ui): add keyboard navigation to character selection screen
Add Up/Down arrow keys to cycle through character list and Enter to
select. Claim arrow key ownership via SetKeyOwner to prevent ImGui nav
from moving focus to other widgets. Lock Enter key until release to
prevent the keypress from activating chat on the game screen.

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-10 19:50:40 +03:00
Pavel Okhlopkov
826a22eed3 fix(animation): prevent creature walk/run animation persisting after arriving
Use destination position (getLatest) instead of dead-reckoned position
(getX/Y/Z) during the overrun window to avoid visible forward-drift and
backward-snap. Only fall back to position-change movement detection for
entities without active movement tracking, preventing residual velocity
drift from keeping walk/run animation playing after arrival.

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-10 19:50:24 +03:00
Pavel Okhlopkov
4ba19d53d7 fix(ui): preserve auto-connect state when navigating back from character screen
Add resetForBack() to RealmScreen that clears selection state without
resetting autoSelectAttempted, preventing single-realm auto-connect from
re-firing when the user navigates back from the character screen.

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-10 19:50:06 +03:00
Kelsi
fce8ccdc45 fix(rendering): restore NPC back panel and apply cape textures (#57)
Some checks failed
Build / Build (arm64) (push) Has been cancelled
Build / Build (x86-64) (push) Has been cancelled
Build / Build (macOS arm64) (push) Has been cancelled
Build / Build (windows-arm64) (push) Has been cancelled
Build / Build (windows-x86-64) (push) Has been cancelled
Security / CodeQL (C/C++) (push) Has been cancelled
Security / Semgrep (push) Has been cancelled
Security / Sanitizer Build (ASan/UBSan) (push) Has been cancelled
The geoset normalization stripped all group 15 (cloak) submeshes but
only re-added them when a cape was equipped. NPCs without capes lost
the "no cape" back panel (geoset 1501), exposing the single-sided
torso mesh. Always add either the cape or no-cape geoset.

Also load and apply cape texture overrides for NPCs that do have capes
equipped via CreatureDisplayInfoExtra, matching the player path.
2026-04-07 03:20:13 -07:00
Kelsi Rae Davis
b41b3d2c71
Merge pull request #58 from ldmonster/feat/add-spells-animation
[feat] rendering: spell visual effects system
2026-04-07 02:19:00 -07:00
Pavel Okhlopkov
b79d9b8fea feat(rendering): implement spell visual effects with bone-tracked ribbons and particles
Add complete spell visual pipeline resolving the DBC chain
(Spell → SpellVisual → SpellVisualKit → SpellVisualEffectName → M2)
with precast/cast/impact phases, bone-attached positioning, and
automatic dual-hand mirroring.

Ribbon rendering fixes:
- Parse visibility track as uint8 (was read as float, suppressing
  all ribbon edges due to ~1.4e-45 failing the >0.5 check)
- Filter garbage emitters with bone=UINT_MAX unconditionally
- Guard against NaN spine positions from corrupt bone data
- Resolve ribbon textures via direct index, not textureLookup table
- Fall back to bone 0 when ribbon bone index is out of range

Particle rendering fixes:
- Reduce spell particle scale from 5x to 1.5x (was oversized)
- Exempt spell effect instances from position-based deduplication

Spell handler integration:
- Trigger precast visuals on SMSG_SPELL_START with server castTimeMs
- Trigger cast/impact visuals on SMSG_SPELL_GO
- Cancel precast visuals on cast interrupt/failure/movement

M2 classifier expansion:
- Add AmbientEmitterType enum for sound system integration
- Add 20+ foliage tokens, 4 spell effect tokens, isSmallFoliage flag
- Add markModelAsSpellEffect() to override disableAnimation

DBC layouts:
- Add SpellVisualID field to Spell.dbc for all expansion configs

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-07 11:27:59 +03:00
Kelsi
0a33e3081c fix(rendering): disable HiZ pyramid, fix WMO interior shadow clamping
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
HiZ occlusion culling built ~11 mip levels with per-level barriers
behind a blocking vkWaitForFences every frame — the main frame-rate
bottleneck.  Disable the pyramid build and fall back to GPU frustum-
only culling which is nearly free behind the fence.

WMO interiors now receive full-strength directional shadows but clamp
minimum brightness at 0.45 with a 0.35 ambient floor, so interiors
get real shadow contrast without going too dark.
2026-04-06 19:36:30 -07:00
Kelsi
4dcea08b90 Revert "fix(rendering): enable backface culling for one-sided M2 materials (#57)"
This reverts commit 7b746a3045.
2026-04-06 18:27:52 -07:00
Kelsi
70a0be9e79 fix(ci): bundle FFmpeg dylibs in macOS app artifact (#53)
dylibbundler misses Homebrew's FFmpeg libraries, causing a launch crash:
  Library not loaded: libavformat.62.12.100.dylib

Add a manual copy block for libavformat, libavcodec, libavutil,
libswscale, and libswresample — same pattern already used for Vulkan
and OpenSSL.  Applied to both build.yml and release.yml.
2026-04-06 18:18:19 -07:00
Kelsi
faf1d70c34 fix(rendering): reduce terrain chunk edge seams (#56)
Two sources of visible chunk-boundary squares:

1. Derivative-based bump mapping (bumpStrength=9) used dFdx/dFdy which
   are invalid across draw-call boundaries, producing strong normal
   discontinuities at every chunk edge.  Fade bump to zero near chunk
   edges using LayerUV as the chunk-space distance metric.

2. sampleAlpha used an abrupt step() to switch between point-sampled
   and 4-tap-blurred alpha, creating a visible ring 2 texels from each
   chunk edge.  Replace with smoothstep transition and a 5-tap average
   that includes the center sample.
2026-04-06 18:18:14 -07:00
Kelsi
7b746a3045 fix(rendering): enable backface culling for one-sided M2 materials (#57)
All M2 pipelines used VK_CULL_MODE_NONE, so back-facing polygons always
rendered.  On NPCs whose torso meshes are single-layer geometry this
made the interior cavity visible through the back.

Create backface-culled pipeline variants (VK_CULL_MODE_BACK_BIT) and
select them at draw time unless the material has the TwoSided flag
(0x04).  Foliage/ground-detail forceCutout batches and the shadow
pipeline keep VK_CULL_MODE_NONE since those cards are inherently
two-sided.
2026-04-06 18:18:05 -07:00
Kelsi
f79110cb14 fix(rendering): clear M2 texture cache on character switch
M2Renderer::clear() reset descriptor pools but left the texture cache
and failed-texture tracking intact.  On re-login the stale cache filled
the budget, failedTextureRetryAt_ blocked reloads, and the client
entered an infinite model-load loop.  Match the cleanup already done in
shutdown() and CharacterRenderer::clear().
2026-04-06 18:06:03 -07:00
Kelsi Rae Davis
996dc56691
Merge pull request #55 from ldmonster/chore/code-quality-cleaup
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
[chore] Code Quality & DRY/KISS Cleanup
2026-04-06 13:46:35 -07:00
Kelsi Rae Davis
20e016798f
Merge pull request #54 from ldmonster/fix/memory-pressure-and-hardening
[fix] Memory, Threading & Network Hardening
2026-04-06 13:45:31 -07:00
Kelsi Rae Davis
5d0d140c61
Merge pull request #52 from ldmonster/feat/hiz-occlusion-culling
[feat] rendering: Hierarchical-Z occlusion culling
2026-04-06 13:44:44 -07:00
Pavel Okhlopkov
744ad6d113 Merge commit 'a7df19232a' into chore/code-quality-cleaup 2026-04-06 22:53:21 +03:00
Pavel Okhlopkov
a7df19232a add doc
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-06 22:52:07 +03:00
Pavel Okhlopkov
97106bd6ae fix(render): code quality cleanup
Magic number elimination:
- Create protocol_constants.hpp, warden_constants.hpp,
  render_constants.hpp, ui_constants.hpp
- Replace ~55 magic numbers across game_handler, warden_handler,
  m2_renderer_render

Reduce nesting depth:
- Extract 5 parseEffect* methods from handleSpellLogExecute
  (max indent 52 → 16 cols)
- Extract resolveSpellSchool/playSpellCastSound/playSpellImpactSound
  from 3× duplicate audio blocks in handleSpellGo
- Flatten SMSG_INVENTORY_CHANGE_FAILURE with early-return guards
- Extract drawScreenEdgeVignette() for 3 duplicate vignette blocks

DRY extract patterns:
- Replace 12 compound expansion checks with isPreWotlk() across
  movement_handler (9), chat_handler (1), social_handler (1)

const to constexpr:
- Promote 23+ static const arrays/scalars to static constexpr across
  12 source files

Error handling:
- Convert PIN auth from exceptions to std::optional<PinProof>
- Add [[nodiscard]] to 15+ initialize/parse methods
- Wrap ~20 unchecked initialize() calls with LOG_WARNING/LOG_ERROR

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-06 22:43:13 +03:00
Pavel Okhlopkov
2e8856bacd memory, threading, network hardening
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-06 21:19:37 +03:00
Pavel Okhlopkov
312994be83 world loading memory pressure detector
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-06 21:05:20 +03:00
Pavel Okhlopkov
4b9b3026f4 feat(rendering): add HiZ occlusion culling & fix WMO interior shadows
Implement GPU-driven Hierarchical-Z occlusion culling for M2 doodads
using a depth pyramid built from the previous frame's depth buffer.
The cull shader projects bounding spheres via prevViewProj (temporal
reprojection) and samples the HiZ pyramid to reject hidden objects
before the main render pass.

Key implementation details:
- Separate early compute submission (beginSingleTimeCommands + fence
  wait) eliminates 2-frame visibility staleness
- Conservative safeguards prevent false culls: screen-edge guard,
  full VP row-vector AABB projection (Cauchy-Schwarz), 50% sphere
  inflation, depth bias, mip+1, min screen size threshold, camera
  motion dampening (auto-disable on fast rotations), and per-instance
  previouslyVisible flag tracking
- Graceful fallback to frustum-only culling if HiZ init fails

Fix dark WMO interiors by gating shadow map sampling on isInterior==0
in the WMO fragment shader. Interior groups (flag 0x2000) now rely
solely on pre-baked MOCV vertex-color lighting + MOHD ambient color.
Disable interiorDarken globally (was incorrectly darkening outdoor M2s
when camera was inside a WMO). Use isInsideInteriorWMO() instead of
isInsideWMO() for correct indoor detection.

New files:
- hiz_system.hpp/cpp: pyramid image management, compute pipeline,
  descriptors, mip-chain build dispatch, resize handling
- hiz_build.comp.glsl: MAX-depth 2x2 reduction compute shader
- m2_cull_hiz.comp.glsl: frustum + HiZ occlusion cull compute shader
- test_indoor_shadows.cpp: 14 unit tests for shadow/interior contracts

Modified:
- CullUniformsGPU expanded 128->272 bytes (HiZ params, viewProj,
  prevViewProj)
- Depth buffer images gain VK_IMAGE_USAGE_SAMPLED_BIT for HiZ reads
- wmo.frag.glsl: interior branch before unlit, shadow skip for 0x2000
- Render graph: hiz_build + compute_cull disabled (run in early compute)
- .gitignore: ignore compiled .spv binaries
- MEGA_BONE_MAX_INSTANCES: 2048 -> 4096

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-06 16:40:59 +03:00
Kelsi
758f7b27b9 fix(logging): downgrade remaining emote registry diagnostics to DEBUG
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Remove temporary NPC death diagnostic from entity_controller and
downgrade emote override/load-count messages from WARNING to DEBUG.
2026-04-05 20:58:11 -07:00
Kelsi
17c1e3ea3b fix(entities): add diagnostic for NPC death callback chain 2026-04-05 20:41:27 -07:00
Kelsi
7f9eed0de9 fix(parsing): add spline header dump to diagnose FINAL_POINT parse failures 2026-04-05 20:24:28 -07:00
Kelsi
c2681eead1 refactor: downgrade shutdown, warden, and misc diagnostics to DEBUG
Demote 44 more LOG_WARNING messages to LOG_DEBUG: warden module chunk
progress, entire shutdown/teardown sequence, transport manager connect,
inventory right-click slot, and warden handshake diagnostics. Keeps real
warnings (texture not found, slow handlers, stalls, integrity hash)
visible in the log.
2026-04-05 20:18:39 -07:00
Kelsi
069dd36698 fix(parsing): bail on suspicious maskBlockCount in CREATE_OBJECT blocks
When spline parsing consumes the wrong number of bytes, the subsequent
blockCount read lands on garbage data (e.g. 71 instead of ~5 for UNIT).
Previously the parser logged a warning but continued, reading garbage
mask/field data until hitting truncation. Now it returns false for
CREATE_OBJECT blocks with suspicious counts, letting the block loop
skip cleanly to the next entity.

Also downgrade ~44 diagnostic LOG_WARNING messages to LOG_DEBUG across
17 files (equipment, transport, DBC, heartbeat, chat, GO raypick, etc.)
to reduce log noise and make real warnings visible.
2026-04-05 20:12:17 -07:00
Kelsi
e32f4fbff9 Merge master into chore/god-object-decomposition-2nd
Resolve conflicts:
- audio_callback_handler.cpp: keep PR's animation_controller include
- movement_handler.cpp: use PR accessors with master's transportResolved logic
- world_packets.cpp: keep PR's decomposed version (functions moved to split files)

Apply overkill field fix to world_packets_entity.cpp (WotLK
SMSG_ATTACKERSTATEUPDATE missing uint32 overkill between damage and
subDamageCount).
2026-04-05 19:42:25 -07:00
Kelsi
0a22b0d41a refactor: remove debug diagnostics from combat and animation code 2026-04-05 19:10:42 -07:00
Kelsi
0e74e0f951 fix(combat): read WotLK overkill field in SMSG_ATTACKERSTATEUPDATE
AzerothCore sends a uint32 overkill field between totalDamage and
subDamageCount. The parser was missing this, causing subDamageCount to
read the first byte of overkill (0 for non-kills) and fail immediately.
This broke all melee swing animations except the killing blow.
2026-04-05 19:02:07 -07:00
Kelsi
e26ed39da8 fix(combat): add diagnostic logging to handleAttackerStateUpdate
Log parse failures with remaining packet size and successful parses with
attacker/target/player GUIDs, damage, and callback status to diagnose
why meleeSwingCallback is never invoked during auto-attack.
2026-04-05 18:54:01 -07:00
Kelsi
53639f9592 fix(animation): re-probe capabilities on melee swing, add combat diagnostics
If the capability probe ran before the model was fully loaded, all melee
animation IDs would be 0 and auto-attack swings would silently fall back
to STAND (no visible animation). Now re-probes when a melee swing fires
but hasMelee is false.

Added WARNING-level logging to triggerMeleeSwing and CombatFSM to
diagnose the night elf stationary combat animation issue.
2026-04-05 17:52:18 -07:00
Kelsi
696baffdf7 fix(movement): upgrade teleport and heartbeat diagnostics to WARNING
MSG_MOVE_TELEPORT_ACK now logs server-sent coordinates AND current
position at WARNING level (was LOG_INFO, invisible in log file).
Heartbeat position audit now logs every ~60 heartbeats (~30s) at
WARNING level to trace position drift before rogue teleports.
2026-04-05 17:39:56 -07:00
Kelsi
722c065089 fix(emotes): use EMOTE_TALK for /fart and /stink (no dedicated anim in DBC) 2026-04-05 17:33:42 -07:00
Kelsi
aff545edef fix(rendering): emote animations, WMO portal culling, transport teleport
Emote animations: fix DBC chain for /laugh, /flirt, /sleep, /fart, /stink.
Previously all emotes with AnimID=0 used emoteRef as animId (wrong DBC
record IDs). Now resolves through Emotes.dbc properly, with per-emote
overrides for emotes whose DBC chain yields 0. Adds Emotes.dbc load
failure warning and diagnostic logging.

WMO culling: skip portal culling when camera is outside all groups (fixes
vanishing Stormwind ground tiles). Also handle indoor/outdoor AABB overlap
by showing all groups when position is in both indoor and outdoor AABBs.

Transport: clear ONTRANSPORT flag and transport state when transport not
found, preventing stale transport data from teleporting player to map
origin. Add area trigger safety net near (0,0,0) on Eastern Kingdoms.
2026-04-05 17:25:25 -07:00
Kelsi
fe29ccad3f fix(transport): guard against untracked transport placing player at map origin
When on-transport flag is set but the transport isn't tracked by
TransportManager, getPlayerWorldPosition() returns localOffset (a small
relative value) as a world position. This overwrites movementInfo with
near-zero coordinates, teleporting the player to map origin on Eastern
Kingdoms (Alterac/Hillsbrad area). Add transport existence checks in
sendMovement() and getComposedWorldPosition() before composing position.
2026-04-05 16:01:14 -07:00
Kelsi Rae Davis
910ba50c26
Merge pull request #49 from ldmonster/chore/ui-callbacks-refactor
[chore] refactor(core): decompose Application::setupUICallbacks() into 7 domain handlers
2026-04-05 15:25:20 -07:00
Paul
52098cc704 ARM64 fix 2026-04-05 20:41:33 +03:00
Paul
41cd059f84 fix 2026-04-05 20:30:15 +03:00
Paul
65839287b4 feat(game): introduce GameHandler domain interfaces and eliminate friend declarations
Add game_interfaces.hpp with five narrow domain contracts that GameHandler now
publishes to its domain handlers, replacing the previous friend-class anti-pattern.

Changes:
- include/game/game_interfaces.hpp (new): IConnectionState, ITargetingState,
  IEntityAccess, ISocialState, IPvpState — each interface exposes only the state
  its consumer legitimately needs
- include/game/game_handler.hpp: GameHandler inherits all five interfaces;
  include of game_interfaces.hpp added
- include/game/movement_handler.hpp: remove `friend class GameHandler`; add
  public named accessors for previously-private fields (monsterMovePacketsThisTickRef,
  timeSinceLastMoveHeartbeatRef, resetMovementClock, setFalling, setFallStartMs)
- include/game/spell_handler.hpp: remove `friend class GameHandler/InventoryHandler/
  CombatHandler/EntityController`; promote private packet handlers (handlePetSpells,
  handleListStabledPets, pet stable commands, DBC loaders) to public; add accessor
  methods for aura cache, known spells, and player aura slot mutation
- src/game/game_handler.cpp, game_handler_callbacks.cpp, game_handler_packets.cpp:
  replace direct private field access with the new accessor API
  (e.g. casting_ → isCasting(), monsterMovePacketsThisTick_ → ...ThisTickRef())
- src/game/inventory_handler.cpp, combat_handler.cpp, entity_controller.cpp:
  replace friend-class private access with public accessor calls

No behaviour change. All 13 test suites pass. Zero build warnings.
2026-04-05 20:25:02 +03:00
Paul
34c0e3ca28 chore(refactor): god-object decomposition and mega-file splits
Split all mega-files by single-responsibility concern and
partially extracting AudioCoordinator and
OverlaySystem from the Renderer facade. No behavioral changes.

Splits:
- game_handler.cpp (5,247 LOC) → core + callbacks + packets (3 files)
- world_packets.cpp (4,453 LOC) → economy/entity/social/world (4 files)
- game_screen.cpp  (5,786 LOC) → core + frames + hud + minimap (4 files)
- m2_renderer.cpp  (3,343 LOC) → core + instance + particles + render (4 files)
- chat_panel.cpp   (3,140 LOC) → core + commands + utils (3 files)
- entity_spawner.cpp (2,750 LOC) → core + player + processing (3 files)

Extractions:
- AudioCoordinator: include/audio/ + src/audio/ (owned by Renderer)
- OverlaySystem: include/rendering/ + src/rendering/overlay_system.*

CMakeLists.txt: registered all 17 new translation units.
Related handler/callback files: minor include fixups post-split.
2026-04-05 19:30:44 +03:00
Paul
6dcc06697b refactor(core): decompose Application::setupUICallbacks() into 7 domain handlers
Extract ~1,700 lines / 60+ inline [this]-capturing lambdas from the monolithic
Application::setupUICallbacks() into 7 focused callback handler classes following
the ToastManager/ChatPanel::setupCallbacks() pattern already in the codebase.

New handlers (include/core/ + src/core/):
  - NPCInteractionCallbackHandler  NPC greeting/farewell/vendor/aggro voice
  - AudioCallbackHandler           Music, positional sound, level-up, achievement, LFG
  - EntitySpawnCallbackHandler     Creature/player/GO spawn, despawn, move, state
  - AnimationCallbackHandler       Death, respawn, combat, emotes, charge, sprint, vehicle
  - TransportCallbackHandler       Mount, taxi, transport spawn/move
  - WorldEntryCallbackHandler      World entry, unstuck, hearthstone, bind point
  - UIScreenCallbackHandler        Auth, realm selection, char selection/creation/deletion

application.cpp:  4,462 → 2,791 lines  (−1,671)
setupUICallbacks: ~1,700 → ~50 lines (thin orchestrator)

Deduplication:
  resolveSoundEntryPath()   — was 3× copy-paste of SoundEntries.dbc lookup
  resolveNpcVoiceType()     — was 4× copy-paste of display-ID→voice detection
  precacheNearbyTiles()     — was 3× copy-paste of 17×17 tile loop
  4 helper lambdas          — promoted to private methods on WorldEntryCallbackHandler

State migration out of Application:
  charge* (6 vars)          → AnimationCallbackHandler
  hearth*/worldEntry*/taxi* → WorldEntryCallbackHandler
  pendingCreatedCharacterName_ → UIScreenCallbackHandler

Bug fixes:
  - Duplicate `namespace core {` in application.hpp caused wowee::std pollution
  - AppState forward decl in ui_screen_callback_handler.hpp was at wrong scope
  - world_loader.cpp accessed moved member vars directly via friend; now uses handler API
2026-04-05 16:48:17 +03:00
Kelsi
a23c2172a8 fix(chat): handle SMSG_GM_MESSAGECHAT format, add chat diagnostics
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
SMSG_GM_MESSAGECHAT has an extra gmNameLen+gmName field after the
standard header that was causing misaligned parsing for non-whisper GM
messages. Strip the GM name before forwarding to the regular parser.

Also improve party state notifications (show member names on join) and
add INFO-level logging for all incoming/outgoing chat messages and
WARNING-level logging for group invite/accept packets to diagnose
recurring phantom party state issues.
2026-04-05 05:16:57 -07:00
Kelsi
e4bd380c0d fix(chat): prevent AFK/DND auto-reply whisper spam loop
Auto-reply was sent on every incoming whisper with no dedup, causing
infinite loops when both players had auto-reply enabled. Now tracks
which senders have been replied to and only sends one auto-reply per
sender per AFK/DND session.
2026-04-05 04:50:40 -07:00
Kelsi
19bfaaef97 fix(movement): stop spoofing player position for area triggers
The area trigger system was temporarily moving the player to the trigger
center and sending a heartbeat before firing CMSG_AREATRIGGER. This told
the server the player was at a different location, causing unexpected
teleports (e.g. Stormwind to Hillsbrad). Just send the area trigger
packet directly — the player is already inside the trigger radius.
2026-04-05 04:40:46 -07:00
Kelsi
09c1469956 fix(ui): show only zone name on character select, drop map/continent 2026-04-05 04:34:39 -07:00
Kelsi
e4c4b6f429 fix(ui): use display name from Map.dbc field 4 instead of internal name
Was reading field 2 (InstanceType) which fell back to field 1 (internal
name like "Azeroth"). Field 4 has the localized display name ("Eastern
Kingdoms").
2026-04-05 04:32:10 -07:00
Kelsi
535ae8aa89 fix(ui): resolve map 0 name and allow guild queries at character screen
getMapName() returned empty for mapId 0 (Eastern Kingdoms) due to an
early return guard. Remove it since 0 is a valid map ID.

queryGuildInfo() required IN_WORLD state but the character screen is at
CHAR_LIST_RECEIVED. The server accepts CMSG_GUILD_QUERY before login,
so just check for a connected socket.
2026-04-05 04:30:32 -07:00
Kelsi
2e30490fc5 fix(ui): show guild name and zone name on character select screen
Load area names from AreaTable.dbc (canonical zone names) in addition
to WorldMapArea.dbc so zone IDs from SMSG_CHAR_ENUM resolve to names.
Use lookupGuildName() to query and display guild names instead of raw
guild IDs.
2026-04-05 04:28:36 -07:00
Kelsi
35be19e74c fix(mail): route GO mailbox open through InventoryHandler
The decomposition PRs moved mail state to InventoryHandler but the GO
interaction code still set stale GameHandler fields. Add openMailbox()
on InventoryHandler and forward from GameHandler so the correct
mailboxGuid_/mailboxOpen_ are set and refreshMailList() works.
2026-04-05 04:22:48 -07:00
Kelsi
62f3f515e2 fix(mail): let SMSG_SHOW_MAILBOX open mailbox instead of stale GameHandler fields
GO interaction was setting the old GameHandler mailbox state instead of
InventoryHandler's, so refreshMailList saw mailboxGuid_=0 and bailed.
2026-04-05 04:17:55 -07:00
Kelsi
53244d025c feat(repair): DBC-based repair cost estimation and UI display
Calculate repair costs client-side using DurabilityCosts.dbc and
DurabilityQuality.dbc. Block repair when player can't afford it and
only apply optimistic durability/gold updates when cost is verified.
Show repair cost next to the Repair All button in the vendor window.
2026-04-05 04:15:48 -07:00
Kelsi Davis
3dec33ecf1 perf(rendering): reduce GPU cull buffer to 24k instances 2026-04-05 03:39:10 -07:00
Kelsi Davis
8bb3702af4 perf(rendering): reduce GPU cull buffer and add CPU fallback for overflow
Halve MAX_CULL_INSTANCES to 32768 and iterate all instances in render,
falling back to CPU frustum culling for any beyond the GPU buffer.
2026-04-05 03:25:27 -07:00
Kelsi Davis
b62df70d09 fix(rendering): game objects invisible due to GPU cull instance limit
MAX_CULL_INSTANCES was 16384 but game object instances were allocated
at indices 20000+, beyond the cull buffer. Increased to 65536.
Also compute fallback boundRadius from vertex extents when M2 header
reports 0, and seed bones for global-sequence-only animated models.
2026-04-05 03:18:52 -07:00
Kelsi
1dd1a431f4 fix(repair): process item durability updates even when entity missing from manager
handleValuesUpdate silently dropped VALUES updates for item GUIDs not in
entityManager, causing repair-all durability changes to be lost. Fall
through to updateItemOnValuesUpdate for items tracked in onlineItems_.
2026-04-05 03:15:03 -07:00
Kelsi
0e308cf5a1 chore: remove HelloWorld test addon 2026-04-05 02:39:10 -07:00
Kelsi Rae Davis
8c587ab13d
Merge pull request #48 from ldmonster/feat/animation-handling
[feat] animation: FSM-Based Animation System — Full Refactor
2026-04-05 02:38:02 -07:00
Kelsi
a86c94c55a chore: add shallow=true to FidelityFX-FSR2 submodule 2026-04-05 02:37:46 -07:00
Pavel Okhlopkov
e386fbb069
Merge branch 'master' into feat/animation-handling 2026-04-05 12:37:08 +03:00
Kelsi
0d188edd75 fix(areatrigger): use actual DBC dimensions instead of inflated minimums
The minimum floor (3.0 for sphere radius, 4.0 for box dimensions) was
inflating narrow triggers like AT 5711 (boxWidth 1.06 → 4.0), causing
false area trigger fires near the Stormwind AH and unexpected teleports.
2026-04-05 02:35:47 -07:00
Paul
292e28b948 fix(ci): skip FidelityFX submodule checkout during actions/checkout
Add update=none and shallow=true to extern/FidelityFX-FSR2 and
extern/FidelityFX-SDK in .gitmodules.

The 'Checkout' step runs git submodule update --init --force --depth=1
on all submodules, including the Kelsidavis FSR2/SDK forks, which fails
because it tries to resolve a specific commit SHA via shallow fetch.
The 'Fetch AMD FSR2 SDK' step already rm -rf's and re-clones both
directories from the correct upstream repos, so the submodule checkout
step is redundant and harmful.

update=none causes git submodule update (and actions/checkout submodules:true)
to skip these two entries.  Local developers who want the FSR2/SDK must
run the manual fetch step or use 'git submodule update --init <path>'
explicitly.
2026-04-05 12:35:34 +03:00
Paul
0da2365154 Merge commit 'bcf1015149' into feat/animation-handling 2026-04-05 12:35:17 +03:00
Kelsi
e0ef682b1e fix(ci): revert FSR2 submodule to remote HEAD, ignore dirty state
The .gitignore commit was never pushed to the FSR2 fork, breaking CI
shallow clones. Revert to 3d22aef and add ignore=dirty to .gitmodules
to suppress generated shader header noise.
2026-04-05 02:35:14 -07:00
Paul
a9a4f606f9 gitignore 2026-04-05 12:35:12 +03:00
Paul
b4989dc11f feat(animation): decompose AnimationController into FSM-based architecture
Replace the 2,200-line monolithic AnimationController (goto-driven,
single class, untestable) with a composed FSM architecture per
refactor.md.

New subsystem (src/rendering/animation/ — 16 headers, 10 sources):
- CharacterAnimator: FSM composer implementing ICharacterAnimator
- LocomotionFSM: idle/walk/run/sprint/jump/swim/strafe
- CombatFSM: melee/ranged/spell cast/stun/hit reaction/charge
- ActivityFSM: emote/loot/sit-down/sitting/sit-up
- MountFSM: idle/run/flight/taxi/fidget/rear-up (per-instance RNG)
- AnimCapabilitySet + AnimCapabilityProbe: probe once at model load,
  eliminate per-frame hasAnimation() linear search
- AnimationManager: registry of CharacterAnimator by GUID
- EmoteRegistry: DBC-backed emote command → animId singleton
- FootstepDriver, SfxStateDriver: extracted from AnimationController

animation_ids.hpp/.cpp moved to animation/ subdirectory (452 named
constants); all include paths updated.

AnimationController retained as thin adapter (~400 LOC): collects
FrameInput, delegates to CharacterAnimator, applies AnimOutput.

Priority order: Mount > Stun > HitReaction > Spell > Charge >
Melee/Ranged > CombatIdle > Emote > Loot > Sit > Locomotion.
STAY_IN_STATE policy when all FSMs return valid=false.

Bugs fixed:
- Remove static mt19937 in mount fidget (shared state across all
  mounted units) — replaced with per-instance seeded RNG
- Remove goto from mounted animation branch (skipped init)
- Remove per-frame hasAnimation() calls (now one probe at load)
- Fix VK_INDEX_TYPE_UINT16 → UINT32 in shadow pass

Tests (4 new suites, all ASAN+UBSan clean):
- test_locomotion_fsm: 167 assertions
- test_combat_fsm: 125 cases
- test_activity_fsm: 112 cases
- test_anim_capability: 56 cases

docs/ANIMATION_SYSTEM.md added (architecture reference).
2026-04-05 12:27:35 +03:00
Kelsi
aee5750759 chore: update FidelityFX-FSR2 submodule (ignore generated shaders) 2026-04-05 02:18:22 -07:00
Kelsi Davis
bcf1015149 fix(rendering): check sampler validity in VkTexture::isValid(), fix Windows build
- VkTexture::isValid() now checks both image AND sampler handles. Previously
  it only checked the image, so a texture with a valid image but NULL sampler
  would pass validation and get bound to a descriptor set. On MoltenVK (macOS)
  this renders as pink/magenta boxes; the fallback white texture is now
  correctly used instead.

- Fix fs::path to std::string implicit conversion in asset extractor that
  broke the Windows (MSYS2/clang) CI build.
2026-04-05 01:34:49 -07:00
Kelsi Rae Davis
50fdfd2e22
Merge pull request #47 from sschepens/patch-2
refactor asset extractor
2026-04-05 01:10:28 -07:00
Kelsi Rae Davis
f9d6ae5ef1
Merge pull request #46 from ldmonster/feat/animation-handling
[feat] animation: Comprehensive Animation System Overhaul — 452 Constants, 30-Phase State Machine
2026-04-05 01:09:51 -07:00
Kelsi Rae Davis
6d60717545
Merge pull request #45 from ldmonster/feat/rendering-performance-architecture
[feat] Rendering Architecture & GPU Performance + Bug Fixes
2026-04-05 01:09:03 -07:00
Paul
e58f9b4b40 feat(animation): 452 named constants, 30-phase character animation state machine
Add animation_ids.hpp/cpp with all 452 WoW animation ID constants (anim::STAND,
anim::RUN, anim::FIRE_BOW, ... anim::FLY_BACKWARDS, etc.), nameFromId() O(1)
lookup, and flyVariant() compact 218-element ground→FLY_* resolver.

Expand AnimationController into a full state machine with 20+ named states:
spell cast (directed→omni→cast fallback chain, instant one-shot release),
hit reactions (WOUND/CRIT/DODGE/BLOCK/SHIELD_BLOCK), stun, wounded idle,
stealth animation substitution, loot, fishing channel, sit/sleep/kneel
down→loop→up transitions, sheathe/unsheathe combat enter/exit, ranged weapons
(BOW/GUN/CROSSBOW/THROWN with reload states), game object OPEN/CLOSE/DESTROY,
vehicle enter/exit, mount flight directionals (FLY_LEFT/RIGHT/UP/DOWN/BACKWARDS),
emote state variants, off-hand/pierce/dual-wield alternation, NPC
birth/spawn/drown/rise, sprint aura override, totem idle, NPC greeting/farewell.

Add spell_defines.hpp with SpellEffect (~45 constants) and SpellMissInfo
(12 constants) namespaces; replace all magic numbers in spell_handler.cpp.

Add GAMEOBJECT_BYTES_1 to update field table (all 4 expansion JSONs) and wire
GameObjectStateCallback. Add DBC cross-validation on world entry.

Expand tools/_ANIM_NAMES from ~35 to 452 entries in m2_viewer.py and
asset_pipeline_gui.py. Add tests/test_animation_ids.cpp.

Bug fixes included:
- Stand state 1 was animating READY_2H(27) — fixed to SITTING(97)
- Spell casts ended freeze-frame — add one-shot release animation
- NPC 2H swing probe chain missing ATTACK_2H_LOOSE (polearm/staff)
- Chair sits (states 2/4/5/6) incorrectly played floor-sit transition
- STOP(3) used for all spell casts — replaced with model-aware chain
2026-04-04 23:02:53 +03:00
sschepens
a381598bf8
fix gitignore 2026-04-04 15:29:50 -03:00
sschepens
1e464dd513
refactor path mapper 2026-04-04 14:34:23 -03:00
sschepens
5542cbaa02
refactor asset extractor
- mpq and locale finding is now case insensitive
- improve extraction order and support more patches
- unified much of the mpq logic for all expansions
- return a list of ordered paths for loading
2026-04-04 14:00:55 -03:00
Paul
d54e262048 feat(rendering): GPU architecture + visual quality fixes
M2 GPU instancing
- M2InstanceGPU SSBO (96 B/entry, double-buffered, 16384 max)
- Group opaque instances by (modelId, LOD); single vkCmdDrawIndexed per group
- boneBase field indexes into mega bone SSBO via gl_InstanceIndex

Indirect terrain drawing
- 24 MB mega index buffer (6M uint32) + 64 MB mega vertex buffer
- CPU builds VkDrawIndexedIndirectCommand per visible chunk
- Single VB/IB bind per frame; shadow pass reuses mega buffers
- Replaced vkCmdDrawIndexedIndirect with direct vkCmdDrawIndexed to fix
  host-mapped buffer race condition that caused terrain flickering

GPU frustum culling (compute shader)
- m2_cull.comp.glsl: 64-thread workgroups, sphere-vs-6-planes + distance cull
- CullInstanceGPU SSBO input, uint visibility[] output, double-buffered
- dispatchCullCompute() runs before main pass via render graph node

Consolidated bone matrix SSBOs
- 16 MB double-buffered mega bone SSBO (2048 instances × 128 bones)
- Eliminated per-instance descriptor sets; one megaBoneSet_ per frame
- prepareRender() packs bone matrices consecutively into current frame slot

Render graph / frame graph
- RenderGraph: RGResource handles, RGPass nodes, Kahn topological sort
- Automatic VkImageMemoryBarrier/VkBufferMemoryBarrier between passes
- Passes: minimap_composite, worldmap_composite, preview_composite,
  shadow_pass, reflection_pass, compute_cull
- beginFrame() uses buildFrameGraph() + renderGraph_->execute(cmd)

Pipeline derivatives
- PipelineBuilder::setFlags/setBasePipeline for VK_PIPELINE_CREATE_DERIVATIVE_BIT
- M2 opaque = base; alphaTest/alpha/additive are derivatives
- Applied to terrain (wireframe) and WMO (alpha-test) renderers

Rendering bug fixes:
- fix(shadow): compute lightSpaceMatrix before updatePerFrameUBO to eliminate
  one-frame lag that caused shadow trails and flicker on moving objects
- fix(shadow): scale depth bias with shadowDistance_ instead of hardcoded 0.8f
  to prevent acne at close range and gaps at far range
- fix(visibility): WMO group distance threshold 500u → 1200u to match terrain
  view distance; buildings were disappearing on the horizon
- fix(precision): camera near plane 0.05 → 0.5 (ratio 600K:1 → 60K:1),
  eliminating Z-fighting and improving frustum plane extraction stability
- fix(streaming): terrain load radius 4 → 6 tiles (~2133u → ~3200u) to exceed
  M2 render distance (2800u) and eliminate pop-in when camera turns;
  unload radius 7 → 9; spawn radius 3 → 4
- fix(visibility): ground-detail M2 distance multiplier 0.75 → 0.9 to reduce
  early pop of grass and debris
2026-04-04 13:43:16 +03:00
Pavel Okhlopkov
ca3cea078b
Merge branch 'Kelsidavis:master' into master 2026-04-04 13:42:02 +03:00
Kelsi Davis
2343b768ce fix: warden mmap on macOS, add external listfile support to asset extractor
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Drop PROT_EXEC from warden module mmap when using Unicorn emulation
(not needed — module image is copied into emulator address space). Use
MAP_JIT on macOS for the native fallback path.

Add --listfile option to asset_extract and SFileAddListFileEntries
support for resolving unnamed MPQ hash table entries from external
listfiles.
2026-04-04 01:16:28 -07:00
Kelsi Davis
2fd9473f3b fix(rendering): alpha-to-coverage for hair, skip eye glow geosets, add missing include
- Enable alpha-to-coverage on alphaTestPipeline for smooth hair edges
  when MSAA is active (both init and recreatePipelines paths)
- Shader uses fwidth()-based alpha rescaling for clean coverage
- Skip group 17/18 geosets (DK/NE eye glow) when no geoset filter is
  set — prevents blue eye glow on all NPCs
- Add missing <libgen.h> include for dirname() on Linux
2026-04-04 01:16:28 -07:00
Kelsi Davis
f577411a15 fix(chat): resolve /r reply target when name arrives after whisper
Whisper sender name may not be in the player name cache when the packet
arrives. Store the sender GUID and lazily resolve the name from the
cache in getLastWhisperSender(). Also backfill lastWhisperSender_ when
the SMSG_NAME_QUERY_RESPONSE arrives.
2026-04-04 01:16:28 -07:00
Kelsi Davis
3f408341e1 fix(rendering): correct alpha test on opaque batches and hair transparency
- alphaTestPipeline_ uses blendDisabled() so surviving pixels are fully
  opaque (was blendAlpha, causing hair to blend with background)
- Remove alphaCutout from alphaTest condition — opaque materials like
  capes no longer alpha-test just because their texture has an alpha
  channel
- Two-pass batch rendering: opaque (blendMode 0) draws first to
  establish depth, then alpha-key/blend draws on top
2026-04-04 01:16:28 -07:00
Kelsi Davis
c95147390b fix(rendering,game): init bone SSBO to identity; stop movement before cast
Bone SSBO buffers were allocated for MAX_BONES (240) entries but only
the first numBones were written. Uninitialized GPU memory in the
remaining slots caused vertex spikes when any bone index exceeded the
model's actual bone count.

Also send MSG_MOVE_STOP before spell casts so the server doesn't reject
cast-time spells (e.g. hearthstone) with "can't do that while moving".
2026-04-04 01:16:28 -07:00
Kelsi Davis
bde9bd20d8 fix(rendering): use separate timer for global sequence bones
Global sequence bones (hair, cape, physics) need time values spanning
their full duration (up to ~968733ms), but animationTime wraps at the
current animation's sequence duration (~2000ms for walk). This caused
vertex spikes projecting from fingers/neck/ponytail as bones got stuck
in the first ~2s of their loop. Add a separate globalSequenceTime
accumulator that is not wrapped at the animation duration.
2026-04-04 01:16:28 -07:00
Kelsi Davis
f520511139 fix: chdir to executable directory at startup for relative asset paths
The binary assumed it was always launched from its own directory, causing
shader/asset loads to fail when run from any other working directory.
2026-04-04 01:16:28 -07:00
k
b3fa8cf5f3 fix: warden mmap on macOS, add external listfile support to asset extractor
Drop PROT_EXEC from warden module mmap when using Unicorn emulation
(not needed — module image is copied into emulator address space). Use
MAP_JIT on macOS for the native fallback path.

Add --listfile option to asset_extract and SFileAddListFileEntries
support for resolving unnamed MPQ hash table entries from external
listfiles.
2026-04-04 00:22:07 -07:00
Kelsi
84108c44f5 fix(rendering): alpha-to-coverage for hair, skip eye glow geosets, add missing include
- Enable alpha-to-coverage on alphaTestPipeline for smooth hair edges
  when MSAA is active (both init and recreatePipelines paths)
- Shader uses fwidth()-based alpha rescaling for clean coverage
- Skip group 17/18 geosets (DK/NE eye glow) when no geoset filter is
  set — prevents blue eye glow on all NPCs
- Add missing <libgen.h> include for dirname() on Linux
2026-04-04 00:21:15 -07:00
Kelsi
5538655383 fix(chat): resolve /r reply target when name arrives after whisper
Whisper sender name may not be in the player name cache when the packet
arrives. Store the sender GUID and lazily resolve the name from the
cache in getLastWhisperSender(). Also backfill lastWhisperSender_ when
the SMSG_NAME_QUERY_RESPONSE arrives.
2026-04-04 00:03:19 -07:00
Kelsi
c85d023329 fix(rendering): correct alpha test on opaque batches and hair transparency
- alphaTestPipeline_ uses blendDisabled() so surviving pixels are fully
  opaque (was blendAlpha, causing hair to blend with background)
- Remove alphaCutout from alphaTest condition — opaque materials like
  capes no longer alpha-test just because their texture has an alpha
  channel
- Two-pass batch rendering: opaque (blendMode 0) draws first to
  establish depth, then alpha-key/blend draws on top
2026-04-04 00:03:19 -07:00
Kelsi
100394a743 fix(rendering,game): init bone SSBO to identity; stop movement before cast
Bone SSBO buffers were allocated for MAX_BONES (240) entries but only
the first numBones were written. Uninitialized GPU memory in the
remaining slots caused vertex spikes when any bone index exceeded the
model's actual bone count.

Also send MSG_MOVE_STOP before spell casts so the server doesn't reject
cast-time spells (e.g. hearthstone) with "can't do that while moving".
2026-04-04 00:03:19 -07:00
Kelsi
aeb295e0bb fix(rendering): use separate timer for global sequence bones
Global sequence bones (hair, cape, physics) need time values spanning
their full duration (up to ~968733ms), but animationTime wraps at the
current animation's sequence duration (~2000ms for walk). This caused
vertex spikes projecting from fingers/neck/ponytail as bones got stuck
in the first ~2s of their loop. Add a separate globalSequenceTime
accumulator that is not wrapped at the animation duration.
2026-04-04 00:03:19 -07:00
k
b54458fe6c fix: chdir to executable directory at startup for relative asset paths
The binary assumed it was always launched from its own directory, causing
shader/asset loads to fail when run from any other working directory.
2026-04-03 23:27:13 -07:00
Kelsi
f79395788a fix(rendering): filter player hair geosets via CharHairGeosets.dbc
buildDefaultPlayerGeosets() was inserting all submeshIds 0-99 into
activeGeosets, showing every hair variation simultaneously. Now uses
the hairGeosetMap_ (from CharHairGeosets.dbc) to select only the
correct hair scalp geoset for the player's race/sex/style, matching
the existing NPC geoset filtering logic in EntitySpawner.
2026-04-03 22:43:37 -07:00
Kelsi
5468a93f2e fix(rendering): handle global sequences in character bone transforms
Hair, cape, and other physics bones use global sequences (continuously
looping timers independent of the character's current animation). The
character renderer was ignoring globalSequence entirely, causing these
bones to fall back to identity transforms and produce deformed/spiked
hair geometry. Added resolveTrackTime() to wrap global sequence time
correctly, matching the M2 renderer's existing behavior.
2026-04-03 22:37:46 -07:00
Kelsi
634bac6c7a Revert "fix(rendering): remap M2 vertex bone indices through bone lookup table"
This reverts commit 04ad88330f.
2026-04-03 22:26:14 -07:00
Kelsi
04ad88330f fix(rendering): remap M2 vertex bone indices through bone lookup table
M2 vertex bone indices are indices into boneLookupTable, not direct bone
array indices. Without remapping, vertices weighted to higher bone
indices (cloak, cape, hair) get the wrong bone transform, causing
vertices to project wildly outward from the character.
2026-04-03 22:22:51 -07:00
Kelsi
1feb6ea63f fix(rendering): sync async upload batches before rendering
Wait on in-flight upload batch fences at the start of each frame and
insert a memory barrier (transfer→fragment shader) so the graphics
queue sees completed layout transitions from the transfer queue.
Fixes VK_IMAGE_LAYOUT_UNDEFINED validation errors for freshly loaded
textures.
2026-04-03 22:09:41 -07:00
Kelsi
1379e74c40 fix(dbc): runtime detection for ItemDisplayInfo texture field indices
Revert static JSON layout changes (15-22 back to 14-21) since WotLK
loads the Classic 23-field DBC. Add getItemDisplayInfoTextureFields()
helper that detects field count at runtime and adjusts the texture
base index accordingly (14 for 23-field, 15 for 25-field).
2026-04-03 22:05:38 -07:00
Kelsi
3111fa50e8 fix(dbc): correct ItemDisplayInfo texture field indices for TBC/WotLK
TBC and WotLK have 25-field ItemDisplayInfo.dbc (vs 23 in Classic/Turtle)
with 2 extra fields before the texture region block. The texture fields
start at index 15 (not 14), shifting all 8 regions by +1.

This caused every equipment texture to be composited into the wrong body
region — e.g. LegLower textures landing in FootTexture, belt textures in
LegLowerTexture instead of LegUpperTexture ("everything shifted by 1").

Verified against raw DBC binary data: Classic field[14]=ArmUpper,
TBC/WotLK field[15]=ArmUpper.
2026-04-03 21:52:20 -07:00
Kelsi
44df2a1e28 chore: track FidelityFX-SDK and FidelityFX-FSR2 as submodules
Both repos were previously untracked clones under extern/ that users had
to fetch manually. Adding them as submodules ensures git pull --recurse
fetches the correct commits with our format-matching patches.

- FidelityFX-FSR2: standalone FSR2 v2.2.1 upscaler (Kelsidavis fork)
- FidelityFX-SDK: full SDK with FSR3 frame generation (Kelsidavis fork)
2026-04-03 21:45:42 -07:00
Kelsi
746ac25c14 fix(rendering): prevent MSAA+FSR2 framebuffer mismatch crash
When saved settings loaded MSAA before FSR2 on startup, the pending MSAA
change (e.g. 8x) was queued before FSR2 was enabled. Since FSR2 checked
the current MSAA (still 1x), it didn't override the pending change.
On the next frame, applyMsaaChange created a 4-attachment MSAA render pass,
then FSR2 lazily created a 2-attachment framebuffer against it — SIGSEGV.

Add guards in both applyMsaaChange (force 1x if FSR2 is blocking) and
setFSR2Enabled (override any pending MSAA >1x when enabling FSR2).
2026-04-03 21:41:14 -07:00
Kelsi
7264ba1706 fix(extractor): lowercase all output paths to prevent duplicate folders
WoW archives contain mixed-case variants of the same path (e.g.,
ARMLOWERTEXTURE vs ArmLowerTexture) which created duplicate directories
on case-sensitive Linux filesystems. Now mapPath() lowercases the entire
output. Also keeps TextureComponents and ObjectComponents directory
names instead of abbreviating them (item/texturecomponents/ instead of
item/texture/) so filesystem paths match the WoW virtual paths used in
manifest lookups.
2026-04-03 21:26:20 -07:00
Kelsi
23bda2d476 fix(vulkan): enable missing device features for FSR2 compute shaders
AMD RADV validation flagged missing shaderStorageImageWriteWithoutFormat,
shaderInt16, shaderFloat16, and deviceCoherentMemory. The first two are
now required device features; shaderFloat16 is optionally enabled via
Vulkan 1.2 feature query; AMD device coherent memory extension and
feature are enabled when available to prevent VMA memory type errors.
2026-04-03 21:20:37 -07:00
Kelsi
161b218fa1 fix(rendering): FSR1/FXAA paths not signaling inline mode to endFrame
executePostProcessing() only set inlineMode=true in the FSR2 path.
The FXAA and FSR1 paths both start INLINE render passes but returned
false, causing endFrame() to record ImGui into a secondary command
buffer and execute it inside the INLINE pass — validation errors and
UI disappearing on AMD RADV when FSR1+MSAA are both enabled.
2026-04-03 21:13:35 -07:00
Kelsi
17c16150d6 fix(vulkan): MSAA crash on AMD RADV due to vkCreateRenderPass2 null dispatch
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Instance was created with Vulkan 1.1 but depthResolveSupported_ was gated
on the physical device's API version (1.2+ on RADV). This caused
vkCreateRenderPass2 (core 1.2) to dispatch through a null function pointer
when MSAA was enabled. Now requests 1.2 instance with 1.1 minimum fallback
and gates depth resolve on the actual instance API version. Also removes
all diagnostic crash-phase instrumentation from the previous investigation.
2026-04-03 20:58:32 -07:00
Kelsi
9c4e61a227 fix(diagnostics): instrument applyMsaaChange to find NULL deref
AMD crash is caused by msaaChangePending_ flipping true from saved settings.
applyMsaaChange() then crashes with faultAddr=(nil). Add LOG_WARNING markers
between pipeline recreation groups to identify the failing call.
2026-04-03 20:42:08 -07:00
Kelsi
45ac7e4d8e fix(diagnostics): log renderer state on each beginFrame for AMD crash
Add per-frame LOG_WARNING with this/vkCtx/camera/postProcess pointers and
msaaChangePending state. If the log prints before crash, the pointer values
tell us what's corrupt. If it doesn't print, crash is in the log itself
(meaning this or vkCtx is corrupt).
2026-04-03 20:37:21 -07:00
Kelsi
cd07e23485 fix(diagnostics): finer beginFrame sub-phase markers for AMD crash
Previous markers showed crash before bf:ubo. Add markers at bf:msaa,
bf:pp, bf:swap, bf:acquire, bf:jitter to isolate which early beginFrame
call does the NULL deref.
2026-04-03 20:32:18 -07:00
Kelsi
5778ba230d fix(diagnostics): add sub-phase markers inside Renderer::beginFrame
AMD RADV crash is renderPhase=beginFrame with faultAddr=(nil) — a NULL
pointer dereference somewhere in the pre-pass chain. Add granular markers
(bf:ubo, bf:minimap, bf:worldmap, bf:preview, bf:shadow, bf:reflection,
bf:renderpass) to pinpoint the exact call.
2026-04-03 20:28:37 -07:00
Kelsi
82267320b0 fix(diagnostics): add render-phase crash markers and improve signal handling
Add signal-safe render-phase markers throughout GameScreen::render() and
Application::render() so the crash handler can report which render call was
active when a SIGSEGV occurs. The AMD RADV crash backtrace only shows 2
frames due to missing frame pointers, making it impossible to identify the
actual crash site.

Changes:
- Add volatile g_crashRenderPhase marker updated before each major render call
- Upgrade Linux signal handler to sigaction with SA_SIGINFO for faulting address
- Set ImGui CheckVkResultFn to log silent Vulkan errors in ImGui backend
- Enable -fno-omit-frame-pointer in all build configs (not just Debug/RelWithDebInfo)
2026-04-03 20:19:33 -07:00
Kelsi
b092bc2e90 fix(rendering): use deferAfterAllFrameFences for bone destruction
destroyInstanceBones loops over both frame slots (i=0,1), deferring
both boneSet[0] and boneSet[1] to the current frame's fence. When
currentFrame=0, boneSet[1] is freed after only slot 0's fence completes
while slot 1's command buffer may still be using it.

Switch both M2Renderer and CharacterRenderer bone destruction from
deferAfterFrameFence to deferAfterAllFrameFences to ensure all
in-flight frames have completed before freeing cross-slot resources.
2026-04-03 19:54:54 -07:00
Kelsi
3ac8c4d95f fix(rendering): wait all frame fences before freeing shared descriptor sets
deferAfterFrameFence only waits for one frame slot's fence, but shared
resources (material descriptor sets, vertex/index buffers) are bound by
both in-flight frames' command buffers. On AMD RADV this caused
vkFreeDescriptorSets errors and eventual SIGSEGV.

Add deferAfterAllFrameFences: queues to every frame slot with a shared
counter so cleanup runs exactly once, after the last slot is fenced.
Use it for WMO, terrain, water, and character model shared resources.
Per-frame bone sets keep using deferAfterFrameFence (already correct).

Also fix character renderer vertex format: R8G8B8A8_UINT -> _SINT to
match shader's ivec4 input (RADV validation rejects the mismatch).
2026-04-03 19:48:43 -07:00
Kelsi
def821055b fix(parsing): validate spline endPoint coords to reject false-positive format matches
The WotLK spline parser tries 6 format variants and accepts the first
that passes minimal validation (pointCount<=256, splineMode<=3). A wrong
format can pass by coincidence, consuming incorrect bytes and corrupting
all subsequent UPDATE_OBJECT blocks (e.g. maskBlockCount=219 garbage).

Add endPoint coordinate validation: reject spline parses where the
endpoint is non-finite or outside world bounds (65k). Also harden the
Turtle parser to keep successfully-parsed blocks on mid-packet failure
instead of discarding the entire packet.
2026-04-03 19:36:34 -07:00
Kelsi
40e72d535e fix(rendering): defer model buffer destruction and per-frame FXAA descriptors
CharacterRenderer::destroyModelGPU now defers vertex/index buffer
destruction when replacing models mid-stream, preventing use-after-free
on AMD RADV. FXAA descriptor sets are now per-frame to eliminate
write-read races between in-flight command buffers. Water reflection
descriptor update narrowed to current frame only.
2026-04-03 19:17:55 -07:00
Kelsi
e19bf76d88 fix(rendering): defer character renderer bone descriptor destruction
CharacterRenderer::destroyInstanceBones had the same immediate-free bug
as M2Renderer — freeing bone descriptor sets and buffers while in-flight
command buffers still reference them. Applies the same deferred pattern
via deferAfterFrameFence for the removeInstance streaming path.
2026-04-03 18:53:06 -07:00
Kelsi
345b41b810 fix(auction): resolve item GUID with fallback and gate packet format
auctionSellItem now resolves the item GUID internally via
backpackSlotGuids_ with resolveOnlineItemGuid fallback, matching the
pattern used by vendor sell and item use. Previously the UI passed
the GUID directly from getBackpackItemGuid() with no fallback, so
items with unset slot GUIDs silently failed to list.

Also gates CMSG_AUCTION_SELL_ITEM format by expansion: Classic/TBC
omits the itemCount and stackCount fields that WotLK requires.
2026-04-03 18:46:49 -07:00
Kelsi
ac5c61203d fix(rendering): defer descriptor set destruction during streaming unload
M2 destroyInstanceBones and WMO destroyGroupGPU freed descriptor sets
and buffers immediately during tile streaming, while in-flight command
buffers still referenced them — causing DEVICE_LOST on AMD RADV.

Now defers GPU resource destruction via deferAfterFrameFence in streaming
paths (removeInstance, removeInstances, unloadModel). Immediate
destruction preserved for shutdown/clear paths that vkDeviceWaitIdle
first.

Also: vkDeviceWaitIdle before WMO backfillNormalMaps descriptor rebinds,
and fillModeNonSolid added to required device features for wireframe
pipelines on AMD.
2026-04-03 18:30:52 -07:00
Kelsi
8fd4dccf6b fix(vendor): preserve repair flag across ListInventory parse
ListInventoryParser::parse() was resetting the entire ListInventoryData
struct, wiping the canRepair flag set by the gossip handler before the
server response arrived. Preserve it across the parse.

Also detect repair capability from UNIT_NPC_FLAG_REPAIR (0x1000) on the
vendor NPC entity, so direct vendors without gossip menus also show the
repair button.
2026-04-03 18:18:53 -07:00
Kelsi
8e1addf7a6 fix(rendering): increase ImGui descriptor pool from 100 to 2048
The pool was exhausted by cached spell/item/talent icon textures,
causing vkAllocateDescriptorSets to fail inside ImGui_ImplVulkan_AddTexture.
The NVIDIA driver crashed on the subsequent invalid descriptor write.

Also add a null-check on the returned descriptor set so pool exhaustion
gracefully returns VK_NULL_HANDLE instead of crashing.
2026-04-03 18:14:46 -07:00
Kelsi
2096e67bf9 fix(rendering): prevent shutdown crash from deferred cleanup use-after-free
During shutdown, VkContext::runDeferredCleanup() was executing lambdas
that called vkFreeDescriptorSets on descriptor pools already destroyed
by Renderer::shutdown(). This corrupted the validation layer's internal
state, causing a SIGSEGV during process exit on AMD RADV.

Clear the deferred queues without executing them — vkDestroyDevice
reclaims all device-child resources anyway. Also guard against the
double shutdown() call (explicit + destructor).
2026-04-03 18:02:24 -07:00
Kelsi
b2cb98e969 fix(rendering): per-image semaphores and depth-format shadow placeholder
Avoid semaphore reuse while the presentation engine still holds a
reference by switching from per-frame-slot to per-swapchain-image
semaphores with a rotating free semaphore for acquire.

Replace the R8G8B8A8_UNORM dummy white texture in CharacterPreview
with a proper D16_UNORM depth texture cleared to 1.0, matching the
sampler2DShadow expectation in shaders. AMD RADV enforces strict
format/sampler type compatibility.
2026-04-03 17:52:48 -07:00
Kelsi
4f7912cf45 fix(rendering): water reflection render pass compat, anisotropy feature, shadow pool race
Three bugs found via AMD RADV crash log:

1. Water reflection render pass used BOTTOM_OF_PIPE as srcStageMask but
   pipelines were created against the main pass (EARLY_FRAGMENT_TESTS |
   COLOR_ATTACHMENT_OUTPUT). AMD enforces strict render pass compatibility
   → SIGSEGV when scene renders into reflection texture.

2. samplerAnisotropy was never enabled during device creation despite being
   used in sampler creation — now requested via PhysicalDeviceSelector.

3. Shadow texture descriptor pool was reset each frame while prior frame's
   command buffers might still reference it. Split into per-frame-slot pools
   so each reset is fence-guarded.
2026-04-03 17:41:14 -07:00
Kelsi
62b8a757a3 fix(rendering): skip TRANSIENT_ATTACHMENT for MSAA on GPUs without lazily allocated memory
AMD RDNA4 (9070XT) crashes with SIGSEGV when MSAA is enabled because the
driver optimizes TRANSIENT images for tile-only storage. Without lazily
allocated memory backing, the MSAA resolve reads unbacked memory. Now we
only set TRANSIENT+LAZILY_ALLOCATED when the device actually exposes that
memory type.
2026-04-03 17:23:52 -07:00
Kelsi
81a9970c91 fix(rendering): add warnings for silent texture fallbacks
M2 particle/ribbon/batch, terrain layer, and WMO material texture
resolution paths were silently falling back to white textures when
indices were out of range — making missing texture issues hard to
diagnose. Add LOG_WARNING at each silent failure point with model
name, index details, and array sizes.
2026-04-03 16:11:45 -07:00
Kelsi
791ea1919e fix(ci): add dylib verification step to macOS builds
After bundling dylibs, verify with otool -L that every non-system
dylib referenced by wowee_bin is present in the app bundle. Fails
the build if any are missing — prevents silent repeat of #36/#41.
Added to both build.yml and release.yml.
2026-04-03 15:58:29 -07:00
Kelsi Rae Davis
cf31464918
Merge pull request #43 from Kelsidavis/fix/bundle-libssl
fix(ci): bundle libssl in macOS app
2026-04-03 15:53:19 -07:00
Paul
7dd50bf1d2 fix(ci): bundle libssl in macOS app 2026-04-03 15:51:51 -07:00
Kelsi Rae Davis
a0dd10a83a
Merge pull request #40 from ldmonster/chore/testing
[chore] Add Testing — Safety Net & CI Hardening
2026-04-03 15:43:38 -07:00
Paul
e1ca43797c fix ci 2026-04-03 19:49:30 +03:00
Paul
bb2bf38921 chore: add catch2 amalgamated files to git tracking
extern/catch2 was covered by the extern/* gitignore pattern without
an exception, causing CI to fail with a missing source file error.
Added !extern/catch2 exception and committed the amalgamated files.
2026-04-03 18:46:15 +03:00
Paul
ae596a485a Merge commit '8d78976904' into chore/testing 2026-04-03 18:41:08 +03:00
Kelsi
8d78976904 refactor(ui): extract shared helpers into ui_helpers.hpp
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
DRY up renderAuraRemaining, fmtDurationCompact, classColorVec4,
classColorU32, entityClassId, classNameStr, kDispelNames, and
kRaidMarkNames — duplicated across game_screen, social_panel,
and combat_ui after the panel extraction refactors.
2026-04-03 03:45:39 -07:00
Kelsi
06a83537cf chore: re-remove dead functions reintroduced by PR #39 merge
The Lua refactor branch was based before the cleanup commit and
brought back allMacroCommands, getMacroShowtooltipArg (game_screen),
lfgJoinResultString, lfgTeleportDeniedString (game_handler).
2026-04-03 03:37:22 -07:00
Kelsi Rae Davis
fda3245550
Merge pull request #39 from ldmonster/chore/refactor-lua-engine
[chore] lua: refactor addon Lua engine API + progress docs
2026-04-03 03:20:20 -07:00
Paul
2cb47bf126 chore(testing): add unit tests and update core render/network pipelines
- add new tests:
  - test_blp_loader.cpp
  - test_dbc_loader.cpp
  - test_entity.cpp
  - test_frustum.cpp
  - test_m2_structs.cpp
  - test_opcode_table.cpp
  - test_packet.cpp
  - test_srp.cpp
  - CMakeLists.txt
- add docs and progress tracking:
  - TESTING.md
  - perf_baseline.md
- update project config/build:
  - .gitignore
  - CMakeLists.txt
  - test.sh
- core engine updates:
  - application.cpp
  - game_handler.cpp
  - world_socket.cpp
  - adt_loader.cpp
  - asset_manager.cpp
  - m2_renderer.cpp
  - post_process_pipeline.cpp
  - renderer.cpp
  - terrain_manager.cpp
  - game_screen.cpp
- add profiler header:
  - profiler.hpp
2026-04-03 09:41:34 +03:00
Paul
a2814ab082 Merge commit '7f4c274e35' into chore/refactor-lua-engine 2026-04-03 07:35:57 +03:00
Paul
a916270a13 chore(lua): refactor addon Lua engine API + progress docs
- Refactor Lua addon integration:
  - Update CMakeLists.txt for addon build paths
  - Enhance addons API headers and Lua engine interface
  - Add new Lua API addon modules (`lua_api_helpers`, `lua_api_registrations`, `lua_services`, `lua_action_api`, `lua_inventory_api`, `lua_quest_api`, `lua_social_api`, `lua_spell_api`, `lua_system_api`, `lua_unit_api`)
  - Update implementation in addon_manager.cpp, lua_engine.cpp, application.cpp, game_handler.cpp
2026-04-03 07:31:06 +03:00
Kelsi
7f4c274e35 fix(ci): limit arm64 Linux build parallelism to avoid OOM
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
The ubuntu-24.04-arm runner is memory-constrained and the full
parallel Release build was being killed by the OOM reaper, causing
the Build step to fail silently with no log output. Cap at 2 jobs.
2026-04-02 14:49:14 -07:00
Kelsi
fe1c4c622b chore: remove dead functions left behind by handler extractions
685 lines of unused code duplicated into extracted handler files
(entity_controller, spell_handler, quest_handler, warden_handler,
social_handler, action_bar_panel, chat_panel, window_manager)
during PRs #33-#38. Build is now warning-free.
2026-04-02 14:47:04 -07:00
Kelsi Rae Davis
0a4cc9ab96
Merge pull request #38 from ldmonster/chore/renderer-refactor
[chore] refactor: renderer extract domains
2026-04-02 13:18:45 -07:00
Paul
6e02b4451c Merge commit '6a938b1181' into chore/renderer-refactor 2026-04-02 13:11:08 +03:00
Paul
5af9f7aa4b chore(renderer): extract AnimationController and remove audio pass-throughs
Extract ~1,500 lines of character animation state from Renderer into a dedicated
AnimationController class, and complete the AudioCoordinator migration by removing
all 10 audio pass-through getters from Renderer.

AnimationController:
- New: include/rendering/animation_controller.hpp (182 lines)
- New: src/rendering/animation_controller.cpp (1,703 lines)
- Moves: locomotion state machine (50+ members), mount animation (40+ members),
  emote system, footstep triggering, surface detection, melee combat animations
- Renderer holds std::unique_ptr<AnimationController> and delegates completely
- AnimationController accesses audio via renderer_->getAudioCoordinator()

Audio caller migration:
- Migrate ~60 external callers from renderer->getXManager() to AudioCoordinator
  directly, grouped by access pattern:
  - UIServices: settings_panel, game_screen, toast_manager, chat_panel,
    combat_ui, window_manager
  - GameServices: game_handler, spell_handler, inventory_handler, quest_handler,
    social_handler, combat_handler
  - Application singleton: application.cpp, auth_screen.cpp, lua_engine.cpp
- Remove 10 pass-through getter definitions from renderer.cpp
- Remove 10 pass-through getter declarations from renderer.hpp
- Remove individual audio manager forward declarations from renderer.hpp
- Redirect 69 internal renderer.cpp audio calls to audioCoordinator_ directly
- game_handler.cpp: withSoundManager template uses services_.audioCoordinator;
  MFP changed from &Renderer::getUiSoundManager to &AudioCoordinator::getUiSoundManager
- GameServices struct: add AudioCoordinator* audioCoordinator member
- settings_panel: applyAudioVolumes(Renderer*) -> applyAudioVolumes(AudioCoordinator*)
2026-04-02 13:06:31 +03:00
Kelsi Rae Davis
6a938b1181
Merge pull request #37 from ldmonster/chore/application-extract-appearance-controller
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
[chore] refactor: application extract appearance controller
2026-04-02 00:26:59 -07:00
Paul
5ef600098a chore(renderer): refactor renderer and add post-process + spell visuals systems
- Updated core render pipeline and renderer integration in CMakeLists.txt, renderer.cpp, renderer.hpp
- Added post-process pipeline module:
  - post_process_pipeline.hpp
  - post_process_pipeline.cpp
- Added spell visual system module:
  - spell_visual_system.hpp
  - spell_visual_system.cpp
- Adjusted application/audio integration:
  - application.cpp
  - audio_coordinator.cpp
2026-04-02 00:21:21 +03:00
Paul
1c0e9dd1df chore(application): extract appearance controller and unify UI flow
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
  - `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
  - application.cpp
- Update component implementations for new controller flow:
  - action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`

These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
Paul
d43397163e refactor: decouple Application singleton by extracting core subsystems and updating interfaces
- Add `audio::AudioCoordinator` interface and implementation
- Modify `Application` to reduce singleton usage and move controller responsibilities:
  - application.hpp
  - application.cpp
- Update UI and audio headers/sources:
  - game_screen.hpp
  - game_screen.cpp
  - ui_manager.hpp
  - audio_coordinator.hpp
  - audio_coordinator.cpp
- Project config touched:
  - CMakeLists.txt
2026-04-01 20:38:37 +03:00
Paul
9b38e64f84 "Fix and refine app initialization flow
- Update core application startup paths and cleanup logic
- Adjust renderer & input subsystem integration for stability
- Address recent staging source updates with robust error handling"
2026-04-01 20:06:26 +03:00
Paul
afeaa13562 chore(application): extract entity spawner + composer, apply app and UI updates
- add include/core/appearance_composer.hpp + src/core/appearance_composer.cpp
- update include/core/application.hpp + src/core/application.cpp
- update src/ui/game_screen.cpp
- adjust CMakeLists.txt and README.md for new composer module
2026-04-01 13:31:48 +03:00
Kelsi
b10a2c28d6 fix(core): guard entity spawner callbacks in asset-failure mode
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
2026-03-31 22:10:20 -07:00
Kelsi
c717330f4f fix(ci): bundle Vulkan loader dylibs in macOS app 2026-03-31 22:00:12 -07:00
Kelsi Rae Davis
2ba54988bb
Merge pull request #35 from ldmonster/chore/application-extract-entity-spawner
[chore] refactor: Extract `EntitySpawner` module from `Application`
2026-03-31 21:52:59 -07:00
Kelsi Rae Davis
9eb8ca2988
Merge pull request #33 from ldmonster/chore/game-screen-extract
[chore] refactor(ui): GameScreen — extract 7 UI sub-panels
2026-03-31 21:52:48 -07:00
Paul
cf3ae3bbfe chore(application): refactor app lifecycle and add entity spawner module
- updated CMakeLists.txt to include new module targets
- refactored application.hpp + application.cpp
- added new `entity_spawner` headers + sources:
  - entity_spawner.hpp
  - entity_spawner.cpp
2026-03-31 22:01:55 +03:00
Paul
b6e4e405b6 Merge commit 'ea8b0d9305' into chore/game-screen-extract 2026-03-31 20:17:21 +03:00
Paul
0e6aaeb44e fix warnings, remove phases from commentaries 2026-03-31 20:11:28 +03:00
Paul
43aecab1ef Merge commit '32bb0becc8' into chore/game-screen-extract 2026-03-31 19:51:37 +03:00
Paul
c9353853f8 chore(game-ui): extract GameScreen domains
- Extracted `GameScreen` functionality into dedicated UI domains
- Added new panels:
  - `action_bar_panel`
  - `combat_ui`
  - `social_panel`
  - `window_manager`
- Updated `game_screen` + CMakeLists.txt integration
- Added new headers and sources under ui and ui
2026-03-31 19:49:52 +03:00
Kelsi
ea8b0d9305 fix: reduce warmup ground-check timeout from 20s to 5s
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
The warmup loop waited up to 20 seconds for getHeightAt() to return a
terrain height within 15 units of spawn Z before accepting the ground
as ready. In practice, the terrain was loaded and the character was
visibly standing on it, but the height sample didn't match closely
enough (terrain LOD, chunk boundary, or server Z vs client height
mismatch).

Reduce the tile-count fallback timeout from 20s to 5s: if at least 4
tiles are loaded after 5 seconds, accept the ground as ready. The
exact height check still runs in the first 5 seconds for fast-path
cases where it does match.
2026-03-31 01:27:32 -07:00
Kelsi
5ad225313d fix: revert Warden HASH_RESULT fallback — silence is correct behavior
ChromieCraft/AzerothCore tolerates no HASH_RESULT response (continues
session without Warden checks), but immediately kicks on a WRONG hash.
The previous commit sent a fallback SHA1 which the server rejected,
breaking login that was working before.

Restore the skip behavior for WotLK/TBC: stay silent on HASH_REQUEST
when no CR match exists, and advance to WAIT_CHECKS so the rest of the
session proceeds normally. Turtle/Classic servers still get the fallback
hash since they're lenient about wrong values.
2026-03-31 01:18:58 -07:00
Kelsi
f3f7511105 fix: send Warden HASH_RESULT fallback instead of skipping response
Previously, WotLK/TBC servers with no CR match would skip the
HASH_REQUEST response entirely to "avoid account bans". This caused
a guaranteed kick-on-timeout for ALL WotLK servers including
permissive ones like ChromieCraft/AzerothCore.

Now sends a best-effort fallback hash (SHA1 of module image or raw
data) for all server types. Permissive servers accept this and
continue the session normally. Strict servers (Warmane) will reject
it but only kick — same outcome as the previous skip behavior, just
faster feedback.

For strict servers, the correct fix remains providing a .cr file
with pre-computed seed→reply entries for each module.
2026-03-31 01:10:43 -07:00
Kelsi
681e25a4f2 fix: clean up unused parameter style in entity_controller
Use nameless parameters instead of /*comment*/ syntax for unused
args in IObjectTypeHandler interface defaults and overrides.
2026-03-31 00:55:40 -07:00
Kelsi
11561184e6 fix: silence all -Wunused-parameter warnings in entity_controller
Suppress 9 unused parameter warnings in IObjectTypeHandler interface
methods and their overrides by commenting out parameter names:
- Base class: onCreate/onValuesUpdate/onMovementUpdate default empty
  implementations (parameters intentionally unused in base)
- ItemTypeHandler::onCreate: entity param forwarded only to onCreateItem
  which doesn't need it
- CorpseTypeHandler::onCreate: entity param not needed for corpse spawn

Build now produces zero warnings (excluding third-party stb headers).
2026-03-31 00:48:03 -07:00
Paul
af9874484a chore(game-ui): extract GameScreen domains into DialogManager + SettingsPanel
- Add `DialogManager` + `SettingsPanel` UI modules
- Refactor `GameScreen` to delegate dialogs and settings to new domains
- Update CMakeLists.txt to include new sources
- Include header/source files:
  - dialog_manager.hpp
  - settings_panel.hpp
  - dialog_manager.cpp
  - settings_panel.cpp
  - game_screen.cpp
- Keep `GameScreen` surface behavior while decoupling feature responsibilities
2026-03-31 10:07:58 +03:00
Paul
9764286cae chore(game-screen): extract toast manager from game screen
- refactor: move toast UI logic into new `ToastManager` component
- add toast_manager.hpp + toast_manager.cpp
- update game_screen.hpp + game_screen.cpp to use `ToastManager`
- adjust app initialization in application.cpp
- keep root CMake target in CMakeLists.txt updated
2026-03-31 09:18:17 +03:00
Paul
0f1cd5fe9a chore(game-ui): extract chat panel into dedicated UI module
- moved chat panel logic out of `game_screen` into `chat_panel`
- added chat_panel.hpp and chat_panel.cpp
- updated game_screen.hpp and game_screen.cpp to integrate new `ChatPanel` component
- updated build config in CMakeLists.txt to include new UI module sources
2026-03-31 08:53:14 +03:00
Kelsi
32bb0becc8 fix: replace placeholder Warden RSA modulus with real Blizzard key
Replace the incorrectly extracted RSA-2048 modulus (which contained
the exponent bytes embedded inside it) with the verified Blizzard
public key used across all pre-Cataclysm clients (1.12.1, 2.4.3,
3.3.5a).

Key confirmed against two independent sources:
- namreeb/WardenSigning ClientKey.hpp (72 verified sniffed modules)
- SkullSecurity wiki Warden_Modules documentation

The modulus starts with 0x6BCE F52D... and ends with ...03F4 AFC7.
Exponent remains 65537 (0x010001).

Verification algorithm: SHA1(module_data + "MAIEV.MOD"), 0xBB-padded
to 256 bytes, RSA verify-recover with raw (no-padding) mode.

Signature failures are non-fatal (log warning, continue loading) so
private-server modules signed with custom keys still work. This is
necessary because servers like ChromieCraft/AzerothCore may use their
own signing keys.

Also update warden_module.hpp status: all implementation items now .
2026-03-30 22:50:47 -07:00
Kelsi
88d047d2fb feat: implement Warden API binding / IAT patching for module imports
Complete the last major Warden stub — the import table parser that
resolves Windows API calls in loaded modules. This is the critical
missing piece for strict servers like Warmane.

Implementation:
- Parse Warden module import table from decompressed data (after
  relocation entries): alternating libraryName\0 / functionName\0
  pairs, terminated by null library name
- For each import, look up the emulator's pre-registered stub address
  (VirtualAlloc, GetTickCount, ReadProcessMemory, etc.)
- Auto-stub unrecognized APIs with a no-op returning 0 — prevents
  module crashes on unimplemented Windows functions
- Patch each IAT slot (sequential dwords at module image base) with
  the resolved stub address
- Add WardenEmulator::getAPIAddress() public accessor for IAT lookups
- Fix initialization order: bindAPIs() now runs inside initializeModule()
  after emulator setup but before entry point call

The full Warden pipeline is now: RC4 decrypt → RSA verify → zlib
decompress → parse executable → relocate → create emulator → register
API hooks → bind imports (IAT patch) → call entry point → extract
exported functions (packetHandler, tick, generateRC4Keys, unload).
2026-03-30 22:38:05 -07:00
Kelsi
248d131af7 feat: implement Warden module callbacks (sendPacket, validateModule, generateRC4)
Implement the three stubbed Warden module callbacks that were previously
TODO placeholders:

- **sendPacket**: Encrypts module output via WardenCrypto RC4 and sends
  as CMSG_WARDEN_DATA through the game socket. Enables modules to send
  responses back to the server (required for strict servers like Warmane).

- **validateModule**: Compares the module's provided 16-byte MD5 hash
  against the hash received during download. Logs error on mismatch
  (indicates corrupted module transit).

- **generateRC4**: Derives new encrypt/decrypt RC4 keys from a 16-byte
  seed using SHA1Randx, then replaces the active WardenCrypto key state.
  Handles mid-session re-keying requested by the module.

Architecture:
- Add setCallbackDependencies() to inject WardenCrypto* and socket send
  function into WardenModule before load() is called
- Use thread_local WardenModule* so C function pointer callbacks (which
  can't capture state) can reach the module's dependencies during init
- Wire dependencies from WardenHandler before module load

Also update warden_module.hpp status markers — RSA verification, zlib,
executable parsing, relocation, and Unicorn emulation are all implemented
(were incorrectly marked as TODO). Only API binding/IAT patching and
RSA modulus verification against real WoW.exe remain as gaps.
2026-03-30 20:29:26 -07:00
Kelsi
7cfaf2c7e9 refactor: complete OpenGL→Vulkan migration (Phase 7)
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Remove all OpenGL/GLEW code and dependencies. The Vulkan renderer has
been the sole active backend for months; these files were dead code.

Deleted (8 files, 641 lines):
- rendering/mesh.cpp+hpp: OpenGL VAO/VBO/EBO wrapper (never instantiated)
- rendering/shader.cpp+hpp: OpenGL GLSL compiler (replaced by VkShaderModule)
- rendering/scene.cpp+hpp: Scene graph holding Mesh objects (created but
  never populated — all rendering uses Vulkan sub-renderers directly)
- rendering/video_player.cpp+hpp: FFmpeg+GL texture uploader (never
  included by any other file — login video feature can be re-implemented
  with VkTexture when needed)

Cleaned up:
- renderer.hpp: remove Scene forward-decl, getScene() accessor, scene member
- renderer.cpp: remove scene.hpp/shader.hpp includes, Scene create/destroy
- application.cpp: remove stale "GL/glew.h removed" comment
- CMakeLists.txt: remove find_package(OpenGL/GLEW), source/header entries,
  and target_link_libraries for OpenGL::GL and GLEW::GLEW
- PKGBUILD: remove glew dependency
- BUILD_INSTRUCTIONS.md: remove glew from all platform install commands
2026-03-30 19:22:36 -07:00
Kelsi
4b379f6fe9 chore: fix executable permissions on 6 scripts
All had shebangs (#!/usr/bin/env bash/python3) but were missing +x:
- restart-worldserver.sh
- tools/diff_classic_turtle_opcodes.py
- tools/gen_opcode_registry.py
- tools/m2_viewer.py
- tools/opcode_map_utils.py
- tools/validate_opcode_maps.py
2026-03-30 18:55:15 -07:00
Kelsi
1e06ea86d7 chore: remove dead code, document water shader wave parameters
- Delete 4 legacy GLSL 330 shaders (basic.vert/frag, terrain.vert/frag)
  left over from OpenGL→Vulkan migration — Vulkan equivalents exist as
  *.glsl files compiled to SPIR-V by the build system
- Delete orphaned mpq_manager.hpp/cpp (694 lines) — not in CMakeLists,
  not included by any file, unreferenced StormLib integration attempt
- Add comments to water.frag.glsl wave constants explaining the
  multi-octave noise design: non-axis-aligned directions prevent tiling,
  frequency increases and amplitude decreases per octave for natural
  water appearance
2026-03-30 18:50:14 -07:00
Kelsi
529985a961 chore: add vendored library version tracking
Add extern/VERSIONS.md documenting pinned versions of all vendored
third-party libraries: Dear ImGui 1.92.6, VMA 3.4.0, miniaudio 0.11.24,
stb_image 2.30, stb_image_write 1.16, Lua 5.1.5.

Notes that Lua 5.1.5 is intentionally old (WoW addon API compatibility).
Helps maintainers track dependency drift and plan upgrades.
2026-03-30 18:46:34 -07:00
Kelsi
4ea36250be chore: expand gitignore, document PKGBUILD deps, fix branch ref
- .gitignore: add compile_commands.json, language server caches
  (.ccls-cache, .cache/clangd), and tag files (ctags, cscope)
- PKGBUILD: add per-dependency comments explaining purpose (SRP auth,
  Warden emulation, MPQ extraction, shader compilation, etc.)
- PKGBUILD: fix source branch from main → master (matches remote HEAD)
2026-03-30 18:42:02 -07:00
Kelsi
3499a7f0ee docs: rewrite architecture.md to reflect current Vulkan-based codebase
Complete rewrite — the previous version extensively referenced OpenGL
(glClearColor, VAO/VBO/EBO, GLSL shaders) throughout all sections.
The project has used Vulkan exclusively for months.

Key changes:
- Replace all OpenGL references with Vulkan equivalents (VkContext,
  VMA, descriptor sets, pipeline cache, SPIR-V shaders)
- Update system diagram to show actual sub-renderer hierarchy
  (TerrainRenderer, WMORenderer, M2Renderer, CharacterRenderer, etc.)
- Document GameHandler SOLID decomposition (8 domain handlers +
  EntityController + GameServices dependency injection)
- Add Warden 4-layer architecture section
- Add audio system section (miniaudio, 5 sound managers)
- Update opcode count from "100+" to 664+
- Update UI section: talent screen and settings are implemented (not TODO)
- Document threading model (async terrain, GPU upload queue, normal maps)
- Fix dependencies list (Vulkan SDK, VMA, vk-bootstrap, Unicorn, FFmpeg)
- Add container builds and CI platforms
- Remove stale "TODO" items for features that are complete
2026-03-30 18:36:09 -07:00
Kelsi
dab534e631 docs: fix stale references across 10 documentation files
- CONTRIBUTING.md: C++17 → C++20 (matches CMakeLists.txt)
- TROUBLESHOOTING.md: fix log path (~/.wowee/logs/ → logs/wowee.log)
- docs/authentication.md: remove stale "next milestone" (char enum
  and world entry have been working for months)
- docs/srp-implementation.md: update session key status (RC4 encryption
  is implemented), fix file reference to actual src/auth/srp.cpp
- docs/packet-framing.md: remove stale "next steps" (realm list is
  fully implemented), update status with tested servers
- docs/WARDEN_IMPLEMENTATION.md: fix file list — handler is in
  warden_handler.cpp not game_handler.cpp, add warden_memory.hpp/cpp
- docs/WARDEN_QUICK_REFERENCE.md: fix header/source paths (include/
  not src/), add warden_handler and warden_memory
- docs/quickstart.md: fix clone command (--recurse-submodules, WoWee
  not wowee), remove obsolete manual ImGui clone step, fix log path
- docs/server-setup.md: update version to v1.8.9-preview, date to
  2026-03-30, add all supported expansions
- assets/textures/README.md: remove broken doc references
  (TURTLEHD_IMPORT.md, TEXTURE_MANIFEST.txt), update integration
  status to reflect working PNG override pipeline
2026-03-30 18:33:21 -07:00
Kelsi
c103743c3a docs: fix stale keybindings, paths, and API examples
GETTING_STARTED.md:
- Fix keybinding table: T→N for talents, Q→L for quest log, W→M for
  world map, add missing keys (C, I, O, J, Y, K), remove nonexistent
  minimap toggle
- Fix extract_assets.ps1 example param (-WowDirectory → positional)
- Fix Data/ directory tree to match actual manifest layout
- Fix log path: ~/.wowee/logs/ → logs/wowee.log (local directory)

EXPANSION_GUIDE.md:
- Add Turtle WoW 1.17 to supported expansions
- Update code examples to use game_utils.hpp helpers
  (isActiveExpansion/isClassicLikeExpansion/isPreWotlk) instead of
  removed ExpansionProfile::getActive() and GameHandler::getInstance()
- Update packet parser references (WotLK is default in domain handlers,
  not a separate packet_parsers_wotlk.cpp file)
- Update references section with game_utils.hpp
2026-03-30 18:26:47 -07:00
Kelsi
47fe6b8468 docs: update README, CHANGELOG, and status to v1.8.9-preview
- README: update status date to 2026-03-30, version to v1.8.9-preview,
  add container builds line, update current focus to code quality
- CHANGELOG: move v1.8.1 entries to their own section, add v1.8.2-v1.8.9
  unreleased section covering architecture (GameHandler decomposition,
  Docker cross-compilation), bug fixes (7 UB/overflow/safety fixes),
  and code quality (30+ constants, 55+ comments, 8 DRY extractions)
- docs/status.md: update last-updated date to 2026-03-30
2026-03-30 17:40:47 -07:00
Kelsi
cb17c69c40 docs: add why-comments to spellbook icon caching and DBC fallback
- Explain icon load deferral strategy: returning null without caching
  allows retry next frame when budget resets, rather than permanently
  blacklisting icons that were deferred due to rate-limiting
- Explain DBC field fallback logic: hard-coded WotLK indices are a
  safety net when dbc_layouts.json is missing; fieldCount >= 200
  distinguishes WotLK (234 fields) from Classic (148)
2026-03-30 17:32:07 -07:00
Kelsi
1ab254273e docs: add M2 format why-comments to character preview
- Explain M2 version 264 threshold (WotLK stores submesh/bone data
  in external .skin files; Classic/TBC embed it in the M2)
- Explain M2 texture types 1 and 6 (skin and hair/scalp; empty
  filenames resolved via CharSections.dbc at runtime)
- Explain 0x20 anim flag (embedded data; when clear, keyframes live
  in external {Model}{SeqID}-{Var}.anim files)
- Explain geoset ID encoding (group × 100 + variant from
  ItemDisplayInfo.dbc; e.g. 801 = sleeves variant 1)
2026-03-30 17:28:47 -07:00
Kelsi
2c50cc94e1 docs: add why-comments to TBC parsers, bell audio, portrait preview
- packet_parsers_tbc: explain spline waypoint cap (DoS prevention),
  spline compression flags (Catmull-Rom 0x80000 / linear 0x2000 use
  uncompressed format, others use packed delta), spell hit target cap
  (128 >> real AOE max of ~20), guild roster cap (1000 safety limit)
- ambient_sound_manager: explain 1.5s bell toll spacing — matches
  retail WoW cadence, allows each toll to ring out before the next
- character_preview.hpp: explain 4:5 portrait aspect ratio for
  full-body character display in creation/selection screen
2026-03-30 17:26:13 -07:00
Kelsi
92369c1cec docs: add why-comments to rendering, packets, and UI code
- charge_effect: explain inversesqrt guard (prevents NaN on stationary
  character) and dust accumulator rate (30 particles/sec * 16ms)
- swim_effects: explain why insect pipeline disables depth test
  (screen-space sprites must render above water geometry)
- packet_parsers_classic: explain spline waypoint cap (DoS prevention)
  and packed GUID compression format (non-zero bytes only, mask byte)
- talent_screen: explain class ID to bitmask conversion (1-indexed
  WoW class IDs → power-of-2 mask for TalentTab.classMask matching)
- auth_screen: explain login music volume reduction (80% so UI sounds
  remain audible over background track)
2026-03-30 17:23:07 -07:00
Kelsi
e8a4a7402f fix: clamp player percentage stats, add scale field why-comment
- entity_controller: clamp block/dodge/parry/crit/rangedCrit percentage
  fields to [0..100] after memcpy from update fields — guards against
  NaN/Inf from corrupted packets reaching the UI renderer
- entity_controller: add why-comment on OBJECT_FIELD_SCALE_X raw==0
  check — IEEE 754 0.0f is all-zero bits, so raw==0 means the field
  was never populated; keeping default 1.0f prevents invisible entities
2026-03-30 15:48:30 -07:00
Kelsi
4215950dcd refactor: extract class/race restriction helpers, add DBC fallback comment
- inventory_screen: extract renderClassRestriction() and
  renderRaceRestriction() from two identical 40-line blocks in quest
  info and item info tooltips. Both used identical bitmask logic,
  strncat formatting, and player-class/race validation (-49 lines net)
- world_map: add why-comment on AreaTable.dbc fallback field indices —
  explains that incorrect indices silently return wrong data and why
  the WotLK stock layout (ID=0, Parent=2, ExploreFlag=3) is chosen
  as the safest default
2026-03-30 15:45:48 -07:00
Kelsi
af604cc442 fix: UB in mouse button polling, null deref in BigNum formatting
- input: fix undefined behavior in SDL mouse button loop — SDL_BUTTON(0)
  computes (1 << -1) which is UB. Start loop at 1 since SDL button
  indices are 1-based (SDL_BUTTON_LEFT=1, RIGHT=3, MIDDLE=2)
- big_num: guard BN_bn2hex/BN_bn2dec against nullptr return on
  OpenSSL allocation failure — previously constructed std::string
  from nullptr which is undefined behavior
2026-03-30 15:37:38 -07:00
Kelsi
fe7912b5fa fix: prevent buffer overflows in Warden PE parsing
- Add bounds checks to readLE32/readLE16 — malformed Warden modules
  could cause out-of-bounds reads on untrusted PE data
- Fix unsigned underflow in PE section loading: if rawDataOffset or
  virtualAddr exceeds buffer size, the subtraction wrapped to a huge
  uint32_t causing memcpy to read/write far beyond bounds. Now skips
  the section entirely and uses std::min with pre-validated maxima
2026-03-30 15:33:03 -07:00
Kelsi
b39f0f3605 refactor: name GUID type and LFG role constants, add why-comments
- world_packets: name kGuidTypeMask/kGuidTypePet/kGuidTypeVehicle
  for chat receiver GUID type detection, with why-comment explaining
  WoW's bits-48-63 entity type encoding and 0xF0FF mask purpose
- lua_engine: name kRoleTank/kRoleHealer/kRoleDamager (0x02/0x04/0x08)
  for WotLK LFG role bitmask, add context on Leader bit (0x01) and
  source packets (SMSG_GROUP_LIST / SMSG_LFG_ROLE_CHECK_UPDATE)
2026-03-30 15:28:18 -07:00
Kelsi
ff72d23db9 refactor: name lighting time constant, replace PI literal with glm
- Name kHalfMinutesPerDay (2880) replacing 8 bare literals across
  time conversion, modulo clamping, and midnight wrap arithmetic.
  Add why-comment: Light.dbc stores time-of-day as half-minutes
  (24h × 60m × 2 = 2880 ticks per day cycle)
- Replace hardcoded 3.14159f with glm::two_pi<float>() in sun
  direction angle calculations (2 occurrences)
2026-03-30 15:23:58 -07:00
Kelsi
55cac39541 refactor: name random/camera constants, add alpha map static_assert
- terrain_manager: extract kRand16Max (65535.0f) from 8 duplicated
  random normalization expressions — 16-bit mask to [0..1] float
- terrain_manager: add static_assert verifying packed alpha unpacks
  to full alpha map size (ALPHA_MAP_PACKED * 2 == ALPHA_MAP_SIZE)
- camera_controller: name kCameraClipEpsilon (0.1f) with why-comment
  preventing character model clipping at near-minimum distance
2026-03-30 15:17:37 -07:00
Kelsi
a389fd2ef4 refactor: name SRP/Warden crypto constants, add why-comments
- srp: name kEphemeralBytes (19 = 152 bits, matches Blizzard client)
  and kMaxEphemeralAttempts (100) with why-comment explaining A != 0
  mod N requirement and near-zero failure probability
- warden_module: add why-comment on 0x400000 module base (default
  PE image base for 32-bit Windows executables)
- warden_module: name kRsaSignatureSize (256 = RSA-2048) with
  why-comment explaining signature stripping (placeholder modulus
  can't verify Blizzard's signatures)
2026-03-30 15:12:27 -07:00
Kelsi
7b4fdaa277 refactor: name memory/taxi constants, add camera jitter why-comment
- memory_monitor: extract kOneGB and kFallbackRAM constants from 6
  duplicated 1024*1024*1024 expressions; name kFieldPrefixLen for
  /proc/meminfo "MemAvailable:" offset (was bare 13)
- camera: add why-comment on projection matrix jitter — column 2 holds
  NDC x/y offset for TAA/FSR2 sub-pixel sampling
- movement_handler: name kMaxTaxiNodeId (384) with why-comment —
  WotLK TaxiNodes.dbc has 384 entries, bitmask is 12 × uint32
2026-03-30 15:07:55 -07:00
Kelsi
548828f2ee refactor: extract color write mask, name frustum epsilon, add comments
- vk_pipeline: extract kColorWriteAll constant from 4 duplicated RGBA
  bitmask expressions across blend mode functions, with why-comment
- frustum: name kMinNormalLenSq epsilon (1e-8) with why-comment —
  prevents division by zero on degenerate planes
- dbc_loader: add why-comment on DBC field width validation — all
  fields are fixed 4-byte uint32 per format spec
- pin_auth: replace 0x30 hex literal with '0' char constant, add
  why-comment on ASCII encoding for server HMAC compatibility
2026-03-30 15:02:47 -07:00
Kelsi
ef787624fe refactor: name M2 sequence flag, replace empty loop with std::advance
- m2_loader: define kM2SeqFlagEmbeddedData (0x20) with why-comment —
  when clear, keyframe data lives in external .anim files and M2 offsets
  are file-relative (reading them from M2 produces garbage). Replaces
  3 bare hex literals across parseAnimTrack and ribbon emitter parsing
- audio_engine: replace empty for-loop iterator advance with
  std::advance() for clarity
2026-03-30 14:59:03 -07:00
Kelsi
d2a7d79f60 refactor: add why-comments to zone tiles, audio cache, socket buffer
- zone_manager: document tile-to-zone key encoding (tileX * 100 + tileY,
  safe because tileY < 64 < 100) and explain that ranges are empirically
  derived from the retail WoW map grid
- audio_engine: expand sample rate comment — miniaudio defaults to
  device rate causing pitch distortion if not set explicitly; name
  kMaxCachedSounds constant with memory budget explanation
- tcp_socket: add why-comment on 4 KB recv buffer sizing — covers
  typical 20-500 byte packets and worst-case ~2 KB UPDATE_OBJECT
2026-03-30 14:52:51 -07:00
Kelsi
8c7db3e6c8 refactor: name FNV-1a/transport constants, fix dead code, add comments
- vk_context: name FNV-1a hash constants (kFnv1aOffsetBasis/kFnv1aPrime)
  with why-comment on algorithm choice for sampler cache
- transport_manager: collapse redundant if/else that both set
  looping=false into single unconditional assignment, add why-comment
  explaining the time-closed path design
- transport_manager: hoist duplicate kMinFallbackZOffset constants out
  of separate if-blocks, add why-comment on icebreaker Z clamping
- entity: expand velocity smoothing comment — explain 65/35 EMA ratio
  and its tradeoff (jitter suppression vs direction change lag)
2026-03-30 14:48:06 -07:00
Kelsi
a940859e6a refactor: name auth security flags, log JSON parse failures
- auth_handler: define kSecurityFlagPin/MatrixCard/Authenticator
  constants (0x01/0x02/0x04) with why-comment explaining WoW login
  challenge securityFlags byte, replace all bare hex literals
- expansion_profile: log warning on jsonInt() parse failure instead
  of silently returning default — makes malformed expansion.json
  diagnosable without debugger
2026-03-30 14:43:50 -07:00
Kelsi
74f0ba010a fix: remove duplicate zone weather, consolidate RNG, name star constants
- weather: remove duplicate setZoneWeather(15) for Dustwallow Marsh —
  second call silently overwrote the first with different parameters
- weather: replace duplicate static RNG in getRandomPosition() with
  shared weatherRng() to avoid redundant generator state
- starfield: extract day/night cycle thresholds into named constants
  (kDuskStart/kNightStart/kDawnStart/kDawnEnd/kFadeDuration)
- skybox: replace while-loop time wrapping with std::fmod — avoids
  O(n) iterations on large time jumps
2026-03-30 14:38:30 -07:00
Kelsi
086f32174f fix: guard fsPath underflow, name WMO doodad mask, add why-comments
- asset_manager: add size guard before fsPath.substr(size-4) in
  tryLoadPngOverride — resolveFile could theoretically return a
  path shorter than the extension
- wmo_loader: name kDoodadNameIndexMask (0x00FFFFFF) with why-comment
  explaining the 24-bit name index / 8-bit flags packing and MODN
  string table reference
- window: add why-comment on LOG_WARNING usage during shutdown —
  intentionally elevated so teardown progress is visible at default
  log levels for crash diagnosis
2026-03-30 14:33:08 -07:00
Kelsi
1151785381 refactor: name ADT vertex constants, add BLP decompression comments
- adt_loader: replace magic 145 with kMCVTVertexCount and 17 with
  kMCVTRowStride — MCVT height grid is 9 outer + 8 inner vertices
  per row across 9 rows
- adt_loader: replace 999999.0f sentinels with numeric_limits
- blp_loader: add why-comments on RGB565→RGB888 bit layout
  (R=bits[15:11], G=[10:5], B=[4:0])
- blp_loader: explain DXT3 4-bit alpha scaling (n * 255 / 15)
- blp_loader: explain palette 4-bit alpha multiply-by-17 trick
  (equivalent to n * 255 / 15, exact for all 16 values)
2026-03-30 14:28:22 -07:00
Kelsi
683e171fd1 fix: VkTexture move/destroy ownsSampler_ flag, extract finalizeSampler
- Fix move constructor and move assignment: set other.ownsSampler_ to
  false after transfer (was incorrectly set to true, leaving moved-from
  object claiming ownership of a null sampler)
- Fix destroy(): reset ownsSampler_ to false after clearing sampler
  handle (was set to true, inconsistent with null handle state)
- Extract finalizeSampler() from 3 duplicated cache-or-create blocks
  in createSampler() overloads and createShadowSampler() (-24 lines)
- Add SPIR-V alignment why-comment in vk_shader.cpp
2026-03-30 14:24:41 -07:00
Kelsi
28e5cd9281 refactor: replace magic bag slot offset 19 with FIRST_BAG_EQUIP_SLOT
- Add Inventory::FIRST_BAG_EQUIP_SLOT = 19 constant with why-comment
  explaining WoW equip slot layout (bags occupy slots 19-22)
- Replace all 19 occurrences of magic number 19 in bag slot calculations
  across inventory_handler, spell_handler, inventory, and game_handler
- Add UNIT_FIELD_FLAGS / UNIT_FLAG_PVP comment in combat_handler
- Add why-comment on network packet budget constants (prevent server
  data bursts from starving the render loop)
2026-03-30 14:20:39 -07:00
Kelsi
4574d203b5 refactor: name M2 renderer magic constants, add why-comments
- Name portal spin wrap value as kTwoPi constant
- Name particle animTime wrap as kParticleWrapMs (3333ms) with
  why-comment: covers longest known emission cycle (~3s torch/campfire)
  while preventing float precision loss over hours of runtime
- Add FBlock interpolation documentation: explain what FBlocks are
  (particle lifetime curves) and note that float/vec3 variants share
  identical logic and must be updated together
2026-03-30 14:14:27 -07:00
Kelsi
6dfac314ee fix: remove dead code, name constants, add why-comments
- renderer: remove no-op assignment (mountAnims_.stand = 0 when already 0)
- renderer: add why-comments on blacksmith WMO ID 96048 (ambient forge
  sounds) with TODO for other smithy buildings
- terrain_renderer: replace 1e30f sentinel with numeric_limits::max(),
  name terrain view distance constant (1200 units ≈ 9 ADT tiles)
- social_handler: add missing LFG case 15, document case 0 nullptr
  return (success = no error message), add enum name comments
2026-03-30 14:10:32 -07:00
Kelsi
4acebff65c refactor: extract fallback textures, add why-comments, name WMO constant
- character_renderer: extract duplicated fallback texture creation
  (white/transparent/flat-normal) into createFallbackTextures() — was
  copy-pasted between initialize() and clear()
- wmo_renderer: replace magic 8192 with kMaxRetryTracked constant,
  add why-comment explaining the fallback-retry set cap (Dalaran has
  2000+ unique WMO groups)
- quest_handler: add why-comment on reqCount=0 fallback — escort/event
  quests can report kill credit without objective counts in query response
2026-03-30 14:06:30 -07:00
Kelsi
f313eec24e refactor: replace magic slot offset 23 with NUM_EQUIP_SLOTS, simplify channel search
- Replace all 11 occurrences of magic number 23 in backpack slot
  calculations with Inventory::NUM_EQUIP_SLOTS across inventory_handler,
  spell_handler, and inventory.cpp
- Add why-comment to NUM_EQUIP_SLOTS explaining WoW slot layout
  (equipment 0-22, backpack starts at 23 in bag 0xFF)
- Add why-comment on 0x80000000 bit mask in item query response
  (high bit flags negative/missing entry response)
- Replace manual channel membership loops with std::find in
  chat_handler.cpp (YOU_JOINED and PLAYER_ALREADY_MEMBER cases)
- Add why-comment on PLAYER_ALREADY_MEMBER reconnect edge case
2026-03-30 14:01:34 -07:00
Kelsi
a9ce22f315 refactor: extract findOnUseSpellId helper, add warden hash comment
- spell_handler: extract duplicated item on-use spell lookup into
  findOnUseSpellId() — was copy-pasted in useItemBySlot and useItemInBag
- warden_handler: add why-comment explaining the door model HMAC-SHA1
  hash table (wall-hack detection for unmodified 3.3.5a client data)
2026-03-30 13:56:45 -07:00
Kelsi
76f493f7d9 refactor: replace goto with structured control flow
- spell_handler.cpp: replace goto-done with do/while(false) for pet
  spell packet parsing — bail on truncated data while always firing
  events afterward
- water_renderer.cpp: replace goto-found_neighbor with immediately
  invoked lambda to break out of nested neighbor search loops
2026-03-30 13:47:14 -07:00
Kelsi
76d29ad669 fix: address PR #31 and #32 review findings
- Dockerfile: fix LLVM apt repo codename (jammy → noble) for ubuntu:24.04
- build-linux.sh: add missing mkdir -p /wowee-build-src before tar extraction
- Dockerfile: remove dead ENV OSXCROSS_VERSION=1.5 and its unset
- CMakeLists: scope -undefined dynamic_lookup to wowee target only
- GameServices: remove redundant game:: qualifier inside namespace game
- application.cpp: zero out gameServices_ after gameHandler reset in shutdown
2026-03-30 13:40:40 -07:00
Kelsi Rae Davis
fe080bed4b
Merge pull request #31 from ldmonster/chore/break-application-from-gamehandler
[chore] refactor: Break Application::getInstance() from GameHandler
2026-03-30 13:26:29 -07:00
Kelsi Rae Davis
9f87c386f7
Merge pull request #32 from ldmonster/chore/container-build-extended
[feat] add multi-platform Docker build system
2026-03-30 13:26:02 -07:00
Paul
af60fe1edc fix cve 2026-03-30 21:15:41 +03:00
Paul
85f8d05061 feat: add multi-platform Docker build system for Linux, macOS, and Windows
Replace the single Ubuntu-based container build with a dedicated
Dockerfile, build script, and launcher for each target platform.

Infrastructure:
- Add .dockerignore to minimize Docker build context
- Add container/builder-linux.Dockerfile (Ubuntu 24.04, GCC, native build)
- Add container/builder-macos.Dockerfile (multi-stage: SDK fetcher + osxcross/Clang 18)
- Add container/builder-windows.Dockerfile (LLVM-MinGW 20240619, vcpkg)
- Add container/macos/sdk-fetcher.py (auto-fetch macOS SDK from Apple catalog)
- Add container/macos/osxcross-toolchain.cmake (auto-detecting CMake toolchain)
- Add container/macos/triplets/arm64-osx-cross.cmake
- Add container/macos/triplets/x64-osx-cross.cmake
- Remove container/builder-ubuntu.Dockerfile (replaced by per-platform Dockerfiles)
- Remove container/build-in-container.sh and container/build-wowee.sh (replaced)

Build scripts (run inside containers):
- Add container/build-linux.sh (tar copy, FidelityFX clone, cmake/ninja)
- Add container/build-macos.sh (arch detection, vcpkg triplet, cross-compile)
- Add container/build-windows.sh (Vulkan import lib via dlltool, cross-compile)

Launcher scripts (run on host):
- Add container/run-linux.sh, run-macos.sh, run-windows.sh (bash)
- Add container/run-linux.ps1, run-macos.ps1, run-windows.ps1 (PowerShell)

Documentation:
- Add container/README.md (quick start, options, file structure, troubleshooting)
- Add container/FLOW.md (comprehensive build flow for each platform)

CMake changes:
- Add macOS cross-compile support (VulkanHeaders, -undefined dynamic_lookup)
- Add LLVM-MinGW/Windows cross-compile support
- Detect osxcross toolchain and vcpkg triplets

Other:
- Update vcpkg.json with ffmpeg feature flags
- Update resources/wowee.rc version string
2026-03-30 20:17:41 +03:00
Paul
a86efaaa18 [refactor] Break Application::getInstance() from GameHandler
Introduce `GameServices` struct — an explicit dependency bundle that
`Application` populates and passes to `GameHandler` at construction time.
Eliminates all 47 hidden `Application::getInstance()` calls in
`src/game/*.cpp`, completing SOLID-D (dependency-inversion) cleanup.

Changes:
- New `include/game/game_services.hpp` — `struct GameServices` carrying
  pointers to `Renderer`, `AssetManager`, `ExpansionRegistry`, and two
  taxi-mount display IDs
- `GameHandler(GameServices&)` replaces default constructor; exposes
  `services() const` accessor for domain handlers
- `Application` holds `game::GameServices gameServices_`; populates it
  after all subsystems are created, then constructs `GameHandler`
  (fixes latent init-order bug: `GameHandler` was previously created
  before `AssetManager` / `ExpansionRegistry`)
- `game_handler.cpp`: duplicate `isActiveExpansion` / `isClassicLikeExpansion` /
  `isPreWotlk` anonymous-namespace helpers removed; `game_utils.hpp`
  included instead
- All domain handlers (`InventoryHandler`, `SpellHandler`, `MovementHandler`,
  `CombatHandler`, `QuestHandler`, `SocialHandler`, `WardenHandler`) replace
  `Application::getInstance().getXxx()` with `owner_.services().xxx`
2026-03-30 09:17:42 +03:00
Kelsi
169595433a debug: add GO interaction diagnostics at every decision point
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Adds [GO-DIAG] WARNING-level logs at:
- Right-click dispatch (raypick hit / re-interact with target)
- interactWithGameObject entry + all BLOCKED paths
- SMSG_SPELL_GO (wasInTimedCast, lastGoGuid, pendingGoGuid state)
- SMSG_LOOT_RESPONSE (items, gold, guid)
- Raypick candidate GO positions (entity pos + hit center + radius)

These logs will pinpoint exactly where the interaction fails:
- No GO-DIAG lines = GOs not in entity manager / not visible
- Raypick GO pos=(0,0,0) = GO position not set from update block
- BLOCKED = guard condition preventing interaction
- SPELL_GO wasInTimedCast=false = timer race (already fixed)
2026-03-29 23:09:28 -07:00
Kelsi
5e83d04f4a fix: GO cast timer fallback cleared state before SMSG_SPELL_GO arrived
The client-side cast timer expires ~50-200ms before the server sends
SMSG_SPELL_GO (float precision + frame timing). Previously the fallback
called resetCastState() which set casting_=false and currentCastSpellId_
=0. When SMSG_SPELL_GO arrived moments later, wasInTimedCast evaluated
to false (false && spellId==0), so the loot path (CMSG_LOOT via
lastInteractedGoGuid_) was never taken. Quest chests never opened.

Now the fallback skips resetCastState() for GO interaction casts, letting
the cast bar sit at 100% until SMSG_SPELL_GO arrives and handles cleanup
properly with wasInTimedCast=true.
2026-03-29 22:58:49 -07:00
Kelsi
cfbae93ce3 fix: client timer fallback re-sent CMSG_GAMEOBJ_USE and cleared loot guid
When the client-side cast timer expired slightly before SMSG_SPELL_GO
arrived, the fallback at update():1367 called performGameObjectInteraction
Now which sent a DUPLICATE CMSG_GAMEOBJ_USE to the server (confusing its
GO state machine), then resetCastState() cleared lastInteractedGoGuid_.
When SMSG_SPELL_GO finally arrived, the guid was gone so CMSG_LOOT was
never sent — quest chests produced no loot window.

Fix: the fallback no longer re-sends USE (server drives the interaction
via SMSG_SPELL_GO). resetCastState() no longer clears
lastInteractedGoGuid_ so the SMSG_SPELL_GO handler can still send LOOT.
2026-03-29 22:45:17 -07:00
Kelsi
785f03a599 fix: stale GO interaction guard broke future casts; premature LOOT interfered
Two remaining GO interaction bugs:

1. pendingGameObjectInteractGuid_ was never cleared after SMSG_SPELL_GO
   or SMSG_CAST_FAILED, leaving it stale. This suppressed CMSG_CANCEL_CAST
   for ALL subsequent spell casts (not just GO casts), causing the server
   to think the player was still casting when they weren't.

2. For chest-like GOs, CMSG_LOOT was sent simultaneously with
   CMSG_GAMEOBJ_USE. If the server starts a timed cast ("Opening"),
   the GO isn't lootable until the cast completes — the premature LOOT
   gets an empty response or is dropped, potentially corrupting the
   server's loot state. Now defers LOOT to handleSpellGo which sends it
   after the cast completes (via lastInteractedGoGuid_).
2026-03-29 22:36:30 -07:00
Kelsi
6b5e924027 fix: GO interaction casts canceled by any movement — quest credit lost
pendingGameObjectInteractGuid_ was always cleared to 0 right before
the interaction, which defeated the cancel-protection guard in
cancelCast(). Any positional movement (WASD, jump) during a GO
interaction cast (e.g., "Opening" on a quest chest) sent
CMSG_CANCEL_CAST to the server, aborting the interaction and
preventing quest objective credit.

Now sets pendingGameObjectInteractGuid_ to the GO guid so:
1. cancelCast() skips CMSG_CANCEL_CAST for GO-triggered casts
2. The cast-completion fallback can re-trigger loot after timer expires
3. isGameObjectInteractionCasting() returns true during GO casts
2026-03-29 22:22:20 -07:00
Kelsi
c1c28d4216 fix: quest objective GOs never granted credit — REPORT_USE skipped for chests
CMSG_GAMEOBJ_REPORT_USE was only sent for non-chest GOs. Chest-type
(type=3) and name-matched chest-like GOs (Bundle of Wood, etc.) went
through a separate path that sent CMSG_GAMEOBJ_USE + CMSG_LOOT but
skipped REPORT_USE. On AzerothCore, REPORT_USE triggers the server-side
HandleGameobjectReportUse which calls GossipHello on the GO script —
this is where many quest objective scripts grant credit.

Restructured so CMSG_GAMEOBJ_USE is sent first for all GO types,
then chest-like GOs additionally send CMSG_LOOT, and REPORT_USE fires
for everything except mailboxes.
2026-03-29 22:04:23 -07:00
Kelsi
f23a7c06d9 fix: nameplate click hitbox used name text width, not health bar width
The click region for targeting via nameplates was bounded by the name
text (nameX to nameX+textSize.x). Short names like "Wolf" produced a
~30px clickable strip, while the health bar below was 80px wide. Clicks
on the bar outside the name text bounds were ignored. Now uses the wider
of name text or health bar for the horizontal hit area.
2026-03-29 21:54:57 -07:00
Kelsi
8376757f7e cleanup: migrate 20 remaining setReadPos(getSize()) to skipAll()
The Packet::skipAll() method was introduced to replace the verbose
setReadPos(getSize()) pattern. 186 instances were migrated earlier,
but 20 survived in domain handler files created after the migration.
Also removes a redundant single-element for-loop wrapper around
SMSG_LOOT_CLEAR_MONEY registration.
2026-03-29 21:51:03 -07:00
Kelsi
ae32b27d6c fix: misplaced brace included 4 unrelated handlers inside instance-difficulty loop
Same class of bug as inventory_handler fix b9ecc26f. The for-loop over
{SMSG_INSTANCE_DIFFICULTY, MSG_SET_DUNGEON_DIFFICULTY} was missing its
closing brace, so GUILD_DECLINE, RAF_EXPIRED, RAF_FAILURE, and
PVP_AFK_RESULT registrations executed inside the loop body — each
registered twice (once per opcode). Currently harmless since duplicate
registration is idempotent, but structurally wrong.
2026-03-29 21:50:55 -07:00
Kelsi
161b7981f9 fix: video player decode loop could spin indefinitely on corrupt files
The while(true) loop retried av_read_frame after seeking to the start
on error. A corrupt file where read fails but seek succeeds would loop
forever, blocking the main thread. Bounded to 500 attempts with a
warning log on exhaustion.
2026-03-29 21:46:10 -07:00
Kelsi
c8dde0985f fix: detached normal-map thread could deadlock shutdown on exception
If generateNormalHeightMapCPU threw (e.g., bad_alloc), the pending
counter was never decremented, causing shutdown() to block forever
waiting for a count that would never reach zero. Added try-catch to
guarantee the decrement. Also strengthened the increment from relaxed
to acq_rel so shutdown()'s acquire load sees the count before the
thread body begins executing.
2026-03-29 21:36:06 -07:00
Kelsi
01ab2a315c fix: achievement message silently truncated by char[256] snprintf
Long achievement names combined with sender name could exceed 256
bytes, silently cutting the message mid-word in chat. Replaced with
std::string concatenation which grows dynamically.
2026-03-29 21:35:56 -07:00
Kelsi
e42f8f1c03 fix: misleading indentation on reputation addon event dispatch
The two fireAddonEvent calls were indented as if conditional on
repChangeCallback_ but actually execute unconditionally (no braces).
Fixed indentation and added clarifying comment.
2026-03-29 21:35:49 -07:00
Kelsi
bbb560f93c fix: data race on collision query profiling counters
queryTimeMs and queryCallCount on WMORenderer and M2Renderer were plain
mutable doubles/uint32s written by getFloorHeight (dispatched on async
threads from CameraController) and read by the main thread. This is
undefined behavior per C++ — thread sanitizer would flag it. Changed to
std::atomic with relaxed ordering (adequate for diagnostics) and updated
QueryTimer to use atomic fetch_add/compare_exchange.
2026-03-29 21:26:11 -07:00
Kelsi
3dd1128ecf fix: unguarded future::get() crashed on render/floor-query worker exceptions
std::future::get() re-throws any exception from the async task. The 6
call sites in the render pipeline (terrain/WMO/M2 workers + animation
worker) and 2 floor-query sites in camera_controller were unguarded,
so a single bad_alloc in any worker would terminate the process with
no recovery. Now wrapped in try-catch with error logging.
2026-03-29 21:26:01 -07:00
Kelsi
74cc048767 fix: watchdog thread called SDL video functions from non-main thread
SDL2 requires video/window functions to be called from the main thread
(the one that called SDL_Init). The watchdog thread was calling
SDL_SetRelativeMouseMode, SDL_ShowCursor, and SDL_SetWindowGrab directly
on stall detection — undefined behavior on macOS (Cocoa requires main-
thread UI calls) and unsafe on other platforms.

Now the watchdog sets an atomic flag, and the main loop checks it at the
top of each iteration, executing the SDL calls on the correct thread.
2026-03-29 21:15:49 -07:00
Kelsi
5583573beb fix: eliminate last std::rand() calls — music shuffle and UI weather
zone_manager.cpp used std::rand() for music track selection with modulo
bias and global state. game_screen.cpp used std::rand() for rain/snow
particle positions. Both now use local std::mt19937 seeded from
random_device. Also removes the global srand(time(nullptr)) call since
no code depends on the C rand() seed anymore.

No std::rand() or srand() calls remain in the codebase.
2026-03-29 21:01:51 -07:00
Kelsi
a55eacfe70 fix: weather particles and cycle durations deterministic due to unseeded rand()
8 rand() calls in weather.cpp used C rand() which defaults to seed 1.
Weather intensity rolls, cycle durations, and particle Y positions were
identical on every launch. Replaced with a file-local mt19937 seeded
from random_device, matching the RNG already present in getRandomPosition.
2026-03-29 20:53:38 -07:00
Kelsi
294c91d84a fix: migrate 197 unsafe packet bounds checks to hasRemaining/getRemainingSize
All domain handler files used 'packet.getSize() - packet.getReadPos()'
which underflows to ~2^64 when readPos exceeds size (documented in
commit ed63b029). The game_handler.cpp and packet_parsers were migrated
to hasRemaining(N) in an earlier cleanup, but the domain handlers were
created after that migration by the PR #23 split, copying the old
unsafe patterns back in. Now uses hasRemaining(N) for comparisons and
getRemainingSize() for assignments across all 7 handler files.
2026-03-29 20:53:26 -07:00
Kelsi
849542d01d fix: doodad/mount animations synchronized due to unseeded rand()
All 8 rand() calls for animation time offsets and variation timers in
m2_renderer.cpp used C rand() which defaults to seed 1 without srand(),
producing identical sequences every launch. Trees, torches, and grass
all swayed in sync. Replaced with std::mt19937 seeded from
random_device. Same fix for 4 mount idle fidget/sound timer sites in
renderer.cpp which mixed rand() with the mt19937 already present.
2026-03-29 20:42:10 -07:00
Kelsi
16aaf58198 fix: M2 readString uint32 overflow in bounds check
offset + length was computed in uint32_t before comparing to size_t.
A crafted M2 with offset=0xFFFFFFFF, length=2 wraps to 1 in uint32,
passing the check and reading out of bounds. Now uses size_t arithmetic,
matching the readArray fix from an earlier round.
2026-03-29 20:41:56 -07:00
Kelsi
fa1643dc90 fix: WMO readArray integer overflow in bounds check
count * sizeof(T) was computed in uint32_t — a large count value from a
crafted WMO file could wrap to a small number, pass the bounds check,
then attempt a multi-GB allocation causing OOM/crash. Now uses 64-bit
arithmetic with a 64MB sanity cap, matching the M2 loader pattern.
2026-03-29 20:32:47 -07:00
Kelsi
b007a525a6 fix: Lua UnitIsAFK/UnitIsDND/UnitIsGhost/UnitIsPVPFreeForAll read wrong field
All four functions read UNIT_FIELD_FLAGS instead of PLAYER_FLAGS.
- AFK (0x01) hit UNIT_FLAG_SERVER_CONTROLLED — vendors flagged as AFK
- DND (0x02) hit UNIT_FLAG_NON_ATTACKABLE — guards flagged as DND
- Ghost (0x100) hit UNIT_FLAG_IMMUNE_TO_PC — immune NPCs flagged as ghost
- FFA PvP (0x80000) hit UNIT_FLAG_PACIFIED — pacified mobs flagged FFA

All now correctly read PLAYER_FLAGS with the right bit masks (0x01,
0x02, 0x10, 0x80 respectively), matching entity_controller.cpp which
already uses the correct field.
2026-03-29 20:32:39 -07:00
Kelsi
f02be1ffac fix: tolower/toupper UB on signed char at 10 remaining call sites
Final sweep across mpq_manager, application, auth_screen, wmo_renderer,
character_renderer, and terrain_manager. All now use the unsigned char
cast pattern. No remaining bare ::tolower/::toupper or std::tolower(c)
calls on signed char in the codebase.
2026-03-29 20:27:16 -07:00
Kelsi
34e384e1b2 fix: tavern music always played first track — index never incremented
tavernTrackIndex was initialized to 0 but never modified, so the player
always heard TavernAlliance01.mp3. Added post-increment to rotate
through the 3 available tracks on each tavern entry.
2026-03-29 20:27:08 -07:00
Kelsi
a1575ec678 fix: WDT MWMO parser used unbounded strlen on chunk data
std::strlen on raw MWMO chunk data has no upper bound if the chunk
lacks a null terminator (truncated/corrupt WDT file). Replaced with
strnlen bounded by chunkSize, matching the ADT parser fix in d776226f.
2026-03-29 20:26:58 -07:00
Kelsi
7f5cad63cd fix: WMO group debug log throttle was per-process, not per-model
static int logCount/batchLogCount inside the per-group parse loop
accumulated globally, so after the first WMO with many sub-chunks
loaded, no subsequent WMO group would ever log. Changed to function-
local / loop-index-based throttle so each group gets its own window.
2026-03-29 20:14:53 -07:00
Kelsi
fa15a3de1f fix: transport elapsed time lost millisecond precision after ~4.5 hours
elapsedTime_ was float (32-bit, ~7 significant digits). At 16384
seconds the float can only represent integers, so elapsedTime_*1000
jumps in 1-second steps — ships and elevators visibly jerk. Changed to
double (53-bit mantissa) which maintains sub-millisecond precision for
~285 million years. Also changed lastServerUpdate to double to match.
2026-03-29 20:14:45 -07:00
Kelsi
ef25785404 fix: terrain chunk UBO allocation failure crashed GPU via null descriptor
vmaCreateBuffer return value was silently discarded in both loadTerrain
and loadTerrainIncremental. If allocation failed (OOM/fragmentation),
the chunk proceeded with a VK_NULL_HANDLE UBO, causing the GPU to read
from an invalid descriptor on the next draw call. Now checks the return
value and skips the chunk on failure.
2026-03-29 20:14:35 -07:00
Kelsi
c1f6364814 cleanup: remove misleading (void)flags — variable IS used for crit check
The cast falsely suggests flags is unused but it's read on the next
line for isCrit = (flags & 0x02). Also inlines the periodicLog discard.
2026-03-29 20:05:45 -07:00
Kelsi
568a14852d fix: WMO MODS parser raw memcpy without bounds check
The doodad set name read used raw memcpy(20 bytes) bypassing the safe
read<T> template that returns {} on OOB. A truncated WMO file would
read past the vector's storage. Added bounds check before the memcpy.
2026-03-29 20:05:37 -07:00
Kelsi
b5fba65277 fix: BLP loader OOB read on ARGB8888 and signed overflow on dimensions
ARGB8888 decompression read pixelCount*4 bytes from mipData without
checking that mipSize was large enough — a truncated BLP caused heap
OOB reads. Also, 'int pixelCount = width * height' overflowed for
large dimensions (signed int UB). Now validates dimensions <= 4096,
uses uint32_t arithmetic, and checks mipSize >= required for ARGB8888.
2026-03-29 20:05:29 -07:00
Kelsi
59bbeaca62 fix: ::toupper/::tolower UB on signed char at 5 remaining call sites
std::toupper(int) and std::tolower(int) have undefined behavior when
passed a negative value. These sites passed raw signed char without
casting to unsigned char first, unlike the rest of the codebase which
already uses the correct pattern. Affects auth (account names), world
packets, and mount sound path matching.
2026-03-29 19:58:36 -07:00
Kelsi
d776226fd1 fix: ADT parser OOB reads on sub-chunk headers and unterminated strings
1. MCNK sub-chunk bounds checks didn't account for the 8-byte header
   skip, so parseMCVT/parseMCNR could read up to 8 bytes past the
   validated buffer when sub-chunk headers are present (the common case).

2. parseMTEX/parseMMDX/parseMWMO used unbounded strlen on raw chunk
   data. A truncated file without a null terminator would read past the
   chunk boundary. Replaced with strnlen bounded by remaining size.

Also removes dead debug code: empty magic buffer copy, cathedral WMO
search, and Stormwind placement dump (which also had ::toupper UB).
2026-03-29 19:58:28 -07:00
Kelsi
f2237c5531 perf: switch 3 spawn queues from vector to deque
pendingPlayerSpawns_, deferredEquipmentQueue_, and
pendingGameObjectSpawns_ are consumed from the front via erase(begin()),
which is O(n) on vector (shifts all elements). With many spawns queued
(entering a city), this made the processing loop O(n²). deque supports
O(1) front erasure. pendingCreatureSpawns_ already used deque.
2026-03-29 19:51:26 -07:00
Kelsi
3b7ac068d2 perf: hoist key array read out of per-sequence loop in parseAnimTrackVanilla
readArray was called inside the loop on every iteration, re-parsing the
entire flat key array via memcpy. For a model with 200 sequences and
10k keys this produced ~24MB of redundant copying. Now reads once before
the loop (matching how allTimestamps was already handled).
2026-03-29 19:51:17 -07:00
Kelsi
c4d2b1709e fix: SMSG_RANDOM_ROLL parsed fields in wrong order — garbled /roll output
WotLK format is min(4)+max(4)+result(4)+guid(8)=20 bytes. The parser
read guid(8) first (treating min|max as a uint64), then targetGuid(8)
(non-existent field), then the actual values at wrong offsets. Every
/roll message showed garbled numbers and a bogus roller identity.

Also adds a hasRemaining guard for the 64 bytes of damage/armor/resist
fields in the item query parser — previously read past end with silent
zero-fill on truncated packets.
2026-03-29 19:51:07 -07:00
Kelsi
0ee57f4257 fix: FBlock comment said 'CImVector 4 bytes RGBA' but code reads C3Vector
The comment would lead a maintainer to "fix" the working code to read
4-byte RGBA instead of 3-float C3Vector. Updated to match the actual
M2 particle FBlock color format (3 floats, values 0-255, per WoWDev).
2026-03-29 19:43:57 -07:00
Kelsi
d731e0112e fix: std::tolower(char) UB on signed char at 3 call sites
std::tolower(int) has undefined behavior when passed a negative value,
which signed char produces for bytes > 127. The rest of the codebase
correctly casts to unsigned char first; these 3 sites were missed.
2026-03-29 19:43:46 -07:00
Kelsi
27b2322444 fix: chat mention highlight only covered first line of wrapped messages
The golden tint rect was drawn before rendering with a hardcoded single-
line height. Multi-line wrapped messages only had the first line
highlighted. Now drawn after EndGroup() using GetItemRectMin/Max so the
rect covers all wrapped lines.

Also fixes std::tolower(char) UB at two call sites — negative char
values (extended ASCII) are undefined behavior without unsigned cast.
2026-03-29 19:43:38 -07:00
Kelsi
ed63b029cd fix: getRemainingSize() underflowed when readPos exceeded data size
Both operands are size_t (unsigned), so if readPos > data.size() the
subtraction wrapped to ~0 instead of returning 0. This could happen
via setReadPos() which has no bounds check. Downstream hasRemaining()
was already safe but getRemainingSize() callers (e.g. hasFullPackedGuid)
would see billions of bytes available.
2026-03-29 19:36:41 -07:00
Kelsi
9da97e5e88 fix: partial send on non-blocking socket silently dropped data
A single send() that returned fewer bytes than requested was logged but
not retried, leaving the server with a truncated packet. This causes an
irreversible TCP framing desync (next header lands mid-payload) that
manifests as a disconnect under network pressure. Added a retry loop
that handles EWOULDBLOCK with a brief yield.

Also rejects payloads > 64KB instead of silently truncating the 16-bit
CMSG size field, which would have written a wrong header while still
appending all bytes.
2026-03-29 19:36:32 -07:00
Kelsi
e5b4e86600 fix: misleading indentation on BAG_UPDATE/UNIT_INVENTORY_CHANGED emits
The two emit calls were indented 12 spaces (suggesting a nested block)
instead of 8 (matching the enclosing if). Same class of maintenance
trap as the PLAYER_ALIVE/PLAYER_UNGHOST fix in b3abf04d.
2026-03-29 19:31:29 -07:00
Kelsi
061a21da8f fix: guild event string never appended; /leave left stale party state
1. Contradictory condition (!numStrings && numStrings >= 1) was always
   false, so unknown guild event messages never included the server's
   context string. Fixed to just numStrings >= 1.

2. leaveParty() only sent the packet without clearing partyData or
   firing addon events, so /leave left party frames visible until the
   server pushed an update. Now delegates to leaveGroup() which handles
   both the packet and local state cleanup.
2026-03-29 19:31:21 -07:00
Kelsi
3da3638790 cleanup: remove misleading (void)reasonType — variable IS used below
The cast falsely suggests reasonType is unused, but it's read on lines
3699-3702 for AFK/vote-kick differentiation. Same class of issue as
the (void)isPlayerTarget fix in commit 6731e584.
2026-03-29 19:23:05 -07:00
Kelsi
84c0ced228 fix: friend cache inserted empty key; ignore erase before server confirm
handleFriendStatus inserted into friendsCache with an empty playerName
when the name query hadn't resolved yet, creating a phantom "" entry.
Now guards with !playerName.empty().

removeIgnore erased from ignoreCache immediately without waiting for
server confirmation, desyncing the cache if the server rejected. Now
only clears the GUID set and lets the next SMSG_IGNORE_LIST rebuild
the cache, consistent with how removeFriend works.
2026-03-29 19:22:55 -07:00
Kelsi
731d9a88fb fix: SMSG_AUCTION_BIDDER_NOTIFICATION read itemEntry at wrong offset
The handler treated the second uint32 (auctionId) as itemEntry. The
real itemEntry is at byte 24 after auctionHouseId(4)+auctionId(4)+
bidderGuid(8)+bidAmount(4)+outbidAmount(4). Outbid chat messages always
referenced the wrong item.
2026-03-29 19:22:44 -07:00
Kelsi
5b9b8b59ba refactor: extract buildItemDef from 4 copy-pasted blocks in rebuildOnlineInventory
The same 25-line block copying ~20 fields from itemInfoCache_ into
ItemDef was duplicated for equipment, backpack, keyring, and bag slots.
Extracted into buildItemDef() so new fields only need adding once.
Net -100 lines.
2026-03-29 19:16:36 -07:00
Kelsi
e72cb4d380 fix: async creature upload budget blocked cache hits and failures
The per-tick GPU upload budget check ran before consuming async futures,
so after 1 upload ALL remaining ready results were deferred — including
permanent failures and cache hits that need zero GPU work. Moved the
budget gate after failure/cache-hit processing so only actual uploads
count. Re-queues over-budget results as pending spawns for next frame.
2026-03-29 19:16:27 -07:00
Kelsi
05cfcaacf6 fix: clearTarget fired PLAYER_TARGET_CHANGED before zeroing targetGuid
Callbacks and addons querying the current target during this event saw
the old (stale) target instead of null. setTarget correctly updates the
GUID before firing — clearTarget now does the same.
2026-03-29 19:16:18 -07:00
Kelsi
bf63d8e385 fix: static lastSpellCount shared across SpellHandler instances
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
The spellbook tab dirty check used a function-local static, meaning
switching to a character with the same spell count would skip the
rebuild and return the previous character's tabs. Changed to an
instance member so each SpellHandler tracks its own count.
2026-03-29 19:08:58 -07:00
Kelsi
e629898bfb fix: nameplate health bar division by zero when maxHealth is 0
Freshly spawned entities have maxHealth=0 before fields populate.
0/0 produces NaN which propagates through all geometry calculations
for that nameplate frame. Guard with a maxHealth>0 check.
2026-03-29 19:08:51 -07:00
Kelsi
7462fdd41f refactor: extract buildForceAck from 5 duplicated force-ACK blocks
All five force-ACK handlers (speed, root, flag, collision-height,
knockback) repeated the same ~25-line GUID+counter+movementInfo+coord-
conversion+send sequence. Extracted into buildForceAck() which returns
a ready-to-send packet with the movement payload already written.

This also fixes a transport coordinate conversion bug: the collision-
height handler was the only one that omitted the ONTRANSPORT check,
causing position desync when riding boats/zeppelins. buildForceAck
handles transport coords uniformly for all callers.

Net -80 lines.
2026-03-29 19:08:42 -07:00
Kelsi
5fcb30be1a cleanup: remove dead debug loop in buildCreatureDisplayLookups
Loop iterated 20 hair geoset lookups for Human Male but the if-body
was empty — the LOG statement that was presumably there was removed
but the loop skeleton was left behind.
2026-03-29 19:01:41 -07:00
Kelsi
a1252a56c9 fix: forceClearTaxiAndMovementState cleared unrelated death/resurrect state
A function for taxi/movement cleanup was resetting 10 death-related
fields (playerDead_, releasedSpirit_, resurrectPending_, etc.), which
could cancel a pending resurrection or mark a dead player as alive
when called during taxi dismount. Death state is owned by
entity_controller and resurrect packet handlers, not movement cleanup.
2026-03-29 19:01:34 -07:00
Kelsi
2e1f0f15ea fix: auction house refresh failed after browse-all (empty name search)
The auto-refresh after successful bid/buyout was gated on
lastAuctionSearch_.name.length() > 0, so a browse-all search (empty
name) would never refresh. Replaced with a hasAuctionSearch_ flag
that's set on any search regardless of the name filter.
2026-03-29 19:01:06 -07:00
Kelsi
a2340dd702 fix: level-up notification never fired — early return skipped it
The character-list level update loop used 'return' instead of 'break',
exiting the handler lambda before the level-up chat message, sound
effect, callback, and PLAYER_LEVEL_UP event could fire. Since the
player GUID is always in the character list, the notification code
was effectively dead — players never saw "You have reached level N!".
2026-03-29 19:00:54 -07:00
Kelsi
6731e5845a cleanup: remove misleading (void)isPlayerTarget cast
The variable is used earlier in the function for hostile attacker
tracking, so the (void) cast falsely suggests it was unused. Leftover
from a prior refactor.
2026-03-29 18:53:39 -07:00
Kelsi
961af04b36 fix: gossip banker sent CMSG_BANKER_ACTIVATE twice; deduplicate quest icons
Icon==6 and text=="GOSSIP_OPTION_BANKER" both sent BANKER_ACTIVATE
independently. Banking NPCs match both, so the packet was sent twice —
some servers toggle the bank window open then closed. Added sentBanker
guard so only one packet is sent.

Also extracts classifyGossipQuests() from two identical 30-line blocks
in handleGossipMessage and handleQuestgiverQuestList. The icon→status
mapping (5/6/10=completable, 3/4=incomplete, 2/7/8=available) is now
in one place with a why-comment explaining these are protocol-defined.
2026-03-29 18:53:30 -07:00
Kelsi
1a6960e3f9 fix: speed ACK sent before validation caused client/server desync
If the server sent a NaN or out-of-range speed, the client echoed it
back in the ACK (confirming it to the server) but then rejected it
locally. This left the server believing the client accepted the speed
while the client used the old value — a desync only fixable by relog.
Moved validation before the ACK so bad speeds are rejected outright.
2026-03-29 18:53:16 -07:00
Kelsi
3f37ffcea3 refactor: extract SpellHandler::resetAllState from selectCharacter
selectCharacter had 30+ if(spellHandler_) guards reaching into
SpellHandler internals (knownSpells_, spellCooldowns_, playerAuras_,
etc.) to clear per-character state. Consolidated into resetAllState()
so SpellHandler owns its own reset logic and new state fields don't
require editing GameHandler.
2026-03-29 18:46:42 -07:00
Kelsi
fc2c6bab40 fix: strict aliasing violation in handleQueryNextMailTime
reinterpret_cast<float*> on raw packet bytes is undefined behavior per
the C++ strict aliasing rule — compilers can optimize assuming uint8_t
and float never alias. Replaced with packet.readFloat() which uses
memcpy internally. Also switched to hasRemaining() for consistency.
2026-03-29 18:46:34 -07:00
Kelsi
2ae14d5d38 fix: RX silence 15s warning fired ~30 times per window
The 10s silence warning used a one-shot bool guard, but the 15s warning
used a 500ms time window — firing every frame (~30 times at 60fps).
Added rxSilence15sLogged_ guard consistent with the 10s pattern.
2026-03-29 18:46:25 -07:00
Kelsi
3712e6c5c1 fix: operator precedence broke stabled pet parsing — only first pet shown
!packet.hasRemaining(4) + 4 + 4 evaluated as (!hasRemaining(4))+8
due to ! binding tighter than +, making the check always truthy and
breaking out of the loop after the first pet. Hunters with multiple
stabled pets would see only one in the stable master UI.
2026-03-29 18:46:15 -07:00
Kelsi
f681de0a08 refactor: use guidToUnitId() instead of inline 4-way GUID comparison
handleSpellStart and handleSpellGo duplicated the player/target/focus/
pet GUID-to-unitId mapping that already exists in guidToUnitId(). If a
new unit-id category is added (e.g. mouseover), these inline copies
would not pick it up.
2026-03-29 18:39:52 -07:00
Kelsi
6f6571fc7a fix: pet opcodes shared unlearn handler despite incompatible formats
SMSG_PET_GUIDS, SMSG_PET_DISMISS_SOUND, and SMSG_PET_ACTION_SOUND were
registered with the same handler as SMSG_PET_UNLEARN_CONFIRM. Their
different formats (GUID lists, sound IDs with position) were misread as
unlearn cost, potentially triggering a bogus unlearn confirmation dialog.

Also extracts resetWardenState() from 13 lines duplicated verbatim
between connect() and disconnect().
2026-03-29 18:39:38 -07:00
Kelsi
bed859d8db fix: buyback used hardcoded WotLK opcode 0x290 bypassing wireOpcode()
Both buyBackItem() and the retry path in handleBuyFailed constructed
packets with a raw opcode constant instead of using the expansion-aware
BuybackItemPacket::build(). This would silently break if any expansion's
CMSG_BUYBACK_ITEM wire mapping diverges from 0x290.
2026-03-29 18:29:07 -07:00
Kelsi
78e2e4ac4d fix: locomotionFlags missing SWIMMING and DESCENDING
The heartbeat throttle bitmask was missing SWIMMING and DESCENDING,
treating swimming/descending players as stationary and using a slower
heartbeat interval. The identical bitmask in movement_handler.cpp
already included SWIMMING — this inconsistency could cause the server
to miss position updates during swim combat.
2026-03-29 18:28:58 -07:00
Kelsi
4e0e234ae9 fix: MSG_MOVE_START_DESCEND never set DESCENDING flag
Only ASCENDING was cleared — the DESCENDING flag was never toggled,
so outgoing movement packets during flight descent had incorrect flags.
Also clears DESCENDING on start-ascend and stop-ascend for symmetry.

Replaces static heartbeat log counter with member variable (was shared
across instances and not thread-safe) and demotes to LOG_DEBUG.
2026-03-29 18:28:49 -07:00
Kelsi
b0aa4445a0 fix: cast/cooldown/unit-cast timers ticked twice per frame
SpellHandler::updateTimers() (added in 209c2577) already ticks down
castTimeRemaining_, unitCastStates_, and spellCooldowns_. But the
GameHandler::update() loop also ticked them manually — causing casts to
complete at 2x speed and cooldowns to expire twice as fast.

Removed the duplicate tick-downs from update(). The GO interaction
completion check remains (client-timed casts need this fallback).
Also uses resetCastState() instead of manually clearing 4 fields,
adds missing castTimeTotal_ reset, and adds loadSpellNameCache()
to getSpellName/getSpellRank (every other DBC getter had it).
2026-03-29 18:21:03 -07:00
Kelsi
fc2526fc18 fix: env damage alias overwrote handler that preserved damage type
SMSG_ENVIRONMENTALDAMAGELOG (alias) registration at line 173 silently
overwrote the canonical SMSG_ENVIRONMENTAL_DAMAGE_LOG handler at line
108. The alias handler discarded envType (fall/lava/drowning), so the
UI couldn't differentiate environmental damage sources. Removed the
dead alias handler and its method; the canonical inline handler with
envType forwarding is now the sole registration.
2026-03-29 18:20:51 -07:00
Kelsi
0795430390 cleanup: remove dead pos=0 reassignment and demote chat logs to DEBUG
Quest log had a redundant pos=0 right after initialization. Chat handler
logged every incoming/outgoing message at WARNING level, flooding the
log and obscuring genuine warnings.
2026-03-29 18:11:49 -07:00
Kelsi
a30c7f4b1a fix: taxi recovery was dead code — flag cleared before check
taxiRecoverPending_ was unconditionally reset to false in the general
state cleanup, 39 lines before the recovery check that reads it. The
recovery block could never execute. Removed the premature clear so
mid-flight disconnect recovery can actually trigger.
2026-03-29 18:11:37 -07:00
Kelsi
35b952bc6f fix: SMSG_IGNORE_LIST read phantom string field after each GUID
The packet only contains uint8 count + count×uint64 GUIDs, but the
handler called readString() after each GUID. This consumed raw bytes of
subsequent GUIDs as a string, corrupting all entries after the first.
Now stores GUIDs in ignoreListGuids_ and resolves names asynchronously
via SMSG_NAME_QUERY_RESPONSE, matching the friends list pattern.

Also fixes unsafe static_pointer_cast in ready check (no type guard)
and removes redundant packetHasRemaining wrapper (duplicates Packet API).
2026-03-29 18:11:29 -07:00
Kelsi
0e814e9c4a refactor: replace 8 copy-pasted spline speed lambdas with factory
All spline speed opcodes share the same PackedGuid+float format,
differing only in which member receives the value. Replaced 8 identical
lambdas (~55 lines) with a makeSplineSpeedHandler factory that captures
a member pointer, cutting duplication and making it trivial to add new
speed types.
2026-03-29 17:59:51 -07:00
Kelsi
298974ebc2 refactor: extract markPlayerDead to deduplicate death/corpse caching
Both the health==0 and dynFlags UNIT_DYNFLAG_DEAD paths duplicated the
same corpse-position caching and death-state logic with a subtle
asymmetry (only health path called stopAutoAttack). Extracted into
markPlayerDead() so coordinate swapping and state changes happen in one
place. stopAutoAttack remains at the health==0 call site since the
dynFlags path doesn't need it.
2026-03-29 17:59:44 -07:00
Kelsi
dc500fede9 refactor: consolidate buildItemLink into game_utils.hpp
Three identical copies (game_handler.cpp, spell_handler.cpp,
quest_handler.cpp) plus two forward declarations (inventory_handler.cpp,
social_handler.cpp) replaced with a single inline definition in
game_utils.hpp. All affected files already include this header, so
quality color table changes now propagate from one source of truth.
2026-03-29 17:57:05 -07:00
Kelsi
0aff4b155c fix: dismount cleared all indefinite auras instead of just mount aura
The dismount path wiped every aura with maxDurationMs < 0, which
includes racial passives, tracking, and zone buffs — not just the mount
spell. Now only clears the specific mountAuraSpellId_ so the buff bar
stays accurate without waiting for a server aura resync.
2026-03-29 17:56:59 -07:00
Kelsi
8993b8329e fix: isReadableQuestText rejected all non-ASCII UTF-8 text
The range check (c > 0x7E) rejected UTF-8 multi-byte sequences, so quest
titles on localized servers (French, German, Russian, etc.) were treated
as unreadable binary and replaced with 'Quest #ID' placeholders. Now
allows bytes >= 0x80 while still requiring at least one ASCII letter to
distinguish real text from binary garbage.
2026-03-29 17:56:52 -07:00
Kelsi
b3abf04dbb fix: misleading indentation on PLAYER_ALIVE/PLAYER_UNGHOST event emits
The emit calls were indented at a level suggesting they were outside the
if/else blocks, but braces placed them inside. Fixed to match the actual
control flow, preventing a future maintainer from "correcting" the
indentation and accidentally changing the logic.
2026-03-29 17:52:56 -07:00
Kelsi
ec24bcd910 fix: Warrior Charge sent 3x SET_FACING by falling through to generic facing
Charge already computed facing and sent SET_FACING, but then fell through
to both the melee-ability facing block and the generic targeted-spell
facing block — sending up to 3 SET_FACING + 1 HEARTBEAT per cast. Added
facingHandled flag so only one block sends facing, reducing redundant
network traffic that could trigger server-side movement validation.
2026-03-29 17:52:51 -07:00
Kelsi
b9ecc26f50 fix: misplaced brace included book handlers inside LOOT_CLEAR_MONEY loop
The for-loop over {SMSG_LOOT_CLEAR_MONEY} was missing its closing brace,
so SMSG_READ_ITEM_OK and SMSG_READ_ITEM_FAILED registrations were inside
the loop body. Works by accident (single iteration) but fragile and
misleading — future additions to the loop would re-register book handlers.
2026-03-29 17:52:43 -07:00
Kelsi
020e016853 fix: quest reward items stuck as 'Item #ID' due to stale pending queries
Two fixes for item name resolution:

1. Clear entry from pendingItemQueries_ even when response parsing fails.
   Previously a malformed response left the entry stuck in pending forever,
   blocking all retries so the UI permanently showed "Item 12345".

2. Add 5-second periodic cleanup of pendingItemQueries_ so lost/dropped
   responses don't permanently block item info resolution.
2026-03-29 17:44:46 -07:00
Kelsi
51da88b120 fix: SMSG_ITEM_PUSH_RESULT read extra byte causing wrong item count
The handler read an extra uint8 (bag) after bagSlot, shifting all
subsequent fields by 1 byte. This caused count to straddle the count
and countInInventory fields — e.g. count=1 read as 0x03000000 (50M).

Also removes cast bar diagnostic overlay and demotes debug logs.
2026-03-29 17:30:44 -07:00
Kelsi
309fd11a7b fix: cast bar invisible due to stale ImGui saved window position
The cast bar window used ImGuiCond_FirstUseEver for positioning, so
ImGui's .ini state restored a stale off-screen position from a prior
session. Switch to ImGuiCond_Always and add NoSavedSettings flag so
the bar always renders centered near the bottom of the screen.

Also demotes remaining diagnostic logs to LOG_DEBUG.
2026-03-29 17:20:02 -07:00
Kelsi
209c257745 fix: wire SpellHandler::updateTimers and remove stale cast state members
SpellHandler::updateTimers() was never called after PR #23 extraction,
so cast bar timers, spell cooldowns, and unit cast state timers never
ticked. Also removes duplicate cast/queue/spell members left in
GameHandler that shadowed the SpellHandler versions, and fixes
MovementHandler writing to those stale members on world portal.

Demotes SMSG_SPELL_START/CAST_RESULT debug logs to LOG_DEBUG.
2026-03-29 16:49:17 -07:00
Kelsi
d32b35c583 fix: restore Classic aura flag normalization and clean up EntityController
- Restore 0x02→0x80 Classic harmful-to-WotLK debuff bit mapping in
  syncClassicAurasFromFields so downstream checks work across expansions
- Extract handleDisplayIdChange helper to deduplicate identical logic
  in onValuesUpdateUnit and onValuesUpdatePlayer
- Remove unused newItemCreated parameter from handleValuesUpdate
- Fix indentation on PLAYER_DEAD/PLAYER_ALIVE/PLAYER_UNGHOST emit calls
2026-03-29 16:29:56 -07:00
Kelsi Rae Davis
1988b53619
Merge pull request #29 from ldmonster/chore/entity-controller-refactoring
[chore] EntityController refactoring (SOLID decomposition)
2026-03-29 16:25:50 -07:00
Paul
b0a07c2472 refactor(game): apply SOLID phases 2-6 to EntityController
- split applyUpdateObjectBlock into handleCreateObject,
  handleValuesUpdate, handleMovementUpdate
-  extract concern helpers — createEntityFromBlock,
  applyPlayerTransportState, applyUnitFieldsOnCreate/OnUpdate,
  applyPlayerStatFields, dispatchEntitySpawn, trackItemOnCreate,
  updateItemOnValuesUpdate, syncClassicAurasFromFields,
  detectPlayerMountChange, updateNonPlayerTransportAttachment
- UnitFieldIndices, PlayerFieldIndices, UnitFieldUpdateResult
  structs with static resolve() — eliminate repeated fieldIndex() calls
- IObjectTypeHandler strategy interface; concrete handlers
  UnitTypeHandler, PlayerTypeHandler, GameObjectTypeHandler,
  ItemTypeHandler, CorpseTypeHandler registered in typeHandlers_ map;
  handleCreateObject and handleValuesUpdate now dispatch via
  getTypeHandler() — adding a new object type requires zero changes
  to existing handler methods
- PendingEvents member bus; all 27 inline owner_.fireAddonEvent()
  calls in the update path replaced with pendingEvents_.emit(); events
  flushed via flushPendingEvents() at the end of each handler, decoupling
  field-parse logic from the addon callback system

entity_controller.cpp: 1520-line monolith → longest method ~200 lines,
cyclomatic complexity ~180 → ~5; zero duplicated CREATE/VALUES blocks
2026-03-29 14:42:38 +03:00
Paul
f5757aca83 refactor(game): extract EntityController from GameHandler (step 1.3)
Moves entity lifecycle, name/creature/game-object caches, transport GUID
tracking, and the entire update-object pipeline out of GameHandler into a
new EntityController class (friend-class pattern, same as CombatHandler
et al.).

What moved:
- applyUpdateObjectBlock() — 1,520-line core of all entity creation,
  field updates, and movement application
- processOutOfRangeObjects() / finalizeUpdateObjectBatch()
- handleUpdateObject() / handleCompressedUpdateObject() / handleDestroyObject()
- handleNameQueryResponse() / handleCreatureQueryResponse()
- handleGameObjectQueryResponse() / handleGameObjectPageText()
- handlePageTextQueryResponse()
- enqueueUpdateObjectWork() / processPendingUpdateObjectWork()
- playerNameCache, playerClassRaceCache_, pendingNameQueries
- creatureInfoCache, pendingCreatureQueries
- gameObjectInfoCache_, pendingGameObjectQueries_
- transportGuids_, serverUpdatedTransportGuids_
- EntityManager (accessed by other handlers via getEntityManager())

8 opcodes re-registered by EntityController::registerOpcodes():
  SMSG_UPDATE_OBJECT, SMSG_COMPRESSED_UPDATE_OBJECT, SMSG_DESTROY_OBJECT,
  SMSG_NAME_QUERY_RESPONSE, SMSG_CREATURE_QUERY_RESPONSE,
  SMSG_GAMEOBJECT_QUERY_RESPONSE, SMSG_GAMEOBJECT_PAGETEXT,
  SMSG_PAGE_TEXT_QUERY_RESPONSE

Other handler files (combat, movement, social, spell, inventory, quest,
chat) updated to access EntityManager via getEntityManager() and the
name cache via getPlayerNameCache() — no logic changes.

Also included:
- .clang-tidy: add modernize-use-nodiscard,
  modernize-use-designated-initializers; set -std=c++20 in ExtraArgs
- test.sh: prepend clang's own resource include dir before GCC's to
  silence xmmintrin.h / ia32intrin.h conflicts during clang-tidy runs

Line counts:
  entity_controller.hpp  147 lines  (new)
  entity_controller.cpp  2172 lines (new)
  game_handler.cpp       8095 lines (was 10143, −2048)

Build: 0 errors, 0 warnings.
2026-03-29 08:21:27 +03:00
Kelsi
4f2a4e5520 debug: log SMSG_SPELL_START to diagnose missing cast bar
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
2026-03-28 17:00:24 -07:00
Kelsi
71cf3ab737 debug: log CMSG_CAST_SPELL packet size to verify format 2026-03-28 16:56:15 -07:00
Kelsi
d68bb5a831 fix: spell facing used atan2(dy,dx) but canonical convention is atan2(-dy,dx)
The canonical yaw convention (documented in coordinates.hpp) is
atan2(-dy, dx) where X=north, Y=west. North=0, East=+PI/2.

The spell facing code used atan2(dy, dx) (no negation on dy), producing
a yaw ~77° off from the correct server orientation. The server rejected
every cast with "unit not in front" because the sent orientation pointed
in the wrong direction.

Fixed in all 3 locations: charge facing, melee facing, and general
pre-cast facing.
2026-03-28 16:51:23 -07:00
Kelsi
7f3c7379b5 debug: log pre-cast facing orientation details 2026-03-28 16:45:57 -07:00
Kelsi
4ff59c6f76 debug: log castSpell calls and SMSG_CAST_RESULT at WARNING level 2026-03-28 16:41:15 -07:00
Kelsi
1aec1c6cf1 fix: send heartbeat after SET_FACING before cast to ensure server has orientation 2026-03-28 16:27:26 -07:00
Kelsi
9cb6c596d5 fix: face target before casting any targeted spell (not just melee)
Only melee abilities sent MSG_MOVE_SET_FACING before the cast packet.
Ranged spells like Smite used whatever orientation was in movementInfo
from the last movement, causing "target not in front" server rejection.

Now sends a facing update toward the target entity before ANY targeted
spell cast. The server checks a ~180° frontal arc for most spells.
2026-03-28 16:23:27 -07:00
Kelsi
c58537e2b8 fix: load binary DBCs from Data/db/ fallback path
CreatureDisplayInfo.dbc (691KB, 24K+ entries) exists at Data/db/ but
the loader only checked DBFilesClient\ (MPQ manifest) and expansion CSV.
The CSV had only 13248 entries (malformed export), so TBC+ creatures
(Mana Wyrms, Blood Elf area) had no display data and were invisible.

Now checks Data/db/ as a fallback for binary DBCs. This path contains
pre-extracted DBCs shared across expansions. Binary DBCs have complete
record data including proper IDs.
2026-03-28 16:17:59 -07:00
Kelsi
d8c768701d fix: visible item field base 284→283 (confirmed by raw field dump)
RAW FIELDS dump shows item entries at odd indices: 283, 285, 287, 289...
With base=283, stride=2: 17 of 19 slots have valid item IDs (14200,
12020, 14378, etc). Slots 12-13 (trinkets) correctly empty.

With base=284: only 5 entries, and values are enchant IDs (913, 905, 904)
— these are the field AFTER each entry, confirming base was off by 1.
2026-03-28 16:12:31 -07:00
Kelsi
fdca990209 debug: dump raw fields for first 3 players (lowered threshold to size>20) 2026-03-28 16:06:03 -07:00
Kelsi
f74b79f1f8 debug: log outgoing heartbeat coords and chat, fix BG filter for SAY type
Heartbeat: log canonical + wire coords every 30th heartbeat to detect
if we're sending wrong position (causing server to teleport us).

Chat: log outgoing messages at WARNING level to confirm packets are sent.

BG filter: announcer uses SAY (type=0) with color codes, not SYSTEM.
Match "BG Queue Announcer" in message body regardless of chat type.
2026-03-28 16:02:36 -07:00
Kelsi
91c6eef967 fix: suspend gravity for 10s after world entry to prevent WMO fall-through
Stormwind WMO collision takes 25+ seconds to fully load. The warmup
ground check couldn't detect the WMO floor because collision data
wasn't finalized yet. Player spawned and immediately fell through
the unloaded WMO floor into the terrain below (Dun Morogh).

New approach: suspendGravityFor(10s) after world entry. Gravity is
disabled (Z position frozen) until either:
1. A floor is detected by the collision system (gravity resumes instantly)
2. The 10-second timer expires (gravity resumes as fallback)

This handles the case where WMO collision loads during the first few
seconds of gameplay — the player hovers at spawn Z until the floor
appears, then lands normally.

Also fixes faction language for chat (ORCISH for Horde, COMMON for
Alliance) and adds SMSG_MESSAGECHAT diagnostic logging.
2026-03-28 15:50:13 -07:00
Kelsi
b1e2b8866d fix: use faction-correct language for outgoing chat (COMMON vs ORCISH)
Chat was always sent with COMMON (7) language. For Horde players,
AzerothCore rejects COMMON and silently drops the message. Alliance
players nearby also couldn't see Horde messages.

Now detects player race and sends ORCISH (1) for Horde races, COMMON (7)
for Alliance. This matches what the real WoW client sends.
2026-03-28 15:42:01 -07:00
Kelsi
2f96bda6fa fix: far same-map teleport blocked packet handler for 41 seconds
loadOnlineWorldTerrain() was called directly from the worldEntryCallback
inside the packet handler, running the 20s warmup loop synchronously.
This blocked ALL packet processing and froze the game for 20-41 seconds.

Now defers the world reload to pendingWorldEntry_ which is processed on
the next frame, outside the packet handler. Position and camera snap
immediately so the player doesn't drift at the old location.

The /y respawn report was actually a server-initiated teleport (possibly
anti-spam or area trigger) that hit this 41-second blocking path.
2026-03-28 15:38:44 -07:00
Kelsi
9666b871f8 fix: BG announcer filter was suppressing ALL chat messages
The filter matched ALL chat types for patterns like "[H:" + "A:" which
are common in normal messages. Any SAY/WHISPER/GUILD message containing
both substrings was silently dropped. This broke all incoming chat.

Now only filters SYSTEM messages and only matches specific BG announcer
keywords: "Queue status", "BG Queue", "BGAnnouncer".
2026-03-28 15:31:16 -07:00
Kelsi
6edcad421b fix: group invite popup never showing (hasPendingGroupInvite stale getter)
hasPendingGroupInvite() and getPendingInviterName() were inline getters
reading GameHandler's stale copies. SocialHandler owns the canonical
pendingGroupInvite/pendingInviterName state. Players were auto-added to
groups without seeing the accept/decline popup.

Now delegates to socialHandler_.
2026-03-28 15:29:19 -07:00
Kelsi
1af1c66b04 fix: SMSG_TRADE_STATUS_EXTENDED format (whichPlayer is uint32, add missing fields)
WotLK trade packet format was wrong in multiple ways:
- whichPlayer was read as uint8, actually uint32
- Missing tradeId field (we read tradeId as tradeCount)
- Per-slot size was 52 bytes, actually 64 (missing suffixFactor,
  randomPropertyId, lockId = 12 bytes)
- tradeCount is 8 (7 trade + 1 "will not be traded"), not capped at 7

Verified: header(4+4=8) + 8×(1+64=65) + gold(4) = 532 bytes matches
the observed packet size exactly.

Note: Classic trade format differs and will need its own parser.
2026-03-28 15:24:26 -07:00
Kelsi
df9dad952d fix: handle TRADE_STATUS_UNACCEPT (status=8), revert to Open state 2026-03-28 15:21:32 -07:00
Kelsi
ce54b196e7 fix: trade COMPLETE resets state before EXTENDED can populate items/gold
SMSG_TRADE_STATUS(COMPLETE) and SMSG_TRADE_STATUS_EXTENDED arrive in the
same packet batch. COMPLETE was calling resetTradeState() which cleared
all trade slots and gold BEFORE EXTENDED could write the final data.
The trade window showed "7c" (garbage gold) because the gold field read
from the wrong offset (slot size was also wrong: 60→52 bytes).

Now COMPLETE just sets status to None without full reset, preserving
trade state for EXTENDED to populate. The TRADE_CLOSED addon event
still fires correctly.
2026-03-28 15:18:33 -07:00
Kelsi
ed7cbccceb fix: trade slot size check 60→52 bytes, add trade diagnostic logging 2026-03-28 15:14:53 -07:00
Kelsi
615db79819 fix: skip all-zero equipment emit, broaden BG announcer filter
Equipment: the first emitOtherPlayerEquipment call fired before any item
queries returned, sending all-zero displayIds that stripped players naked.
Now skips the callback when resolved=0 (waiting for queries). Equipment
only applies once at least one item resolves, preventing the naked flash.

BG announcer: broadened filter to match ALL chat types (not just SYSTEM),
and added more patterns: "BGAnnouncer", "[H: N, A: N]" with spaces.

Also added diagnostic logging in setOnlinePlayerEquipment to trace
displayId counts reaching the renderer.
2026-03-28 15:09:52 -07:00
Kelsi
12f5aaf286 fix: filter BG queue announcer spam from system chat
ChromieCraft/AzerothCore BG queue announcer module floods chat with
SYSTEM messages like "Queue status for Alterac Valley [H: 12/40, A: 15/40]".
Now filtered by detecting common patterns: "Queue status", "BG Queue",
"Announcer]", and "[H:...A:..." format.

Equipment status: resolved items ARE rendering (head, shoulders, chest,
legs confirmed with displayIds). Remaining unresolved slots (weapons)
are item queries the server hasn't responded to yet — timing issue,
not a client bug. Items trickle in over ~5 seconds as queries return.
2026-03-28 15:01:25 -07:00
Kelsi
11571c582b fix: hearthstone from action bar, far teleport loading screen
Action bar hearthstone: the slot was type SPELL (spell 8690) not ITEM.
castSpell sends CMSG_CAST_SPELL which the server rejects for item-use
spells. Now detects item-use spells via getItemIdForSpell() and routes
through useItemById() instead, sending CMSG_USE_ITEM correctly.

Far same-map teleport: hearthstone on the same continent (e.g., Westfall
→ Stormwind on Azeroth) skipped the loading screen, so the player fell
through unloaded terrain. Now triggers a full world reload with loading
screen for teleports > 500 units, with the warmup ground check ensuring
WMO floors are loaded before spawning.
2026-03-28 14:55:58 -07:00
Kelsi
4e709692f1 fix: filter officer chat for non-officers (server sends to all guild members)
Some private servers (AzerothCore/ChromieCraft) send OFFICER chat type
to all guild members regardless of rank. The real WoW client checks the
GR_RIGHT_OFFCHATLISTEN (0x80) guild rank permission before displaying.

Now checks the player's guild rank rights from the roster data and
suppresses officer chat if the permission bit is not set.
2026-03-28 14:45:51 -07:00
Kelsi
21fb2aa11c fix: backpack window jumps position when selling items (missing ##id in title) 2026-03-28 14:42:21 -07:00
Kelsi
504d112625 fix: gossip/vendor windows not closing when opening mailbox/trainer/taxi
Domain handlers were setting `owner_.gossipWindowOpen = false` directly
on GameHandler's stale member, but isGossipWindowOpen() delegates to
QuestHandler's copy. The gossip window stayed open because the
delegating getter never saw the close.

Fix: use owner_.closeGossip() / owner_.closeVendor() which properly
delegate to QuestHandler/InventoryHandler to close the canonical state.

Affected: InventoryHandler (3 sites: mail, trainer, bank opening),
MovementHandler (1 site: taxi opening), QuestHandler (2 sites: gossip
opening closes vendor).
2026-03-28 14:36:14 -07:00
Kelsi
e5959dceb5 fix: add bare-points spline fallback for flying/falling splines (0x10000) 2026-03-28 12:47:37 -07:00
Kelsi
47bea0d233 fix: use delegating getters for vendor buyback refresh (stale member read) 2026-03-28 12:45:59 -07:00
Kelsi
b81c616785 fix: delegate gossip/quest detail getters to QuestHandler (NPC dialog broken)
4 more stale getters from PR #23 split:
- isGossipWindowOpen() — QuestHandler owns gossipWindowOpen_
- getCurrentGossip() — QuestHandler owns currentGossip_
- isQuestDetailsOpen() — QuestHandler owns questDetailsOpen_
- getQuestDetails() — QuestHandler owns currentQuestDetails_

Also fix GameHandler::update() distance-close checks to use delegating
getters instead of stale member variables for vendor/gossip/taxi/trainer.

Map state (currentMapId_, worldStateZoneId_, exploredZones_) confirmed
NOT stale — domain handlers write via owner_. reference to GameHandler's
members. Those getters are correct as-is.
2026-03-28 12:43:44 -07:00
Kelsi
ee02faa183 fix: delegate all 113 stale GameHandler getters to domain handlers
PR #23 split GameHandler into 8 domain handlers but left 113 inline
getters reading stale duplicate member variables. Every feature that
relied on these getters was silently broken (showing empty/stale data):

InventoryHandler (32): bank, mail, auction house, guild bank, trainer,
  loot rolls, vendor, buyback, item text, master loot candidates
SocialHandler (43): guild roster, battlegrounds, LFG, duels, petitions,
  arena teams, instance lockouts, ready check, who results, played time
SpellHandler (10): talents, craft queue, GCD, pet unlearn, queued spell
QuestHandler (13): quest log, gossip POIs, quest offer/request windows,
  tracked quests, shared quests, NPC quest statuses
MovementHandler (15): all 8 server speeds, taxi state, taxi nodes/data

All converted from inline `{ return member_; }` to out-of-line
delegations: `return handler_ ? handler_->getter() : fallback;`
2026-03-28 12:18:14 -07:00
Kelsi
d6b387ae35 fix: increase tile-count fallback from 10s to 20s to prevent premature spawn 2026-03-28 12:04:54 -07:00
Kelsi
2633a490eb fix: remove reinterpret_cast UB in trade slot delegation
The TradeSlot structs differ between GameHandler (has bag/slot fields)
and InventoryHandler (no bag/slot). The reinterpret_cast was undefined
behavior that corrupted memory, potentially causing the teleport bug.

Now properly copies fields between the two struct layouts.

NOTE: 113 stale getters remain in GameHandler that read duplicate member
variables never updated by domain handlers. These need systematic fixing.
2026-03-28 12:02:08 -07:00
Kelsi
f37994cc1b fix: trade accept dialog not showing (stale state from domain handler split)
GameHandler::hasPendingTradeRequest() and all trade getters were reading
GameHandler's own tradeStatus_/tradeSlots_ which are never written after
the PR #23 split. InventoryHandler owns the canonical trade state.

Delegate all trade getters to InventoryHandler:
- getTradeStatus, hasPendingTradeRequest, isTradeOpen, getTradePeerName
- getMyTradeSlots, getPeerTradeSlots, getMyTradeGold, getPeerTradeGold

Also fix InventoryHandler::isTradeOpen() to include Accepted state.
2026-03-28 11:58:02 -07:00
Kelsi
99ac31987f fix: add missing <algorithm> include for std::clamp (Windows build) 2026-03-28 11:51:34 -07:00
Kelsi
b9ac3de498 tweak: camera defaults stiffness=30, pivot height=1.6 2026-03-28 11:45:21 -07:00
Kelsi
416e091498 feat: add Camera Stiffness and Pivot Height settings for motion comfort
Camera Stiffness (default 20, range 5-100): controls how tightly the
camera follows the player. Higher values = less sway/lag. Users who
experience motion sickness can increase this to reduce floaty camera.

Camera Pivot Height (default 1.8, range 0-3): height of the camera
orbit point above the player's feet. Lower values reduce the
"detached/floating" feel that can cause nausea. Setting to 0 puts the
pivot at foot level (ground-locked camera).

Both settings saved to settings file and applied via sliders in the
Gameplay tab of the Settings window.
2026-03-28 11:39:37 -07:00
Kelsi
5a8ab87a78 fix: warmup checks WMO floor proximity, not just terrain existence
Stormwind players stand on WMO floors ~95m above terrain. The previous
check only tested if terrain existed at the spawn XY (it did — far below).
Now checks WMO floor first, then terrain, requiring the ground to be within
15 units of spawn Z. Falls back to tile count after 10s.

Also adds diagnostic logging for useItemBySlot (hearthstone debug).
2026-03-28 11:34:07 -07:00
Kelsi
8aaa2e7ff3 fix: warmup checks WMO floor + terrain + tile count before spawning
Stormwind players stand on WMO floors, not terrain. The terrain-only
check passed immediately (terrain exists below the city) but the WMO
floor hadn't loaded yet, so the player fell through.

Now checks three ground sources in order:
1. Terrain height at spawn point
2. WMO floor height at spawn point (for cities/buildings)
3. After 8s, accepts if 4+ terrain tiles are loaded (fallback)

Won't exit warmup until at least one ground source returns valid height,
or the 25s hard cap is reached.
2026-03-28 11:23:18 -07:00
Kelsi
7b26938e45 fix: warmup terrain check uses server spawn coords, not character position
The terrain readiness check was using getCharacterPosition() which is
(0,0,0) during warmup — always returned a valid height and exited
immediately, causing the player to spawn before terrain loaded.

Now uses the server-provided spawn coordinates (x,y,z from world entry)
converted to render coords for the terrain query. Also logs when terrain
isn't ready after 5 seconds to show warmup progress.

Player spawn callbacks and equipment re-emit chain confirmed working.
2026-03-28 11:18:36 -07:00
Kelsi
ada95756ce fix: don't exit warmup until terrain under player is loaded
Added terrain readiness check to the warmup exit condition: the loading
screen won't drop until getHeightAt(playerPos) returns a valid height,
ensuring the ground exists under the player's feet before spawning.

Also increased warmup hard cap from 15s to 25s to give terrain more time
to load in cities like Stormwind with dense WMO/M2 assets.

Equipment re-emit chain confirmed working: items resolve 3-4 seconds
after spawn and equipment is re-applied with valid displayIds.
2026-03-28 11:09:36 -07:00
Kelsi
15f6aaadb2 fix: revert stride to 2 (correct for WotLK visible items), add re-emit tracing
Stride 4 was wrong — the raw dump shows entries at 284, 288, 292 which
are slots 0, 2, 4 with stride 2 (slot 1=NECK is zero because necks are
invisible). Stride 2 with base 284 correctly maps 19 equipment slots.

Added WARNING-level log when item query responses trigger equipment
re-emit for other players, to confirm the re-emit chain works.

The falling-through-world issue is likely terrain chunks not loading
fast enough — the terrain streaming stalls are still present.
2026-03-28 11:07:17 -07:00
Kelsi
05ab9922c4 fix: visible item stride 2→4 (confirmed from raw field dump)
RAW FIELDS dump shows equipment entries at indices 284, 288, 292, 296, 300
— stride 4, not 2. Each visible item slot occupies 4 fields (entry +
enchant + 2 padding), not 2 as previously assumed.

Field dump evidence:
  [284]=3817(Reinforced Buckler) [288]=3808(Double Mail Boots)
  [292]=3252 [296]=3823 [300]=3845 [312]=3825 [314]=3827

With stride 2, slots 0-18 read indices 284,286,288,290... which interleaves
entries with enchant/padding values, producing mostly zeros for equipment.
With stride 4, slots correctly map to entry-only fields.
2026-03-28 11:04:29 -07:00
Kelsi
f70beba07c fix: visible item field base 408→286 (computed from INV_SLOT_HEAD=324)
PLAYER_VISIBLE_ITEM_1_ENTRYID = PLAYER_FIELD_INV_SLOT_HEAD(324) - 19*2
= 286. The previous value of 408 landed far past inventory slots in
string/name data, producing garbage entry IDs (ASCII fragments like
"mant", "alk ", "ryan") that the server rejected as invalid items.

Derivation: 19 visible item slots × 2 fields (entry + enchant) = 38
fields immediately before PLAYER_FIELD_INV_SLOT_HEAD at index 324.
2026-03-28 10:58:34 -07:00
Kelsi
37300d65ce fix: remove Classic spline fallback, add no-parabolic WotLK variant
The Classic fallback silently succeeded on WotLK data by false-positive
matching, consuming wrong bytes and producing corrupt entity data that
was silently dropped — resulting in zero other players/NPCs visible.

Now tries 4 WotLK-only variants in order:
1. Full WotLK (durationMod+durationModNext+vertAccel+effectStart+compressed)
2. Full WotLK uncompressed
3. WotLK without parabolic fields (durationMod+durationModNext+points)
4. WotLK without parabolic, compressed

This covers servers that don't unconditionally send vertAccel+effectStart
(the MEMORY.md says AzerothCore does, but other cores may not).
2026-03-28 10:55:46 -07:00
Kelsi
559f100204 fix: restore Classic spline fallback to prevent UPDATE_OBJECT packet loss
The previous fix (b8a9efb7) that returned false on spline failure was too
aggressive — it aborted the ENTIRE UPDATE_OBJECT packet, not just one
block. Since many entity spawns (NPCs, other players) share the same
packet, a single spline parse failure killed ALL entities in the batch.

Restored the Classic-format fallback as a last resort after WotLK format
fails. The key difference from the original bug is that WotLK is now
tried FIRST (with proper position save/restore), and Classic only fires
if WotLK fails. This prevents the false-positive match that originally
caused corruption while still handling edge-case spline formats.
2026-03-28 10:52:26 -07:00
Kelsi
f4a2a631ab fix: visible item field base 284→408 (was reading quest log, not equipment)
PLAYER_VISIBLE_ITEM_1_ENTRYID for WotLK 3.3.5a is at UNIT_END(148) + 260
= field index 408 with stride 2. The previous default of 284 (UNIT_END+136)
was in the quest log field range, causing item IDs like "Lesser Invisibility
Potion" and "Deathstalker Report" to be read as equipment entries.

This was the root cause of other players appearing naked — item queries
returned valid responses but for the WRONG items (quest log entries instead
of equipment), so displayInfoIds were consumable/quest item appearances.

The heuristic auto-detection still overrides for Classic/TBC (different
stride per expansion), so this only affects the WotLK default before
detection runs.

Also filter addon whispers (GearScore GS_*, DBM, oRA, BigWigs, tab-prefixed)
from chat display — these are invisible in the real WoW client.
2026-03-28 10:49:00 -07:00
Kelsi
1bcb05aac4 fix: only show fishing message for player's own bobber, not others'
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
SMSG_GAMEOBJECT_CUSTOM_ANIM with animId=0 on a fishing node (type 17)
was triggering "A fish is on your line!" for ALL fishing bobbers in
range, including other players'. Now checks OBJECT_FIELD_CREATED_BY
(fields 6-7) matches the local player GUID before showing the message.
2026-03-28 10:35:53 -07:00
Kelsi
b8a9efb721 fix: abort movement block on spline parse failure instead of corrupting stream
When both WotLK compressed and uncompressed spline point parsing fail,
the parser was silently continuing with a corrupted read position (16
bytes of WotLK spline header already consumed). This caused the update
mask to read garbage (maskBlockCount=40), corrupting the current entity
AND all remaining blocks in the same UPDATE_OBJECT packet.

Now returns false on spline failure, cleanly aborting the single block
parse and allowing the remaining blocks to be recovered (if the parser
can resync). Also logs the failing GUID and spline flags for debugging.

This fixes:
- Entities spawning with displayId=0/entry=0 (corrupted parse)
- "Unknown update type: 128" errors from reading garbage
- Falling through the ground (terrain entities lost in corrupted batch)
- Phantom "fish on your line" from fishing bobber entity parse failure
2026-03-28 10:31:53 -07:00
Kelsi
ed8ff5c8ac fix: increase packet parse/callback budgets to fix Warden module stall
Warden module download (18756 bytes, 38 chunks of 500 bytes) stalled at
32 chunks because the per-pump packet parse budget was 16 — after two
2ms pump cycles (32 packets), the TCP receive buffer filled and the
server stopped sending. Character list never arrived.

- kDefaultMaxParsedPacketsPerUpdate: 16 → 64
- kDefaultMaxPacketCallbacksPerUpdate: 6 → 48

Also adds WARNING-level diagnostic logs for auth pipeline packets and
Warden module download progress (previously DEBUG-only, invisible in
production logs).
2026-03-28 10:28:20 -07:00
Kelsi Rae Davis
6a46e573bb
Merge pull request #23 from ldmonster/chore/split-game-handler
[chore] GameHandler: extract 8 domain handler classes
2026-03-28 10:10:16 -07:00
Paul
285ebc88dd add linter 2026-03-28 11:45:09 +03:00
Paul
888a78d775 fixin critical bugs, non critical bugs, sendmail implementation 2026-03-28 11:35:10 +03:00
Paul
b2710258dc refactor(game): split GameHandler into domain handlers
Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:

- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
               defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
                    read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module

Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.

game_handler.cpp reduced from ~10,188 to ~9,432 lines.

Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
2026-03-28 09:42:37 +03:00
Kelsi
3762dceaa6 docs: add CONTRIBUTING.md and CHANGELOG.md; optimize chat parser allocation
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
CONTRIBUTING.md: code style, PR process, architecture pointers, packet
handler pattern, key files for new contributors.

CHANGELOG.md: grouped changes since v1.8.1-preview into Performance,
Bug Fixes, Features, Security, and Code Quality sections.

Chat parser: use stack-allocated std::array<char, 256> for typical chat
messages instead of heap-allocated std::string. Only falls back to heap
for messages > 256 bytes. Reduces allocator pressure on high-frequency
chat packet handling.
2026-03-27 18:47:35 -07:00
Kelsi
e2383725f0 security: path traversal rejection, packet length validation; code quality
Security:
- Asset loader rejects paths containing ".." sequences (path traversal)
- Chat message parser validates length against remaining packet bytes
  before resize(), preventing memory exhaustion from malformed packets

Code quality:
- Extract 11 named geoset constants (kGeosetBareForearms, kGeosetWithCape,
  etc.) replacing ~40 magic number sites across 4 code paths
- Add build-debug/ and .claude/ to .gitignore
- Remove .claude/scheduled_tasks.lock from tracking
2026-03-27 18:42:48 -07:00
Kelsi
e61b23626a perf: entity/skill/DBC/warden maps to unordered_map; fix 3x contacts scan
Entity storage: std::map<uint64_t, shared_ptr<Entity>> → unordered_map for
O(1) entity lookups instead of O(log n). No code depends on GUID ordering.

Player skills: std::map<uint32_t, PlayerSkill> → unordered_map.
DBC ID cache: std::map<uint32_t, uint32_t> → unordered_map.
Warden: apiHandlers_ and allocations_ → unordered_map (freeBlocks_ kept
as std::map since its coalescing logic requires ordered iteration).

Contacts: handleFriendStatus() did 3 separate O(n) find_if scans per
packet. Consolidated to single find_if with iterator reuse. O(3n) → O(n).
2026-03-27 18:28:36 -07:00
Kelsi
2af3594ce8 perf: eliminate per-frame heap allocs in M2 renderer; UI polish and report
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
M2 renderer: move 3 per-frame local containers to member variables:
- particleGroups_ (unordered_map): reuse bucket structure across frames
- ribbonDraws_ (vector): reuse draw call buffer
- shadowTexSetCache_ (unordered_map): reuse descriptor cache
Eliminates ~3 heap allocations per frame in particle/ribbon/shadow passes.

UI polish:
- Nameplate hover tooltip showing level, class (players), guild name
- Bag window titles show slot counts: "Backpack (12/16)"

Player report: CMSG_COMPLAIN packet builder and reportPlayer() method.
"Report Player" option in target frame right-click menu for other players.
Server response handler (SMSG_COMPLAIN_RESULT) was already implemented.
2026-03-27 18:21:47 -07:00
Kelsi
dee90d2951 fix: NPC/player attack animation uses weapon-appropriate anim ID
NPC and other-player melee swing callback was hardcoded to animation 16
(unarmed attack). Now tries 17 (1H weapon), 18 (2H weapon) first with
hasAnimation() check, falling back to 16 if neither exists on the model.
2026-03-27 18:14:29 -07:00
Kelsi
dce11a0d3f perf: skip bone animation for LOD3 models, frustum-cull water surfaces
M2 renderer: skip bone matrix computation for instances beyond 150 units
(LOD 3 threshold). These models use minimal static geometry with no visible
skeletal animation. Last-computed bone matrices are retained for GPU upload.
Removes unnecessary float matrix operations for hundreds of distant NPCs
in crowded zones.

Water renderer: add per-surface AABB frustum culling before draw calls.
Computes tight AABB from surface corners and height range, tests against
camera frustum. Skips descriptor binding and vkCmdDrawIndexed for surfaces
outside the view. Handles both ADT and WMO water (rotated step vectors).
2026-03-27 18:11:20 -07:00
Kelsi
cccd52b32f fix: equipment visibility (remove layout verification gate), follow uses run speed
Equipment: removed the visibleItemLayoutVerified_ gate from
updateOtherPlayerVisibleItems(). The default WotLK field layout (base=284,
stride=2) is correct and should be used immediately. The verification
heuristic was silently blocking ALL other-player equipment rendering by
queuing for auto-inspect (which doesn't return items in WotLK anyway).

Follow: auto-follow now uses run speed (autoRunning) instead of walk speed.
Also uses squared distance for the distance checks.

Commands: /quit, /exit aliases for /logout; /difficulty normal/heroic/25/25heroic
sends CMSG_CHANGEPLAYER_DIFFICULTY.
2026-03-27 18:05:42 -07:00
Kelsi
b366773f29 fix: inspect (packed GUID), follow (client-side auto-walk); add loot/raid commands
Inspect: CMSG_INSPECT was writing full uint64 GUID instead of packed GUID.
Server silently rejected the malformed packet. Fixed both InspectPacket and
QueryInspectAchievementsPacket to use writePackedGuid().

Follow: was a no-op (only stored GUID). Added client-side auto-follow system:
camera controller walks toward followed entity, faces target, cancels on
WASD/mouse input, stops within 3 units, cancels at 40+ units distance.

Party commands:
- /lootmethod (ffa/roundrobin/master/group/nbg) sends CMSG_LOOT_METHOD
- /lootthreshold (0-5 or quality name) sets minimum loot quality
- /raidconvert converts party to raid (leader only)

Equipment diagnostic logging still active for debugging naked players.
2026-03-27 17:54:56 -07:00
Kelsi
16fc3ebfdf feat: target frame right-click context menu; add equipment diagnostic logging
Target frame: add Follow, Clear Target, and Set Raid Mark submenu to the
right-click context menu (Inspect, Trade, Duel were already present).

Equipment diagnostics: add LOG_INFO traces to updateOtherPlayerVisibleItems()
and emitOtherPlayerEquipment() to debug why other players appear naked.
Logs the visible item entry IDs received from the server and the resolved
displayIds from itemInfoCache. Check the log for "emitOtherPlayerEquipment"
to see if entries arrive as zeros (server not sending fields) or if
displayIds are zero (item templates not cached yet).
2026-03-27 17:41:37 -07:00
Kelsi
a20f46f0b6 feat: render shoulder armor M2 models on other players and NPCs
Shoulder pieces are M2 model attachments (like helmets), not body geosets.
Load left shoulder at attachment point 5, right shoulder at point 6.
Models resolved from ItemDisplayInfo.dbc LeftModel/RightModel fields,
with race/gender suffix variants tried first. Applied to both online
player and NPC equipment paths.
2026-03-27 17:35:42 -07:00
Kelsi
0396a42beb feat: render equipment on other players (helmets, weapons, belts, wrists)
Other players previously appeared partially naked — only chest, legs, feet,
hands, cape, and tabard rendered. Now renders full equipment:

- Helmet M2 model: loads from ItemDisplayInfo.dbc with race/gender suffix,
  attaches at head bone (point 0/11), hides hair geoset under helm
- Weapons: mainhand (attachment 1) and offhand (attachment 2) M2 models
  loaded from ItemDisplayInfo, with Weapon/Shield path fallback
- Wrist/bracer geoset (group 8): applies when no chest sleeve overrides
- Belt/waist geoset (group 18): reads GeosetGroup1 from ItemDisplayInfo
- Shoulder M2 attachments deferred (separate bone attachment system)

Also applied same wrist/waist geosets to NPC and character preview paths.

Minimap: batch 9 individual vkUpdateDescriptorSets into single call.
2026-03-27 17:30:35 -07:00
Kelsi
50a3eb7f07 fix: mail money uint64, other-player cape textures, zone toast dedup, TCP_NODELAY
Mail: change money/COD fields from uint32 to uint64 in CMSG_SEND_MAIL and
SMSG_MAIL_LIST_RESULT for WotLK 3.3.5a. Classic keeps uint32 on the wire.
Fixes money truncation and packet misalignment causing mail failures.

Other-player capes: add cape texture loading to setOnlinePlayerEquipment().
The cape geoset was enabled but no texture was loaded, leaving capes blank.
Now mirrors the local-player path: looks up ItemDisplayInfo.dbc, finds cape
texture candidates, applies via setGroupTextureOverride/setTextureSlotOverride.

Zone toasts: suppress duplicate zone toast when the zone text overlay is
already showing the same zone name. Fixes double "Entering: Stormwind City".

Network: enable TCP_NODELAY on both auth and world sockets after connect(),
disabling Nagle's algorithm to eliminate up to 200ms buffering delay on
small packets (movement, spell casts, chat).

Rendering: track material and bone descriptor sets in M2 renderer to skip
redundant vkCmdBindDescriptorSets calls between batches sharing same textures.
2026-03-27 17:20:31 -07:00
Kelsi
6b1c728377 perf: eliminate double map lookups, dynamic_cast in render loops, div by 255
- Replace count()+operator[] double lookups with find() or try_emplace()
  in gameObjectInstances_, playerTextureSlotsByModelId_, onlinePlayerAppearance_
- Add Entity::isUnit() helper; replace 5 dynamic_cast<Unit*> in per-frame
  UI rendering (nameplates, combat text, pet frame) with isUnit()+static_cast
- Add constexpr kInv255 reciprocal for per-pixel normal map generation loops
  in character_renderer and wmo_renderer
2026-03-27 17:04:13 -07:00
Kelsi
6f2c8962e5 fix: use expansion context for spline parsing; preload DBC caches at world entry
Spline parsing: remove Classic format fallback from the WotLK parser. The
PacketParsers hierarchy already dispatches to expansion-specific parsers
(Classic/TBC/WotLK/Turtle), so the WotLK parseMovementBlock should only
attempt WotLK spline format. The Classic fallback could false-positive when
durationMod bytes resembled a valid point count, corrupting downstream parsing.

Preload DBC caches: call loadSpellNameCache() and 5 other lazy DBC caches
during handleLoginVerifyWorld() on initial world entry. This moves the ~170ms
Spell.csv load from the first SMSG_SPELL_GO handler to the loading screen,
eliminating the mid-gameplay stall.

WMO portal culling: move per-instance portalVisibleGroups vector and
portalVisibleGroupSet to reusable member variables, eliminating heap
allocations per WMO instance per frame.
2026-03-27 16:58:39 -07:00
Kelsi
a795239e77 fix: spline parse order (WotLK-first) fixes missing NPCs; bound WMO liquid loading
Spline auto-detection: try WotLK format before Classic to prevent false-positive
matches where durationMod float bytes resemble a valid Classic pointCount. This
caused the movement block to consume wrong byte count, corrupting the update mask
read (maskBlockCount=57/129/203 instead of ~5) and silently dropping NPC spawns.

Terrain latency: bound WMO liquid group loading to 4 groups per advanceFinalization
call. Large WMOs (e.g., Stormwind canals with 40+ liquid groups) previously loaded
all groups in one unbounded loop, blowing past the 8ms frame budget and causing
stalls up to 1300ms. Now yields back to processReadyTiles() after 4 groups so the
time budget check can break out.
2026-03-27 16:51:13 -07:00
Kelsi
d26eed1e7c perf: constexpr reciprocals, cache redundant lookups, consolidate texture maps
- Hoist DBC field index lookups before loops in game_handler (7 DBC iteration loops)
- Cache getSkybox()/getPosition() calls instead of redundant per-frame queries
- Merge textureHasAlphaByPtr_ + textureColorKeyBlackByPtr_ into single map
- Add constexpr for DEG_TO_RAD, reciprocal constants, physics delta
- Add reserve() for WMO/M2 collision grid queries and portal BFS
- Frustum plane normalize: inversesqrt instead of length+divide
- M2 particle emission: inversesqrt for direction normalization
- Parse creature display IDs from query response
- UI: show spell names/IDs as fallback instead of "Unknown"
2026-03-27 16:47:30 -07:00
Kelsi
b0466e9029 perf: eliminate ~70 unnecessary sqrt ops per frame, optimize caches and threading
Squared distance optimizations across 30 files:
- Convert glm::length() comparisons to glm::dot() (no sqrt)
- Use glm::inversesqrt() for check-then-normalize patterns (1 rsqrt vs 2 sqrt)
- Defer sqrt to after early-out checks in collision/movement code
- Hottest paths: camera_controller (21), weather particles, WMO collision,
  transport movement, creature interpolation, nameplate culling

Container and algorithm improvements:
- std::map<string> → std::unordered_map for asset/DBC/MPQ/warden caches
- std::mutex → std::shared_mutex for asset_manager and mpq_manager caches
- std::sort → std::partial_sort in lighting_manager (top-2 of N volumes)
- Double-lookup find()+operator[] → insert_or_assign in game_handler
- Add reserve() for per-frame vectors: weather, swim_effects, WMO/M2 collision

Threading and synchronization:
- Replace 1ms busy-wait polling with condition_variable in character_renderer
- Move timestamp capture before mutex in logger
- Use memory_order_acquire/release for normal map completion signaling

API additions:
- DBC getStringView()/getStringViewByOffset() for zero-copy string access
- Parse creature display IDs from SMSG_CREATURE_QUERY_SINGLE_RESPONSE
2026-03-27 16:33:16 -07:00
Kelsi
cf0e2aa240 refactor: deduplicate pomSampleTable in wmo_renderer, last static const array
Move duplicate pomSampleTable (2 copies → 1 constexpr) to file-scope
anonymous namespace. All static const primitive arrays outside src/game/
are now constexpr.
2026-03-27 15:34:48 -07:00
Kelsi
c762688202 refactor: promote static const arrays to constexpr across audio/core/rendering
audio: birdPaths, cricketPaths, races
core/application: componentDirs (4 instances), compDirs
rendering/character_preview: componentDirs
rendering/character_renderer: regionCoords256, regionSizes256
2026-03-27 15:31:21 -07:00
Kelsi
d6769172d1 refactor: remove remaining shadowed arrays in lua_engine, constexpr batch
Remove 4 more local arrays that shadowed the file-scope constexpr
constants added in the previous commit (kLuaClasses×2, kLuaRaces×1,
kCls×1, kQualityHex×1).

Promote 7 remaining static const char* arrays to constexpr
(kFamilies, kItemClasses, kInvTypes, kTypes, kDiff, kIcons, kClassFiles).
2026-03-27 15:27:47 -07:00
Kelsi
ad209b81bd fix: check lua_pcall return in ACTIONBAR_PAGE_CHANGED; deduplicate 17 arrays
Fix unchecked lua_pcall that leaked an error message onto the Lua stack
when an ACTIONBAR_PAGE_CHANGED handler errored.

Move 17 duplicated static arrays to file-scope constexpr constants:
- kLuaClasses (5 copies → 1), kLuaRaces (3 → 1), kLuaPowerNames (2 → 1)
- kQualHexNoAlpha (5 → 1), kQualHexAlpha (2 → 1)
2026-03-27 15:24:19 -07:00
Kelsi
e805eae33c refactor: add [[nodiscard]] to shader/asset load functions, suppress warnings
Add [[nodiscard]] to VkShaderModule::loadFromFile, Shader::loadFromFile/
loadFromSource, AssetManifest::load, DbcLoader::load — all return bool
indicating success/failure that callers should check.

Suppress with (void) at 17 call sites where validity is checked via
isValid() after loading rather than the return value (m2_renderer
recreatePipelines, swim_effects recreatePipelines).
2026-03-27 15:17:19 -07:00
Kelsi
b5b84fbc19 fix: guard texture log dedup sets with mutex for thread safety
loadTexture() is called from terrain worker threads, but the static
unordered_set dedup caches for missing-texture and decode-failure
warnings had no synchronization. Add std::mutex guards around both
log-dedup blocks to prevent data races.
2026-03-27 15:12:36 -07:00
Kelsi
fb3bfe42c9 refactor: add kCastGreen/kQueueGreen constants, remove dead code
Add kCastGreen (interruptible cast bar, 5 uses) and kQueueGreen
(queue status / talent met, 7 uses across game_screen + talent_screen).

Remove commented-out renderQuestMarkers call (replaced by 3D billboards).
2026-03-27 15:01:12 -07:00
Kelsi
53a4377ed7 refactor: extract magic numbers in terrain alpha map and texture compositing
terrain_manager: replace bare 4096/2048/0x80/0x7F with named constants
ALPHA_MAP_SIZE, ALPHA_MAP_PACKED, ALPHA_FILL_FLAG, ALPHA_COUNT_MASK
— documents the WoW alpha map RLE format.

character_renderer: replace bare 256/512 texture sizes with
kBaseTexSize/kUpscaleTexSize for NPC skin upscaling logic.
2026-03-27 14:57:20 -07:00
Kelsi
5b91ef398e fix: return UINT32_MAX from findMemType on failure, add [[nodiscard]]
The findMemType/findMemoryType helper in auth_screen, loading_screen,
and vk_context returned 0 on failure — a valid memory type index.
Changed to return UINT32_MAX and log an error, so vkAllocateMemory
receives an invalid index and fails cleanly rather than silently
using the wrong memory type.

Add [[nodiscard]] to VkBuffer::uploadToGPU/createMapped and
VkContext::initialize/recreateSwapchain so callers that ignore
failure are flagged at compile time. Suppress with (void) cast at
3 call sites where failure is non-actionable (resize best-effort).
2026-03-27 14:53:29 -07:00
Kelsi
7028dd64c1 refactor: promote remaining static const arrays to constexpr across UI
game_screen: fsrScales, fsrScaleFactors, kTotemInfo, kRaidMarks,
kTimerInfo, kNPMarks, kCellMarks, kPartyMarks, kMMMarks, kCatOrder
keybinding_manager: actionMap

All static const arrays in UI files are now constexpr where possible.
2026-03-27 14:47:58 -07:00
Kelsi
d2430faa51 refactor: promote 7 more static const arrays to constexpr
inventory_screen: groups, tiers, leftSlots, rightSlots, weaponSlots
character_create_screen: kAllRaces, kAllClasses
2026-03-27 14:46:31 -07:00
Kelsi
e474dca2be refactor: add 9 button/bar color constants, batch constexpr promotions
New ui_colors.hpp constants: kBtnGreen, kBtnGreenHover, kBtnRed,
kBtnRedHover, kBtnDkGreen/Hover, kBtnDkRed/Hover, kMidHealthYellow
— replacing 21 inline literals across accept/decline button and
health bar patterns.

Deduplicate kMon/kMonths month arrays (2 copies → 1 kMonthAbbrev).

Promote 22 remaining static const char*/int arrays to constexpr
(kQualHex, resLabels, kRepRankNames, kTotemNames, kReactLabels,
kChatHelp, kMacroHelp, kHelpLines, kMarkWords, componentDirs,
keyLabels, kRollLabels, gossipIcons, kMarkNames, kDiffLabels,
kStatLabels, kCatHeaders, kSlotNames, kResolutions, displayToInternal).
2026-03-27 14:44:52 -07:00
Kelsi
4981d162c5 refactor: deduplicate item-set DBC key arrays, widen totem timer buffer
- Move itemKeys/spellKeys/thrKeys to shared kItemSetItemKeys/
  kItemSetSpellKeys/kItemSetThresholdKeys in ui_colors.hpp, removing
  5 identical local definitions across game_screen and inventory_screen
- Widen totem timer snprintf buffer from 8 to 16 bytes (defensive)
- Promote kStatTooltips to constexpr
2026-03-27 14:40:44 -07:00
Kelsi
92d8262f96 refactor: move kClassMasks, kRaceMasks, kSocketTypes to shared ui_colors.hpp
Deduplicate class/race bitmask arrays (3 copies each → 1 shared) and
socket type definitions (3 copies → 1 shared). Eliminates ~80 lines of
repeated struct definitions across game_screen.cpp and inventory_screen.cpp.
2026-03-27 14:35:16 -07:00
Kelsi
cd29c6d50b refactor: deduplicate class name functions in talent_screen and game_screen
Replace local getClassName()/classNameStr() with shared
game::getClassName() from character.hpp, removing 2 duplicate
name-lookup implementations (static arrays + wrapper functions).
2026-03-27 14:30:45 -07:00
Kelsi
f5a3ebc774 refactor: deduplicate arrays in inventory_screen, add kDarkYellow constant
- Move kSocketTypes to file-scope constexpr, removing 2 identical local
  definitions across tooltip render functions
- Move kResistNames to file-scope constexpr, removing 3 identical local
  definitions (Holy..Arcane resistance labels)
- Move kRepRankNames to file-scope constexpr, removing 2 identical local
  definitions (Hated..Exalted reputation rank labels)
- Add kDarkYellow color constant, replacing 3 inline literals
2026-03-27 14:25:54 -07:00
Kelsi
cbb42ac58f fix: guard spline point loop against unsigned underflow when pointCount==1
The uncompressed spline skip loop used `pointCount - 1` in its bound
without guarding pointCount > 1. While pointCount==0 is already handled
by an early return, pointCount==1 would correctly iterate 0 times, but
the explicit guard makes the intent clearer and prevents future issues
if the early return is ever removed.
2026-03-27 14:20:28 -07:00
Kelsi
6783ead4ba fix: guard hexDecode std::stoul; extract duration formatting helpers
- Wrap std::stoul in auth_screen hexDecode() with try-catch to prevent
  crash on malformed saved password hex data
- Add fmtDurationCompact() helper replacing 3 identical duration format
  blocks (hours/minutes/seconds for aura icon overlays)
- Add renderAuraRemaining() helper replacing 5 identical "Remaining: Xm Ys"
  tooltip blocks across player/target/focus/raid aura tooltips
2026-03-27 14:17:28 -07:00
Kelsi
0ae7360255 refactor: deduplicate kRaidMarkNames, promote 4 more arrays to constexpr
- Move kRaidMarkNames to file-scope constexpr, removing 3 duplicate
  local definitions across target/raid/party frame menus
- Promote kReactColors, kEnchantSlotColors, kRollColors from static
  const to static constexpr
2026-03-27 14:13:16 -07:00
Kelsi
f1ecf8be53 refactor: deduplicate kDispelNames, use constexpr arrays, remove std::to_string in IDs
- Move kDispelNames to file-scope constexpr, removing 2 duplicate local
  definitions in raid/party frame rendering
- Promote kTotemColors and kReactDimColors from static const to constexpr
- Replace std::to_string + string concat for ImGui widget IDs with
  snprintf into stack buffers (avoids heap allocations in render loops)
2026-03-27 14:11:05 -07:00
Kelsi
22d0b9cd4c refactor: deduplicate class color functions, add 9 color constants
Move classColor/classColorU32 to shared getClassColor()/getClassColorU32()
in ui_colors.hpp, eliminating duplicate 10-case switch in character_screen
and game_screen.

New ui_colors.hpp constants: kInactiveGray, kVeryLightGray, kSymbolGold,
kLowHealthRed, kDangerRed, kEnergyYellow, kHappinessGreen, kRunicRed,
kSoulShardPurple — replacing 36 inline literals across 4 files.
2026-03-27 14:07:36 -07:00
Kelsi
54006fad83 refactor: add 9 color constants, replace 36 more inline literals
New constants in ui_colors.hpp:
- Power types: kEnergyYellow, kHappinessGreen, kRunicRed, kSoulShardPurple
- UI elements: kInactiveGray, kVeryLightGray, kSymbolGold, kLowHealthRed, kDangerRed

Replacements across game_screen(30), inventory_screen(5), character_screen(1).
2026-03-27 14:05:32 -07:00
Kelsi
762daebc75 refactor: replace 23 more inline color literals across 3 UI files
game_screen: kWhite(3), kSilver(4)
inventory_screen: kWarmGold(8), kFriendlyGreen(2), kSocketGreen(4), kActiveGreen(2)
talent_screen: kHealthGreen(1), kWhite(3), kRed(1)
2026-03-27 14:00:15 -07:00
Kelsi
c38fa6d9ec refactor: replace 31 more inline color literals with named constants in game_screen
Replace inline ImVec4 literals with shared constants from ui_colors.hpp:
kHealthGreen(5), kOrange(5), kWarmGold(5), kFriendlyGreen(3),
kActiveGreen(3), kLightGreen(4), kSocketGreen(2), new constants
kSocketGreen/kActiveGreen/kLightGreen/kHealthGreen/kWarmGold/kOrange/kFriendlyGreen
added to ui_colors.hpp.
2026-03-27 13:57:29 -07:00
Kelsi
e3c999d844 refactor: add 4 color constants, replace 31 more inline literals
Add kDarkRed, kSoftRed, kHostileRed, kMediumGray to ui_colors.hpp and
replace 31 inline ImVec4 literals across game_screen, character_screen,
inventory_screen, and performance_hud. Also replace local color aliases
in performance_hud with shared constants.
2026-03-27 10:20:45 -07:00
Kelsi
dec23423d8 chore: remove duplicate #include directives in camera_controller and auth_screen 2026-03-27 10:20:40 -07:00
Kelsi
ff77febb36 fix: guard std::stoi/stof calls at input boundaries against exceptions
Wrap string-to-number conversions in try-catch where input comes from
external sources (realm address port, last_world.cfg, keybinding config,
ADT tile filenames) to prevent crashes on malformed data.
2026-03-27 10:14:49 -07:00
Kelsi
ee20f823f7 refactor: replace 8 more inline color literals with existing constants
Replace kYellow (5), kRed (2), kGray (1), kLightGray (1) inline ImVec4
literals in realm_screen, spellbook_screen, talent_screen, game_screen,
and inventory_screen.
2026-03-27 10:14:47 -07:00
Kelsi
4090041431 refactor: add 6 color constants, replace 61 inline literals, remove const_cast
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
  talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
Kelsi
be694be558 fix: resolve infinite recursion, operator precedence bugs, and compiler warnings
- isPreWotlk() was calling itself instead of checking expansion (infinite recursion)
- luaReturnNil/Zero/False were calling themselves instead of pushing Lua values
- hasRemaining(N) * M had wrong operator precedence (should be hasRemaining(N * M))
- Misleading indentation in PARTY_LEADER_CHANGED handler (fireAddonEvent always fires)
- Remove unused standalone hasFullPackedGuid() (superseded by Packet method)
- Suppress unused-parameter warnings in fish/cancel-auto-repeat lambdas
- Remove unused settings default variables
2026-03-27 10:08:22 -07:00
Kelsi
33f8a63c99 refactor: replace 11 inline white color literals with colors::kWhite
Some checks failed
Build / Build (arm64) (push) Has been cancelled
Build / Build (x86-64) (push) Has been cancelled
Build / Build (macOS arm64) (push) Has been cancelled
Build / Build (windows-arm64) (push) Has been cancelled
Build / Build (windows-x86-64) (push) Has been cancelled
Security / CodeQL (C/C++) (push) Has been cancelled
Security / Semgrep (push) Has been cancelled
Security / Sanitizer Build (ASan/UBSan) (push) Has been cancelled
Replace ImVec4(1.0f, 1.0f, 1.0f, 1.0f) literals in game_screen (10)
and character_screen (1) with the shared kWhite constant.
2026-03-25 19:37:22 -07:00
Kelsi
eb40478b5e refactor: replace 20 more kTooltipGold inline literals across UI files
Replace remaining ImVec4(1.0f, 0.82f, 0.0f, 1.0f) gold color literals
in game_screen.cpp (19) and talent_screen.cpp (1) with the shared
colors::kTooltipGold constant. Zero inline gold literals remain.
2026-03-25 19:30:23 -07:00
Kelsi
7015e09f90 refactor: add kTooltipGold color constant, replace 14 inline literals
Add colors::kTooltipGold to ui_colors.hpp and replace 14 inline
ImVec4(1.0f, 0.82f, 0.0f, 1.0f) literals in inventory_screen.cpp
for item set names, unique markers, and quest item indicators.
2026-03-25 19:18:54 -07:00
Kelsi
7484ce6c2d refactor: extract getInventorySlotName and renderBindingType into shared UI utils
Add getInventorySlotName() and renderBindingType() to ui_colors.hpp,
replacing 3 copies of the 26-case slot name switch (2 inventory_screen
+ 1 game_screen) and 2 copies of the binding type switch. Removes ~80
lines of duplicate tooltip code.
2026-03-25 19:05:10 -07:00
Kelsi
97b44bf833 refactor: consolidate duplicate enchantment name cache in inventory tooltips
Extract getEnchantmentNames() to share a single SpellItemEnchantment.dbc
cache between both renderItemTooltip overloads, replacing two identical
19-line lazy-load blocks with single-line references.
2026-03-25 18:08:08 -07:00
Kelsi
a491202f93 refactor: convert final 7 getRemainingSize() comparisons to hasRemaining()
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Fix extra-paren variants in world_packets and packet_parsers_tbc.
getRemainingSize() is now exclusively arithmetic across the entire
codebase — all bounds checks use hasRemaining().
2026-03-25 16:32:38 -07:00
Kelsi
d50bca21c4 refactor: migrate remaining getRemainingSize() comparisons to hasRemaining()
Convert 33 remaining getRemainingSize() comparison patterns including
ternary expressions and extra-paren variants. getRemainingSize() is
now only used for arithmetic (byte counting), never for bounds checks.
2026-03-25 16:27:42 -07:00
Kelsi
618b479818 refactor: migrate 521 getRemainingSize() comparisons to hasRemaining()
Replace getRemainingSize()>=N with hasRemaining(N) and
getRemainingSize()<N with !hasRemaining(N) across all 4 packet files.
hasRemaining() is now the canonical bounds-check idiom with 680+ uses.
2026-03-25 16:22:47 -07:00
Kelsi
ca08d4313a refactor: replace 13 remaining getReadPos()+N bounds checks in game_handler
Convert final getReadPos()+N>getSize() patterns to hasRemaining(N),
completing the migration across all 5 packet-handling files.
2026-03-25 16:17:36 -07:00
Kelsi
e9d0a58e0a refactor: replace 47 getReadPos()+N bounds checks in packet parsers
Replace verbose bounds checks with hasRemaining(N) in
packet_parsers_classic (7) and packet_parsers_tbc (40), completing
the migration across all packet-handling files.
2026-03-25 16:12:46 -07:00
Kelsi
2b4d910a4a refactor: replace 79 getReadPos()+N bounds checks with hasRemaining(N)
Replace verbose getReadPos()+N>getSize() patterns in world_packets.cpp
with the existing Packet::hasRemaining(N) method, matching the style
already used in game_handler.cpp.
2026-03-25 16:10:48 -07:00
Kelsi
d086d68a2f refactor: extract Interface settings tab into dedicated method
Extract renderSettingsInterfaceTab() (108 lines) from
renderSettingsWindow(). 6 of 7 tabs now have dedicated methods;
only Video remains inline (shares init state with parent).
2026-03-25 16:07:04 -07:00
Kelsi
d0e2d0423f refactor: extract Gameplay and Controls settings tabs
Extract renderSettingsGameplayTab() (162 lines) and
renderSettingsControlsTab() (96 lines) from renderSettingsWindow().
5 of 7 settings tabs are now in dedicated methods; only Video and
Interface remain inline (they share resolution/display local state).
2026-03-25 15:54:14 -07:00
Kelsi
c7a82923ac refactor: extract 3 settings tabs into dedicated methods
Extract renderSettingsAudioTab() (110 lines), renderSettingsChatTab()
(49 lines), and renderSettingsAboutTab() (48 lines) from the 1013-line
renderSettingsWindow(). Reduces it to ~806 lines.
2026-03-25 15:49:38 -07:00
Kelsi
b1a87114ad refactor: extract updateAutoAttack() from update()
Move 98 lines of auto-attack leash range, melee resync, facing
alignment, and hostile attacker orientation into a dedicated method.
update() is now ~180 lines (74% reduction from original 704).
2026-03-25 15:37:19 -07:00
Kelsi
3215832fed refactor: extract updateTaxiAndMountState() from update()
Move 131 lines of taxi flight detection, mount reconciliation, taxi
activation timeout, and flight recovery into a dedicated method.
update() is now ~277 lines (61% reduction from original 704).
2026-03-25 15:32:51 -07:00
Kelsi
123a19ce1c refactor: extract updateEntityInterpolation() from update()
Move entity movement interpolation loop (distance-culled per-entity
update) into its own method. update() is now ~406 lines (down from
original 704, a 42% reduction across 3 extractions).
2026-03-25 15:27:31 -07:00
Kelsi
6343ceb151 refactor: extract updateTimers() from GameHandler::update()
Move 164 lines of timer/pending-state logic into updateTimers():
auction delay, quest accept timeouts, money delta, GO loot retries,
name query resync, loot money notifications, auto-inspect throttling.
update() is now ~430 lines (down from original 704).
2026-03-25 15:23:31 -07:00
Kelsi
7066062136 refactor: extract updateNetworking() from GameHandler::update()
Move socket update, packet processing, Warden async drain, RX silence
detection, disconnect handling, and Warden gate logging into a separate
updateNetworking() method. Reduces update() from ~704 to ~591 lines.
2026-03-25 15:19:03 -07:00
Kelsi
b2e2ad12c6 refactor: add registerWorldHandler() for state-guarded dispatch entries
Add registerWorldHandler() that wraps handler calls with an IN_WORLD
state check. Replaces 8 state-guarded lambda dispatch entries with
concise one-line registrations.
2026-03-25 15:11:15 -07:00
Kelsi
6694a0aa66 refactor: add registerHandler() to replace 120 lambda dispatch wrappers
Add registerHandler() using member function pointers, replacing 120
single-line lambda dispatch entries of the form
[this](Packet& p) { handleFoo(p); } with concise
registerHandler(Opcode::X, &GameHandler::handleFoo) calls.
2026-03-25 15:08:22 -07:00
Kelsi
d73c84d98d refactor: convert remaining 6 skipAll lambdas to registerSkipHandler
Replace all remaining inline skipAll dispatch lambdas with
registerSkipHandler() calls, including 2 standalone entries and
3 for-loop groups covering ~96 opcodes total.
2026-03-25 14:58:34 -07:00
Kelsi
5fe12f3f62 refactor: deduplicate 4 NPC window distance checks in update()
Replace 4 identical 10-line NPC distance check blocks (vendor, gossip,
taxi, trainer) with a shared lambda, reducing 40 lines to 16.
2026-03-25 14:53:16 -07:00
Kelsi
313a1877d5 refactor: add registerSkipHandler/registerErrorHandler for dispatch table
Add helpers for common dispatch table patterns: registerSkipHandler()
for opcodes that just discard data (14 sites), registerErrorHandler()
for opcodes that show an error message (3 sites). Reduces boilerplate
in registerOpcodeHandlers().
2026-03-25 14:50:18 -07:00
Kelsi
12355316b3 refactor: add Packet::hasData(), replace 52 position checks and 14 more Lua guards
Add Packet::hasData() for 'has remaining data' checks, replacing 52
verbose getReadPos()<getSize() comparisons across 3 files. Also replace
14 more standalone Lua return patterns with luaReturnNil/Zero helpers.
2026-03-25 14:39:01 -07:00
Kelsi
4c26b1a8ae refactor: add Lua return helpers to replace 178 inline guard patterns
Add luaReturnNil/luaReturnZero/luaReturnFalse helpers and replace 178
braced guard returns (109 nil, 31 zero, 38 false) in lua_engine.cpp.
Reduces visual noise in Lua binding functions.
2026-03-25 14:33:57 -07:00
Kelsi
0d9aac2656 refactor: add Packet::skipAll() to replace 186 setReadPos(getSize()) calls
Add skipAll() convenience method and replace 186 instances of the
verbose 'discard remaining packet data' idiom across game_handler
and world_packets.
2026-03-25 14:27:26 -07:00
Kelsi
4309c8c69b refactor: add isPreWotlk() helper to replace 24 compound expansion checks
Extract isPreWotlk() = isClassicLikeExpansion() || isActiveExpansion("tbc")
to replace 24 instances of the repeated compound check across packet
handlers. Clarifies intent: these code paths handle pre-WotLK packet
format differences.
2026-03-25 14:24:03 -07:00
Kelsi
e4194b1fc0 refactor: add isInWorld() and replace 119 inline state+socket checks
Add GameHandler::isInWorld() helper that encapsulates the repeated
'state == IN_WORLD && socket' guard. Replace 99 negative checks and
20 positive checks across game_handler.cpp, plus fix 2 remaining
C-style casts.
2026-03-25 14:21:19 -07:00
Kelsi
56f8f5c592 refactor: extract loadWeaponM2() to deduplicate weapon model loading
Extract shared M2+skin loading logic into Application::loadWeaponM2(),
replacing duplicate 15-line blocks in loadEquippedWeapons() and
tryAttachCreatureVirtualWeapons(). Future weapon loading changes only
need to update one place.
2026-03-25 14:17:19 -07:00
Kelsi
43caf7b5e6 refactor: add Packet::writePackedGuid, remove redundant static methods
Add writePackedGuid() to Packet class for read/write symmetry. Remove
now-redundant UpdateObjectParser::readPackedGuid and
MovementPacket::writePackedGuid static methods. Replace 6 internal
readPackedGuid calls, 9 writePackedGuid calls, and 1 inline 14-line
transport GUID write with Packet method calls.
2026-03-25 14:06:42 -07:00
Kelsi
2c79d82446 refactor: add Packet::readPackedGuid() and replace 121 static method calls
Move packed GUID reading into Packet class alongside readUInt8/readFloat.
Replace 121 UpdateObjectParser::readPackedGuid(packet) calls with
packet.readPackedGuid() across 4 files, reducing coupling between
Packet and UpdateObjectParser.
2026-03-25 13:58:48 -07:00
Kelsi
3f54d8bcb8 refactor: replace 37 reinterpret_cast writeBytes with writeFloat
Replace 37 verbose reinterpret_cast<const uint8_t*> float writes with
the existing Packet::writeFloat() method across world_packets,
packet_parsers_classic, and packet_parsers_tbc.
2026-03-25 13:54:10 -07:00
Kelsi
58839e611e chore: remove 3 unused includes from game_screen.cpp
Remove character_preview.hpp, spawn_presets.hpp, and blp_loader.hpp
which are included but not used in game_screen.cpp.
2026-03-25 13:50:22 -07:00
Kelsi
0f19ed40f8 refactor: convert 15 more renderer+sound patterns to withSoundManager
Replace 15 additional 3-line renderer acquisition + sound manager
null-check blocks with single-line withSoundManager() calls. Total
22 sites now use the helper; 11 remaining have complex multi-line
bodies or non-sound renderer usage.
2026-03-25 13:45:05 -07:00
Kelsi
ea15740e17 refactor: add withSoundManager() template to reduce renderer boilerplate
Add GameHandler::withSoundManager() that encapsulates the repeated
getInstance()->getRenderer()->getSoundManager() null-check chain.
Replace 6 call sites, with helper available for future consolidation
of remaining 25 sites.
2026-03-25 13:35:29 -07:00
Kelsi
a0267e6e95 refactor: consolidate 26 playerNameCache.find() calls to use lookupName()
Replace 26 direct playerNameCache lookups with the existing lookupName()
helper, which also provides entity-name fallback. Eliminates duplicate
cache+entity lookup patterns in chat, social, loot, and combat handlers.
Simplifies getCachedPlayerName() to delegate to lookupName().
2026-03-25 13:29:10 -07:00
Kelsi
f02fa10126 refactor: make DBC name caches mutable to eliminate 13 const_cast hacks
Mark spellNameCache_, titleNameCache_, factionNameCache_, areaNameCache_,
mapNameCache_, lfgDungeonNameCache_ and their loaded flags as mutable.
Update 6 lazy-load methods to const. Removes all 13 const_cast<GameHandler*>
calls, allowing const getters to lazily populate caches without UB.
2026-03-25 13:21:02 -07:00
Kelsi
fe043b5da8 refactor: extract getUnitByGuid() to replace 10 entity lookup + dynamic_cast patterns
Add GameHandler::getUnitByGuid() that combines entityManager.getEntity()
with dynamic_cast<Unit*>. Replaces 10 two-line lookup+cast blocks with
single-line calls.
2026-03-25 13:12:51 -07:00
Kelsi
c8617d20c8 refactor: use getSpellName/getSpellSchoolMask helpers instead of raw cache access
Replace 8 direct spellNameCache_.find() patterns with existing helper
methods: getSpellName() for name lookups, getSpellSchoolMask() for
school mask checks. Eliminates redundant loadSpellNameCache() calls
and 3-line cache lookup boilerplate at each site.
2026-03-25 13:08:10 -07:00
Kelsi
03aa915a05 refactor: move packetHasRemaining into Packet::hasRemaining method
Add Packet::hasRemaining(size_t) and remove free function from
game_handler.cpp. Replaces 8 call sites with method calls.
2026-03-25 13:02:49 -07:00
Kelsi
25d1a7742d refactor: add renderCoinsFromCopper() to eliminate copper decomposition boilerplate
Add renderCoinsFromCopper(uint64_t) overload in ui_colors.hpp that
decomposes copper into gold/silver/copper and renders. Replace 14
manual 3-line decomposition blocks across game_screen and
inventory_screen with single-line calls.
2026-03-25 12:59:31 -07:00
Kelsi
f39271453b refactor: extract applyAudioVolumes() to deduplicate 30-line audio settings block
Extract identical 30-line audio volume application block into
GameScreen::applyAudioVolumes(), replacing two copies (startup init
and settings dialog lambda) with single-line calls.
2026-03-25 12:52:07 -07:00
Kelsi
40dd39feed refactor: move hasFullPackedGuid into Packet class, deduplicate 2 definitions
Add Packet::hasFullPackedGuid() method and remove identical standalone
definitions from game_handler.cpp and packet_parsers_classic.cpp.
Replace 53 free-function calls with method calls.
2026-03-25 12:46:44 -07:00
Kelsi
376d0a0f77 refactor: add Packet::getRemainingSize() to replace 656 arithmetic expressions
Add getRemainingSize() one-liner to Packet class and replace all 656
instances of getSize()-getReadPos() across game_handler, world_packets,
and both packet parser files.
2026-03-25 12:42:56 -07:00
Kelsi
b66033c6d8 fix: toLowerInPlace infinite recursion + remove redundant callback guards
Fix toLowerInPlace() which was accidentally self-recursive (would stack
overflow on any Lua string lowering). Remove 30 redundant
if(addonEventCallback_) wrappers around pure fireAddonEvent blocks.
Extract color constants in performance_hud.cpp (24 inline literals).
2026-03-25 12:37:29 -07:00
Kelsi
b892dca0e5 refactor: replace 60+ inline color literals with shared ui::colors constants
Use kRed, kBrightGreen, kDarkGray, kLightGray from ui_colors.hpp across
8 UI files, eliminating duplicate ImVec4 color definitions throughout
the UI layer.
2026-03-25 12:29:44 -07:00
Kelsi
4d46641ac2 refactor: consolidate UI colors, quality colors, and renderCoinsText
Create shared include/ui/ui_colors.hpp with common ImGui color constants,
item quality color lookup, and renderCoinsText utility. Remove 3 duplicate
renderCoinsText implementations and 3 duplicate quality color switch
blocks across game_screen, inventory_screen, and quest_log_screen.
2026-03-25 12:27:43 -07:00
Kelsi
eea205ffc9 refactor: extract toHexString utility, more color constants, final cast cleanup
Add core::toHexString() utility in logger.hpp to replace 11 duplicate
hex-dump loops across world_packets, world_socket, and game_handler.
Add kColorBrightGreen/kColorDarkGray constants in game_screen.cpp
replacing 26 inline literals. Replace remaining ~37 C-style casts in
16 files. Normalize keybinding_manager.hpp to #pragma once.
2026-03-25 12:12:03 -07:00
Kelsi
ba99d505dd refactor: remaining C-style casts, color constants, and header guard cleanup
Replace ~37 remaining C-style casts with static_cast across 16 files.
Extract named color constants (kColorRed/Green/Yellow/Gray) and dialog
window flags (kDialogFlags) in game_screen.cpp, replacing 72 inline
literals. Normalize keybinding_manager.hpp to #pragma once.
2026-03-25 11:57:22 -07:00
Kelsi
05f2bedf88 refactor: replace C-style casts with static_cast and extract toLowerInPlace
Replace ~300 C-style casts ((int), (float), (uint32_t), etc.) with
static_cast across 15 source files. Extract toLowerInPlace() helper in
lua_engine.cpp to replace 72 identical tolower loop patterns.
2026-03-25 11:40:49 -07:00
Kelsi
d646a0451d refactor: add fireAddonEvent() helper to eliminate 170+ null checks
Add inline fireAddonEvent() that wraps the addonEventCallback_ null
check. Replace ~120 direct addonEventCallback_ calls with fireAddonEvent,
eliminating redundant null checks at each callsite and reducing
boilerplate by ~30 lines.
2026-03-25 11:34:22 -07:00
Kelsi
98b9e502c5 refactor: extract guidToUnitId/getQuestTitle helpers and misc cleanup
- Extract guidToUnitId(), getQuestTitle(), findQuestLogEntry() helpers
  to replace 14 duplicated GUID-to-unitId patterns and 7 quest log
  search patterns in game_handler.cpp
- Remove duplicate #include in renderer.cpp
- Remove commented-out model cleanup code in terrain_manager.cpp
- Replace C-style casts with static_cast in auth and transport code
2026-03-25 11:25:44 -07:00
Kelsi
087e42d7a1 fix: remove 12 duplicate dispatch registrations and fix addonEventCallback null-check bugs
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Remove duplicate opcode registrations introduced during the switch-to-dispatch-table
refactor (PR #22), keeping the better-commented second copies. Fix 4 instances where
addonEventCallback_("UNIT_QUEST_LOG_CHANGED") was either called unconditionally
(missing braces) or had incorrect indentation inside braces.
2026-03-24 23:33:00 -07:00
Kelsi Rae Davis
3ca8f20585
Merge pull request #22 from ldmonster/chore/split-mega-switch-to-map
[chore] refactor(game): replace 3,300-line switch with dispatch table in GameHandler
2026-03-24 23:12:49 -07:00
Paul
fa2e8ad0fe Merge commit '6bfa3dc402' into chore/split-mega-switch-to-map 2026-03-25 07:27:03 +03:00
Paul
15f12d86b3 split mega switch 2026-03-25 07:26:38 +03:00
Kelsi
6bfa3dc402 fix: suppress spell sounds and melee swing for crafting/profession spells
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Crafting spells (bandages, smelting, etc.) were playing magic precast/
cast-complete audio and triggering melee weapon swing animations because
they have physical school mask (1). Re-add isProfessionSpell check to
skip spell sounds and melee animation for tradeskill spells. The
character still plays the generic cast animation via spellCastAnimCallback.
2026-03-24 14:33:22 -07:00
Kelsi
432da20b3e feat: enable crafting sounds and add Create All button
Remove the isProfessionSpell sound suppression so crafting spells play
precast and cast-complete audio like combat spells. Crafting was
previously silent by design but users expect audio feedback.

Add "Create All" button to the tradeskill UI that queues 999 crafts.
The server automatically stops the queue when materials run out
(SPELL_FAILED_REAGENTS cancels the craft queue). This matches the
real WoW client's behavior for batch crafting.
2026-03-24 14:22:28 -07:00
Kelsi
1dd3823013 perf: use second GPU queue for parallel texture/buffer uploads
Request 2 queues from the graphics family when available (NVIDIA
exposes 16, AMD 2+). Upload batches now submit to queue[1] while
rendering uses queue[0], enabling parallel GPU transfers without
queue-family ownership transfer barriers (same family).

Falls back to single-queue path on GPUs with only 1 queue in the
graphics family. Transfer command pool is separate to avoid contention.
2026-03-24 14:09:16 -07:00
Kelsi
ed0cb0ad25 perf: time-budget tile finalization to prevent 1+ second main-loop stalls
processReadyTiles was calling advanceFinalization with a step limit of 1
but a single step (texture upload or M2 model load) could take 1060ms.
Replace the step counter with an 8ms wall-clock time budget (16ms during
taxi) so finalization yields to the render loop before causing a visible
stall. Heavy tiles spread across multiple frames instead of blocking.
2026-03-24 13:56:20 -07:00
Kelsi
05e85d9fa7 fix: correct melee swing sound paths to match WoW MPQ layout
The melee swing clips used non-existent paths (SwordSwing, MeleeSwing)
instead of the actual WoW 3.3.5a weapon swing files: WeaponSwings/
mWooshMedium and mWooshLarge for hit swings, MissSwings/MissWhoosh
for misses. Fixes "No melee swing SFX found in assets" warning.
2026-03-24 13:46:01 -07:00
Kelsi
7a5d80e801 fix: flush GPU before first render frame after world load
Add vkDeviceWaitIdle after world loading completes to ensure all async
texture uploads and resource creation are fully flushed before the
first render frame. Mitigates intermittent NVIDIA driver crashes at
vkCmdBeginRenderPass during initial world entry.
2026-03-24 13:34:52 -07:00
Kelsi
891b9e5822 fix: show friendly map names on loading screen (Outland not Expansion01)
Add mapDisplayName() with friendly names for continents: "Eastern
Kingdoms", "Kalimdor", "Outland", "Northrend". The loading screen
previously showed WDT directory names like "Expansion01" when
Map.dbc's localized name field was empty or matched the internal name.
2026-03-24 13:20:06 -07:00
Kelsi
9a6a430768 fix: track render pass subpass mode to prevent ImGui secondary violation
When parallel recording is active, the scene pass uses
VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS. Post-processing paths
(FSR/FXAA) end the scene pass and begin a new INLINE render pass for
the swapchain output. ImGui rendering must use the correct mode —
secondary buffers for SECONDARY passes, direct calls for INLINE.

Previously the check used a static condition based on enabled features
(!fsr && !fsr2 && !fxaa && parallel), which could mismatch if a
feature was enabled but initialization failed. Replace with
endFrameInlineMode_ flag that tracks the actual current render pass
mode at runtime, eliminating the validation error
VUID-vkCmdDrawIndexed-commandBuffer-recording that caused intermittent
NVIDIA driver crashes.
2026-03-24 13:05:27 -07:00
Kelsi
a152023e5e fix: add VkSampler cache to prevent sampler exhaustion crash
Validation layers revealed 9965 VkSamplers allocated against a device
limit of 4000 — every VkTexture created its own sampler even when
configurations were identical. This exhausted NVIDIA's sampler pool
and caused intermittent SIGSEGV in vkCmdBeginRenderPass.

Add a thread-safe sampler cache in VkContext that deduplicates samplers
by FNV-1a hash of all 14 VkSamplerCreateInfo fields. All texture,
render target, renderer, water, and loading screen sampler creation
now goes through getOrCreateSampler(). Textures set ownsSampler_=false
so shared samplers aren't double-freed.

Also auto-disable anisotropy in the cache when the physical device
doesn't support the samplerAnisotropy feature, fixing the validation
error VUID-VkSamplerCreateInfo-anisotropyEnable-01070.
2026-03-24 11:44:54 -07:00
Kelsi
1556559211 fix: skip VkPipelineCache on NVIDIA to prevent driver crash
VkPipelineCache causes vkCmdBeginRenderPass to SIGSEGV inside
libnvidia-glcore.so on NVIDIA 590.x drivers. Skip pipeline cache
creation on NVIDIA GPUs — NVIDIA drivers already provide built-in
shader disk caching, so the Vulkan-level cache is redundant.
Pipeline cache still works on AMD and other vendors.
2026-03-24 10:30:25 -07:00
Kelsi Rae Davis
0a32c0fa27
Merge pull request #21 from ldmonster/chore/add-classification-to-render
refactor(rendering): extract M2 classification into pure functions
2026-03-24 10:18:24 -07:00
Kelsi
d44411c304 fix: convert PLAY_OBJECT_SOUND positions to render coords for 3D audio
Entity positions are in canonical WoW coords (X=north, Y=west) but the
audio listener uses render coords (X=west, Y=north) from the camera.
Without conversion, distance attenuation was computed on swapped axes,
making NPC ambient sounds (peasant voices, etc.) play at wrong volumes
regardless of actual distance.
2026-03-24 10:17:47 -07:00
Kelsi
c09a443b18 cleanup: remove temporary PLAY_SOUND diagnostic logging 2026-03-24 10:13:31 -07:00
Kelsi
4fcb92dfdc fix: skip FSR3 frame gen on non-AMD GPUs to prevent NVIDIA driver crash
The AMD FidelityFX FSR3 runtime corrupts Vulkan driver state when
context creation fails on NVIDIA GPUs, causing vkCmdBeginRenderPass
to SIGSEGV inside libnvidia-glcore. Gate FSR3 frame gen initialization
behind isAmdGpu() check — FSR2 upscaling still works on all GPUs.
2026-03-24 10:11:21 -07:00
Kelsi
ceb8006c3d fix: prevent hang on FSR3 upscale context creation failure
When ffxCreateContext for the upscaler fails (e.g. on NVIDIA with the
AMD FidelityFX runtime), the shutdown() path called dlclose() on the
runtime library which could hang — the library's global destructors may
block waiting for GPU operations that never completed.

Skip dlclose() on context creation failure: just clean up function
pointers and mark as failed. The library stays loaded (harmless) and
the game continues with FSR2 fallback instead of hanging.
2026-03-24 10:06:57 -07:00
Kelsi
d2a396df11 feat: log GPU vendor/name at init, add PLAY_SOUND diagnostics
Log GPU name and vendor ID during VkContext initialization for easier
debugging of GPU-specific issues (FSR3, driver compat, etc.). Add
isAmdGpu()/isNvidiaGpu() accessors.

Temporarily log SMSG_PLAY_SOUND and SMSG_PLAY_OBJECT_SOUND at WARN
level (sound ID, name, file path) to diagnose unidentified ambient
NPC sounds reported by the user.
2026-03-24 09:56:54 -07:00
Paul
cbfe7d5f44 refactor(rendering): extract M2 classification into pure functions 2026-03-24 19:55:24 +03:00
Kelsi
c8c01f8ac0 perf: add Vulkan pipeline cache persistence for faster startup
Create a VkPipelineCache at device init, loaded from disk if available.
All 65 pipeline creation calls across 19 renderer files now use the
shared cache. On shutdown, the cache is serialized to disk so subsequent
launches skip redundant shader compilation.

Cache path: ~/.local/share/wowee/pipeline_cache.bin (Linux),
~/Library/Caches/wowee/ (macOS), %APPDATA%\wowee\ (Windows).
Stale/corrupt caches are handled gracefully (fallback to empty cache).
2026-03-24 09:47:03 -07:00
Kelsi
c18720f0f0 feat: server-synced bag sort, fix world map continent bounds, update docs
Inventory sort: clicking "Sort Bags" now generates CMSG_SWAP_ITEM packets
to move items server-side (one swap per frame to avoid race conditions).
Client-side sort runs immediately for visual preview; server swaps follow.
New Inventory::computeSortSwaps() computes minimal swap sequence using
selection-sort permutation on quality→itemId→stackCount comparator.

World map: fix continent bounds derivation that used intersection (max/min)
instead of union (min/max) of child zone bounds, causing continent views
to display zoomed-in/clipped.

Update README.md and docs/status.md with current features, release info,
and known gaps (v1.8.2-preview, 664 opcode handlers, NPC voices, bag
independence, CharSections auto-detect, quest GO server limitation).
2026-03-24 09:24:09 -07:00
Kelsi
62e99da1c2 fix: remove forced backpack-open from toggleBag for full bag independence
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
2026-03-24 08:38:06 -07:00
Kelsi
9ab70c7b1f cleanup: remove unused spline diagnostic variables 2026-03-24 08:29:43 -07:00
Kelsi
d083ac11fa fix: suppress false-positive maskBlockCount warnings for VALUES updates
VALUES update blocks don't carry an objectType field (it defaults to 0),
so the sanity check incorrectly used the non-PLAYER threshold (10) for
player character updates that legitimately need 42-46 mask blocks. Allow
up to 55 blocks for VALUES updates (could be any entity type including
PLAYER). Only enforce strict limits on CREATE_OBJECT blocks where the
objectType is known.
2026-03-24 08:23:14 -07:00
Kelsi Davis
2e136e9fdc fix: enable Vulkan portability drivers on macOS for MoltenVK compatibility
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Homebrew's vulkan-loader hides portability ICDs (like MoltenVK) from
pre-instance extension enumeration by default, causing SDL2 to fail
with "doesn't implement VK_KHR_surface". Set VK_LOADER_ENABLE_PORTABILITY_DRIVERS
before loading the Vulkan library so the loader includes MoltenVK and
its surface extensions.
2026-03-23 19:16:12 -07:00
Kelsi
5e8d4e76c8 fix: allow closing any bag independently and reset off-screen positions
Remove the forced backpack-open constraint that prevented closing the
backpack while other bags were open. Each bag window is now independently
closable regardless of which others are open.

Add off-screen position reset to individual bag windows (renderBagWindow)
so bags saved at positions outside the current resolution snap back to
their default stack position.
2026-03-23 18:16:23 -07:00
Kelsi
b10c8b7aea fix: send GAMEOBJ_USE+LOOT together for chests, reset off-screen bag pos
Chest-type GOs now send CMSG_GAMEOBJ_USE immediately followed by
CMSG_LOOT in the same frame. The USE handler opens the chest, then the
LOOT handler reads the contents — both processed sequentially by the
server. Previously only CMSG_LOOT was sent (no USE), which failed on
AzerothCore because the chest wasn't activated first.

Reset the Bags window position to bottom-right if the saved position
is outside the current screen resolution (e.g. after a resolution
change or moving between monitors).
2026-03-23 18:10:16 -07:00
Kelsi
8a617e842b fix: direct CMSG_LOOT for chest GOs and increase M2 descriptor pools
Chest-type game objects (quest pickups, treasure chests) now send
CMSG_LOOT directly with the GO GUID instead of CMSG_GAMEOBJ_USE +
delayed CMSG_LOOT. The server's loot handler activates the GO and
sends SMSG_LOOT_RESPONSE in one step. The old approach failed because
CMSG_GAMEOBJ_USE opened+despawned the GO before CMSG_LOOT arrived.

Double M2 bone and material descriptor pool sizes (8192 → 16384) to
handle the increased NPC count from the spline parsing fix — patrolling
NPCs that were previously invisible now spawn correctly, exhausting
the old pool limits.
2026-03-23 17:41:42 -07:00
Kelsi
98cc282e7e fix: stop sending CMSG_LOOT after CMSG_GAMEOBJ_USE (releases server loot)
AzerothCore handles loot automatically in the CMSG_GAMEOBJ_USE handler
(calls SendLoot internally). Sending a redundant CMSG_LOOT 200ms later
triggers DoLootRelease() on the server, which closes the loot the server
just opened — before SMSG_LOOT_RESPONSE ever reaches the client. This
broke quest GO interactions (Bundle of Wood, etc.) because the loot
window never appeared and quest items were never granted.

Also remove temporary diagnostic logging from GO interaction path.
2026-03-23 17:23:49 -07:00
Kelsi
a3934807af fix: restore WMO wall collision threshold to cos(50°) ≈ 0.65
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
The wall/floor classification threshold was lowered from 0.65 to 0.35
in a prior optimization commit, causing surfaces at 35-65° from
horizontal (steep walls, angled building geometry) to be classified as
floors and skipped during wall collision. This allowed the player to
clip through angled WMO walls.

Restore the threshold to 0.65 (cos 50°) in both the collision grid
builder and the runtime checkWallCollision skip, matching the
MAX_WALK_SLOPE limit used for slope-slide physics.
2026-03-23 16:43:15 -07:00
Kelsi
2c3bd06898 fix: read unconditional parabolic fields in WotLK spline parsing
AzerothCore/ChromieCraft always writes verticalAcceleration(float) +
effectStartTime(uint32) after durationMod in the spline movement block,
regardless of whether the PARABOLIC spline flag (0x800) is set. The
parser only read these 8 bytes when PARABOLIC was flagged, causing it
to read the wrong offset as pointCount (0 instead of e.g. 11). This
made every patrolling NPC fail to parse — invisible with no displayId.

Also fix splineStart calculation (was off by 4 bytes) and remove
temporary diagnostic logging.
2026-03-23 16:32:59 -07:00
Kelsi
1a3146395a fix: validate splineMode in Classic spline parse to prevent desync
When a WotLK NPC has durationMod=0.0, the Classic-first spline parser
reads it as pointCount=0 and "succeeds", then consumes garbage bytes as
splineMode and endPoint. This desynchronizes the read position for all
subsequent update blocks in the packet, causing cascading failures
(truncated update mask, unknown update type) that leave NPCs without
displayIds — making them invisible.

Fix: after reading splineMode, reject the Classic parse if splineMode > 3
(valid values are 0-3) and fall through to the WotLK format parser.
2026-03-23 11:09:15 -07:00
Kelsi
503f9ed650 fix: auto-detect CharSections.dbc layout and add Blood Elf/Draenei NPC voices
CharSections.dbc has different field layouts between stock WotLK (textures
at field 4-6) and Classic/TBC/Turtle/HD-textured WotLK (VariationIndex at
field 4). Add detectCharSectionsFields() that probes field-4 values at
runtime to determine the correct layout, so both stock and modded clients
work without JSON changes.

Also add BLOODELF_MALE/FEMALE and DRAENEI_MALE/FEMALE voice types to the
NPC voice system — previously all Blood Elf and Draenei NPCs fell through
to GENERIC (random dwarf/gnome/night elf/orc mix).
2026-03-23 11:00:49 -07:00
Kelsi
d873f27070 feat: add GetNetStats (latency) and AcceptBattlefieldPort (BG queue)
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
GetNetStats() returns bandwidthIn, bandwidthOut, latencyHome,
latencyWorld — with real latency from getLatencyMs(). Used by
latency display addons and the default UI's network indicator.

AcceptBattlefieldPort(index, accept) accepts or declines a
battleground queue invitation. Backed by existing acceptBattlefield
and declineBattlefield methods.
2026-03-23 04:17:25 -07:00
Kelsi
a53342e23f feat: add taxi/flight path API (NumTaxiNodes, TaxiNodeName, TakeTaxiNode)
NumTaxiNodes() returns the count of taxi nodes available at the
current flight master. TaxiNodeName(index) returns the node name.
TaxiNodeGetType(index) returns whether the node is known/reachable.
TakeTaxiNode(index) activates the flight to the selected node.

Uses existing taxiNodes_ data and activateTaxi() method.
Enables flight path addons and taxi map overlay addons.
2026-03-23 04:07:34 -07:00
Kelsi
285c4caf24 feat: add quest interaction (accept, decline, complete, abandon, rewards)
AcceptQuest() / DeclineQuest() respond to quest offer dialogs.
CompleteQuest() sends quest completion to server.
AbandonQuest(questId) abandons a quest from the quest log.
GetNumQuestRewards() / GetNumQuestChoices() return reward counts
for the selected quest log entry.

Backed by existing GameHandler methods. Enables quest helper addons
to accept/decline/complete quests programmatically.
2026-03-23 03:57:53 -07:00
Kelsi
d9c12c733d feat: add gossip/NPC dialog API for quest and vendor addons
GetNumGossipOptions() returns option count for current NPC dialog.
GetGossipOptions() returns pairs of (text, type) for each option
where type is "gossip", "vendor", "taxi", "trainer", etc.
SelectGossipOption(index) selects a dialog option.
GetNumGossipAvailableQuests() / GetNumGossipActiveQuests() return
quest counts in the gossip dialog.
CloseGossip() closes the NPC dialog.

Uses existing GossipMessageData from SMSG_GOSSIP_MESSAGE handler.
Enables gossip addons and quest helper dialog interaction.
2026-03-23 03:53:54 -07:00
Kelsi
93dabe83e4 feat: add connection, equipment, focus, and realm queries
IsConnectedToServer() checks if connected to the game server.
UnequipItemSlot(slot) moves an equipped item to the backpack.
HasFocus() checks if the player has a focus target set.
GetRealmName() / GetNormalizedRealmName() return realm name.

All backed by existing GameHandler methods.
2026-03-23 03:42:26 -07:00
Kelsi
c874ffc6b6 feat: add player commands (helm/cloak toggle, PvP, minimap ping, /played)
ShowHelm() / ShowCloak() toggle helm/cloak visibility.
TogglePVP() toggles PvP flag.
Minimap_Ping(x, y) sends a minimap ping to the group.
RequestTimePlayed() requests /played data from server.

All backed by existing GameHandler methods.
2026-03-23 03:37:18 -07:00
Kelsi
9a47def27c feat: add chat channel management (Join, Leave, GetChannelName)
JoinChannelByName(name, password) joins a chat channel.
LeaveChannelByName(name) leaves a chat channel.
GetChannelName(index) returns channel info (name, header, collapsed,
channelNumber, count, active, category) — 7-field signature.

Backed by existing joinChannel/leaveChannel/getChannelByIndex methods.
Enables chat channel management addons and channel autojoining.
2026-03-23 03:33:09 -07:00
Kelsi
2b582f8a20 feat: add Logout, CancelLogout, RandomRoll, FollowUnit
Logout() sends logout request to server.
CancelLogout() cancels a pending logout.
RandomRoll(min, max) sends /roll (defaults 1-100).
FollowUnit() is a stub (requires movement system).

Backed by existing requestLogout, cancelLogout, randomRoll methods.
2026-03-23 03:27:45 -07:00
Kelsi
6c873622dc feat: add party management (InviteUnit, UninviteUnit, LeaveParty)
InviteUnit(name) invites a player to the group.
UninviteUnit(name) removes a player from the group.
LeaveParty() leaves the current party/raid.

Backed by existing inviteToGroup, uninvitePlayer, leaveGroup methods.
2026-03-23 03:22:30 -07:00
Kelsi
7927d98e79 feat: add guild management (invite, kick, promote, demote, leave, notes)
GuildInvite(name) invites a player to the guild.
GuildUninvite(name) kicks a member (uses kickGuildMember).
GuildPromote(name) / GuildDemote(name) change rank.
GuildLeave() leaves the guild.
GuildSetPublicNote(name, note) sets a member's public note.

All backed by existing GameHandler methods that send the appropriate
guild management CMSG packets.
2026-03-23 03:18:03 -07:00
Kelsi
2f68282afc feat: add DoEmote for /dance /wave /bow and 30+ emote commands
DoEmote(token) maps emote token strings to TextEmote DBC IDs and
sends them via sendTextEmote. Supports 30+ common emotes: WAVE,
BOW, DANCE, CHEER, CHICKEN, CRY, EAT, FLEX, KISS, LAUGH, POINT,
ROAR, RUDE, SALUTE, SHY, SILLY, SIT, SLEEP, SPIT, THANK, CLAP,
KNEEL, LAY, NO, YES, BEG, ANGRY, FAREWELL, HELLO, WELCOME, etc.

Targets the current target if one exists.
2026-03-23 03:12:30 -07:00
Kelsi
a503d09d9b feat: add friend/ignore management (AddFriend, RemoveFriend, AddIgnore, DelIgnore)
AddFriend(name, note) and RemoveFriend(name) manage the friends list.
AddIgnore(name) and DelIgnore(name) manage the ignore list.
ShowFriends() is a stub (friends panel is ImGui-rendered).

All backed by existing GameHandler methods that send the appropriate
CMSG_ADD_FRIEND, CMSG_DEL_FRIEND, CMSG_ADD_IGNORE, CMSG_DEL_IGNORE
packets to the server.
2026-03-23 03:07:24 -07:00
Kelsi
75db30c91e feat: add Who system API for player search addons
GetNumWhoResults() returns result count and total online players.
GetWhoInfo(index) returns name, guild, level, race, class, zone,
classFileName — the standard 7-field /who result signature.

SendWho(query) sends a /who search to the server.
SetWhoToUI() is a stub for addon compatibility.

Uses existing whoResults_ from SMSG_WHO handler and queryWho().
Enables /who replacement addons and social panel search.
2026-03-23 03:03:01 -07:00
Kelsi
da5b464cf6 feat: add IsPlayerSpell, IsCurrentSpell, and spell state queries
IsPlayerSpell(spellId) checks if the spell is in the player's known
spells set. Used by action bar addons to distinguish permanent spells
from temporary proc/buff-granted abilities.

IsCurrentSpell(spellId) checks if the spell is currently being cast.
IsSpellOverlayed() and IsAutoRepeatSpell() are stubs for addon compat.
2026-03-23 02:53:34 -07:00
Kelsi
9cd2cfa46e feat: add title API (GetCurrentTitle, GetTitleName, SetCurrentTitle)
GetCurrentTitle() returns the player's chosen title bit index.
GetTitleName(bit) returns the formatted title string from
CharTitles.dbc (e.g., "Commander %s", "%s the Explorer").
SetCurrentTitle() is a stub for title switching.

Used by title display addons and the character panel title selector.
2026-03-23 02:47:30 -07:00
Kelsi
0dc3e52d32 feat: add inspect and clear stubs for inspection addons
GetInspectSpecialization() returns the inspected player's active
talent group from the cached InspectResult data.

NotifyInspect() and ClearInspectPlayer() are stubs — inspection is
auto-triggered by the C++ side when targeting players. These prevent
nil errors in inspection addons that call them.
2026-03-23 02:38:18 -07:00
Kelsi
4f28187661 feat: add honor/arena currency, played time, and bind location
GetHonorCurrency() returns honor points from update fields.
GetArenaCurrency() returns arena points.
GetTimePlayed() returns total time played and level time played
in seconds (populated from SMSG_PLAYED_TIME).
GetBindLocation() returns the hearthstone bind zone name.

Used by currency displays, /played addons, and hearthstone tooltip.
2026-03-23 02:33:32 -07:00
Kelsi
a20984ada2 feat: add instance lockout API for raid reset tracking
GetNumSavedInstances() returns count of saved instance lockouts.
GetSavedInstanceInfo(index) returns 9-field WoW signature: name,
mapId, resetTimeRemaining, difficulty, locked, extended,
instanceIDMostSig, isRaid, maxPlayers.

Uses existing instanceLockouts_ from SMSG_RAID_INSTANCE_INFO.
Enables SavedInstances and lockout tracking addons to display
which raids/dungeons the player is locked to and when they reset.
2026-03-23 02:28:38 -07:00
Kelsi
2e9dd01d12 feat: add battleground scoreboard API for PvP addons
GetNumBattlefieldScores() returns player count in the BG scoreboard.
GetBattlefieldScore(index) returns 12-field WoW API signature: name,
killingBlows, honorableKills, deaths, honorGained, faction, rank,
race, class, classToken, damageDone, healingDone.

GetBattlefieldWinner() returns winning faction (0=Horde, 1=Alliance)
or nil if BG is still in progress.

RequestBattlefieldScoreData() sends MSG_PVP_LOG_DATA to refresh the
scoreboard from the server.

Uses existing BgScoreboardData from MSG_PVP_LOG_DATA handler.
Enables BG scoreboard addons and PvP tracking.
2026-03-23 02:17:16 -07:00
Kelsi
47e317debf feat: add UnitIsPVP, UnitIsPVPFreeForAll, and GetBattlefieldStatus
UnitIsPVP(unit) checks UNIT_FLAG_PVP (0x1000) on the unit's flags
field. Used by unit frame addons to show PvP status indicators.

UnitIsPVPFreeForAll(unit) checks for FFA PvP flag.

GetBattlefieldStatus() returns stub ("none") for addons that check
BG queue state on login. Full BG scoreboard data exists in
GameHandler but is rendered via ImGui.
2026-03-23 01:42:57 -07:00
Kelsi
d0743c5fee feat: show Unique-Equipped on items with that flag
Items with the Unique-Equipped flag (itemFlags & 0x1000000) now
display "Unique-Equipped" in the tooltip header. This is distinct
from "Unique" (maxCount=1) — Unique-Equipped means you can carry
multiple but only equip one (e.g., trinkets, rings with the flag).
2026-03-23 01:32:37 -07:00
Kelsi
757fc857cd feat: show 'This Item Begins a Quest' on quest-starting item tooltips
Items with a startQuestId now display "This Item Begins a Quest" in
gold text at the bottom of the tooltip, matching WoW's behavior.
Helps players identify quest-starting drops in their inventory.

Passes startsQuest flag through _GetItemTooltipData from the
startQuestId field in ItemQueryResponseData.
2026-03-23 01:28:28 -07:00
Kelsi
b8c33c7d9b feat: resolve spell \$o1 periodic totals from base points × ticks
Spell descriptions now substitute \$o1/\$o2/\$o3 with the total
periodic damage/healing: base_per_tick × (duration / 3sec).

Example: SW:Pain with base=4 (5 per tick), duration=18sec (6 ticks):
  Before: "Causes X Shadow damage over 18 sec"
  After:  "Causes 30 Shadow damage over 18 sec"

Combined with \$s1 (per-tick/instant) and \$d (duration), the three
most common spell template variables are now fully resolved. This
covers the vast majority of spell tooltips.
2026-03-23 01:02:31 -07:00
Kelsi
11ecc475c8 feat: resolve spell \$d duration to real seconds from SpellDuration.dbc
Spell descriptions now substitute \$d with actual duration values:
  Before: "X damage over X sec"
  After:  "30 damage over 18 sec"

Implementation:
- DurationIndex field (40) added to all expansion Spell.dbc layouts
- SpellDuration.dbc loaded during cache build: maps index → base ms
- cleanSpellDescription substitutes \$d with resolved seconds/minutes
- getSpellDuration() accessor on GameHandler

Combined with \$s1/\$s2/\$s3 from the previous commit, most common
spell description templates are now fully resolved with real values.
2026-03-23 01:00:18 -07:00
Kelsi
a5aa1faf7a feat: resolve spell \$s1/\$s2/\$s3 to real DBC damage/heal values
Spell descriptions now substitute \$s1/\$s2/\$s3 template variables
with actual effect base points from Spell.dbc (field 80/81/82).
For example: "causes \$s1 Fire Damage" → "causes 562 Fire Damage".

Implementation:
- Added EffectBasePoints0/1/2 to all 4 expansion DBC layouts
- SpellNameEntry now stores effectBasePoints[3]
- loadSpellNameCache reads base points during DBC iteration
- cleanSpellDescription substitutes \$s1→abs(base)+1 when available
- getSpellEffectBasePoints() accessor on GameHandler

Values are DBC base points (before spell power scaling). Still uses
"X" placeholder for unresolved variables (\$d, \$o1, etc.).
2026-03-23 00:51:19 -07:00
Kelsi
f9464dbacd feat: add CalendarGetDate and calendar stubs
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
CalendarGetDate() returns real weekday, month, day, year from the
system clock. Used by calendar addons and date-aware UI elements.

CalendarGetNumPendingInvites() and CalendarGetNumDayEvents() return 0
as stubs — prevents nil errors in addons that check calendar state.
2026-03-23 00:33:09 -07:00
Kelsi
df610ff472 feat: add GetDifficultyInfo for instance difficulty display
GetDifficultyInfo(id) returns name, groupType, isHeroic, maxPlayers
for WotLK instance difficulties:
  0: "5 Player" (party, normal, 5)
  1: "5 Player (Heroic)" (party, heroic, 5)
  2: "10 Player" (raid, normal, 10)
  3: "25 Player" (raid, normal, 25)
  4/5: 10/25 Heroic raids

Used by boss mod addons (DBM, BigWigs) and instance info displays
to show the current dungeon difficulty.
2026-03-23 00:27:34 -07:00
Kelsi
d1d3645d2b feat: add WEATHER_CHANGED event and GetWeatherInfo query
Fire WEATHER_CHANGED(weatherType, intensity) when the server sends
SMSG_WEATHER with a new weather state. Enables weather-aware addons
to react to rain/snow/storm transitions.

GetWeatherInfo() returns current weatherType (0=clear, 1=rain, 2=snow,
3=storm) and intensity (0.0-1.0). Weather data is already tracked by
game_handler and used by the renderer for particle effects and fog.
2026-03-23 00:23:22 -07:00
Kelsi
2947e31375 feat: add GuildRoster request and SortGuildRoster stub
GuildRoster() triggers CMSG_GUILD_ROSTER to request updated guild
member data from the server. Called by guild roster addons and the
social panel to refresh the member list.

SortGuildRoster() is a no-op (sorting is handled client-side by
the ImGui guild roster display).
2026-03-22 23:42:44 -07:00
Kelsi
79bc3a7fb6 feat: add BuyMerchantItem and SellContainerItem for vendor interaction
BuyMerchantItem(index, count) purchases an item from the current
vendor by merchant slot index. Resolves itemId and slot from the
vendor's ListInventoryData.

SellContainerItem(bag, slot) sells an item from the player's
inventory to the vendor. Supports backpack (bag=0) and bags 1-4.

Enables auto-sell addons (Scrap, AutoVendor) and vendor UI addons
to buy/sell items programmatically.
2026-03-22 23:37:29 -07:00
Kelsi
0a2667aa05 feat: add RepairAllItems for vendor auto-repair addons
RepairAllItems(useGuildBank) sends CMSG_REPAIR_ITEM to repair all
equipped items at the current vendor. Checks CanMerchantRepair before
sending. Optional useGuildBank flag for guild bank repairs.

One of the most commonly needed addon functions — enables auto-repair
addons to fix all gear in a single call when visiting a repair vendor.
2026-03-22 23:32:20 -07:00
Kelsi
397185d33c feat: add trade API (AcceptTrade, CancelTrade, InitiateTrade)
AcceptTrade() locks in the trade offer via CMSG_ACCEPT_TRADE.
CancelTrade() cancels an open trade via CMSG_CANCEL_TRADE.
InitiateTrade(unit) starts a trade with a target player.

Uses existing GameHandler trade functions and TradeStatus tracking.
Enables trade addons and macro-based trade acceptance.
2026-03-22 23:28:25 -07:00
Kelsi
adc1b40290 feat: add GetAuctionItemLink for AH item link generation — commit #100
Quality-colored item links for auction house items, enabling AH addons
to display clickable item links in their UI and chat output.

This is the 100th commit of this session, bringing the total to
408 API functions across all WoW gameplay systems.
2026-03-22 23:22:08 -07:00
Kelsi
fa25d8b6b9 feat: add auction house API for Auctioneer and AH addons
GetNumAuctionItems(listType) returns item count and total for
browse/owner/bidder lists. GetAuctionItemInfo(type, index) returns
the full 17-field WoW API signature: name, texture, count, quality,
canUse, level, minBid, minIncrement, buyoutPrice, bidAmount,
highBidder, owner, saleStatus, itemId.

GetAuctionItemTimeLeft(type, index) returns time category 1-4
(short/medium/long/very long).

Uses existing auctionBrowseResults_, auctionOwnerResults_, and
auctionBidderResults_ from SMSG_AUCTION_LIST_RESULT. Enables
Auctioneer, Auctionator, and AH scanning addons.
2026-03-22 23:14:07 -07:00
Kelsi
3f324ddf3e feat: add mail inbox API for postal and mail addons
GetInboxNumItems() returns count of mail messages.
GetInboxHeaderInfo(index) returns the full 13-field WoW API signature:
packageIcon, stationeryIcon, sender, subject, money, COD, daysLeft,
hasItem, wasRead, wasReturned, textCreated, canReply, isGM.

GetInboxText(index) returns the mail body text.
HasNewMail() checks for unread mail (minimap icon indicator).

Uses existing mailInbox_ populated from SMSG_MAIL_LIST_RESULT.
Enables postal addons (Postal, MailOpener) to read inbox data.
2026-03-22 22:57:45 -07:00
Kelsi
97ec915e48 feat: add glyph socket API for WotLK talent customization — 400 APIs
GetNumGlyphSockets() returns 6 (WotLK glyph slot count).
GetGlyphSocketInfo(index, talentGroup) returns enabled, glyphType
(1=major, 2=minor), glyphSpellID, and icon for each socket.

Uses existing learnedGlyphs_ array populated from SMSG_TALENTS_INFO.
Enables talent/glyph inspection addons.

This commit brings the total API count to exactly 400 functions.
2026-03-22 22:48:30 -07:00
Kelsi
c25151eff1 feat: add achievement API (GetAchievementInfo, GetNumCompleted)
GetNumCompletedAchievements() returns count of earned achievements.
GetAchievementInfo(id) returns the full 12-field WoW API signature:
id, name, points, completed, month, day, year, description, flags,
icon, rewardText, isGuildAchievement.

Uses existing earnedAchievements_ set, achievement name/description/
points caches from Achievement.dbc, and completion date tracking
from SMSG_ALL_ACHIEVEMENT_DATA / SMSG_ACHIEVEMENT_EARNED.

Enables achievement tracking addons (Overachiever, etc.) to query
and display achievement progress.
2026-03-22 22:43:10 -07:00
Kelsi
ee6551b286 feat: add CastPetAction, TogglePetAutocast, PetDismiss, IsPetAttackActive
Complete the pet action bar interaction:

- CastPetAction(index) — cast the pet spell at the given bar slot
  by sending the packed action via sendPetAction
- TogglePetAutocast(index) — toggle autocast for the pet spell
  at the given slot via togglePetSpellAutocast
- PetDismiss() — send dismiss pet command
- IsPetAttackActive() — whether pet is currently in attack mode

Together with the previous pet bar functions (HasPetUI, GetPetActionInfo,
PetAttack, PetFollow, PetWait, PetPassiveMode, PetDefensiveMode), this
completes the pet action bar system for hunters/warlocks/DKs.
2026-03-22 22:37:24 -07:00
Kelsi
3f3ed22f78 feat: add pet action bar API for hunter/warlock pet control
Implement pet action bar functions using existing pet data:

- HasPetUI() — whether player has an active pet
- GetPetActionInfo(index) — name, icon, isActive, autoCastEnabled
  for each of the 10 pet action bar slots
- GetPetActionCooldown(index) — cooldown state stub
- PetAttack() — send attack command to current target
- PetFollow() — send follow command
- PetWait() — send stay command
- PetPassiveMode() — set passive react mode
- PetDefensiveMode() — set defensive react mode

All backed by existing SMSG_PET_SPELLS data (petActionSlots_,
petCommand_, petReact_, petAutocastSpells_) and sendPetAction().
2026-03-22 22:33:18 -07:00
Kelsi
81bd0791aa feat: add GetActionBarPage and ChangeActionBarPage for bar switching
GetActionBarPage() returns the current action bar page (1-6).
ChangeActionBarPage(page) switches pages and fires ACTIONBAR_PAGE_CHANGED
via the Lua frame event system. Used by action bar addons and the
default UI's page arrows / shift+number keybinds.

Action bar page state tracked in Lua global __WoweeActionBarPage.
2026-03-22 22:24:34 -07:00
Kelsi
3a4d2e30bc feat: add CastShapeshiftForm and CancelShapeshiftForm
CastShapeshiftForm(index) casts the spell for the given form slot:
- Warrior: Battle Stance(2457), Defensive(71), Berserker(2458)
- Druid: Bear(5487), Travel(783), Cat(768), Flight(40120),
  Moonkin(24858), Tree(33891)
- Death Knight: Blood(48266), Frost(48263), Unholy(48265)
- Rogue: Stealth(1784)

This makes stance bar buttons functional — clicking a form button
actually casts the corresponding spell to switch forms.

CancelShapeshiftForm stub for cancelling current form.
2026-03-22 22:17:39 -07:00
Kelsi
9986de0529 feat: add GetShapeshiftFormInfo for stance/form bar display
GetShapeshiftFormInfo(index) returns icon, name, isActive, isCastable
for each shapeshift form slot. Provides complete form tables for:

- Warrior: Battle Stance, Defensive Stance, Berserker Stance
- Druid: Bear, Travel, Cat, Swift Flight, Moonkin, Tree of Life
- Death Knight: Blood/Frost/Unholy Presence
- Rogue: Stealth

isActive is true when the form matches the current shapeshiftFormId_.
GetShapeshiftFormCooldown stub returns no cooldown.

Together with GetShapeshiftForm and GetNumShapeshiftForms from the
previous commit, this completes the stance bar API that addons use
to render and interact with form/stance buttons.
2026-03-22 22:14:24 -07:00
Kelsi
587c0ef60d feat: track shapeshift form and fire UPDATE_SHAPESHIFT_FORM events
Add UNIT_FIELD_BYTES_1 to all expansion update field tables (Classic=133,
TBC/WotLK=137). Byte 3 of this field contains the shapeshift form ID
(Bear=1, Cat=3, Travel=4, Moonkin=31, Tree=36, Battle Stance=17, etc.).

Track form changes in the VALUES update handler and fire
UPDATE_SHAPESHIFT_FORM + UPDATE_SHAPESHIFT_FORMS events when the
form changes. This enables stance bar addons and druid form tracking.

New Lua functions:
- GetShapeshiftForm() — returns current form ID (0 = no form)
- GetNumShapeshiftForms() — returns form count by class (Warrior=3,
  Druid=6, DK=3, Rogue=1, Priest=1, Paladin=3)
2026-03-22 22:12:17 -07:00
Kelsi
b9a1b0244b feat: add GetEnchantInfo for enchantment name resolution
GetEnchantInfo(enchantId) looks up the enchantment name from
SpellItemEnchantment.dbc (field 14). Returns the display name
like "Crusader", "+22 Intellect", or "Mongoose" for a given
enchant ID.

Used by equipment comparison addons and tooltip addons to display
enchantment names on equipped gear. The enchant ID comes from the
item's ITEM_FIELD_ENCHANTMENT update field.

Also adds getEnchantName() to GameHandler for C++ access.
2026-03-22 21:53:58 -07:00
Kelsi
922d6bc8f6 feat: clean spell description template variables for readable tooltips
Spell descriptions from DBC contain raw template variables like
\$s1, \$d, \$o1 that refer to effect values resolved at runtime.
Without DBC effect data loaded, these showed as literal "\$s1" in
tooltips, making descriptions hard to read.

Now strips template variables and replaces with readable placeholders:
- \$s1/\$s2/\$s3 → "X" (effect base points)
- \$d → "X sec" (duration)
- \$o1 → "X" (periodic total)
- \$a1 → "X" (radius)
- \$\$ → "$" (literal dollar sign)
- \${...} blocks → stripped

Result: "Hurls a fiery ball that causes X Fire Damage" instead of
"Hurls a fiery ball that causes \$s1 Fire Damage". Not as informative
as real values, but significantly more readable.
2026-03-22 21:32:31 -07:00
Kelsi
42f2873c0d feat: add Mixin, CreateFromMixins, and MergeTable utilities
Implement the WoW Mixin pattern used by modern addons:
- Mixin(obj, ...) — copies fields from mixin tables into obj
- CreateFromMixins(...) — creates a new table from mixin templates
- CreateAndInitFromMixin(mixin, ...) — creates and calls Init()
- MergeTable(dest, src) — shallow-merge src into dest

These enable OOP-style addon architecture used by LibSharedMedia,
WeakAuras, and many Ace3-based addons for class/object creation.
2026-03-22 21:23:00 -07:00
Kelsi
7b88b0c6ec feat: add strgfind and tostringall WoW Lua utilities
strgfind = string.gmatch alias (deprecated WoW function used by
older addons that haven't migrated to string.gmatch).

tostringall(...) converts all arguments to strings and returns
them. Used by chat formatting and debug addons that need to safely
stringify mixed-type argument lists.
2026-03-22 21:17:59 -07:00
Kelsi
f4d78e5820 feat: add SpellStopCasting, name aliases, and targeting stubs
SpellStopCasting() cancels the current cast via cancelCast(). Used by
macro addons and cast-cancel logic (e.g., /stopcasting macro command).

UnitFullName/GetUnitName aliases for UnitName — some addons use these
variant names.

SpellIsTargeting() returns false (no AoE targeting reticle in this
client). SpellStopTargeting() is a no-op stub. Both prevent errors
in addons that check targeting state.
2026-03-22 21:08:18 -07:00
Kelsi
e2fec0933e feat: add GetClassColor and QuestDifficultyColors for UI coloring
GetClassColor(className) returns r, g, b, colorString from the
RAID_CLASS_COLORS table. Used by unit frame addons, chat addons,
and party/raid frames to color player names by class.

QuestDifficultyColors table provides standard quest difficulty
color mappings (impossible=red, verydifficult=orange, difficult=yellow,
standard=green, trivial=gray, header=gold). Used by quest log and
quest tracker addons for level-appropriate coloring.
2026-03-22 21:05:33 -07:00
Kelsi
02456ec7c6 feat: add GetMaxPlayerLevel and GetAccountExpansionLevel
GetMaxPlayerLevel() returns the level cap for the active expansion:
60 (Classic/Turtle), 70 (TBC), 80 (WotLK). Used by XP bar addons
and leveling trackers.

GetAccountExpansionLevel() returns the expansion tier: 1 (Classic),
2 (TBC), 3 (WotLK). Used by addons that adapt features based on
which expansion is active.

Both read from the ExpansionRegistry's active profile at runtime.
2026-03-22 21:01:56 -07:00
Kelsi
bafe036e79 feat: fire CURRENT_SPELL_CAST_CHANGED when player begins casting
CURRENT_SPELL_CAST_CHANGED fires when the player starts a new cast
via handleSpellStart. Some addons register for this as a catch-all
signal that the current spell state changed, complementing the more
specific UNIT_SPELLCAST_START/STOP/FAILED events.
2026-03-22 20:49:25 -07:00
Kelsi
abe5cc73df feat: fire PLAYER_COMBO_POINTS and LOOT_READY events
PLAYER_COMBO_POINTS now fires from the existing SMSG_UPDATE_COMBO_POINTS
handler — the handler already updated comboPoints_ but never notified
Lua addons. Rogue/druid combo point displays and DPS rotation addons
register for this event.

LOOT_READY fires alongside LOOT_OPENED when a loot window opens. Some
addons register for this WoW 5.x+ event name instead of LOOT_OPENED.
2026-03-22 20:46:53 -07:00
Kelsi
3b4909a140 fix: populate item subclass names for TBC expansion
The TBC item query parser left subclassName empty, so TBC items showed
no weapon/armor type in tooltips or the character sheet (e.g., "Sword",
"Plate", "Shield" were all blank). The Classic and WotLK parsers
correctly map subClass IDs to names.

Fix: call getItemSubclassName() in the TBC parser, same as WotLK.
Expose getItemSubclassName() in the header (was static, now shared
across parser files).
2026-03-22 20:30:08 -07:00
Kelsi
7967878cd9 feat: show Unique and Heroic tags on item tooltips
Items with maxCount=1 now show "Unique" in white text below the name.
Items with the Heroic flag (0x8) show "Heroic" in green text. Both
display before the bind type line, matching WoW's tooltip order.

Heroic items (from heroic dungeon/raid drops) are visually
distinguished from their normal-mode counterparts. Unique items
(trinkets, quest items, etc.) show the carry limit clearly.
2026-03-22 20:25:41 -07:00
Kelsi
1b075e17f1 feat: show item spell effects in tooltips (Use/Equip/Chance on Hit)
Item tooltips now display spell effects in green text:
- "Use: Restores 2200 health over 30 sec" (trigger 0)
- "Equip: Increases attack power by 120" (trigger 1)
- "Chance on hit: Strikes the enemy for 95 Nature damage" (trigger 2)

Passes up to 5 item spell entries through _GetItemTooltipData with
spellId, trigger type, spell name, and spell description from DBC.
The tooltip builder maps trigger IDs to "Use: ", "Equip: ", or
"Chance on hit: " prefixes.

This completes the item tooltip with all major WoW tooltip sections:
quality name, bind type, equip slot/type, armor, damage/DPS/speed,
primary stats, combat ratings, resistances, spell effects, gem sockets,
required level, flavor text, and sell price.
2026-03-22 20:17:50 -07:00
Kelsi
216c83d445 feat: add spell descriptions to tooltips via GetSpellDescription
Spell tooltips now show the spell description text (e.g., "Hurls a
fiery ball that causes 565 to 655 Fire Damage") in gold/yellow text
between the cast info and cooldown display.

New GetSpellDescription(spellId) C function exposes the description
field from SpellNameEntry (loaded from Spell.dbc via the spell name
cache). Descriptions contain the raw DBC text which may include
template variables ($s1, $d, etc.) — these show as-is until template
substitution is implemented.
2026-03-22 20:03:18 -07:00
Kelsi
572b3ce7ca feat: show gem sockets and item set ID in item tooltips
Add gem socket display to item tooltips — shows [Meta Socket],
[Red Socket], [Yellow Socket], [Blue Socket], or [Prismatic Socket]
based on socketColor mask from ItemQueryResponseData.

Also pass itemSetId through _GetItemTooltipData for addons that
track set bonuses.
2026-03-22 19:59:52 -07:00
Kelsi
a70f42d4f6 feat: add UnitRage, UnitEnergy, UnitFocus, UnitRunicPower aliases
Register power-type-specific aliases (UnitRage, UnitEnergy, UnitFocus,
UnitRunicPower) that map to the existing lua_UnitPower function. Some
Classic/TBC addons call these directly instead of the generic UnitPower.
All return the unit's current power value regardless of type — the
underlying function reads from the entity's power field.
2026-03-22 19:37:58 -07:00
Kelsi
2f4065cea0 feat: wire PlaySound to real audio engine for addon sound feedback
Replace PlaySound no-op stub with a real implementation that maps
WoW sound IDs and names to the UiSoundManager methods:

By ID: 856/1115→button click, 840→quest activate, 841→quest complete,
       862→bag open, 863→bag close, 888→level up
By name: IGMAINMENUOPTION→click, IGQUESTLISTOPEN→quest activate,
         IGQUESTLISTCOMPLETE→quest complete, IGBACKPACKOPEN/CLOSE→bags,
         LEVELUPSOUND→level up, TALENTSCREEN→character sheet

This gives addons audio feedback when they call PlaySound() — button
clicks, quest sounds, and other UI sounds now actually play instead
of being silently swallowed.
2026-03-22 19:35:14 -07:00
Kelsi
aa164478e1 feat: fire DISPLAY_SIZE_CHANGED and UNIT_QUEST_LOG_CHANGED events
DISPLAY_SIZE_CHANGED fires when the window is resized via
SDL_WINDOWEVENT_RESIZED, allowing UI addons to adapt their layout
to the new screen dimensions (5 FrameXML registrations).

UNIT_QUEST_LOG_CHANGED("player") fires alongside QUEST_LOG_UPDATE
at all 6 quest log modification points, for addons that register
for this variant instead (4 FrameXML registrations).
2026-03-22 19:29:06 -07:00
Kelsi
2365091266 fix: tighten addon message detection to avoid suppressing regular chat
The tab-based addon message detection was too aggressive — any chat
message containing a tab character was treated as an addon message
and silently dropped from regular chat display. This could suppress
legitimate player messages containing tabs (from copy-paste).

Now only matches as addon message when:
- Chat type is PARTY/RAID/GUILD/WHISPER/etc. (not SAY/YELL/EMOTE)
- Prefix before tab is <=16 chars (WoW addon prefix limit)
- Prefix contains no spaces (addon prefixes are identifiers)

This prevents false positives while still correctly detecting addon
messages formatted as "DBM4\ttimer:start:10".
2026-03-22 19:17:24 -07:00
Kelsi
40907757b0 feat: fire UNIT_QUEST_LOG_CHANGED alongside QUEST_LOG_UPDATE
UNIT_QUEST_LOG_CHANGED("player") now fires at all 6 locations where
QUEST_LOG_UPDATE fires — quest accept, complete, objective update,
abandon, and server-driven quest log changes. Some addons register
for this event instead of QUEST_LOG_UPDATE (4 registrations in
FrameXML). Both events are semantically equivalent for the player.
2026-03-22 19:12:29 -07:00
Kelsi
96c5f27160 feat: fire CHAT_MSG_ADDON for inter-addon communication messages
Detect addon messages in the SMSG_MESSAGECHAT handler by looking for
the 'prefix\ttext' format (tab delimiter). When detected, fire
CHAT_MSG_ADDON with args (prefix, message, channel, sender) instead
of the regular CHAT_MSG_* event, and suppress the raw message from
appearing in chat.

This enables inter-addon communication used by:
- Boss mods (DBM, BigWigs) for timer/alert synchronization
- Raid tools (oRA3) for ready checks and cooldown tracking
- Group coordination addons for pull countdowns and assignments

Works with the existing SendAddonMessage/RegisterAddonMessagePrefix
functions that format outgoing messages as 'prefix\ttext'.
2026-03-22 19:08:51 -07:00
Kelsi
491dd2b673 feat: fire SPELL_UPDATE_USABLE alongside ACTIONBAR_UPDATE_USABLE
Some addons register for SPELL_UPDATE_USABLE instead of
ACTIONBAR_UPDATE_USABLE to detect when spell usability changes
due to power fluctuations. Fire both events together when the
player's mana/rage/energy changes.
2026-03-22 19:00:34 -07:00
Kelsi
661fba12c0 feat: fire ACTIONBAR_UPDATE_USABLE on player power changes
When the player's mana/rage/energy changes, action bar addons need
ACTIONBAR_UPDATE_USABLE to update button dimming (grey out abilities
the player can't afford). Now fires from both the SMSG_POWER_UPDATE
handler and the SMSG_UPDATE_OBJECT VALUES power field change path.

Without this event, action bar buttons showed as usable even when the
player ran out of mana — the usability state only refreshed on spell
cast attempts, not on power changes.
2026-03-22 18:53:11 -07:00
Kelsi
8fd1dfb4f1 feat: add UnitArmor and UnitResistance for character sheet addons
UnitArmor(unit) returns base, effective, armor, posBuff, negBuff
matching WoW's 5-return signature. Uses server-authoritative armor
from UNIT_FIELD_RESISTANCES[0].

UnitResistance(unit, school) returns base, effective, posBuff, negBuff
for physical (school 0 = armor) and magical resistances (1-6:
Holy/Fire/Nature/Frost/Shadow/Arcane).

Needed by character sheet addons (PaperDollFrame, DejaCharacterStats)
to display armor and resistance values.
2026-03-22 18:44:43 -07:00
Kelsi
f951dbb95d feat: add merchant/vendor API for auto-sell and vendor addons
Implement core vendor query functions from ListInventoryData:

- GetMerchantNumItems() — count of items for sale
- GetMerchantItemInfo(index) — name, texture, price, stackCount,
  numAvailable, isUsable for each vendor item
- GetMerchantItemLink(index) — quality-colored item link
- CanMerchantRepair() — whether vendor offers repair service

Enables auto-sell addons (AutoVendor, Scrap) to read vendor inventory
and check repair capability. Data sourced from SMSG_LIST_INVENTORY
via currentVendorItems + itemInfoCache for names/icons.
2026-03-22 18:39:27 -07:00
Kelsi
81180086e3 feat: fire UNIT_DISPLAYPOWER when unit power type changes
When a unit's power type changes (e.g., druid shifting from mana to
rage in bear form, or warrior switching stances), fire UNIT_DISPLAYPOWER
so unit frame addons can switch the power bar display (mana blue bar →
rage red bar). Detected via UNIT_FIELD_BYTES_0 byte 3 changes in the
VALUES update path.
2026-03-22 18:33:56 -07:00
Kelsi
92a1e9b0c3 feat: add RAID_ROSTER_UPDATE and UNIT_LEVEL events
RAID_ROSTER_UPDATE now fires alongside GROUP_ROSTER_UPDATE when the
group type is raid, matching the event that raid frame addons register
for (6 registrations in FrameXML). Fires from group list updates and
group uninvite handlers.

UNIT_LEVEL fires when any tracked unit (player, target, focus, pet)
changes level via VALUES update fields. Used by unit frame addons to
update level display (5 registrations in FrameXML).
2026-03-22 18:30:07 -07:00
Kelsi
e46919cc2c fix: use rich tooltip display for SetAction items and SetHyperlink
SetAction's item branch and SetHyperlink's item/spell branches
showed only the item name, ignoring the full tooltip system we built.

SetAction item path now uses _WoweePopulateItemTooltip (shows armor,
stats, damage, bind type, sell price etc.).

SetHyperlink item path now uses _WoweePopulateItemTooltip; spell
path now uses SetSpellByID (shows cost, range, cast time, cooldown).

This means shift-clicking an item link in chat, hovering an item on
the action bar, or viewing any hyperlink tooltip now shows the full
stat breakdown instead of just the name.
2026-03-22 18:22:56 -07:00
Kelsi
8d4478b87c feat: enhance spell tooltips with cost, range, cast time, and cooldown
SetSpellByID now shows comprehensive spell information:
- Mana/Rage/Energy/Runic Power cost
- Range in yards (or omitted for self-cast)
- Cast time ("1.5 sec cast" or "Instant")
- Active cooldown remaining in red

Uses existing GetSpellInfo (castTime, range from DBC), GetSpellPowerCost
(mana cost from DBC), and GetSpellCooldown (remaining CD) to populate
the tooltip with real spell data.
2026-03-22 18:19:51 -07:00
Kelsi
22f8b721c7 feat: show sell price on item tooltips in gold/silver/copper format
Item tooltips now display the vendor sell price at the bottom, formatted
as "Sell Price: 12g 50s 30c". Uses the vendorPrice field from GetItemInfo
(field 11, sourced from SMSG_ITEM_QUERY_SINGLE_RESPONSE sellPrice).

Helps players quickly assess item value when looting or sorting bags.
2026-03-22 18:17:21 -07:00
Kelsi
5678de562f feat: add combat ratings, resistances to item tooltip display
Extend item tooltips with secondary stats and resistances:

- Extra stats from ItemQueryResponseData.extraStats: Hit Rating,
  Crit Rating, Haste Rating, Resilience, Attack Power, Spell Power,
  Defense Rating, Dodge/Parry/Block Rating, Expertise, Armor Pen,
  Mana/Health per 5 sec, Spell Penetration — all in green text
- Elemental resistances: Fire/Nature/Frost/Shadow/Arcane Resistance

Also passes extraStats as an array of {type, value} pairs and
resistance fields through _GetItemTooltipData for Lua consumption.

Stat type IDs follow the WoW ItemMod enum (3=Agi, 7=Sta, 31=Hit,
32=Crit, 36=Haste, 45=SpellPower, etc.).
2026-03-22 18:13:23 -07:00
Kelsi
0e78427767 feat: add full item stat display to tooltips (armor, damage, stats, bind)
Enhance _WoweePopulateItemTooltip to show complete item information:
- Bind type (Binds when picked up / equipped / used)
- Armor value for armor items
- Weapon damage range, speed, and DPS for weapons
- Primary stats (+Stamina, +Strength, +Agility, +Intellect, +Spirit)
  in green text
- Required level
- Flavor/lore description text in gold

Backed by new _GetItemTooltipData(itemId) C function that returns a
Lua table with armor, bindType, damageMin/Max, speed, primary stats,
requiredLevel, and description from ItemQueryResponseData.
2026-03-22 18:07:58 -07:00
Kelsi
e72d6ad852 feat: enhance item tooltips with item level, equip slot, and subclass
Replace minimal name-only item tooltips with proper WoW-style display:
- Quality-colored item name header
- Item level line for equipment (gold text)
- Equip slot and weapon/armor type on a double line
  (e.g., "Head" / "Plate" or "One-Hand" / "Sword")
- Item class for non-equipment items

Shared _WoweePopulateItemTooltip() helper used by both
SetInventoryItem and SetBagItem for consistent tooltip formatting.
Maps INVTYPE_* strings to display names (Head, Chest, Two-Hand, etc.).
2026-03-22 18:02:46 -07:00
Kelsi
101ea9fd17 fix: populate item class, subclass, and equip slot in GetItemInfo
GetItemInfo returned empty strings for item class (field 6), subclass
(field 7), and equip slot (field 9). Now returns:

- Class: mapped from itemClass enum (Consumable, Weapon, Armor, etc.)
- Subclass: from parsed subclassName (Sword, Mace, Shield, etc.)
- EquipSlot: mapped from inventoryType to INVTYPE_ strings
  (INVTYPE_HEAD, INVTYPE_CHEST, INVTYPE_WEAPON, etc.)

These fields are used by equipment comparison addons, character sheet
displays, and bag sorting addons to categorize and filter items.
2026-03-22 17:58:39 -07:00
Kelsi
a7e8a6eb83 fix: unify time epoch across GetTime, cooldowns, auras, and cast bars
Four independent static local steady_clock start times were used as
time origins in GetTime(), GetSpellCooldown(), UnitBuff expiration,
and UnitCastingInfo — each initializing on first call at slightly
different times. This created systematic timestamp mismatches.

When addons compute (start + duration - GetTime()), the start value
from GetSpellCooldown and the GetTime() return used different epochs,
causing cooldown sweeps and buff timers to appear offset.

Replace all four independent statics with a single file-scope
kLuaTimeEpoch constant and luaGetTimeNow() helper, ensuring all
time-returning Lua functions share exactly the same origin.
2026-03-22 17:48:37 -07:00
Kelsi
c3fd6d2f85 feat: add keybinding query API for action bar tooltips and binding UI
Implement GetBindingKey(command) and GetBindingAction(key) with
default action button mappings (ACTIONBUTTON1-12 → "1"-"9","0","-","=").
Action bar addons display bound keys on button tooltips via
GetBindingKey("ACTIONBUTTON"..slot).

Also add stubs for GetNumBindings, GetBinding, SetBinding, SaveBindings,
SetOverrideBindingClick, and ClearOverrideBindings to prevent nil-call
errors in FrameXML keybinding UI code (37 call sites).
2026-03-22 17:43:54 -07:00
Kelsi
b25dba8069 fix: fire ACTIONBAR_SLOT_CHANGED when assigning spells to action bar
setActionBarSlot (called from PickupAction/PlaceAction drag-drop and
from server-driven action button updates) updated the slot data and
notified the server, but never fired the Lua addon event. Action bar
addons (Bartender4, Dominos) register for ACTIONBAR_SLOT_CHANGED to
refresh button textures, tooltips, and cooldown state when slots change.

Also fires ACTIONBAR_UPDATE_STATE for general action bar refresh.
2026-03-22 17:37:33 -07:00
Kelsi
d00ebd00a0 fix: fire PLAYER_DEAD, PLAYER_ALIVE, and PLAYER_UNGHOST death events
PLAYER_DEAD only fired from SMSG_FORCED_DEATH_UPDATE (GM kill) — the
normal death path (health dropping to 0 via VALUES update) never fired
it. Death-related addons and the default release spirit dialog depend
on this event.

Also add PLAYER_ALIVE (fires when resurrected without having been a
ghost) and PLAYER_UNGHOST (fires when player rezzes from ghost form)
at the health-restored-from-zero VALUES path. These events control
the transition from ghost form back to alive, letting addons restore
normal UI state after death.
2026-03-22 17:33:22 -07:00
Kelsi
2a2db5cfb5 fix: fire CHAT_MSG_TEXT_EMOTE for incoming text emotes
handleTextEmote pushed emote messages directly to chatHistory
instead of using addLocalChatMessage, so Lua chat addons never
received CHAT_MSG_TEXT_EMOTE events for /wave, /dance, /bow, etc.
from other players. Use addLocalChatMessage which fires the event
and also notifies the C++ display callback.
2026-03-22 17:28:33 -07:00
Kelsi
25b35d5224 fix: include GCD in GetSpellCooldown and GetActionCooldown returns
GetSpellCooldown only returned per-spell cooldowns from the
spellCooldowns map, ignoring the Global Cooldown. Addons like OmniCC
and action bar addons rely on GetSpellCooldown returning GCD timing
when no individual spell cooldown is active — this is what drives the
cooldown sweep animation on action bar buttons after casting.

Now falls back to GCD timing (from getGCDRemaining/getGCDTotal) when
the spell has no individual cooldown but the GCD is active. Returns
proper (start, duration) values so addons can compute elapsed/remaining.

Same fix applied to GetActionCooldown for spell-type action bar slots.
2026-03-22 17:23:52 -07:00
Kelsi
015574f0bd feat: add modifier key queries and resting/exhaustion state events
Implement keyboard modifier queries using ImGui IO:
- IsShiftKeyDown, IsControlKeyDown, IsAltKeyDown
- IsModifiedClick(action) — CHATLINK=Shift, DRESSUP=Ctrl, SELFCAST=Alt
- GetModifiedClick/SetModifiedClick for keybind configuration

Fire UPDATE_EXHAUSTION and PLAYER_UPDATE_RESTING events when rest state
changes (entering/leaving inns and cities) and when rested XP updates.
XP bar addons use UPDATE_EXHAUSTION to show the rested bonus indicator.
2026-03-22 17:19:57 -07:00
Kelsi
bf8c0aaf1a feat: add modifier key queries and IsModifiedClick for input handling
Implement keyboard modifier state queries using ImGui IO state:
- IsShiftKeyDown, IsControlKeyDown, IsAltKeyDown — direct key queries
- IsModifiedClick(action) — checks if the modifier matching a named
  action is held (CHATLINK/SPLITSTACK=Shift, DRESSUP=Ctrl, SELFCAST=Alt)
- GetModifiedClick(action) — returns the assigned key name for an action
- SetModifiedClick — no-op stub for compatibility

These are fundamental input functions used by virtually all interactive
addons: shift-click to link items in chat, ctrl-click for dressup,
alt-click for self-cast, shift-click to split stacks, etc.
2026-03-22 17:12:57 -07:00
Kelsi
ebe52d3eba fix: color item links by quality in GetItemInfo and GameTooltip:GetItem
GetItemInfo returned item links with hardcoded white color (|cFFFFFFFF)
regardless of quality. Now uses quality-appropriate colors: gray for
Poor, white for Common, green for Uncommon, blue for Rare, purple for
Epic, orange for Legendary, gold for Artifact, cyan for Heirloom.

Also fix GameTooltip:GetItem() to use the quality-colored link from
GetItemInfo instead of hardcoded white.
2026-03-22 17:08:31 -07:00
Kelsi
74ef454538 feat: add raid roster info, threat colors, and unit watch for raid frames
GetRaidRosterInfo(index) returns name, rank, subgroup, level, class,
fileName, zone, online, isDead, role, isML — the core function raid
frame addons (Grid, Healbot, VuhDo) use to populate unit frame data.
Resolves class from UNIT_FIELD_BYTES_0 when entity is available.

GetThreatStatusColor(status) returns RGB for threat indicator coloring
(gray/yellow/orange/red). Used by unit frames and threat meters.

GetReadyCheckStatus(unit) stub returns nil (no check in progress).
RegisterUnitWatch/UnregisterUnitWatch stubs for secure frame compat.
2026-03-22 17:04:56 -07:00
Kelsi
4e04050f91 feat: add GetItemQualityColor, GetItemCount, and UseContainerItem
GetItemQualityColor(quality) returns r, g, b, hexString for item
quality coloring (Poor=gray through Heirloom=cyan). Used by bag
addons, tooltips, and item frames to color item names/borders.

GetItemCount(itemId) counts total stacks across backpack + bags.
Used by addons to check material availability, quest item counts,
and consumable tracking.

UseContainerItem(bag, slot) uses/equips an item from a container
slot, delegating to useItemById for the actual equip/use action.
2026-03-22 17:01:16 -07:00
Kelsi
e4da47b0d7 feat: add UI_ERROR_MESSAGE events and quest removal notifications
Fire UI_ERROR_MESSAGE from addUIError() so Lua addons can react to
error messages like "Not enough mana" or "Target is too far away".
Previously only the C++ overlay callback was notified. Also add
addUIInfoMessage() helper for informational system messages.

Fire QUEST_REMOVED and QUEST_LOG_UPDATE when quests are removed from
the quest log — both via server-driven removal (SMSG_QUEST_UPDATE_FAILED
etc.) and player-initiated abandon (CMSG_QUESTLOG_REMOVE_QUEST). Quest
tracking addons like Questie register for these events to update their
map markers and objective displays.
2026-03-22 16:53:35 -07:00
Kelsi
8fd735f4a3 fix: fire UNIT_AURA event for Classic/Turtle field-based aura updates
Classic and Turtle WoW don't use SMSG_AURA_UPDATE packets — they
pack aura data into UNIT_FIELD_AURAS update fields. The code correctly
rebuilds playerAuras from these fields in both CREATE_OBJECT and VALUES
update paths, but never fired the UNIT_AURA("player") addon event.

Buff frame addons (Buffalo, ElkBuffBars, etc.) register for UNIT_AURA
to refresh their display. Without this event, buff frames on Classic
and Turtle never update when buffs are gained or lost.
2026-03-22 16:47:49 -07:00
Kelsi
a8c241f6bd fix: fire CHAT_MSG_* events for player's own messages and system text
addLocalChatMessage only pushed to chatHistory and called the C++
display callback — it never fired Lua addon events. This meant the
player's own sent messages (local echoes from sendChatMessage) and
system messages (loot, XP gains, errors) were invisible to Lua chat
frame addons.

Now fires CHAT_MSG_{type} with the full 12-arg WoW signature from
addLocalChatMessage, matching the incoming message path. Uses the
active character name as sender for player-originated messages.
2026-03-22 16:43:13 -07:00
Kelsi
f37a83fc52 feat: fire CHAT_MSG_* events for all incoming chat messages
The SMSG_MESSAGECHAT handler stored messages in chatHistory and
triggered chat bubbles, but never fired Lua addon events. Chat frame
addons (Prat, Chatter, WIM) and the default ChatFrame all register for
CHAT_MSG_SAY, CHAT_MSG_WHISPER, CHAT_MSG_PARTY, CHAT_MSG_GUILD, etc.
to display incoming messages.

Now fires CHAT_MSG_{type} for every incoming message with the full WoW
event signature: message, senderName, language, channelName, displayName,
specialFlags, zoneChannelID, channelIndex, channelBaseName, unused,
lineID, and senderGUID. Covers all chat types: SAY, YELL, WHISPER,
PARTY, RAID, GUILD, OFFICER, CHANNEL, EMOTE, SYSTEM, MONSTER_SAY, etc.
2026-03-22 16:38:34 -07:00
Kelsi
7425881e98 feat: add UI panel management, scroll frames, and macro parsing stubs
Implement high-frequency FrameXML infrastructure functions:

- ShowUIPanel/HideUIPanel/ToggleFrame — UI panel show/hide (240+ calls
  in FrameXML). ShowUIPanel delegates to frame:Show(), HideUIPanel to
  frame:Hide().
- TEXT(str) — localization identity function (549 calls)
- FauxScrollFrame_GetOffset/Update/SetOffset/OnVerticalScroll — scroll
  list helpers used by quest log, guild roster, friends list, etc.
- SecureCmdOptionParse — basic macro conditional parser, returns
  unconditional fallback text
- ChatFrame_AddMessageGroup/RemoveMessageGroup/AddChannel/RemoveChannel
  — chat frame configuration stubs
- UIPanelWindows table, GetUIPanel, CloseWindows stubs
2026-03-22 16:33:57 -07:00
Kelsi
9a570b49db feat: implement cursor/drag-drop system for action bar and inventory
Add the complete cursor state machine needed for drag-and-drop:
- PickupAction(slot) — pick up or swap action bar slots
- PlaceAction(slot) — place cursor content into action bar
- PickupSpell / PickupSpellBookItem — drag spells from spellbook
- PickupContainerItem(bag, slot) — drag items from bags
- PickupInventoryItem(slot) — drag equipped items
- ClearCursor / DeleteCursorItem — clear cursor state
- GetCursorInfo — returns cursor content type/id
- CursorHasItem / CursorHasSpell — query cursor state
- AutoEquipCursorItem — equip item from cursor

Cursor state tracks type (SPELL/ITEM/ACTION), id, and source slot.
PickupAction on empty slots with a spell cursor auto-assigns spells
to the action bar. Enables spellbook-to-action-bar drag-drop and
inventory management through the WoW UI.
2026-03-22 16:30:31 -07:00
Kelsi
be4cbad0b0 fix: unify lava UV scroll timer across render passes to prevent flicker
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Lava M2 models used independent static-local start times in pass 1
and pass 2 for UV scroll animation. Since static locals initialize
on first call, the two timers started at slightly different times
(microseconds to frames apart), causing a permanent UV offset mismatch
between passes — visible as texture flicker/jumping on lava surfaces.

Replace both function-scoped statics with a single file-scoped
kLavaAnimStart constant, ensuring both passes compute identical UV
offsets from the same epoch.
2026-03-22 16:25:32 -07:00
Kelsi
b6047cdce8 feat: add world map navigation API for WorldMapFrame compatibility
Implement the core map functions needed by WorldMapFrame.lua:
- SetMapToCurrentZone — sets map view from player's current mapId/zone
- GetCurrentMapContinent — returns continent (1=Kalimdor, 2=EK, etc.)
- GetCurrentMapZone — returns current zone ID
- SetMapZoom(continent, zone) — navigate map view
- GetMapContinents — returns continent name list
- GetMapZones(continent) — returns zone names per continent
- GetNumMapLandmarks — stub (returns 0)

Maps game mapId (0=EK, 1=Kalimdor, 530=Outland, 571=Northrend) to
WoW's continent numbering. Internal state tracks which continent/zone
the map UI is currently viewing.
2026-03-22 16:18:52 -07:00
Kelsi
f9856c1046 feat: implement GameTooltip methods with real item/spell/aura data
Replace empty stub GameTooltip methods with working implementations:

- SetInventoryItem(unit, slot): populates tooltip with equipped item
  name and quality-colored text via GetInventoryItemLink + GetItemInfo
- SetBagItem(bag, slot): populates from GetContainerItemInfo + GetItemInfo
- SetSpellByID(spellId): populates with spell name/rank from GetSpellInfo
- SetAction(slot): delegates to SetSpellByID or item lookup via GetActionInfo
- SetUnitBuff/SetUnitDebuff: populates from UnitBuff/UnitDebuff data
- SetHyperlink: parses item: and spell: links to populate name
- GetItem/GetSpell: now return real item/spell data when tooltip is populated

Also fix GetCVar/SetCVar conflict — remove Lua-side overrides that
were shadowing the C-side implementations (which return real screen
dimensions and sensible defaults for common CVars).
2026-03-22 16:13:39 -07:00
Kelsi
31ab76427f fix: remove dead duplicate ufNpcFlags check and add missing UNIT_MODEL_CHANGED events
In the CREATE update block, the ufNpcFlags check at the end of the
else-if chain was unreachable dead code — it was already handled
earlier in the same chain. Remove the duplicate.

In the VALUES update block, mount display changes via field updates
fired mountCallback_ but not the UNIT_MODEL_CHANGED addon event,
unlike the CREATE path which fired both. Add the missing event so
Lua addons are notified when the player mounts/dismounts via VALUES
updates (the common case for aura-based mounting).

Also fire UNIT_MODEL_CHANGED for target/focus/pet display ID changes
in the VALUES displayIdChanged path, matching the CREATE path behavior.
2026-03-22 16:09:57 -07:00
Kelsi
cbdf03c07e feat: add quest objective leaderboard API for WatchFrame quest tracking
Implement GetNumQuestLeaderBoards and GetQuestLogLeaderBoard — the core
functions WatchFrame.lua and QuestLogFrame.lua use to display objective
progress like "Kobold Vermin slain: 3/8" or "Linen Cloth: 2/6".

GetNumQuestLeaderBoards counts kill + item objectives from the parsed
SMSG_QUEST_QUERY_RESPONSE data. GetQuestLogLeaderBoard returns the
formatted progress text, type ("monster"/"item"/"object"), and
completion status for each objective.

Also adds ExpandQuestHeader/CollapseQuestHeader (no-ops for flat quest
list) and GetQuestLogSpecialItemInfo stub.
2026-03-22 16:04:33 -07:00
Kelsi
296121f5e7 feat: add GetPlayerFacing, GetCVar/SetCVar for minimap and addon settings
GetPlayerFacing() returns player orientation in radians, needed by
minimap addons for arrow rotation and facing-dependent mechanics.

GetCVar(name) returns sensible defaults for commonly queried CVars
(uiScale, screen dimensions, nameplate visibility, sound toggles,
autoLoot). SetCVar is a no-op stub for addon compatibility.
2026-03-22 15:58:45 -07:00
Kelsi
73ce601bb5 feat: fire PLAYER_ENTERING_WORLD and critical login events for addons
PLAYER_ENTERING_WORLD is the single most important WoW addon event —
virtually every addon registers for it to initialize UI, state, and
data structures. It was never fired, causing widespread addon init
failures on login and after teleports.

Now fired from:
- handleLoginVerifyWorld (initial login + same-map teleports)
- handleNewWorld (cross-map teleports, instance transitions)

Also fires:
- PLAYER_LOGIN on initial world entry only
- ZONE_CHANGED_NEW_AREA on all world entries
- UPDATE_WORLD_STATES on initial entry
- SPELLS_CHANGED + LEARNED_SPELL_IN_TAB after SMSG_INITIAL_SPELLS
  (so spell book addons can initialize on login)
2026-03-22 15:50:05 -07:00
Kelsi
5086520354 feat: add spell book tab API for SpellBookFrame addon compatibility
Implement GetNumSpellTabs, GetSpellTabInfo, GetSpellBookItemInfo, and
GetSpellBookItemName — the core functions SpellBookFrame.lua needs to
organize known spells into class skill line tabs.

Tabs are built lazily from knownSpells grouped by SkillLineAbility.dbc
mappings (category 7 = class). A "General" tab collects spells not in
any class skill line. Tabs auto-rebuild when the spell count changes.

Also adds SpellBookTab struct and getSpellBookTabs() to GameHandler.
2026-03-22 15:40:40 -07:00
Kelsi
f29ebbdd71 feat: add quest watch/tracking and selection Lua API for WatchFrame
Implement the quest tracking functions needed by WatchFrame.lua:
- SelectQuestLogEntry/GetQuestLogSelection — quest log selection state
- GetNumQuestWatches — count of tracked quests
- GetQuestIndexForWatch(watchIdx) — map Nth watched quest to log index
- AddQuestWatch/RemoveQuestWatch — toggle quest tracking by log index
- IsQuestWatched — check if a quest log entry is tracked
- GetQuestLink — generate colored quest link string

Backed by existing trackedQuestIds_ set and questLog_ vector.
Adds selectedQuestLogIndex_ state to GameHandler for quest selection.
2026-03-22 15:36:25 -07:00
584 changed files with 182015 additions and 85493 deletions

47
.clang-tidy Normal file
View file

@ -0,0 +1,47 @@
# clang-tidy configuration for WoWee
# Targets C++20. Checks are tuned for a Vulkan/game-engine codebase:
# - reinterpret_cast, pointer arithmetic, and magic numbers are frequent
# in low-level graphics/network code, so the most aggressive
# cppcoreguidelines and readability-magic-numbers checks are disabled.
---
Checks: >
bugprone-*,
clang-analyzer-*,
performance-*,
modernize-use-nullptr,
modernize-use-override,
modernize-use-default-member-init,
modernize-use-emplace,
modernize-loop-convert,
modernize-deprecated-headers,
modernize-make-unique,
modernize-make-shared,
modernize-use-nodiscard,
modernize-use-designated-initializers,
readability-braces-around-statements,
readability-container-size-empty,
readability-delete-null-pointer,
readability-else-after-return,
readability-misplaced-array-index,
readability-non-const-parameter,
readability-redundant-control-flow,
readability-redundant-declaration,
readability-simplify-boolean-expr,
readability-string-compare,
-bugprone-easily-swappable-parameters,
-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
-performance-avoid-endl
WarningsAsErrors: ''
# Suppress the noise from GCC-only LTO flags in compile_commands.json.
# clang doesn't support -fno-fat-lto-objects; this silences the harmless warning.
ExtraArgs:
- -std=c++20
- -Wno-ignored-optimization-argument
HeaderFilterRegex: '^.*/include/.*\.hpp$'
CheckOptions:
- key: modernize-use-default-member-init.UseAssignment
value: true

View file

@ -1 +0,0 @@
{"sessionId":"55a28c7e-8043-44c2-9829-702f303c84ba","pid":3880168,"acquiredAt":1773085726967}

50
.dockerignore Normal file
View file

@ -0,0 +1,50 @@
# .dockerignore — Exclude files from the Docker build context.
# Keeps the context small and prevents leaking build artifacts or secrets.
# Build outputs
build/
cache/
# Git history
.git/
.gitignore
.github/
# Large external directories (fetched at build time inside the container)
extern/FidelityFX-FSR2/
extern/FidelityFX-SDK/
# IDE / editor files
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Documentation (not needed for build)
docs/
*.md
!container/*.md
# Test / tool outputs
logs/
# Host build scripts that run outside the container (not needed inside)
build.sh
build.bat
build.ps1
rebuild.sh
rebuild.bat
rebuild.ps1
clean.sh
debug_texture.*
extract_assets.*
extract_warden_rsa.py
restart-worldserver.sh
test.sh
# macOS SDK tarballs that may be temporarily placed here
*.tar.xz
*.tar.gz
*.tar.bz2

View file

@ -2,9 +2,9 @@ name: Build
on:
push:
branches: [master]
branches: [ master ]
pull_request:
branches: [master]
branches: [ master ]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@ -21,105 +21,110 @@ jobs:
fail-fast: false
matrix:
include:
- arch: x86-64
runner: ubuntu-24.04
deb_arch: amd64
- arch: arm64
runner: ubuntu-24.04-arm
deb_arch: arm64
- arch: x86-64
runner: ubuntu-24.04
deb_arch: amd64
build_jobs: $(nproc)
- arch: arm64
runner: ubuntu-24.04-arm
deb_arch: arm64
build_jobs: 2
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Cache apt packages
uses: actions/cache@v4
with:
path: /var/cache/apt/archives/*.deb
key: apt-${{ matrix.arch }}-${{ hashFiles('.github/workflows/build.yml') }}
restore-keys: apt-${{ matrix.arch }}-
- name: Cache apt packages
uses: actions/cache@v4
with:
path: /var/cache/apt/archives/*.deb
key: apt-${{ matrix.arch }}-${{ hashFiles('.github/workflows/build.yml') }}
restore-keys: apt-${{ matrix.arch }}-
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
cmake \
build-essential \
pkg-config \
libsdl2-dev \
libglew-dev \
libglm-dev \
libssl-dev \
zlib1g-dev \
libvulkan-dev \
vulkan-tools \
glslc \
libavformat-dev \
libavcodec-dev \
libswscale-dev \
libavutil-dev \
libunicorn-dev \
libx11-dev
sudo apt-get install -y libstorm-dev || true
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
cmake \
build-essential \
pkg-config \
libsdl2-dev \
libglew-dev \
libglm-dev \
libssl-dev \
zlib1g-dev \
libvulkan-dev \
vulkan-tools \
glslc \
libavformat-dev \
libavcodec-dev \
libswscale-dev \
libavutil-dev \
libunicorn-dev \
libx11-dev
sudo apt-get install -y libstorm-dev || true
- name: Fetch AMD FSR2 SDK
run: |
rm -rf extern/FidelityFX-FSR2
git clone --depth 1 --branch "${WOWEE_AMD_FSR2_REF}" "${WOWEE_AMD_FSR2_REPO}" extern/FidelityFX-FSR2
rm -rf extern/FidelityFX-SDK
git clone --depth 1 --branch "${WOWEE_FFX_SDK_REF}" "${WOWEE_FFX_SDK_REPO}" extern/FidelityFX-SDK
- name: Fetch AMD FSR2 SDK
run: |
rm -rf extern/FidelityFX-FSR2
git clone --depth 1 --branch "${WOWEE_AMD_FSR2_REF}" "${WOWEE_AMD_FSR2_REPO}" extern/FidelityFX-FSR2
rm -rf extern/FidelityFX-SDK
git clone --depth 1 --branch "${WOWEE_FFX_SDK_REF}" "${WOWEE_FFX_SDK_REPO}" extern/FidelityFX-SDK
- name: Check AMD FSR2 Vulkan permutation headers
run: |
set -euo pipefail
SDK_DIR="$PWD/extern/FidelityFX-FSR2"
OUT_DIR="$SDK_DIR/src/ffx-fsr2-api/vk/shaders"
if [ -f "$OUT_DIR/ffx_fsr2_accumulate_pass_permutations.h" ]; then
echo "AMD FSR2 Vulkan permutation headers detected."
else
echo "AMD FSR2 Vulkan permutation headers not found in SDK checkout."
echo "WoWee CMake will bootstrap vendored headers."
fi
- name: Check AMD FSR2 Vulkan permutation headers
run: |
set -euo pipefail
SDK_DIR="$PWD/extern/FidelityFX-FSR2"
OUT_DIR="$SDK_DIR/src/ffx-fsr2-api/vk/shaders"
if [ -f "$OUT_DIR/ffx_fsr2_accumulate_pass_permutations.h" ]; then
echo "AMD FSR2 Vulkan permutation headers detected."
else
echo "AMD FSR2 Vulkan permutation headers not found in SDK checkout."
echo "WoWee CMake will bootstrap vendored headers."
fi
- name: Check FidelityFX-SDK Kits framegen headers
run: |
set -euo pipefail
KITS_DIR="$PWD/extern/FidelityFX-SDK/Kits/FidelityFX"
test -f "$KITS_DIR/upscalers/fsr3/include/ffx_fsr3upscaler.h"
test -f "$KITS_DIR/framegeneration/fsr3/include/ffx_frameinterpolation.h"
test -f "$KITS_DIR/framegeneration/fsr3/include/ffx_opticalflow.h"
test -f "$KITS_DIR/backend/vk/ffx_vk.h"
echo "FidelityFX-SDK Kits framegen headers detected."
- name: Check FidelityFX-SDK Kits framegen headers
run: |
set -euo pipefail
KITS_DIR="$PWD/extern/FidelityFX-SDK/Kits/FidelityFX"
test -f "$KITS_DIR/upscalers/fsr3/include/ffx_fsr3upscaler.h"
test -f "$KITS_DIR/framegeneration/fsr3/include/ffx_frameinterpolation.h"
test -f "$KITS_DIR/framegeneration/fsr3/include/ffx_opticalflow.h"
test -f "$KITS_DIR/backend/vk/ffx_vk.h"
echo "FidelityFX-SDK Kits framegen headers detected."
- name: Configure (AMD ON)
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DWOWEE_ENABLE_AMD_FSR2=ON
- name: Configure (AMD ON)
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DWOWEE_ENABLE_AMD_FSR2=ON -DWOWEE_BUILD_TESTS=ON
- name: Assert AMD FSR2 target
run: cmake --build build --target wowee_fsr2_amd_vk --parallel $(nproc)
- name: Assert AMD FSR2 target
run: cmake --build build --target wowee_fsr2_amd_vk --parallel ${{ matrix.build_jobs }}
- name: Assert AMD FSR3 framegen probe target (if present)
run: |
set -euo pipefail
if cmake --build build --target help | grep -q 'wowee_fsr3_framegen_amd_vk_probe'; then
cmake --build build --target wowee_fsr3_framegen_amd_vk_probe --parallel $(nproc)
else
echo "FSR3 framegen probe target not generated for this SDK layout; continuing."
fi
- name: Assert AMD FSR3 framegen probe target (if present)
run: |
set -euo pipefail
if cmake --build build --target help | grep -q 'wowee_fsr3_framegen_amd_vk_probe'; then
cmake --build build --target wowee_fsr3_framegen_amd_vk_probe --parallel ${{ matrix.build_jobs }}
else
echo "FSR3 framegen probe target not generated for this SDK layout; continuing."
fi
- name: Build
run: cmake --build build --parallel $(nproc)
- name: Build
run: cmake --build build --parallel ${{ matrix.build_jobs }}
- name: Package (DEB)
run: cd build && cpack -G DEB
- name: Run tests
run: cd build && ctest --output-on-failure
- name: Upload DEB
uses: actions/upload-artifact@v4
with:
name: wowee-linux-${{ matrix.arch }}-deb
path: build/wowee-*.deb
if-no-files-found: error
- name: Package (DEB)
run: cd build && cpack -G DEB
- name: Upload DEB
uses: actions/upload-artifact@v4
with:
name: wowee-linux-${{ matrix.arch }}-deb
path: build/wowee-*.deb
if-no-files-found: error
build-macos:
name: Build (macOS ${{ matrix.arch }})
@ -128,311 +133,396 @@ jobs:
fail-fast: false
matrix:
include:
- arch: arm64
runner: macos-15
- arch: arm64
runner: macos-15
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Install dependencies
run: |
brew install cmake pkg-config sdl2 glew glm openssl@3 zlib ffmpeg unicorn \
stormlib vulkan-loader vulkan-headers shaderc dylibbundler || true
# dylibbundler may not be in all brew mirrors; install separately to not block others
brew install dylibbundler 2>/dev/null || true
- name: Install dependencies
run: |
brew install cmake pkg-config sdl2 glew glm openssl@3 zlib ffmpeg unicorn \
stormlib vulkan-loader vulkan-headers shaderc dylibbundler || true
# dylibbundler may not be in all brew mirrors; install separately to not block others
brew install dylibbundler 2>/dev/null || true
- name: Fetch AMD FSR2 SDK
run: |
rm -rf extern/FidelityFX-FSR2
git clone --depth 1 --branch "${WOWEE_AMD_FSR2_REF}" "${WOWEE_AMD_FSR2_REPO}" extern/FidelityFX-FSR2
rm -rf extern/FidelityFX-SDK
git clone --depth 1 --branch "${WOWEE_FFX_SDK_REF}" "${WOWEE_FFX_SDK_REPO}" extern/FidelityFX-SDK
- name: Fetch AMD FSR2 SDK
run: |
rm -rf extern/FidelityFX-FSR2
git clone --depth 1 --branch "${WOWEE_AMD_FSR2_REF}" "${WOWEE_AMD_FSR2_REPO}" extern/FidelityFX-FSR2
rm -rf extern/FidelityFX-SDK
git clone --depth 1 --branch "${WOWEE_FFX_SDK_REF}" "${WOWEE_FFX_SDK_REPO}" extern/FidelityFX-SDK
- name: Configure
run: |
BREW=$(brew --prefix)
export PKG_CONFIG_PATH="$BREW/lib/pkgconfig:$(brew --prefix ffmpeg)/lib/pkgconfig:$(brew --prefix openssl@3)/lib/pkgconfig:$(brew --prefix vulkan-loader)/lib/pkgconfig:$(brew --prefix shaderc)/lib/pkgconfig"
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="$BREW" \
-DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" \
-DWOWEE_ENABLE_AMD_FSR2=ON
- name: Configure
run: |
BREW=$(brew --prefix)
export PKG_CONFIG_PATH="$BREW/lib/pkgconfig:$(brew --prefix ffmpeg)/lib/pkgconfig:$(brew --prefix openssl@3)/lib/pkgconfig:$(brew --prefix vulkan-loader)/lib/pkgconfig:$(brew --prefix shaderc)/lib/pkgconfig"
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="$BREW" \
-DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" \
-DWOWEE_ENABLE_AMD_FSR2=ON \
-DWOWEE_BUILD_TESTS=ON
- name: Assert AMD FSR2 target
run: cmake --build build --target wowee_fsr2_amd_vk --parallel $(sysctl -n hw.logicalcpu)
- name: Assert AMD FSR2 target
run: cmake --build build --target wowee_fsr2_amd_vk --parallel $(sysctl -n hw.logicalcpu)
- name: Assert AMD FSR3 framegen probe target (if present)
run: |
if cmake --build build --target help | grep -q 'wowee_fsr3_framegen_amd_vk_probe'; then
cmake --build build --target wowee_fsr3_framegen_amd_vk_probe --parallel $(sysctl -n hw.logicalcpu)
else
echo "FSR3 framegen probe target not generated for this SDK layout; continuing."
- name: Assert AMD FSR3 framegen probe target (if present)
run: |
if cmake --build build --target help | grep -q 'wowee_fsr3_framegen_amd_vk_probe'; then
cmake --build build --target wowee_fsr3_framegen_amd_vk_probe --parallel $(sysctl -n hw.logicalcpu)
else
echo "FSR3 framegen probe target not generated for this SDK layout; continuing."
fi
- name: Build
run: cmake --build build --parallel $(sysctl -n hw.logicalcpu)
- name: Run tests
run: cd build && ctest --output-on-failure
- name: Create .app bundle
run: |
mkdir -p Wowee.app/Contents/{MacOS,Frameworks,Resources}
# Wrapper launch script — cd to MacOS/ so ./assets/ resolves correctly
printf '#!/bin/bash\ncd "$(dirname "$0")"\nexec ./wowee_bin "$@"\n' \
> Wowee.app/Contents/MacOS/wowee
chmod +x Wowee.app/Contents/MacOS/wowee
# Actual binary
cp build/bin/wowee Wowee.app/Contents/MacOS/wowee_bin
# Assets (exclude proprietary music)
rsync -a --exclude='Original Music' build/bin/assets/ \
Wowee.app/Contents/MacOS/assets/
# Bundle dylibs (if dylibbundler available)
if command -v dylibbundler &>/dev/null; then
dylibbundler -od -b \
-x Wowee.app/Contents/MacOS/wowee_bin \
-d Wowee.app/Contents/Frameworks/ \
-p @executable_path/../Frameworks/
fi
# dylibbundler may miss Homebrew's Vulkan loader on some runner images.
# Copy all vulkan-loader dylib names so wowee_bin can resolve whichever
# install_name it was linked against (e.g. libvulkan.1.4.341.dylib).
VULKAN_LIB_DIR="$(brew --prefix vulkan-loader)/lib"
for lib in "${VULKAN_LIB_DIR}"/libvulkan*.dylib; do
[ -e "${lib}" ] || continue
cp -f "${lib}" Wowee.app/Contents/Frameworks/
done
if ! ls Wowee.app/Contents/Frameworks/libvulkan*.dylib >/dev/null 2>&1; then
echo "Missing Vulkan loader dylib(s) in app bundle Frameworks/" >&2
exit 1
fi
# dylibbundler may miss Homebrew's OpenSSL on some runner images.
# Copy libssl and libcrypto so wowee_bin can resolve them at runtime.
OPENSSL_LIB_DIR="$(brew --prefix openssl@3)/lib"
for lib in "${OPENSSL_LIB_DIR}"/libssl*.dylib "${OPENSSL_LIB_DIR}"/libcrypto*.dylib; do
[ -e "${lib}" ] || continue
cp -f "${lib}" Wowee.app/Contents/Frameworks/
done
if ! ls Wowee.app/Contents/Frameworks/libssl*.dylib >/dev/null 2>&1; then
echo "Missing OpenSSL libssl dylib(s) in app bundle Frameworks/" >&2
exit 1
fi
# dylibbundler may miss Homebrew's FFmpeg libraries.
# Copy libavformat, libavcodec, libavutil, and libswscale.
FFMPEG_LIB_DIR="$(brew --prefix ffmpeg)/lib"
for lib in "${FFMPEG_LIB_DIR}"/libavformat*.dylib \
"${FFMPEG_LIB_DIR}"/libavcodec*.dylib \
"${FFMPEG_LIB_DIR}"/libavutil*.dylib \
"${FFMPEG_LIB_DIR}"/libswscale*.dylib \
"${FFMPEG_LIB_DIR}"/libswresample*.dylib; do
[ -e "${lib}" ] || continue
cp -f "${lib}" Wowee.app/Contents/Frameworks/
done
if ! ls Wowee.app/Contents/Frameworks/libavformat*.dylib >/dev/null 2>&1; then
echo "Missing FFmpeg libavformat dylib(s) in app bundle Frameworks/" >&2
exit 1
fi
# Info.plist
cat > Wowee.app/Contents/Info.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleExecutable</key><string>wowee</string>
<key>CFBundleIdentifier</key><string>com.wowee.app</string>
<key>CFBundleName</key><string>Wowee</string>
<key>CFBundleVersion</key><string>1.0.0</string>
<key>CFBundleShortVersionString</key><string>1.0.0</string>
<key>CFBundlePackageType</key><string>APPL</string>
</dict></plist>
EOF
# Ad-hoc codesign (allows running on the local machine)
codesign --force --deep --sign - Wowee.app
- name: Verify bundled dylibs
run: |
set -euo pipefail
echo "=== dylib references for wowee_bin ==="
otool -L Wowee.app/Contents/MacOS/wowee_bin
# Every non-system dylib referenced by the binary must resolve inside
# the bundle. System paths (/usr/lib, /System) are always present on
# macOS and don't need bundling.
missing=0
while IFS= read -r dep; do
# Strip leading whitespace and version info: " /path/to/lib.dylib (compat ...)"
path=$(echo "$dep" | sed 's/^[[:space:]]*//;s/ (compat.*//')
case "$path" in
/usr/lib/*|/System/*|@executable_path/*|@rpath/*) continue ;;
esac
# Resolve @loader_path relative to the binary
resolved="${path/#@loader_path/Wowee.app/Contents/MacOS}"
if [ ! -f "$resolved" ]; then
basename=$(basename "$path")
if [ ! -f "Wowee.app/Contents/Frameworks/$basename" ]; then
echo "ERROR: unbundled dylib: $path" >&2
missing=$((missing + 1))
fi
fi
done < <(otool -L Wowee.app/Contents/MacOS/wowee_bin | tail -n +2)
- name: Build
run: cmake --build build --parallel $(sysctl -n hw.logicalcpu)
if [ "$missing" -gt 0 ]; then
echo ""
echo "=== Frameworks directory ==="
ls -la Wowee.app/Contents/Frameworks/
echo ""
echo "FAIL: $missing dylib(s) missing from app bundle" >&2
exit 1
fi
echo "All non-system dylibs are bundled."
- name: Create .app bundle
run: |
mkdir -p Wowee.app/Contents/{MacOS,Frameworks,Resources}
- name: Create DMG
run: |
set -euo pipefail
rm -f Wowee.dmg
# CI runners can occasionally leave a mounted volume around; detach if present.
if [ -d "/Volumes/Wowee" ]; then
hdiutil detach "/Volumes/Wowee" -force || true
sleep 2
fi
# Wrapper launch script — cd to MacOS/ so ./assets/ resolves correctly
printf '#!/bin/bash\ncd "$(dirname "$0")"\nexec ./wowee_bin "$@"\n' \
> Wowee.app/Contents/MacOS/wowee
chmod +x Wowee.app/Contents/MacOS/wowee
# Actual binary
cp build/bin/wowee Wowee.app/Contents/MacOS/wowee_bin
# Assets (exclude proprietary music)
rsync -a --exclude='Original Music' build/bin/assets/ \
Wowee.app/Contents/MacOS/assets/
# Bundle dylibs (if dylibbundler available)
if command -v dylibbundler &>/dev/null; then
dylibbundler -od -b \
-x Wowee.app/Contents/MacOS/wowee_bin \
-d Wowee.app/Contents/Frameworks/ \
-p @executable_path/../Frameworks/
ok=0
for attempt in 1 2 3 4 5; do
if hdiutil create -volname Wowee -srcfolder Wowee.app -ov -format UDZO Wowee.dmg; then
ok=1
break
fi
# Info.plist
cat > Wowee.app/Contents/Info.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleExecutable</key><string>wowee</string>
<key>CFBundleIdentifier</key><string>com.wowee.app</string>
<key>CFBundleName</key><string>Wowee</string>
<key>CFBundleVersion</key><string>1.0.0</string>
<key>CFBundleShortVersionString</key><string>1.0.0</string>
<key>CFBundlePackageType</key><string>APPL</string>
</dict></plist>
EOF
# Ad-hoc codesign (allows running on the local machine)
codesign --force --deep --sign - Wowee.app
- name: Create DMG
run: |
set -euo pipefail
rm -f Wowee.dmg
# CI runners can occasionally leave a mounted volume around; detach if present.
echo "hdiutil create failed on attempt ${attempt}; retrying..."
if [ -d "/Volumes/Wowee" ]; then
hdiutil detach "/Volumes/Wowee" -force || true
sleep 2
fi
sleep 3
done
ok=0
for attempt in 1 2 3 4 5; do
if hdiutil create -volname Wowee -srcfolder Wowee.app -ov -format UDZO Wowee.dmg; then
ok=1
break
fi
echo "hdiutil create failed on attempt ${attempt}; retrying..."
if [ -d "/Volumes/Wowee" ]; then
hdiutil detach "/Volumes/Wowee" -force || true
fi
sleep 3
done
if [ "$ok" -ne 1 ] || [ ! -f Wowee.dmg ]; then
echo "Failed to create Wowee.dmg after retries."
exit 1
fi
if [ "$ok" -ne 1 ] || [ ! -f Wowee.dmg ]; then
echo "Failed to create Wowee.dmg after retries."
exit 1
fi
- name: Upload DMG
uses: actions/upload-artifact@v4
with:
name: wowee-macos-${{ matrix.arch }}-dmg
path: Wowee.dmg
if-no-files-found: error
- name: Upload DMG
uses: actions/upload-artifact@v4
with:
name: wowee-macos-${{ matrix.arch }}-dmg
path: Wowee.dmg
if-no-files-found: error
build-windows-arm:
name: Build (windows-arm64)
runs-on: windows-11-arm
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Set up MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: CLANGARM64
update: true
install: >-
mingw-w64-clang-aarch64-cmake
mingw-w64-clang-aarch64-clang
mingw-w64-clang-aarch64-ninja
mingw-w64-clang-aarch64-pkgconf
mingw-w64-clang-aarch64-SDL2
mingw-w64-clang-aarch64-glew
mingw-w64-clang-aarch64-glm
mingw-w64-clang-aarch64-openssl
mingw-w64-clang-aarch64-zlib
mingw-w64-clang-aarch64-ffmpeg
mingw-w64-clang-aarch64-unicorn
mingw-w64-clang-aarch64-vulkan-loader
mingw-w64-clang-aarch64-vulkan-headers
mingw-w64-clang-aarch64-shaderc
git
- name: Set up MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: CLANGARM64
update: true
install: >-
mingw-w64-clang-aarch64-cmake
mingw-w64-clang-aarch64-clang
mingw-w64-clang-aarch64-ninja
mingw-w64-clang-aarch64-pkgconf
mingw-w64-clang-aarch64-SDL2
mingw-w64-clang-aarch64-glew
mingw-w64-clang-aarch64-glm
mingw-w64-clang-aarch64-openssl
mingw-w64-clang-aarch64-zlib
mingw-w64-clang-aarch64-ffmpeg
mingw-w64-clang-aarch64-unicorn
mingw-w64-clang-aarch64-vulkan-loader
mingw-w64-clang-aarch64-vulkan-headers
mingw-w64-clang-aarch64-shaderc
git
- name: Build StormLib from source
shell: msys2 {0}
run: |
git clone --depth 1 https://github.com/ladislav-zezula/StormLib.git /tmp/StormLib
# Disable x86 inline asm in bundled libtomcrypt (bswapl/movl) —
# __MINGW32__ is defined on CLANGARM64 which incorrectly enables x86 asm
cmake -S /tmp/StormLib -B /tmp/StormLib/build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$MINGW_PREFIX" \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_C_FLAGS="-DLTC_NO_BSWAP" \
-DCMAKE_CXX_FLAGS="-DLTC_NO_BSWAP"
cmake --build /tmp/StormLib/build --parallel $(nproc)
cmake --install /tmp/StormLib/build
- name: Build StormLib from source
shell: msys2 {0}
run: |
git clone --depth 1 https://github.com/ladislav-zezula/StormLib.git /tmp/StormLib
# Disable x86 inline asm in bundled libtomcrypt (bswapl/movl) —
# __MINGW32__ is defined on CLANGARM64 which incorrectly enables x86 asm
cmake -S /tmp/StormLib -B /tmp/StormLib/build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$MINGW_PREFIX" \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_C_FLAGS="-DLTC_NO_BSWAP" \
-DCMAKE_CXX_FLAGS="-DLTC_NO_BSWAP"
cmake --build /tmp/StormLib/build --parallel $(nproc)
cmake --install /tmp/StormLib/build
- name: Fetch AMD FSR2 SDK
shell: msys2 {0}
run: |
rm -rf extern/FidelityFX-FSR2
git clone --depth 1 --branch "${WOWEE_AMD_FSR2_REF}" "${WOWEE_AMD_FSR2_REPO}" extern/FidelityFX-FSR2
rm -rf extern/FidelityFX-SDK
git clone --depth 1 --branch "${WOWEE_FFX_SDK_REF}" "${WOWEE_FFX_SDK_REPO}" extern/FidelityFX-SDK
- name: Fetch AMD FSR2 SDK
shell: msys2 {0}
run: |
rm -rf extern/FidelityFX-FSR2
git clone --depth 1 --branch "${WOWEE_AMD_FSR2_REF}" "${WOWEE_AMD_FSR2_REPO}" extern/FidelityFX-FSR2
rm -rf extern/FidelityFX-SDK
git clone --depth 1 --branch "${WOWEE_FFX_SDK_REF}" "${WOWEE_FFX_SDK_REPO}" extern/FidelityFX-SDK
- name: Configure
shell: msys2 {0}
run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DWOWEE_ENABLE_AMD_FSR2=ON
- name: Configure
shell: msys2 {0}
run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DWOWEE_ENABLE_AMD_FSR2=ON
- name: Assert AMD FSR2 target
shell: msys2 {0}
run: cmake --build build --target wowee_fsr2_amd_vk --parallel $(nproc)
- name: Assert AMD FSR2 target
shell: msys2 {0}
run: cmake --build build --target wowee_fsr2_amd_vk --parallel $(nproc)
- name: Assert AMD FSR3 framegen probe target (if present)
shell: msys2 {0}
run: |
if cmake --build build --target help | grep -q 'wowee_fsr3_framegen_amd_vk_probe'; then
cmake --build build --target wowee_fsr3_framegen_amd_vk_probe --parallel $(nproc)
else
echo "FSR3 framegen probe target not generated for this SDK layout; continuing."
fi
- name: Assert AMD FSR3 framegen probe target (if present)
shell: msys2 {0}
run: |
if cmake --build build --target help | grep -q 'wowee_fsr3_framegen_amd_vk_probe'; then
cmake --build build --target wowee_fsr3_framegen_amd_vk_probe --parallel $(nproc)
else
echo "FSR3 framegen probe target not generated for this SDK layout; continuing."
fi
- name: Build
shell: msys2 {0}
run: cmake --build build --parallel $(nproc)
- name: Build
shell: msys2 {0}
run: cmake --build build --parallel $(nproc)
- name: Bundle DLLs
shell: msys2 {0}
run: |
ldd build/bin/wowee.exe \
| awk '/=> \// { print $3 }' \
| grep -iv '^/c/Windows' \
| xargs -I{} sh -c 'cp -n "{}" build/bin/ 2>/dev/null || true'
- name: Bundle DLLs
shell: msys2 {0}
run: |
ldd build/bin/wowee.exe \
| awk '/=> \// { print $3 }' \
| grep -iv '^/c/Windows' \
| xargs -I{} sh -c 'cp -n "{}" build/bin/ 2>/dev/null || true'
- name: Package (ZIP)
shell: msys2 {0}
run: cd build && cpack -G ZIP
- name: Package (ZIP)
shell: msys2 {0}
run: cd build && cpack -G ZIP
- name: Upload ZIP
uses: actions/upload-artifact@v4
with:
name: wowee-windows-arm64-zip
path: build/wowee-*.zip
if-no-files-found: error
- name: Upload ZIP
uses: actions/upload-artifact@v4
with:
name: wowee-windows-arm64-zip
path: build/wowee-*.zip
if-no-files-found: error
build-windows:
name: Build (windows-x86-64)
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Set up MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
update: false
install: >-
mingw-w64-x86_64-cmake
mingw-w64-x86_64-gcc
mingw-w64-x86_64-ninja
mingw-w64-x86_64-pkgconf
mingw-w64-x86_64-SDL2
mingw-w64-x86_64-glew
mingw-w64-x86_64-glm
mingw-w64-x86_64-openssl
mingw-w64-x86_64-zlib
mingw-w64-x86_64-ffmpeg
mingw-w64-x86_64-unicorn
mingw-w64-x86_64-vulkan-loader
mingw-w64-x86_64-vulkan-headers
mingw-w64-x86_64-shaderc
mingw-w64-x86_64-nsis
git
- name: Set up MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
update: false
install: >-
mingw-w64-x86_64-cmake
mingw-w64-x86_64-gcc
mingw-w64-x86_64-ninja
mingw-w64-x86_64-pkgconf
mingw-w64-x86_64-SDL2
mingw-w64-x86_64-glew
mingw-w64-x86_64-glm
mingw-w64-x86_64-openssl
mingw-w64-x86_64-zlib
mingw-w64-x86_64-ffmpeg
mingw-w64-x86_64-unicorn
mingw-w64-x86_64-vulkan-loader
mingw-w64-x86_64-vulkan-headers
mingw-w64-x86_64-shaderc
mingw-w64-x86_64-nsis
git
- name: Build StormLib from source
shell: msys2 {0}
run: |
git clone --depth 1 https://github.com/ladislav-zezula/StormLib.git /tmp/StormLib
cmake -S /tmp/StormLib -B /tmp/StormLib/build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$MINGW_PREFIX" \
-DBUILD_SHARED_LIBS=OFF
cmake --build /tmp/StormLib/build --parallel $(nproc)
cmake --install /tmp/StormLib/build
- name: Build StormLib from source
shell: msys2 {0}
run: |
git clone --depth 1 https://github.com/ladislav-zezula/StormLib.git /tmp/StormLib
cmake -S /tmp/StormLib -B /tmp/StormLib/build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$MINGW_PREFIX" \
-DBUILD_SHARED_LIBS=OFF
cmake --build /tmp/StormLib/build --parallel $(nproc)
cmake --install /tmp/StormLib/build
- name: Fetch AMD FSR2 SDK
shell: msys2 {0}
run: |
rm -rf extern/FidelityFX-FSR2
git clone --depth 1 --branch "${WOWEE_AMD_FSR2_REF}" "${WOWEE_AMD_FSR2_REPO}" extern/FidelityFX-FSR2
rm -rf extern/FidelityFX-SDK
git clone --depth 1 --branch "${WOWEE_FFX_SDK_REF}" "${WOWEE_FFX_SDK_REPO}" extern/FidelityFX-SDK
- name: Fetch AMD FSR2 SDK
shell: msys2 {0}
run: |
rm -rf extern/FidelityFX-FSR2
git clone --depth 1 --branch "${WOWEE_AMD_FSR2_REF}" "${WOWEE_AMD_FSR2_REPO}" extern/FidelityFX-FSR2
rm -rf extern/FidelityFX-SDK
git clone --depth 1 --branch "${WOWEE_FFX_SDK_REF}" "${WOWEE_FFX_SDK_REPO}" extern/FidelityFX-SDK
- name: Configure
shell: msys2 {0}
run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DWOWEE_ENABLE_AMD_FSR2=ON
- name: Configure
shell: msys2 {0}
run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DWOWEE_ENABLE_AMD_FSR2=ON
- name: Assert AMD FSR2 target
shell: msys2 {0}
run: cmake --build build --target wowee_fsr2_amd_vk --parallel $(nproc)
- name: Assert AMD FSR2 target
shell: msys2 {0}
run: cmake --build build --target wowee_fsr2_amd_vk --parallel $(nproc)
- name: Assert AMD FSR3 framegen probe target (if present)
shell: msys2 {0}
run: |
if cmake --build build --target help | grep -q 'wowee_fsr3_framegen_amd_vk_probe'; then
cmake --build build --target wowee_fsr3_framegen_amd_vk_probe --parallel $(nproc)
else
echo "FSR3 framegen probe target not generated for this SDK layout; continuing."
fi
- name: Assert AMD FSR3 framegen probe target (if present)
shell: msys2 {0}
run: |
if cmake --build build --target help | grep -q 'wowee_fsr3_framegen_amd_vk_probe'; then
cmake --build build --target wowee_fsr3_framegen_amd_vk_probe --parallel $(nproc)
else
echo "FSR3 framegen probe target not generated for this SDK layout; continuing."
fi
- name: Build
shell: msys2 {0}
run: cmake --build build --parallel $(nproc)
- name: Build
shell: msys2 {0}
run: cmake --build build --parallel $(nproc)
- name: Bundle DLLs
shell: msys2 {0}
run: |
ldd build/bin/wowee.exe \
| awk '/=> \// { print $3 }' \
| grep -iv '^/c/Windows' \
| xargs -I{} sh -c 'cp -n "{}" build/bin/ 2>/dev/null || true'
- name: Bundle DLLs
shell: msys2 {0}
run: |
ldd build/bin/wowee.exe \
| awk '/=> \// { print $3 }' \
| grep -iv '^/c/Windows' \
| xargs -I{} sh -c 'cp -n "{}" build/bin/ 2>/dev/null || true'
- name: Package (NSIS)
shell: msys2 {0}
run: cd build && cpack -G NSIS
- name: Package (NSIS)
shell: msys2 {0}
run: cd build && cpack -G NSIS
- name: Upload installer
uses: actions/upload-artifact@v4
with:
name: wowee-windows-x86-64-installer
path: build/wowee-*.exe
if-no-files-found: error
- name: Upload installer
uses: actions/upload-artifact@v4
with:
name: wowee-windows-x86-64-installer
path: build/wowee-*.exe
if-no-files-found: error

View file

@ -2,7 +2,7 @@ name: Release
on:
push:
tags: ['v*']
tags: [ 'v*' ]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@ -18,201 +18,274 @@ jobs:
fail-fast: false
matrix:
include:
- arch: x86-64
runner: ubuntu-24.04
- arch: arm64
runner: ubuntu-24.04-arm
- arch: x86-64
runner: ubuntu-24.04
- arch: arm64
runner: ubuntu-24.04-arm
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Cache apt packages
uses: actions/cache@v4
with:
path: /var/cache/apt/archives/*.deb
key: apt-${{ matrix.arch }}-${{ hashFiles('.github/workflows/release.yml') }}
restore-keys: apt-${{ matrix.arch }}-
- name: Cache apt packages
uses: actions/cache@v4
with:
path: /var/cache/apt/archives/*.deb
key: apt-${{ matrix.arch }}-${{ hashFiles('.github/workflows/release.yml') }}
restore-keys: apt-${{ matrix.arch }}-
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
cmake \
build-essential \
pkg-config \
libsdl2-dev \
libglew-dev \
libglm-dev \
libssl-dev \
zlib1g-dev \
libvulkan-dev \
vulkan-tools \
glslc \
libavformat-dev \
libavcodec-dev \
libswscale-dev \
libavutil-dev \
libunicorn-dev \
libx11-dev
sudo apt-get install -y libstorm-dev || true
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
cmake \
build-essential \
pkg-config \
libsdl2-dev \
libglew-dev \
libglm-dev \
libssl-dev \
zlib1g-dev \
libvulkan-dev \
vulkan-tools \
glslc \
libavformat-dev \
libavcodec-dev \
libswscale-dev \
libavutil-dev \
libunicorn-dev \
libx11-dev
sudo apt-get install -y libstorm-dev || true
- name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
- name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
- name: Verify Release Config
run: |
cmake -LA -N build | grep -E '^CMAKE_BUILD_TYPE:STRING=Release$'
- name: Verify Release Config
run: |
cmake -LA -N build | grep -E '^CMAKE_BUILD_TYPE:STRING=Release$'
- name: Build
run: cmake --build build --parallel $(nproc)
- name: Build
run: cmake --build build --parallel $(nproc)
- name: Package
run: |
TAG="${GITHUB_REF_NAME}"
STAGING="wowee-${TAG}-linux-${{ matrix.arch }}"
mkdir -p "${STAGING}"
- name: Package
run: |
TAG="${GITHUB_REF_NAME}"
STAGING="wowee-${TAG}-linux-${{ matrix.arch }}"
mkdir -p "${STAGING}"
# Binary
cp build/bin/wowee "${STAGING}/"
# Binary
cp build/bin/wowee "${STAGING}/"
# Asset extraction tool (if built — requires StormLib)
if [ -f build/bin/asset_extract ]; then
cp build/bin/asset_extract "${STAGING}/"
fi
# Asset extraction tool (if built — requires StormLib)
if [ -f build/bin/asset_extract ]; then
cp build/bin/asset_extract "${STAGING}/"
fi
# Extraction scripts and GUI
cp extract_assets.sh "${STAGING}/"
cp tools/asset_pipeline_gui.py "${STAGING}/"
# Extraction scripts and GUI
cp extract_assets.sh "${STAGING}/"
cp tools/asset_pipeline_gui.py "${STAGING}/"
# Assets (exclude proprietary music)
rsync -a --exclude='Original Music' build/bin/assets/ "${STAGING}/assets/"
# Assets (exclude proprietary music)
rsync -a --exclude='Original Music' build/bin/assets/ "${STAGING}/assets/"
# Data directory (git-tracked files only)
git ls-files Data/ | while read -r f; do
mkdir -p "${STAGING}/$(dirname "$f")"
cp "$f" "${STAGING}/$f"
done
# Data directory (git-tracked files only)
git ls-files Data/ | while read -r f; do
mkdir -p "${STAGING}/$(dirname "$f")"
cp "$f" "${STAGING}/$f"
done
tar czf "${STAGING}.tar.gz" "${STAGING}"
tar czf "${STAGING}.tar.gz" "${STAGING}"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: wowee-linux-${{ matrix.arch }}
path: wowee-*.tar.gz
if-no-files-found: error
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: wowee-linux-${{ matrix.arch }}
path: wowee-*.tar.gz
if-no-files-found: error
build-macos:
name: Build (macOS arm64)
runs-on: macos-15
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Install dependencies
run: |
brew install cmake pkg-config sdl2 glew glm openssl@3 zlib ffmpeg unicorn \
stormlib vulkan-loader vulkan-headers shaderc dylibbundler || true
brew install dylibbundler 2>/dev/null || true
- name: Install dependencies
run: |
brew install cmake pkg-config sdl2 glew glm openssl@3 zlib ffmpeg unicorn \
stormlib vulkan-loader vulkan-headers shaderc dylibbundler || true
brew install dylibbundler 2>/dev/null || true
- name: Configure
run: |
BREW=$(brew --prefix)
export PKG_CONFIG_PATH="$BREW/lib/pkgconfig:$(brew --prefix ffmpeg)/lib/pkgconfig:$(brew --prefix openssl@3)/lib/pkgconfig:$(brew --prefix vulkan-loader)/lib/pkgconfig:$(brew --prefix shaderc)/lib/pkgconfig"
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="$BREW" \
-DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)"
- name: Configure
run: |
BREW=$(brew --prefix)
export PKG_CONFIG_PATH="$BREW/lib/pkgconfig:$(brew --prefix ffmpeg)/lib/pkgconfig:$(brew --prefix openssl@3)/lib/pkgconfig:$(brew --prefix vulkan-loader)/lib/pkgconfig:$(brew --prefix shaderc)/lib/pkgconfig"
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="$BREW" \
-DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)"
- name: Verify Release Config
run: |
cmake -LA -N build | grep -E '^CMAKE_BUILD_TYPE:STRING=Release$'
- name: Verify Release Config
run: |
cmake -LA -N build | grep -E '^CMAKE_BUILD_TYPE:STRING=Release$'
- name: Build
run: cmake --build build --parallel $(sysctl -n hw.logicalcpu)
- name: Build
run: cmake --build build --parallel $(sysctl -n hw.logicalcpu)
- name: Create .app bundle
run: |
TAG="${GITHUB_REF_NAME}"
mkdir -p Wowee.app/Contents/{MacOS,Frameworks,Resources}
- name: Create .app bundle
run: |
TAG="${GITHUB_REF_NAME}"
mkdir -p Wowee.app/Contents/{MacOS,Frameworks,Resources}
# Wrapper launch script
printf '#!/bin/bash\ncd "$(dirname "$0")"\nexec ./wowee_bin "$@"\n' \
> Wowee.app/Contents/MacOS/wowee
chmod +x Wowee.app/Contents/MacOS/wowee
# Wrapper launch script
printf '#!/bin/bash\ncd "$(dirname "$0")"\nexec ./wowee_bin "$@"\n' \
> Wowee.app/Contents/MacOS/wowee
chmod +x Wowee.app/Contents/MacOS/wowee
# Actual binary
cp build/bin/wowee Wowee.app/Contents/MacOS/wowee_bin
# Actual binary
cp build/bin/wowee Wowee.app/Contents/MacOS/wowee_bin
# Asset extraction tool (if built — requires StormLib)
if [ -f build/bin/asset_extract ]; then
cp build/bin/asset_extract Wowee.app/Contents/MacOS/
fi
# Asset extraction tool (if built — requires StormLib)
if [ -f build/bin/asset_extract ]; then
cp build/bin/asset_extract Wowee.app/Contents/MacOS/
fi
# Extraction scripts and GUI
cp extract_assets.sh Wowee.app/Contents/MacOS/
cp tools/asset_pipeline_gui.py Wowee.app/Contents/MacOS/
# Extraction scripts and GUI
cp extract_assets.sh Wowee.app/Contents/MacOS/
cp tools/asset_pipeline_gui.py Wowee.app/Contents/MacOS/
# Assets (exclude proprietary music)
rsync -a --exclude='Original Music' build/bin/assets/ \
Wowee.app/Contents/MacOS/assets/
# Assets (exclude proprietary music)
rsync -a --exclude='Original Music' build/bin/assets/ \
Wowee.app/Contents/MacOS/assets/
# Data directory (git-tracked files only)
git ls-files Data/ | while read -r f; do
mkdir -p "Wowee.app/Contents/MacOS/$(dirname "$f")"
cp "$f" "Wowee.app/Contents/MacOS/$f"
done
# Data directory (git-tracked files only)
git ls-files Data/ | while read -r f; do
mkdir -p "Wowee.app/Contents/MacOS/$(dirname "$f")"
cp "$f" "Wowee.app/Contents/MacOS/$f"
done
# Bundle dylibs
if command -v dylibbundler &>/dev/null; then
# Bundle dylibs
if command -v dylibbundler &>/dev/null; then
dylibbundler -od -b \
-x Wowee.app/Contents/MacOS/wowee_bin \
-d Wowee.app/Contents/Frameworks/ \
-p @executable_path/../Frameworks/
if [ -f Wowee.app/Contents/MacOS/asset_extract ]; then
dylibbundler -od -b \
-x Wowee.app/Contents/MacOS/wowee_bin \
-x Wowee.app/Contents/MacOS/asset_extract \
-d Wowee.app/Contents/Frameworks/ \
-p @executable_path/../Frameworks/
if [ -f Wowee.app/Contents/MacOS/asset_extract ]; then
dylibbundler -od -b \
-x Wowee.app/Contents/MacOS/asset_extract \
-d Wowee.app/Contents/Frameworks/ \
-p @executable_path/../Frameworks/
fi
fi
# dylibbundler may miss Homebrew's Vulkan loader on some runner images.
# Copy all vulkan-loader dylib names so wowee_bin can resolve whichever
# install_name it was linked against (e.g. libvulkan.1.4.341.dylib).
VULKAN_LIB_DIR="$(brew --prefix vulkan-loader)/lib"
for lib in "${VULKAN_LIB_DIR}"/libvulkan*.dylib; do
[ -e "${lib}" ] || continue
cp -f "${lib}" Wowee.app/Contents/Frameworks/
done
if ! ls Wowee.app/Contents/Frameworks/libvulkan*.dylib >/dev/null 2>&1; then
echo "Missing Vulkan loader dylib(s) in app bundle Frameworks/" >&2
exit 1
fi
# dylibbundler may miss Homebrew's OpenSSL on some runner images.
# Copy libssl and libcrypto so wowee_bin can resolve them at runtime.
OPENSSL_LIB_DIR="$(brew --prefix openssl@3)/lib"
for lib in "${OPENSSL_LIB_DIR}"/libssl*.dylib "${OPENSSL_LIB_DIR}"/libcrypto*.dylib; do
[ -e "${lib}" ] || continue
cp -f "${lib}" Wowee.app/Contents/Frameworks/
done
if ! ls Wowee.app/Contents/Frameworks/libssl*.dylib >/dev/null 2>&1; then
echo "Missing OpenSSL libssl dylib(s) in app bundle Frameworks/" >&2
exit 1
fi
# dylibbundler may miss Homebrew's FFmpeg libraries.
# Copy libavformat, libavcodec, libavutil, and libswscale.
FFMPEG_LIB_DIR="$(brew --prefix ffmpeg)/lib"
for lib in "${FFMPEG_LIB_DIR}"/libavformat*.dylib \
"${FFMPEG_LIB_DIR}"/libavcodec*.dylib \
"${FFMPEG_LIB_DIR}"/libavutil*.dylib \
"${FFMPEG_LIB_DIR}"/libswscale*.dylib \
"${FFMPEG_LIB_DIR}"/libswresample*.dylib; do
[ -e "${lib}" ] || continue
cp -f "${lib}" Wowee.app/Contents/Frameworks/
done
if ! ls Wowee.app/Contents/Frameworks/libavformat*.dylib >/dev/null 2>&1; then
echo "Missing FFmpeg libavformat dylib(s) in app bundle Frameworks/" >&2
exit 1
fi
# Info.plist
cat > Wowee.app/Contents/Info.plist << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleExecutable</key><string>wowee</string>
<key>CFBundleIdentifier</key><string>com.wowee.app</string>
<key>CFBundleName</key><string>Wowee</string>
<key>CFBundleVersion</key><string>${TAG}</string>
<key>CFBundleShortVersionString</key><string>${TAG}</string>
<key>CFBundlePackageType</key><string>APPL</string>
</dict></plist>
EOF
# Ad-hoc codesign
codesign --force --deep --sign - Wowee.app
- name: Verify bundled dylibs
run: |
set -euo pipefail
echo "=== dylib references for wowee_bin ==="
otool -L Wowee.app/Contents/MacOS/wowee_bin
missing=0
while IFS= read -r dep; do
path=$(echo "$dep" | sed 's/^[[:space:]]*//;s/ (compat.*//')
case "$path" in
/usr/lib/*|/System/*|@executable_path/*|@rpath/*) continue ;;
esac
resolved="${path/#@loader_path/Wowee.app/Contents/MacOS}"
if [ ! -f "$resolved" ]; then
basename=$(basename "$path")
if [ ! -f "Wowee.app/Contents/Frameworks/$basename" ]; then
echo "ERROR: unbundled dylib: $path" >&2
missing=$((missing + 1))
fi
fi
done < <(otool -L Wowee.app/Contents/MacOS/wowee_bin | tail -n +2)
# Info.plist
cat > Wowee.app/Contents/Info.plist << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleExecutable</key><string>wowee</string>
<key>CFBundleIdentifier</key><string>com.wowee.app</string>
<key>CFBundleName</key><string>Wowee</string>
<key>CFBundleVersion</key><string>${TAG}</string>
<key>CFBundleShortVersionString</key><string>${TAG}</string>
<key>CFBundlePackageType</key><string>APPL</string>
</dict></plist>
EOF
if [ "$missing" -gt 0 ]; then
ls -la Wowee.app/Contents/Frameworks/
echo "FAIL: $missing dylib(s) missing from app bundle" >&2
exit 1
fi
echo "All non-system dylibs are bundled."
# Ad-hoc codesign
codesign --force --deep --sign - Wowee.app
- name: Create DMG
run: |
TAG="${GITHUB_REF_NAME}"
hdiutil create -volname Wowee -srcfolder Wowee.app -ov -format UDZO \
"wowee-${TAG}-macos-arm64.dmg"
- name: Create DMG
run: |
TAG="${GITHUB_REF_NAME}"
hdiutil create -volname Wowee -srcfolder Wowee.app -ov -format UDZO \
"wowee-${TAG}-macos-arm64.dmg"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: wowee-macos-arm64
path: wowee-*.dmg
if-no-files-found: error
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: wowee-macos-arm64
path: wowee-*.dmg
if-no-files-found: error
build-windows:
name: Build (windows-${{ matrix.arch }})
@ -221,159 +294,145 @@ jobs:
fail-fast: false
matrix:
include:
- arch: x86-64
runner: windows-latest
msystem: MINGW64
prefix: mingw-w64-x86_64
- arch: arm64
runner: windows-11-arm
msystem: CLANGARM64
prefix: mingw-w64-clang-aarch64
- arch: x86-64
runner: windows-latest
msystem: MINGW64
prefix: mingw-w64-x86_64
- arch: arm64
runner: windows-11-arm
msystem: CLANGARM64
prefix: mingw-w64-clang-aarch64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Set up MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: ${{ matrix.msystem }}
update: ${{ matrix.arch == 'arm64' }}
install: >-
${{ matrix.prefix }}-cmake
${{ matrix.arch == 'x86-64' && format('{0}-gcc', matrix.prefix) || format('{0}-clang', matrix.prefix) }}
${{ matrix.prefix }}-ninja
${{ matrix.prefix }}-pkgconf
${{ matrix.prefix }}-SDL2
${{ matrix.prefix }}-glew
${{ matrix.prefix }}-glm
${{ matrix.prefix }}-openssl
${{ matrix.prefix }}-zlib
${{ matrix.prefix }}-ffmpeg
${{ matrix.prefix }}-unicorn
${{ matrix.prefix }}-vulkan-loader
${{ matrix.prefix }}-vulkan-headers
${{ matrix.prefix }}-shaderc
git
- name: Set up MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: ${{ matrix.msystem }}
update: ${{ matrix.arch == 'arm64' }}
install: >-
${{ matrix.prefix }}-cmake ${{ matrix.arch == 'x86-64' && format('{0}-gcc', matrix.prefix) || format('{0}-clang', matrix.prefix) }} ${{ matrix.prefix }}-ninja ${{ matrix.prefix }}-pkgconf ${{ matrix.prefix }}-SDL2 ${{ matrix.prefix }}-glew ${{ matrix.prefix }}-glm ${{ matrix.prefix }}-openssl ${{ matrix.prefix }}-zlib ${{ matrix.prefix }}-ffmpeg ${{ matrix.prefix }}-unicorn ${{ matrix.prefix }}-vulkan-loader ${{ matrix.prefix }}-vulkan-headers ${{ matrix.prefix }}-shaderc git
- name: Install optional packages
shell: msys2 {0}
run: |
pacman -S --noconfirm --needed zip
- name: Install optional packages
shell: msys2 {0}
run: |
pacman -S --noconfirm --needed zip
- name: Build StormLib from source
shell: msys2 {0}
run: |
git clone --depth 1 https://github.com/ladislav-zezula/StormLib.git /tmp/StormLib
# Disable x86 inline asm in bundled libtomcrypt (bswapl/movl) —
# __MINGW32__ is defined on CLANGARM64 which incorrectly enables x86 asm
cmake -S /tmp/StormLib -B /tmp/StormLib/build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$MINGW_PREFIX" \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_C_FLAGS="-DLTC_NO_BSWAP" \
-DCMAKE_CXX_FLAGS="-DLTC_NO_BSWAP"
cmake --build /tmp/StormLib/build --parallel $(nproc)
cmake --install /tmp/StormLib/build
- name: Build StormLib from source
shell: msys2 {0}
run: |
git clone --depth 1 https://github.com/ladislav-zezula/StormLib.git /tmp/StormLib
# Disable x86 inline asm in bundled libtomcrypt (bswapl/movl) —
# __MINGW32__ is defined on CLANGARM64 which incorrectly enables x86 asm
cmake -S /tmp/StormLib -B /tmp/StormLib/build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$MINGW_PREFIX" \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_C_FLAGS="-DLTC_NO_BSWAP" \
-DCMAKE_CXX_FLAGS="-DLTC_NO_BSWAP"
cmake --build /tmp/StormLib/build --parallel $(nproc)
cmake --install /tmp/StormLib/build
- name: Configure
shell: msys2 {0}
run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
- name: Configure
shell: msys2 {0}
run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
- name: Verify Release Config
shell: msys2 {0}
run: |
cmake -LA -N build | grep -E '^CMAKE_BUILD_TYPE:STRING=Release$'
- name: Verify Release Config
shell: msys2 {0}
run: |
cmake -LA -N build | grep -E '^CMAKE_BUILD_TYPE:STRING=Release$'
- name: Build
shell: msys2 {0}
run: cmake --build build --parallel $(nproc)
- name: Build
shell: msys2 {0}
run: cmake --build build --parallel $(nproc)
- name: Bundle DLLs
shell: msys2 {0}
run: |
for exe in build/bin/wowee.exe build/bin/asset_extract.exe; do
[ -f "$exe" ] || continue
ldd "$exe" \
| awk '/=> \// { print $3 }' \
| grep -iv '^/c/Windows' \
| xargs -I{} sh -c 'cp -n "{}" build/bin/ 2>/dev/null || true'
done
- name: Bundle DLLs
shell: msys2 {0}
run: |
for exe in build/bin/wowee.exe build/bin/asset_extract.exe; do
[ -f "$exe" ] || continue
ldd "$exe" \
| awk '/=> \// { print $3 }' \
| grep -iv '^/c/Windows' \
| xargs -I{} sh -c 'cp -n "{}" build/bin/ 2>/dev/null || true'
done
- name: Package
shell: msys2 {0}
run: |
TAG="${GITHUB_REF_NAME}"
STAGING="wowee-${TAG}-windows-${{ matrix.arch }}"
mkdir -p "${STAGING}"
- name: Package
shell: msys2 {0}
run: |
TAG="${GITHUB_REF_NAME}"
STAGING="wowee-${TAG}-windows-${{ matrix.arch }}"
mkdir -p "${STAGING}"
# Binary and DLLs
cp build/bin/wowee.exe "${STAGING}/"
cp build/bin/*.dll "${STAGING}/" 2>/dev/null || true
# Binary and DLLs
cp build/bin/wowee.exe "${STAGING}/"
cp build/bin/*.dll "${STAGING}/" 2>/dev/null || true
# Asset extraction tool (if built — requires StormLib)
if [ -f build/bin/asset_extract.exe ]; then
cp build/bin/asset_extract.exe "${STAGING}/"
fi
# Asset extraction tool (if built — requires StormLib)
if [ -f build/bin/asset_extract.exe ]; then
cp build/bin/asset_extract.exe "${STAGING}/"
fi
# Extraction scripts and GUI
cp extract_assets.ps1 "${STAGING}/"
cp extract_assets.bat "${STAGING}/"
cp tools/asset_pipeline_gui.py "${STAGING}/"
# Extraction scripts and GUI
cp extract_assets.ps1 "${STAGING}/"
cp extract_assets.bat "${STAGING}/"
cp tools/asset_pipeline_gui.py "${STAGING}/"
# Assets (exclude proprietary music)
mkdir -p "${STAGING}/assets"
for d in build/bin/assets/*/; do
dirname="$(basename "$d")"
[ "$dirname" = "Original Music" ] && continue
cp -r "$d" "${STAGING}/assets/"
done
# Copy top-level asset files
cp build/bin/assets/* "${STAGING}/assets/" 2>/dev/null || true
# Assets (exclude proprietary music)
mkdir -p "${STAGING}/assets"
for d in build/bin/assets/*/; do
dirname="$(basename "$d")"
[ "$dirname" = "Original Music" ] && continue
cp -r "$d" "${STAGING}/assets/"
done
# Copy top-level asset files
cp build/bin/assets/* "${STAGING}/assets/" 2>/dev/null || true
# Data directory (git-tracked files only)
git ls-files Data/ | while read -r f; do
mkdir -p "${STAGING}/$(dirname "$f")"
cp "$f" "${STAGING}/$f"
done
# Data directory (git-tracked files only)
git ls-files Data/ | while read -r f; do
mkdir -p "${STAGING}/$(dirname "$f")"
cp "$f" "${STAGING}/$f"
done
# Create ZIP
zip -r "${STAGING}.zip" "${STAGING}"
# Create ZIP
zip -r "${STAGING}.zip" "${STAGING}"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: wowee-windows-${{ matrix.arch }}
path: wowee-*.zip
if-no-files-found: error
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: wowee-windows-${{ matrix.arch }}
path: wowee-*.zip
if-no-files-found: error
release:
name: Create Release
needs: [build-linux, build-macos, build-windows]
needs: [ build-linux, build-macos, build-windows ]
runs-on: ubuntu-latest
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts/
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts/
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
TAG="${GITHUB_REF_NAME}"
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
TAG="${GITHUB_REF_NAME}"
# Collect all release files
FILES=()
for f in artifacts/*/*; do
FILES+=("$f")
done
# Collect all release files
FILES=()
for f in artifacts/*/*; do
FILES+=("$f")
done
gh release create "${TAG}" \
--title "Wowee ${TAG}" \
--generate-notes \
"${FILES[@]}"
gh release create "${TAG}" \
--title "Wowee ${TAG}" \
--generate-notes \
"${FILES[@]}"

48
.gitignore vendored
View file

@ -1,5 +1,7 @@
# Build directories
build/
build_asan/
build-debug/
build-sanitize/
bin/
lib/
@ -17,6 +19,7 @@ Makefile
*.obj
*.slo
*.lo
*.spv
# Compiled Dynamic libraries
*.so
@ -34,6 +37,9 @@ Makefile
*.app
wowee
# Claude Code internal state
.claude/
# IDE files
.vscode/
.idea/
@ -42,13 +48,31 @@ wowee
*~
.DS_Store
# Compilation database (regenerated by cmake)
compile_commands.json
# Language server caches
.ccls
.ccls-cache/
.cache/clangd/
# Tags
tags
TAGS
.tags
cscope.out
# External dependencies (except submodules and vendored headers)
extern/*
!extern/.gitkeep
!extern/catch2
!extern/imgui
!extern/vk-bootstrap
!extern/FidelityFX-SDK
!extern/FidelityFX-FSR2
!extern/vk_mem_alloc.h
!extern/lua-5.1.5
!extern/VERSIONS.md
# ImGui state
imgui.ini
@ -68,27 +92,9 @@ saves/
wowee_[0-9][0-9][0-9][0-9]
# Extracted assets (run ./extract_assets.sh or .\extract_assets.ps1 to generate)
Data/db/
Data/character/
Data/creature/
Data/terrain/
Data/world/
Data/interface/
Data/item/
Data/sound/
Data/spell/
Data/environment/
Data/misc/
Data/enUS/
Data/Character/
Data/Creature/
Data/World/
Data/manifest.json
Data/expansions/*/manifest.json
Data/expansions/*/assets/
Data/expansions/*/overlay/
Data/expansions/*/db/*.csv
Data/hd/
Data/*
!Data/opcodes
ingest/
# Asset pipeline state and texture packs

10
.gitmodules vendored
View file

@ -6,3 +6,13 @@
path = extern/vk-bootstrap
url = https://github.com/charles-lunarg/vk-bootstrap.git
shallow = true
[submodule "extern/FidelityFX-SDK"]
path = extern/FidelityFX-SDK
url = https://github.com/Kelsidavis/FidelityFX-SDK.git
shallow = true
update = none
[submodule "extern/FidelityFX-FSR2"]
path = extern/FidelityFX-FSR2
url = https://github.com/Kelsidavis/FidelityFX-FSR2.git
shallow = true
ignore = dirty

View file

@ -12,7 +12,7 @@ This document provides platform-specific build instructions for WoWee.
sudo apt update
sudo apt install -y \
build-essential cmake pkg-config git \
libsdl2-dev libglew-dev libglm-dev \
libsdl2-dev libglm-dev \
libssl-dev zlib1g-dev \
libvulkan-dev vulkan-tools glslc \
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev \
@ -28,7 +28,7 @@ sudo apt install -y \
```bash
sudo pacman -S --needed \
base-devel cmake pkgconf git \
sdl2 glew glm openssl zlib \
sdl2 glm openssl zlib \
vulkan-headers vulkan-icd-loader vulkan-tools shaderc \
ffmpeg unicorn stormlib
```
@ -83,7 +83,7 @@ Vulkan on macOS is provided via MoltenVK (a Vulkan-to-Metal translation layer),
which is included in the `vulkan-loader` Homebrew package.
```bash
brew install cmake pkg-config sdl2 glew glm openssl@3 zlib ffmpeg unicorn \
brew install cmake pkg-config sdl2 glm openssl@3 zlib ffmpeg unicorn \
stormlib vulkan-loader vulkan-headers shaderc
```
@ -137,7 +137,6 @@ pacman -S --needed \
mingw-w64-x86_64-ninja \
mingw-w64-x86_64-pkgconf \
mingw-w64-x86_64-SDL2 \
mingw-w64-x86_64-glew \
mingw-w64-x86_64-glm \
mingw-w64-x86_64-openssl \
mingw-w64-x86_64-zlib \
@ -174,7 +173,7 @@ For users who prefer Visual Studio over MSYS2.
### vcpkg Dependencies
```powershell
vcpkg install sdl2 glew glm openssl zlib ffmpeg stormlib --triplet x64-windows
vcpkg install sdl2 glm openssl zlib ffmpeg stormlib --triplet x64-windows
```
### Clone

166
CHANGELOG.md Normal file
View file

@ -0,0 +1,166 @@
# Changelog
## [Unreleased] — changes since v1.8.9-preview
### Architecture
- Break Application::getInstance() singleton from GameHandler via GameServices struct
- EntityController refactoring (SOLID decomposition)
- Extract 8 domain handler classes from GameHandler
- Replace 3,300-line switch with dispatch table
- Multi-platform Docker build system (Linux, macOS arm64/x86_64, Windows cross-compilation)
- Decompose ChatPanel monolith into 15+ modules under `src/ui/chat/` with IChatCommand interface, ChatCommandRegistry, MacroEvaluator, ChatMarkupParser/Renderer, ChatBubbleManager, ChatTabManager, GameStateAdapter, and 11 command modules (PR #62)
- Decompose WorldMap (1,360 LOC) into 16 modules under `src/rendering/world_map/` with WorldMapFacade (PIMPL), CompositeRenderer, DataRepository, CoordinateProjection, ViewStateMachine, 9 overlay layers (PR #61)
- Extract reusable CatmullRomSpline module to `src/math/` with O(log n) binary search and fused position+tangent evaluation (PR #60)
- Decompose TransportManager (1,200→500 LOC): extract TransportPathRepository, TransportClockSync, TransportAnimator; consolidate 7 duplicated spline parsers into `spline_packet.cpp` (PR #60)
### World Editor (tools/editor/)
- Standalone world editor for creating custom WoW zones (14.7k+ lines, 59 files)
- 6 editing modes: Sculpt, Paint, Objects, Water, NPCs, Quests
- 30+ terrain tools: procedural generators (hill, mesa, crater, canyon, island, ridge, dunes), thermal erosion, noise, mirror/rotate, stamp copy/paste with file persistence
- Multi-select objects (Ctrl+Shift+Click), Select All (Ctrl+A), Select by Type (M2/WMO)
- Time-of-day lighting with dawn/dusk/night transitions and color pickers
- Texture eyedropper (Alt+Click), brush size presets + bracket keys
- Object tools: snap to ground, align to slope, flatten terrain around buildings, scatter with auto-align
- River/road path tool with click-to-set points and translucent preview ribbon
- Quest chains with circular reference detection, inline editing, load/save
- 631 creature presets across 8 categories with patrol path editing
- Full undo/redo for ALL terrain operations (generators, transforms, paint)
- Auto-save with configurable interval, unsaved changes quit confirmation
- Zone rename, recent zones menu, adjacent tile export with edge stitching
- Zone metadata panel: configurable Map ID, Display Name, Description
- Zone gameplay flags: Allow Flying, PvP, Indoor, Sanctuary (serialized to zone.json)
- Zone audio configuration: music track, day/night ambience, volume sliders, presets
- PNG/JPG/BMP/TGA heightmap image import (any resolution, 8/16-bit, undoable)
- Collision slope overlay on minimap (steep terrain visualization)
- Client-side WOC collision loading with walkability queries
- Zone map image export: colored top-down PNG with terrain, water, objects
- SQL spawn export for AzerothCore/TrinityCore (creature_template, creature,
waypoint_data, quest_template — ready-to-import .sql files)
- Server module generator: one-click AzerothCore module with map registration,
spawns, teleport command, zone flags, conf snippet, and admin README
- Biome vegetation auto-population: one-click procedural placement of
trees, rocks, bushes, ferns per biome (10 biomes with density rules)
- Live open format validation (0-7 score) in File menu
### Novel Open Formats (7/7 Blizzard format replacements)
- ADT → WOT/WHM: terrain metadata + binary heightmap with alpha maps and doodad/WMO placements
- WDT → zone.json: map definition with full placement arrays
- BLP → PNG: texture override system
- DBC → JSON: data tables via DBCFile::loadJSON()
- M2 → WOM (WOM1/WOM2): static models + animated models with bones, keyframes, skeletal binding
- WMO → WOB (WOB1): buildings with material flags/shader/blendMode, doodad rotation
- Collision → WOC (WOC1): walkability mesh with slope classification, hole support, water flags
- WCP (WCP1): content pack archive with categorized file list
- Terrain stamps: portable terrain features saved as JSON
- All formats documented in FORMAT_SPEC.md v1.1
- Client auto-loads open formats from custom_zones/ and output/ directories
- Batch convert: M2→WOM and WMO→WOB from filesystem or asset manifest
- WCP Import & Load: one-click unpack + auto-open for editing
- 328 test assertions across 84 test cases (DBC binary+JSON, WOB, WHM, WOT, WOC)
### Features
- Spell visual effects system with bone-tracked ribbons and particles (PR #58)
- GM command support: 190-command data table with dot-prefix interception, tab-completion, `/gmhelp` with category filter (PR #62)
- ZMP pixel-accurate zone hover detection on world map (PR #63)
- Textured player arrow (MinimapArrow.blp) on world map (PR #63)
- Multi-segment path interpolation for entity movement (PR #59)
- Character screen keyboard navigation (Up/Down/Enter) (PR #59)
### Bug Fixes (v1.8.10+)
- Fix walk/run animation persisting after entity arrival (PR #59)
- Fix entity teleport during dead-reckoning overrun phase (PR #59)
- Fix Vulkan crash on window resize when minimized (0×0 extent) (PR #59)
- Fix quest log not populating on quest accept (PR #59)
- Fix hit-reaction animation being overridden on next frame (PR #59)
- Fix ChatType enum values to match WoW wire protocol (SAY=0x01 not 0x00) (PR #62)
- Fix BG_SYSTEM_* values from 8284 (UB in bitmask shifts) to 0x240x26 (PR #62)
- Fix infinite Enter key loop after teleport (PR #62)
- Remove stale kVOffset (-0.15) from zone hover detection causing ~15% vertical offset
- Add null guard for cachedGameHandler_ in ChatPanel input callback
- Fix cosmic highlight aspect ratio with resolution-independent square rendering
- Skip transport waypoints with broken coordinate conversion instead of silent use
- Fix spline endpoint validation bypass for entities near world origin
- Fix off-by-one in chat link insertion buffer capacity check
- Zero window border in world map to eliminate content/window gap
### Tests
- Add 19 new test files (27 total, up from 8):
- Chat: chat_markup_parser, chat_tab_completer, gm_commands, macro_evaluator
- World map: world_map, coordinate_projection, exploration_state, map_resolver, view_state_machine, zone_metadata
- Transport/spline: spline, transport_components, transport_path_repo
- Animation: animation_ids, locomotion_fsm, combat_fsm, activity_fsm, anim_capability, indoor_shadows
### Bug Fixes (v1.8.2v1.8.9)
- Fix VkTexture ownsSampler_ flag after move/destroy (prevented double-free)
- Fix unsigned underflow in Warden PE section loading (buffer overflow on malformed modules)
- Add bounds checks to Warden readLE32/readLE16 (out-of-bounds on untrusted PE data)
- Fix undefined behavior: SDL_BUTTON(0) computed 1 << -1 (negative shift)
- Fix BigNum::toHex/toDecimal null dereference on OpenSSL allocation failure
- Remove duplicate zone weather entry silently overwriting Dustwallow Marsh
- Fix LLVM apt repo codename (jammy→noble) in macOS Docker build
- Add missing mkdir in Linux Docker build script
- Clamp player percentage stats (block/dodge/parry/crit) to prevent NaN from corrupted packets
- Guard fsPath underflow in tryLoadPngOverride
### Code Quality (v1.8.2v1.8.9)
- 30+ named constants replacing magic numbers across game, rendering, and pipeline code
- 55+ why-comments documenting WoW protocol quirks, format specifics, and design rationale
- 8 DRY extractions (findOnUseSpellId, createFallbackTextures, finalizeSampler,
renderClassRestriction/renderRaceRestriction, and more)
- Scope macOS -undefined dynamic_lookup linker flag to wowee target only
- Replace goto patterns with structured control flow (do/while(false), lambdas)
- Zero out GameServices in Application::shutdown to prevent dangling pointers
---
## [v1.8.1-preview] — 2026-03-23
### Performance
- Eliminate ~70 unnecessary sqrt ops per frame; constexpr reciprocals and cache optimizations
- Skip bone animation for LOD3 models; frustum-cull water surfaces
- Eliminate per-frame heap allocations in M2 renderer
- Convert entity/skill/DBC/warden maps to unordered_map; fix 3x contacts scan
- Eliminate double map lookups and dynamic_cast in render loops
- Use second GPU queue for parallel texture/buffer uploads
- Time-budget tile finalization to prevent 1+ second main-loop stalls
- Add Vulkan pipeline cache persistence for faster startup
### Bug Fixes
- Fix spline parsing with expansion context; preload DBC caches at world entry
- Fix NPC/player attack animation to use weapon-appropriate anim ID
- Fix equipment visibility and follow-target run speed
- Fix inspect (packed GUID) and client-side auto-walk for follow
- Fix mail money uint64, other-player cape textures, zone toast dedup, TCP_NODELAY
- Guard spline point loop against unsigned underflow; guard hexDecode/stoi/stof
- Fix infinite recursion in toLowerInPlace and operator precedence bugs
- Fix 3D audio coords for PLAY_OBJECT_SOUND; correct melee swing sound paths
- Prevent Vulkan sampler exhaustion crash; skip pipeline cache on NVIDIA
- Skip FSR3 frame gen on non-AMD GPUs to prevent driver crash
- Fix chest GO interaction (send GAMEOBJ_USE+LOOT together)
- Restore WMO wall collision threshold; fix off-screen bag positions
- Guard texture log dedup sets with mutex for thread safety
- Fix lua_pcall return check in ACTIONBAR_PAGE_CHANGED
### Features
- Render equipment on other players (helmets, weapons, belts, wrists, shoulders)
- Target frame right-click context menu
- Crafting sounds and Create All button
- Server-synced bag sort
- Log GPU vendor/name at init
### Security
- Add path traversal rejection and packet length validation
### Code Quality
- Packet API: add readPackedGuid, writePackedGuid, writeFloat, getRemainingSize,
hasRemaining, hasData, skipAll (replacing 1300+ verbose expressions)
- GameHandler helpers: isInWorld, isPreWotlk, guidToUnitId, lookupName,
getUnitByGuid, fireAddonEvent, withSoundManager
- Dispatch table: registerHandler, registerSkipHandler, registerWorldHandler,
registerErrorHandler (replacing 120+ lambda wrappers)
- Shared ui_colors.hpp with named constants replacing 200+ inline color literals
- Promote 50+ static const arrays to constexpr across audio/core/rendering/UI
- Deduplicate class name/color functions, enchantment cache, item-set DBC keys
- Extract settings tabs, GameHandler::update() phases, loadWeaponM2 into methods
- Remove 12 duplicate dispatch registrations and C-style casts
- Extract toHexString, toLowerInPlace, duration formatting, Lua return helpers

View file

@ -25,8 +25,9 @@ endif()
# Options
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
option(WOWEE_BUILD_TESTS "Build tests" OFF)
option(WOWEE_BUILD_TESTS "Build tests" ON)
option(WOWEE_ENABLE_ASAN "Enable AddressSanitizer (Debug builds)" OFF)
option(WOWEE_ENABLE_TRACY "Enable Tracy profiler instrumentation" OFF)
option(WOWEE_ENABLE_AMD_FSR2 "Enable AMD FidelityFX FSR2 backend when SDK is present" ON)
option(WOWEE_ENABLE_AMD_FSR3_FRAMEGEN "Enable AMD FidelityFX SDK FSR3 frame generation interface probe when SDK is present" ON)
option(WOWEE_BUILD_AMD_FSR3_RUNTIME "Build native AMD FidelityFX VK runtime (Path A) from extern/FidelityFX-SDK/Kits" ON)
@ -248,34 +249,98 @@ endif()
find_package(SDL2 REQUIRED)
find_package(Vulkan QUIET)
if(NOT Vulkan_FOUND)
# Fallback: some distros / CMake versions need pkg-config to locate Vulkan.
find_package(PkgConfig QUIET)
if(PkgConfig_FOUND)
pkg_check_modules(VULKAN_PKG vulkan)
if(VULKAN_PKG_FOUND)
add_library(Vulkan::Vulkan INTERFACE IMPORTED)
set_target_properties(Vulkan::Vulkan PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${VULKAN_PKG_INCLUDE_DIRS}"
INTERFACE_LINK_LIBRARIES "${VULKAN_PKG_LIBRARIES}"
)
if(VULKAN_PKG_LIBRARY_DIRS)
# For Windows cross-compilation the host pkg-config finds the Linux libvulkan-dev
# and injects /usr/include as an INTERFACE_INCLUDE_DIRECTORY, which causes
# MinGW clang to pull in glibc headers (bits/libc-header-start.h) instead of
# the MinGW sysroot headers. Skip the host pkg-config path entirely and instead
# locate Vulkan via vcpkg-installed vulkan-headers or the MinGW toolchain.
if(CMAKE_CROSSCOMPILING AND WIN32)
# The cross-compile build script generates a Vulkan import library
# (libvulkan-1.a) in ${CMAKE_BINARY_DIR}/vulkan-import from the headers.
set(_VULKAN_IMPORT_DIR "${CMAKE_BINARY_DIR}/vulkan-import")
find_package(VulkanHeaders CONFIG QUIET)
if(VulkanHeaders_FOUND)
if(NOT TARGET Vulkan::Vulkan)
add_library(Vulkan::Vulkan INTERFACE IMPORTED)
endif()
# Vulkan::Headers is provided by vcpkg's vulkan-headers port and carries
# the correct MinGW include path — no Linux system headers involved.
set_property(TARGET Vulkan::Vulkan APPEND PROPERTY
INTERFACE_LINK_LIBRARIES Vulkan::Headers)
# Link against the Vulkan loader import library (vulkan-1.dll).
if(EXISTS "${_VULKAN_IMPORT_DIR}/libvulkan-1.a")
set_property(TARGET Vulkan::Vulkan APPEND PROPERTY
INTERFACE_LINK_DIRECTORIES "${VULKAN_PKG_LIBRARY_DIRS}")
INTERFACE_LINK_DIRECTORIES "${_VULKAN_IMPORT_DIR}")
set_property(TARGET Vulkan::Vulkan APPEND PROPERTY
INTERFACE_LINK_LIBRARIES vulkan-1)
endif()
set(Vulkan_FOUND TRUE)
message(STATUS "Found Vulkan via pkg-config: ${VULKAN_PKG_LIBRARIES}")
message(STATUS "Found Vulkan headers for Windows cross-compile via vcpkg VulkanHeaders")
else()
# Last-resort: check the LLVM-MinGW toolchain sysroot directly.
find_path(_VULKAN_MINGW_INCLUDE NAMES vulkan/vulkan.h
PATHS /opt/llvm-mingw/x86_64-w64-mingw32/include NO_DEFAULT_PATH)
if(_VULKAN_MINGW_INCLUDE)
if(NOT TARGET Vulkan::Vulkan)
add_library(Vulkan::Vulkan INTERFACE IMPORTED)
endif()
set_target_properties(Vulkan::Vulkan PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_VULKAN_MINGW_INCLUDE}")
# Link against the Vulkan loader import library (vulkan-1.dll).
if(EXISTS "${_VULKAN_IMPORT_DIR}/libvulkan-1.a")
set_property(TARGET Vulkan::Vulkan APPEND PROPERTY
INTERFACE_LINK_DIRECTORIES "${_VULKAN_IMPORT_DIR}")
set_property(TARGET Vulkan::Vulkan APPEND PROPERTY
INTERFACE_LINK_LIBRARIES vulkan-1)
endif()
set(Vulkan_FOUND TRUE)
message(STATUS "Found Vulkan headers in LLVM-MinGW sysroot: ${_VULKAN_MINGW_INCLUDE}")
endif()
endif()
elseif(CMAKE_CROSSCOMPILING AND CMAKE_SYSTEM_NAME STREQUAL "Darwin")
# macOS cross-compilation: use vcpkg-installed vulkan-headers.
# The host pkg-config would find Linux libvulkan-dev headers which the
# macOS cross-compiler cannot use (different sysroot).
find_package(VulkanHeaders CONFIG QUIET)
if(VulkanHeaders_FOUND)
if(NOT TARGET Vulkan::Vulkan)
add_library(Vulkan::Vulkan INTERFACE IMPORTED)
endif()
set_property(TARGET Vulkan::Vulkan APPEND PROPERTY
INTERFACE_LINK_LIBRARIES Vulkan::Headers)
set(Vulkan_FOUND TRUE)
message(STATUS "Found Vulkan headers for macOS cross-compile via vcpkg VulkanHeaders")
endif()
else()
# Fallback: some distros / CMake versions need pkg-config to locate Vulkan.
find_package(PkgConfig QUIET)
if(PkgConfig_FOUND)
pkg_check_modules(VULKAN_PKG vulkan)
if(VULKAN_PKG_FOUND)
add_library(Vulkan::Vulkan INTERFACE IMPORTED)
set_target_properties(Vulkan::Vulkan PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${VULKAN_PKG_INCLUDE_DIRS}"
INTERFACE_LINK_LIBRARIES "${VULKAN_PKG_LIBRARIES}"
)
if(VULKAN_PKG_LIBRARY_DIRS)
set_property(TARGET Vulkan::Vulkan APPEND PROPERTY
INTERFACE_LINK_DIRECTORIES "${VULKAN_PKG_LIBRARY_DIRS}")
endif()
set(Vulkan_FOUND TRUE)
message(STATUS "Found Vulkan via pkg-config: ${VULKAN_PKG_LIBRARIES}")
endif()
endif()
endif()
if(NOT Vulkan_FOUND)
message(FATAL_ERROR "Could not find Vulkan. Install libvulkan-dev (Linux), vulkan-loader (macOS), or the Vulkan SDK (Windows).")
endif()
endif()
# GL/GLEW kept temporarily for unconverted sub-renderers during Vulkan migration.
# These files compile against GL types but their code is never called — the Vulkan
# path is the only active rendering backend. Remove in Phase 7 when all renderers
# are converted and grep confirms zero GL references.
find_package(OpenGL QUIET)
find_package(GLEW QUIET)
# macOS cross-compilation: the Vulkan loader (MoltenVK) is not available at link
# time. Allow unresolved Vulkan symbols — they are resolved at runtime.
if(CMAKE_CROSSCOMPILING AND CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(WOWEE_MACOS_CROSS_COMPILE TRUE)
endif()
find_package(OpenSSL REQUIRED)
find_package(Threads REQUIRED)
find_package(ZLIB REQUIRED)
@ -422,11 +487,26 @@ endif()
set(WOWEE_SOURCES
# Core
src/core/application.cpp
src/core/entity_spawner.cpp
src/core/entity_spawner_player.cpp
src/core/entity_spawner_processing.cpp
src/core/appearance_composer.cpp
src/core/world_loader.cpp
src/core/npc_interaction_callback_handler.cpp
src/core/audio_callback_handler.cpp
src/core/entity_spawn_callback_handler.cpp
src/core/animation_callback_handler.cpp
src/core/transport_callback_handler.cpp
src/core/world_entry_callback_handler.cpp
src/core/ui_screen_callback_handler.cpp
src/core/window.cpp
src/core/input.cpp
src/core/logger.cpp
src/core/memory_monitor.cpp
# Math
src/math/spline.cpp
# Network
src/network/socket.cpp
src/network/packet.cpp
@ -450,16 +530,35 @@ set(WOWEE_SOURCES
src/game/opcode_table.cpp
src/game/update_field_table.cpp
src/game/game_handler.cpp
src/game/game_handler_packets.cpp
src/game/game_handler_callbacks.cpp
src/game/chat_handler.cpp
src/game/movement_handler.cpp
src/game/combat_handler.cpp
src/game/spell_handler.cpp
src/game/inventory_handler.cpp
src/game/social_handler.cpp
src/game/quest_handler.cpp
src/game/entity_controller.cpp
src/game/warden_handler.cpp
src/game/warden_crypto.cpp
src/game/warden_module.cpp
src/game/warden_emulator.cpp
src/game/warden_memory.cpp
src/game/transport_manager.cpp
src/game/transport_path_repository.cpp
src/game/transport_clock_sync.cpp
src/game/transport_animator.cpp
src/game/world.cpp
src/game/player.cpp
src/game/entity.cpp
src/game/opcodes.cpp
src/game/world_packets.cpp
src/game/world_packets_social.cpp
src/game/world_packets_entity.cpp
src/game/world_packets_world.cpp
src/game/world_packets_economy.cpp
src/game/spline_packet.cpp
src/game/packet_parsers_tbc.cpp
src/game/packet_parsers_classic.cpp
src/game/character.cpp
@ -468,6 +567,7 @@ set(WOWEE_SOURCES
# Audio
src/audio/audio_engine.cpp
src/audio/audio_coordinator.cpp
src/audio/music_manager.cpp
src/audio/footstep_manager.cpp
src/audio/activity_sound_manager.cpp
@ -489,6 +589,12 @@ set(WOWEE_SOURCES
src/pipeline/wmo_loader.cpp
src/pipeline/adt_loader.cpp
src/pipeline/wdt_loader.cpp
src/pipeline/wowee_terrain_loader.cpp
src/pipeline/wowee_model.cpp
src/pipeline/wowee_model_fromm2.cpp
src/pipeline/wowee_building.cpp
src/pipeline/wowee_collision.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/dbc_layout.cpp
src/pipeline/terrain_mesh.cpp
@ -505,12 +611,9 @@ set(WOWEE_SOURCES
# Rendering
src/rendering/renderer.cpp
src/rendering/amd_fsr3_runtime.cpp
src/rendering/shader.cpp
src/rendering/mesh.cpp
src/rendering/camera.cpp
src/rendering/camera_controller.cpp
src/rendering/material.cpp
src/rendering/scene.cpp
src/rendering/terrain_renderer.cpp
src/rendering/terrain_manager.cpp
src/rendering/frustum.cpp
@ -529,15 +632,53 @@ set(WOWEE_SOURCES
src/rendering/character_preview.cpp
src/rendering/wmo_renderer.cpp
src/rendering/m2_renderer.cpp
src/rendering/m2_renderer_render.cpp
src/rendering/m2_renderer_particles.cpp
src/rendering/m2_renderer_instance.cpp
src/rendering/m2_model_classifier.cpp
src/rendering/render_graph.cpp
src/rendering/hiz_system.cpp
src/rendering/quest_marker_renderer.cpp
src/rendering/minimap.cpp
src/rendering/world_map.cpp
src/rendering/world_map/coordinate_projection.cpp
src/rendering/world_map/map_resolver.cpp
src/rendering/world_map/exploration_state.cpp
src/rendering/world_map/zone_metadata.cpp
src/rendering/world_map/data_repository.cpp
src/rendering/world_map/view_state_machine.cpp
src/rendering/world_map/composite_renderer.cpp
src/rendering/world_map/overlay_renderer.cpp
src/rendering/world_map/input_handler.cpp
src/rendering/world_map/world_map_facade.cpp
src/rendering/world_map/layers/player_marker_layer.cpp
src/rendering/world_map/layers/party_dot_layer.cpp
src/rendering/world_map/layers/taxi_node_layer.cpp
src/rendering/world_map/layers/poi_marker_layer.cpp
src/rendering/world_map/layers/quest_poi_layer.cpp
src/rendering/world_map/layers/corpse_marker_layer.cpp
src/rendering/world_map/layers/zone_highlight_layer.cpp
src/rendering/world_map/layers/coordinate_display.cpp
src/rendering/world_map/layers/subzone_tooltip_layer.cpp
src/rendering/swim_effects.cpp
src/rendering/mount_dust.cpp
src/rendering/levelup_effect.cpp
src/rendering/charge_effect.cpp
src/rendering/spell_visual_system.cpp
src/rendering/post_process_pipeline.cpp
src/rendering/overlay_system.cpp
src/rendering/animation_controller.cpp
src/rendering/animation/animation_ids.cpp
src/rendering/animation/emote_registry.cpp
src/rendering/animation/footstep_driver.cpp
src/rendering/animation/sfx_state_driver.cpp
src/rendering/animation/anim_capability_probe.cpp
src/rendering/animation/locomotion_fsm.cpp
src/rendering/animation/combat_fsm.cpp
src/rendering/animation/activity_fsm.cpp
src/rendering/animation/mount_fsm.cpp
src/rendering/animation/character_animator.cpp # Renamed from player_animator.cpp; npc_animator.cpp removed
src/rendering/animation/animation_manager.cpp
src/rendering/loading_screen.cpp
$<$<BOOL:${HAVE_FFMPEG}>:${CMAKE_CURRENT_SOURCE_DIR}/src/rendering/video_player.cpp>
# UI
src/ui/ui_manager.cpp
@ -546,6 +687,42 @@ set(WOWEE_SOURCES
src/ui/character_create_screen.cpp
src/ui/character_screen.cpp
src/ui/game_screen.cpp
src/ui/game_screen_frames.cpp
src/ui/game_screen_hud.cpp
src/ui/game_screen_minimap.cpp
src/ui/chat_panel.cpp
src/ui/chat/chat_settings.cpp
src/ui/chat/chat_input.cpp
src/ui/chat/chat_utils.cpp
src/ui/chat/chat_tab_manager.cpp
src/ui/chat/chat_bubble_manager.cpp
src/ui/chat/chat_markup_parser.cpp
src/ui/chat/chat_markup_renderer.cpp
src/ui/chat/item_tooltip_renderer.cpp
src/ui/chat/chat_command_registry.cpp
src/ui/chat/chat_tab_completer.cpp
src/ui/chat/macro_evaluator.cpp
src/ui/chat/macro_eval_convenience.cpp
src/ui/chat/game_state_adapter.cpp
src/ui/chat/input_modifier_adapter.cpp
src/ui/chat/commands/system_commands.cpp
src/ui/chat/commands/social_commands.cpp
src/ui/chat/commands/channel_commands.cpp
src/ui/chat/commands/combat_commands.cpp
src/ui/chat/commands/group_commands.cpp
src/ui/chat/commands/guild_commands.cpp
src/ui/chat/commands/target_commands.cpp
src/ui/chat/commands/emote_commands.cpp
src/ui/chat/commands/misc_commands.cpp
src/ui/chat/commands/help_commands.cpp
src/ui/chat/commands/gm_commands.cpp
src/ui/toast_manager.cpp
src/ui/dialog_manager.cpp
src/ui/settings_panel.cpp
src/ui/combat_ui.cpp
src/ui/social_panel.cpp
src/ui/action_bar_panel.cpp
src/ui/window_manager.cpp
src/ui/inventory_screen.cpp
src/ui/quest_log_screen.cpp
src/ui/spellbook_screen.cpp
@ -555,6 +732,13 @@ set(WOWEE_SOURCES
# Addons
src/addons/addon_manager.cpp
src/addons/lua_engine.cpp
src/addons/lua_unit_api.cpp
src/addons/lua_spell_api.cpp
src/addons/lua_inventory_api.cpp
src/addons/lua_quest_api.cpp
src/addons/lua_social_api.cpp
src/addons/lua_system_api.cpp
src/addons/lua_action_api.cpp
src/addons/toc_parser.cpp
# Main
@ -624,12 +808,9 @@ set(WOWEE_HEADERS
include/rendering/vk_pipeline.hpp
include/rendering/vk_render_target.hpp
include/rendering/renderer.hpp
include/rendering/shader.hpp
include/rendering/mesh.hpp
include/rendering/camera.hpp
include/rendering/camera_controller.hpp
include/rendering/material.hpp
include/rendering/scene.hpp
include/rendering/terrain_renderer.hpp
include/rendering/terrain_manager.hpp
include/rendering/frustum.hpp
@ -644,11 +825,30 @@ set(WOWEE_HEADERS
include/rendering/lightning.hpp
include/rendering/swim_effects.hpp
include/rendering/world_map.hpp
include/rendering/world_map/world_map_types.hpp
include/rendering/world_map/coordinate_projection.hpp
include/rendering/world_map/map_resolver.hpp
include/rendering/world_map/exploration_state.hpp
include/rendering/world_map/zone_metadata.hpp
include/rendering/world_map/data_repository.hpp
include/rendering/world_map/view_state_machine.hpp
include/rendering/world_map/composite_renderer.hpp
include/rendering/world_map/overlay_renderer.hpp
include/rendering/world_map/input_handler.hpp
include/rendering/world_map/world_map_facade.hpp
include/rendering/world_map/layers/player_marker_layer.hpp
include/rendering/world_map/layers/party_dot_layer.hpp
include/rendering/world_map/layers/taxi_node_layer.hpp
include/rendering/world_map/layers/poi_marker_layer.hpp
include/rendering/world_map/layers/quest_poi_layer.hpp
include/rendering/world_map/layers/corpse_marker_layer.hpp
include/rendering/world_map/layers/zone_highlight_layer.hpp
include/rendering/world_map/layers/coordinate_display.hpp
include/rendering/world_map/layers/subzone_tooltip_layer.hpp
include/rendering/character_renderer.hpp
include/rendering/character_preview.hpp
include/rendering/wmo_renderer.hpp
include/rendering/loading_screen.hpp
include/rendering/video_player.hpp
include/ui/ui_manager.hpp
include/ui/auth_screen.hpp
@ -664,12 +864,16 @@ set(WOWEE_HEADERS
set(WOWEE_PLATFORM_SOURCES)
if(WIN32)
# Copy icon into build tree so llvm-rc can find it via the relative path in wowee.rc
# Copy icon into build tree so windres can find it via the relative path
# in wowee.rc ("assets\\wowee.ico"). Tell the RC compiler to also search
# the build directory — GNU windres uses cwd (already the build dir) but
# llvm-windres resolves relative to the .rc file, so it needs the hint.
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/assets/Wowee.ico
${CMAKE_CURRENT_BINARY_DIR}/assets/wowee.ico
COPYONLY
)
set(CMAKE_RC_FLAGS "${CMAKE_RC_FLAGS} -I ${CMAKE_CURRENT_BINARY_DIR} -I ${CMAKE_CURRENT_SOURCE_DIR}")
list(APPEND WOWEE_PLATFORM_SOURCES resources/wowee.rc)
endif()
@ -696,9 +900,23 @@ endif()
# Create executable
add_executable(wowee ${WOWEE_SOURCES} ${WOWEE_HEADERS} ${WOWEE_PLATFORM_SOURCES})
# Tracy profiler — zero overhead when WOWEE_ENABLE_TRACY is OFF
if(WOWEE_ENABLE_TRACY)
target_sources(wowee PRIVATE ${CMAKE_SOURCE_DIR}/extern/tracy/public/TracyClient.cpp)
target_compile_definitions(wowee PRIVATE TRACY_ENABLE)
target_include_directories(wowee SYSTEM PRIVATE ${CMAKE_SOURCE_DIR}/extern/tracy/public)
message(STATUS "Tracy profiler: ENABLED")
endif()
if(TARGET opcodes-generate)
add_dependencies(wowee opcodes-generate)
endif()
# macOS cross-compilation: MoltenVK is not available at link time.
# Allow unresolved Vulkan symbols — resolved at runtime. Scoped to wowee only.
if(WOWEE_MACOS_CROSS_COMPILE)
target_link_options(wowee PRIVATE "-undefined" "dynamic_lookup")
endif()
# FidelityFX-SDK headers can trigger compiler-specific pragma/unused-static noise
# when included through the runtime bridge; keep suppression scoped to that TU.
@ -739,14 +957,6 @@ target_link_libraries(wowee PRIVATE
${CMAKE_DL_LIBS}
)
# GL/GLEW linked temporarily for unconverted sub-renderers (removed in Phase 7)
if(TARGET OpenGL::GL)
target_link_libraries(wowee PRIVATE OpenGL::GL)
endif()
if(TARGET GLEW::GLEW)
target_link_libraries(wowee PRIVATE GLEW::GLEW)
endif()
if(HAVE_FFMPEG)
target_compile_definitions(wowee PRIVATE HAVE_FFMPEG)
target_link_libraries(wowee PRIVATE ${FFMPEG_LIBRARIES})
@ -836,12 +1046,20 @@ else()
# -g3 — maximum DWARF debug info (includes macro definitions)
# -Og — optimise for debugging (better than -O0, keeps most frames)
# -fno-omit-frame-pointer — preserve frame pointers so stack traces are clean
# Frame pointers in all configs (negligible perf cost, critical for crash backtraces)
target_compile_options(wowee PRIVATE
$<$<CONFIG:Debug>:-g3 -Og -fno-omit-frame-pointer>
$<$<CONFIG:RelWithDebInfo>:-g -fno-omit-frame-pointer>
-fno-omit-frame-pointer
$<$<CONFIG:Debug>:-g3 -Og>
$<$<CONFIG:RelWithDebInfo>:-g>
)
endif()
# ── Unit tests (Catch2) ──────────────────────────────────────
if(WOWEE_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
# AddressSanitizer — catch buffer overflows, use-after-free, etc.
# Enable with: cmake ... -DWOWEE_ENABLE_ASAN=ON -DCMAKE_BUILD_TYPE=Debug
if(WOWEE_ENABLE_ASAN)
@ -853,10 +1071,10 @@ if(WOWEE_ENABLE_ASAN)
$<$<CONFIG:Release>:/MD>
)
else()
target_compile_options(wowee PRIVATE -fsanitize=address -fno-omit-frame-pointer)
target_link_options(wowee PRIVATE -fsanitize=address)
target_compile_options(wowee PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer)
target_link_options(wowee PRIVATE -fsanitize=address,undefined)
endif()
message(STATUS "AddressSanitizer: ENABLED")
message(STATUS "AddressSanitizer + UBSan: ENABLED")
endif()
# Release build optimizations
@ -883,6 +1101,17 @@ add_custom_command(TARGET wowee POST_BUILD
COMMENT "Syncing assets to $<TARGET_FILE_DIR:wowee>/assets"
)
# Symlink Data/ next to the executable so expansion profiles, opcode tables,
# and other runtime data files are found when running from the build directory.
if(NOT WIN32)
add_custom_command(TARGET wowee POST_BUILD
COMMAND ${CMAKE_COMMAND} -E create_symlink
${CMAKE_CURRENT_SOURCE_DIR}/Data
$<TARGET_FILE_DIR:wowee>/Data
COMMENT "Symlinking Data to $<TARGET_FILE_DIR:wowee>/Data"
)
endif()
# On Windows, SDL 2.28+ uses LoadLibraryExW with LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
# which does NOT include System32. Copy vulkan-1.dll into the output directory so
# SDL_Vulkan_LoadLibrary can locate it without needing a full system PATH search.
@ -946,7 +1175,15 @@ if(STORMLIB_LIBRARY AND STORMLIB_INCLUDE_DIR)
tools/asset_extract/extractor.cpp
tools/asset_extract/path_mapper.cpp
tools/asset_extract/manifest_writer.cpp
tools/asset_extract/open_format_emitter.cpp
src/pipeline/dbc_loader.cpp
src/pipeline/blp_loader.cpp
src/pipeline/m2_loader.cpp
src/pipeline/wmo_loader.cpp
src/pipeline/adt_loader.cpp
src/pipeline/wowee_model.cpp
src/pipeline/wowee_building.cpp
src/pipeline/wowee_collision.cpp
src/core/logger.cpp
)
target_include_directories(asset_extract PRIVATE
@ -954,6 +1191,9 @@ if(STORMLIB_LIBRARY AND STORMLIB_INCLUDE_DIR)
${CMAKE_CURRENT_SOURCE_DIR}/tools/asset_extract
${STORMLIB_INCLUDE_DIR}
)
target_include_directories(asset_extract SYSTEM PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/extern
)
target_link_libraries(asset_extract PRIVATE
${STORMLIB_LIBRARY}
ZLIB::ZLIB
@ -986,6 +1226,7 @@ add_executable(dbc_to_csv
)
target_include_directories(dbc_to_csv PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/extern
)
target_link_libraries(dbc_to_csv PRIVATE Threads::Threads)
set_target_properties(dbc_to_csv PROPERTIES
@ -1054,6 +1295,126 @@ set_target_properties(blp_convert PROPERTIES
)
install(TARGETS blp_convert RUNTIME DESTINATION bin)
# ---- Tool: wowee_editor (Standalone World Editor) ----
add_executable(wowee_editor
tools/editor/main.cpp
tools/editor/editor_app.cpp
tools/editor/editor_camera.cpp
tools/editor/editor_viewport.cpp
tools/editor/editor_ui.cpp
tools/editor/editor_brush.cpp
tools/editor/editor_history.cpp
tools/editor/terrain_editor.cpp
tools/editor/texture_painter.cpp
tools/editor/object_placer.cpp
tools/editor/npc_spawner.cpp
tools/editor/npc_presets.cpp
tools/editor/sql_exporter.cpp
tools/editor/server_module_gen.cpp
tools/editor/quest_editor.cpp
tools/editor/transform_gizmo.cpp
tools/editor/zone_manifest.cpp
tools/editor/content_pack.cpp
tools/editor/wowee_terrain.cpp
tools/editor/editor_project.cpp
tools/editor/texture_exporter.cpp
tools/editor/dbc_exporter.cpp
tools/editor/asset_browser.cpp
tools/editor/editor_water.cpp
tools/editor/editor_markers.cpp
tools/editor/adt_writer.cpp
# Pipeline (asset loading)
src/pipeline/blp_loader.cpp
src/pipeline/dbc_loader.cpp
src/pipeline/dbc_layout.cpp
src/pipeline/asset_manager.cpp
src/pipeline/asset_manifest.cpp
src/pipeline/loose_file_reader.cpp
src/pipeline/m2_loader.cpp
src/pipeline/wmo_loader.cpp
src/pipeline/adt_loader.cpp
src/pipeline/wdt_loader.cpp
src/pipeline/wowee_terrain_loader.cpp
src/pipeline/wowee_model.cpp
src/pipeline/wowee_model_fromm2.cpp
src/pipeline/wowee_building.cpp
src/pipeline/wowee_collision.cpp
src/pipeline/custom_zone_discovery.cpp
src/pipeline/terrain_mesh.cpp
# Rendering core
src/rendering/vk_context.cpp
src/rendering/vk_utils.cpp
src/rendering/vk_shader.cpp
src/rendering/vk_texture.cpp
src/rendering/vk_buffer.cpp
src/rendering/vk_pipeline.cpp
src/rendering/camera.cpp
src/rendering/terrain_renderer.cpp
src/rendering/m2_renderer.cpp
src/rendering/m2_renderer_instance.cpp
src/rendering/m2_renderer_particles.cpp
src/rendering/m2_renderer_render.cpp
src/rendering/m2_model_classifier.cpp
src/rendering/wmo_renderer.cpp
src/rendering/frustum.cpp
# Core
src/core/window.cpp
src/core/logger.cpp
src/core/memory_monitor.cpp
# stb_image (needed by AssetManager for PNG overrides)
tools/editor/stb_image_impl.cpp
)
target_include_directories(wowee_editor PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/tools/editor
)
target_include_directories(wowee_editor SYSTEM PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/extern
${CMAKE_CURRENT_SOURCE_DIR}/extern/vk-bootstrap/src
)
target_link_libraries(wowee_editor PRIVATE
SDL2::SDL2
Vulkan::Vulkan
Threads::Threads
ZLIB::ZLIB
${CMAKE_DL_LIBS}
imgui
vk-bootstrap
)
if(TARGET glm::glm)
target_link_libraries(wowee_editor PRIVATE glm::glm)
elseif(glm_FOUND)
target_include_directories(wowee_editor PRIVATE ${GLM_INCLUDE_DIRS})
endif()
if(UNIX AND NOT APPLE)
find_package(X11 QUIET)
if(X11_FOUND)
target_link_libraries(wowee_editor PRIVATE X11)
endif()
endif()
if(WIN32)
target_link_libraries(wowee_editor PRIVATE ws2_32)
if(TARGET SDL2::SDL2main)
target_link_libraries(wowee_editor PRIVATE SDL2::SDL2main)
endif()
endif()
if(NOT MSVC)
target_compile_options(wowee_editor PRIVATE -Wall -Wextra -Wpedantic -Wno-missing-field-initializers)
endif()
if(GLSLC)
add_dependencies(wowee_editor wowee_shaders)
endif()
set_target_properties(wowee_editor PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
install(TARGETS wowee_editor RUNTIME DESTINATION bin)
message(STATUS " wowee_editor tool: ENABLED")
# Print configuration summary
message(STATUS "")
message(STATUS "Wowee Configuration:")

80
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,80 @@
# Contributing to Wowee
## Build Setup
See [BUILD_INSTRUCTIONS.md](BUILD_INSTRUCTIONS.md) for full platform-specific details.
The short version: CMake + Make on Linux/macOS, MSYS2 on Windows.
```
cmake -B build -DCMAKE_BUILD_TYPE=Debug
make -C build -j$(nproc)
```
## Code Style
- **C++20**. Use `#pragma once` for include guards.
- Namespaces: `wowee::game`, `wowee::rendering`, `wowee::rendering::world_map`, `wowee::ui`, `wowee::ui::chat`, `wowee::math`, `wowee::core`, `wowee::network`.
- Conventional commit messages in imperative mood:
- `feat:` new feature
- `fix:` bug fix
- `refactor:` code restructuring with no behavior change
- `perf:` performance improvement
- Prefer `constexpr` over `static const` for compile-time data.
- Mark functions whose return value should not be ignored with `[[nodiscard]]`.
## Pull Request Process
1. Branch from `master`.
2. Keep commits focused -- one logical change per commit.
3. Describe *what* changed and *why* in the PR description.
4. Ensure the project compiles cleanly before submitting.
5. Manual testing against a WoW 3.3.5a server (e.g. AzerothCore/ChromieCraft) is expected
for gameplay-affecting changes.
## Architecture Overview
See [docs/architecture.md](docs/architecture.md) for the full picture. Key namespaces:
| Namespace | Responsibility |
|---|---|
| `wowee::game` | Game state, packet handling (`GameHandler`), opcode dispatch, spline parsing |
| `wowee::rendering` | Vulkan renderer, M2/WMO/terrain, sky system |
| `wowee::rendering::world_map` | Modular world map (16 components: facade, compositor, layers, etc.) |
| `wowee::ui` | ImGui windows and HUD (`GameScreen`) |
| `wowee::ui::chat` | Modular chat system (15+ components: commands, markup, macros, etc.) |
| `wowee::math` | Reusable math modules (CatmullRomSpline) |
| `wowee::core` | Coordinates, math, utilities |
| `wowee::network` | Connection, `Packet` read/write API |
## Packet Handlers
The standard pattern for adding a new server packet handler:
1. Define a `struct FooData` holding the parsed fields.
2. Write `void GameHandler::handleFoo(network::Packet& packet)` to parse into `FooData`.
3. Register it in the dispatch table: `registerHandler(LogicalOpcode::SMSG_FOO, &GameHandler::handleFoo)`.
Helper variants: `registerWorldHandler` (requires `isInWorld()`), `registerSkipHandler` (discard),
`registerErrorHandler` (log warning).
## Testing
27 unit tests cover core systems, animation, transport/spline, world map, and chat.
See [TESTING.md](TESTING.md) for the full guide. Run with `./test.sh --test`.
Manual testing against WoW 3.3.5a private servers (primarily ChromieCraft/AzerothCore)
is expected for gameplay-affecting changes.
## Key Files for New Contributors
| File / Directory | What it does |
|---|---|
| `include/game/game_handler.hpp` | Central game state and all packet handler declarations |
| `src/game/game_handler.cpp` | Packet dispatch registration and handler implementations |
| `include/network/packet.hpp` | `Packet` class -- the read/write API every handler uses |
| `include/ui/game_screen.hpp` | Main gameplay UI screen (ImGui) |
| `src/ui/chat/` | Modular chat system (commands, markup, macros, tab completion) |
| `src/rendering/world_map/` | Modular world map (facade, compositor, layers, coordinate projection) |
| `src/math/spline.cpp` | Reusable CatmullRomSpline math |
| `src/game/spline_packet.cpp` | Unified spline packet parsing for all expansions |
| `src/rendering/m2_renderer.cpp` | M2 model loading and rendering |
| `docs/architecture.md` | High-level system architecture reference |

View file

@ -1,92 +1,48 @@
{
"Spell": {
"AreaTable": {
"ExploreFlag": 3,
"ID": 0,
"Attributes": 5,
"AttributesEx": 6,
"IconID": 117,
"Name": 120,
"Tooltip": 147,
"Rank": 129,
"SchoolEnum": 1,
"CastingTimeIndex": 15,
"PowerType": 28,
"ManaCost": 29,
"RangeIndex": 33,
"DispelType": 4
"MapID": 1,
"ParentAreaNum": 2
},
"SpellRange": {
"MaxRange": 2
},
"ItemDisplayInfo": {
"ID": 0,
"LeftModel": 1,
"LeftModelTexture": 3,
"InventoryIcon": 5,
"GeosetGroup1": 7,
"GeosetGroup3": 9,
"TextureArmUpper": 14,
"TextureArmLower": 15,
"TextureHand": 16,
"TextureTorsoUpper": 17,
"TextureTorsoLower": 18,
"TextureLegUpper": 19,
"TextureLegLower": 20,
"TextureFoot": 21
},
"CharSections": {
"CharHairGeosets": {
"GeosetID": 4,
"RaceID": 1,
"SexID": 2,
"Variation": 3
},
"CharSections": {
"BaseSection": 3,
"VariationIndex": 4,
"ColorIndex": 5,
"Flags": 9,
"RaceID": 1,
"SexID": 2,
"Texture1": 6,
"Texture2": 7,
"Texture3": 8,
"Flags": 9
"VariationIndex": 4
},
"SpellIcon": {
"ID": 0,
"Path": 1
"CharacterFacialHairStyles": {
"Geoset100": 3,
"Geoset200": 5,
"Geoset300": 4,
"RaceID": 0,
"SexID": 1,
"Variation": 2
},
"FactionTemplate": {
"CreatureDisplayInfo": {
"ExtraDisplayId": 3,
"ID": 0,
"Faction": 1,
"FactionGroup": 3,
"FriendGroup": 4,
"EnemyGroup": 5,
"Enemy0": 6,
"Enemy1": 7,
"Enemy2": 8,
"Enemy3": 9
},
"Faction": {
"ID": 0,
"ReputationRaceMask0": 2,
"ReputationRaceMask1": 3,
"ReputationRaceMask2": 4,
"ReputationRaceMask3": 5,
"ReputationBase0": 10,
"ReputationBase1": 11,
"ReputationBase2": 12,
"ReputationBase3": 13
},
"AreaTable": {
"ID": 0,
"MapID": 1,
"ParentAreaNum": 2,
"ExploreFlag": 3
"ModelID": 1,
"Skin1": 6,
"Skin2": 7,
"Skin3": 8
},
"CreatureDisplayInfoExtra": {
"ID": 0,
"RaceID": 1,
"SexID": 2,
"SkinID": 3,
"FaceID": 4,
"HairStyleID": 5,
"HairColorID": 6,
"FacialHairID": 7,
"BakeName": 20,
"EquipDisplay0": 8,
"EquipDisplay1": 9,
"EquipDisplay10": 18,
"EquipDisplay2": 10,
"EquipDisplay3": 11,
"EquipDisplay4": 12,
@ -95,128 +51,89 @@
"EquipDisplay7": 15,
"EquipDisplay8": 16,
"EquipDisplay9": 17,
"EquipDisplay10": 18,
"BakeName": 20
},
"CreatureDisplayInfo": {
"FaceID": 4,
"FacialHairID": 7,
"HairColorID": 6,
"HairStyleID": 5,
"ID": 0,
"ModelID": 1,
"ExtraDisplayId": 3,
"Skin1": 6,
"Skin2": 7,
"Skin3": 8
},
"TaxiNodes": {
"ID": 0,
"MapID": 1,
"X": 2,
"Y": 3,
"Z": 4,
"Name": 5
},
"TaxiPath": {
"ID": 0,
"FromNode": 1,
"ToNode": 2,
"Cost": 3
},
"TaxiPathNode": {
"ID": 0,
"PathID": 1,
"NodeIndex": 2,
"MapID": 3,
"X": 4,
"Y": 5,
"Z": 6
},
"TalentTab": {
"ID": 0,
"Name": 1,
"ClassMask": 12,
"OrderIndex": 14,
"BackgroundFile": 15
},
"Talent": {
"ID": 0,
"TabID": 1,
"Row": 2,
"Column": 3,
"RankSpell0": 4,
"PrereqTalent0": 9,
"PrereqRank0": 12
},
"SkillLineAbility": {
"SkillLineID": 1,
"SpellID": 2
},
"SkillLine": {
"ID": 0,
"Category": 1,
"Name": 3
},
"Map": {
"ID": 0,
"InternalName": 1
"RaceID": 1,
"SexID": 2,
"SkinID": 3
},
"CreatureModelData": {
"ID": 0,
"ModelPath": 2
},
"CharHairGeosets": {
"RaceID": 1,
"SexID": 2,
"Variation": 3,
"GeosetID": 4
},
"CharacterFacialHairStyles": {
"RaceID": 0,
"SexID": 1,
"Variation": 2,
"Geoset100": 3,
"Geoset300": 4,
"Geoset200": 5
},
"GameObjectDisplayInfo": {
"ID": 0,
"ModelName": 1
},
"Emotes": {
"ID": 0,
"AnimID": 2
"AnimID": 2,
"ID": 0
},
"EmotesText": {
"ID": 0,
"Command": 1,
"EmoteRef": 2,
"OthersTargetTextID": 3,
"SenderTargetTextID": 5,
"ID": 0,
"OthersNoTargetTextID": 7,
"SenderNoTargetTextID": 9
"OthersTargetTextID": 3,
"SenderNoTargetTextID": 9,
"SenderTargetTextID": 5
},
"EmotesTextData": {
"ID": 0,
"Text": 1
},
"Faction": {
"ID": 0,
"ReputationBase0": 10,
"ReputationBase1": 11,
"ReputationBase2": 12,
"ReputationBase3": 13,
"ReputationRaceMask0": 2,
"ReputationRaceMask1": 3,
"ReputationRaceMask2": 4,
"ReputationRaceMask3": 5
},
"FactionTemplate": {
"Enemy0": 6,
"Enemy1": 7,
"Enemy2": 8,
"Enemy3": 9,
"EnemyGroup": 5,
"Faction": 1,
"FactionGroup": 3,
"FriendGroup": 4,
"ID": 0
},
"GameObjectDisplayInfo": {
"ID": 0,
"ModelName": 1
},
"ItemDisplayInfo": {
"GeosetGroup1": 7,
"GeosetGroup3": 9,
"ID": 0,
"InventoryIcon": 5,
"LeftModel": 1,
"LeftModelTexture": 3,
"TextureArmLower": 15,
"TextureArmUpper": 14,
"TextureFoot": 21,
"TextureHand": 16,
"TextureLegLower": 20,
"TextureLegUpper": 19,
"TextureTorsoLower": 18,
"TextureTorsoUpper": 17
},
"Light": {
"ID": 0,
"MapID": 1,
"X": 2,
"Z": 3,
"Y": 4,
"InnerRadius": 5,
"OuterRadius": 6,
"LightParamsID": 7,
"LightParamsIDRain": 8,
"LightParamsIDUnderwater": 9
},
"LightParams": {
"LightParamsID": 0
},
"LightIntBand": {
"BlockIndex": 1,
"NumKeyframes": 2,
"TimeKey0": 3,
"Value0": 19
"LightParamsIDUnderwater": 9,
"MapID": 1,
"OuterRadius": 6,
"X": 2,
"Y": 4,
"Z": 3
},
"LightFloatBand": {
"BlockIndex": 1,
@ -224,33 +141,127 @@
"TimeKey0": 3,
"Value0": 19
},
"WorldMapArea": {
"LightIntBand": {
"BlockIndex": 1,
"NumKeyframes": 2,
"TimeKey0": 3,
"Value0": 19
},
"LightParams": {
"LightParamsID": 0
},
"Map": {
"ID": 0,
"MapID": 1,
"AreaID": 2,
"AreaName": 3,
"LocLeft": 4,
"LocRight": 5,
"LocTop": 6,
"LocBottom": 7,
"DisplayMapID": 8,
"ParentWorldMapID": 10
"InternalName": 1
},
"SkillLine": {
"Category": 1,
"ID": 0,
"Name": 3
},
"SkillLineAbility": {
"SkillLineID": 1,
"SpellID": 2
},
"Spell": {
"Attributes": 5,
"AttributesEx": 6,
"CastingTimeIndex": 15,
"DispelType": 4,
"DurationIndex": 40,
"EffectBasePoints0": 80,
"EffectBasePoints1": 81,
"EffectBasePoints2": 82,
"ID": 0,
"IconID": 117,
"ManaCost": 29,
"Name": 120,
"PowerType": 28,
"RangeIndex": 33,
"Rank": 129,
"SchoolEnum": 1,
"SpellVisualID": 115,
"Tooltip": 147
},
"SpellIcon": {
"ID": 0,
"Path": 1
},
"SpellRange": {
"MaxRange": 2
},
"SpellVisual": {
"ID": 0,
"CastKit": 2,
"ID": 0,
"ImpactKit": 3,
"MissileModel": 8
"MissileModel": 8,
"PrecastKit": 1
},
"SpellVisualEffectName": {
"FilePath": 2,
"ID": 0
},
"SpellVisualKit": {
"ID": 0,
"BaseEffect": 5,
"BreathEffect": 8,
"ChestEffect": 4,
"HeadEffect": 3,
"ID": 0,
"LeftHandEffect": 6,
"RightHandEffect": 7,
"SpecialEffect0": 11,
"SpecialEffect1": 12,
"SpecialEffect2": 13
},
"SpellVisualEffectName": {
"Talent": {
"Column": 3,
"ID": 0,
"FilePath": 2
"PrereqRank0": 12,
"PrereqTalent0": 9,
"RankSpell0": 4,
"Row": 2,
"TabID": 1
},
"TalentTab": {
"BackgroundFile": 15,
"ClassMask": 12,
"ID": 0,
"Name": 1,
"OrderIndex": 14
},
"TaxiNodes": {
"ID": 0,
"MapID": 1,
"Name": 5,
"X": 2,
"Y": 3,
"Z": 4
},
"TaxiPath": {
"Cost": 3,
"FromNode": 1,
"ID": 0,
"ToNode": 2
},
"TaxiPathNode": {
"ID": 0,
"MapID": 3,
"NodeIndex": 2,
"PathID": 1,
"X": 4,
"Y": 5,
"Z": 6
},
"WorldMapArea": {
"AreaID": 2,
"AreaName": 3,
"DisplayMapID": 8,
"ID": 0,
"LocBottom": 7,
"LocLeft": 4,
"LocRight": 5,
"LocTop": 6,
"MapID": 1,
"ParentWorldMapID": 10
}
}

View file

@ -1,48 +1,50 @@
{
"CONTAINER_FIELD_NUM_SLOTS": 48,
"CONTAINER_FIELD_SLOT_1": 50,
"GAMEOBJECT_DISPLAYID": 8,
"GAMEOBJECT_BYTES_1": 14,
"ITEM_FIELD_DURABILITY": 48,
"ITEM_FIELD_MAXDURABILITY": 49,
"ITEM_FIELD_STACK_COUNT": 14,
"OBJECT_FIELD_ENTRY": 3,
"OBJECT_FIELD_SCALE_X": 4,
"UNIT_FIELD_TARGET_LO": 16,
"UNIT_FIELD_TARGET_HI": 17,
"PLAYER_BYTES": 191,
"PLAYER_BYTES_2": 192,
"PLAYER_END": 1282,
"PLAYER_EXPLORED_ZONES_START": 1111,
"PLAYER_FIELD_BANKBAG_SLOT_1": 612,
"PLAYER_FIELD_BANK_SLOT_1": 564,
"PLAYER_FIELD_COINAGE": 1176,
"PLAYER_FIELD_INV_SLOT_HEAD": 486,
"PLAYER_FIELD_PACK_SLOT_1": 532,
"PLAYER_FLAGS": 190,
"PLAYER_NEXT_LEVEL_XP": 717,
"PLAYER_QUEST_LOG_START": 198,
"PLAYER_REST_STATE_EXPERIENCE": 1175,
"PLAYER_SKILL_INFO_START": 718,
"PLAYER_XP": 716,
"UNIT_DYNAMIC_FLAGS": 143,
"UNIT_END": 188,
"UNIT_FIELD_AURAFLAGS": 98,
"UNIT_FIELD_AURAS": 50,
"UNIT_FIELD_BYTES_0": 36,
"UNIT_FIELD_HEALTH": 22,
"UNIT_FIELD_POWER1": 23,
"UNIT_FIELD_MAXHEALTH": 28,
"UNIT_FIELD_MAXPOWER1": 29,
"UNIT_FIELD_LEVEL": 34,
"UNIT_FIELD_BYTES_1": 133,
"UNIT_FIELD_DISPLAYID": 131,
"UNIT_FIELD_FACTIONTEMPLATE": 35,
"UNIT_FIELD_FLAGS": 46,
"UNIT_FIELD_DISPLAYID": 131,
"UNIT_FIELD_HEALTH": 22,
"UNIT_FIELD_LEVEL": 34,
"UNIT_FIELD_MAXHEALTH": 28,
"UNIT_FIELD_MAXPOWER1": 29,
"UNIT_FIELD_MOUNTDISPLAYID": 133,
"UNIT_FIELD_AURAS": 50,
"UNIT_FIELD_AURAFLAGS": 98,
"UNIT_NPC_FLAGS": 147,
"UNIT_DYNAMIC_FLAGS": 143,
"UNIT_FIELD_POWER1": 23,
"UNIT_FIELD_RESISTANCES": 154,
"UNIT_FIELD_STAT0": 138,
"UNIT_FIELD_STAT1": 139,
"UNIT_FIELD_STAT2": 140,
"UNIT_FIELD_STAT3": 141,
"UNIT_FIELD_STAT4": 142,
"UNIT_END": 188,
"PLAYER_FLAGS": 190,
"PLAYER_BYTES": 191,
"PLAYER_BYTES_2": 192,
"PLAYER_XP": 716,
"PLAYER_NEXT_LEVEL_XP": 717,
"PLAYER_REST_STATE_EXPERIENCE": 1175,
"PLAYER_FIELD_COINAGE": 1176,
"PLAYER_QUEST_LOG_START": 198,
"PLAYER_FIELD_INV_SLOT_HEAD": 486,
"PLAYER_FIELD_PACK_SLOT_1": 532,
"PLAYER_FIELD_BANK_SLOT_1": 564,
"PLAYER_FIELD_BANKBAG_SLOT_1": 612,
"PLAYER_SKILL_INFO_START": 718,
"PLAYER_EXPLORED_ZONES_START": 1111,
"PLAYER_END": 1282,
"GAMEOBJECT_DISPLAYID": 8,
"ITEM_FIELD_STACK_COUNT": 14,
"ITEM_FIELD_DURABILITY": 48,
"ITEM_FIELD_MAXDURABILITY": 49,
"CONTAINER_FIELD_NUM_SLOTS": 48,
"CONTAINER_FIELD_SLOT_1": 50
"UNIT_FIELD_TARGET_HI": 17,
"UNIT_FIELD_TARGET_LO": 16,
"UNIT_NPC_FLAGS": 147
}

View file

@ -1,97 +1,53 @@
{
"Spell": {
"AreaTable": {
"ExploreFlag": 3,
"ID": 0,
"Attributes": 5,
"AttributesEx": 6,
"IconID": 124,
"Name": 127,
"Tooltip": 154,
"Rank": 136,
"SchoolMask": 215,
"CastingTimeIndex": 22,
"PowerType": 35,
"ManaCost": 36,
"RangeIndex": 40,
"DispelType": 3
"MapID": 1,
"ParentAreaNum": 2
},
"SpellRange": {
"MaxRange": 4
},
"ItemDisplayInfo": {
"ID": 0,
"LeftModel": 1,
"LeftModelTexture": 3,
"InventoryIcon": 5,
"GeosetGroup1": 7,
"GeosetGroup3": 9,
"TextureArmUpper": 14,
"TextureArmLower": 15,
"TextureHand": 16,
"TextureTorsoUpper": 17,
"TextureTorsoLower": 18,
"TextureLegUpper": 19,
"TextureLegLower": 20,
"TextureFoot": 21
},
"CharSections": {
"CharHairGeosets": {
"GeosetID": 4,
"RaceID": 1,
"SexID": 2,
"Variation": 3
},
"CharSections": {
"BaseSection": 3,
"VariationIndex": 4,
"ColorIndex": 5,
"Flags": 9,
"RaceID": 1,
"SexID": 2,
"Texture1": 6,
"Texture2": 7,
"Texture3": 8,
"Flags": 9
},
"SpellIcon": {
"ID": 0,
"Path": 1
},
"FactionTemplate": {
"ID": 0,
"Faction": 1,
"FactionGroup": 3,
"FriendGroup": 4,
"EnemyGroup": 5,
"Enemy0": 6,
"Enemy1": 7,
"Enemy2": 8,
"Enemy3": 9
},
"Faction": {
"ID": 0,
"ReputationRaceMask0": 2,
"ReputationRaceMask1": 3,
"ReputationRaceMask2": 4,
"ReputationRaceMask3": 5,
"ReputationBase0": 10,
"ReputationBase1": 11,
"ReputationBase2": 12,
"ReputationBase3": 13
"VariationIndex": 4
},
"CharTitles": {
"ID": 0,
"Title": 2,
"TitleBit": 20
},
"AreaTable": {
"CharacterFacialHairStyles": {
"Geoset100": 3,
"Geoset200": 5,
"Geoset300": 4,
"RaceID": 0,
"SexID": 1,
"Variation": 2
},
"CreatureDisplayInfo": {
"ExtraDisplayId": 3,
"ID": 0,
"MapID": 1,
"ParentAreaNum": 2,
"ExploreFlag": 3
"ModelID": 1,
"Skin1": 6,
"Skin2": 7,
"Skin3": 8
},
"CreatureDisplayInfoExtra": {
"ID": 0,
"RaceID": 1,
"SexID": 2,
"SkinID": 3,
"FaceID": 4,
"HairStyleID": 5,
"HairColorID": 6,
"FacialHairID": 7,
"BakeName": 20,
"EquipDisplay0": 8,
"EquipDisplay1": 9,
"EquipDisplay10": 18,
"EquipDisplay2": 10,
"EquipDisplay3": 11,
"EquipDisplay4": 12,
@ -100,158 +56,80 @@
"EquipDisplay7": 15,
"EquipDisplay8": 16,
"EquipDisplay9": 17,
"EquipDisplay10": 18,
"BakeName": 20
},
"CreatureDisplayInfo": {
"FaceID": 4,
"FacialHairID": 7,
"HairColorID": 6,
"HairStyleID": 5,
"ID": 0,
"ModelID": 1,
"ExtraDisplayId": 3,
"Skin1": 6,
"Skin2": 7,
"Skin3": 8
},
"TaxiNodes": {
"ID": 0,
"MapID": 1,
"X": 2,
"Y": 3,
"Z": 4,
"Name": 5,
"MountDisplayIdAllianceFallback": 12,
"MountDisplayIdHordeFallback": 13,
"MountDisplayIdAlliance": 14,
"MountDisplayIdHorde": 15
},
"TaxiPath": {
"ID": 0,
"FromNode": 1,
"ToNode": 2,
"Cost": 3
},
"TaxiPathNode": {
"ID": 0,
"PathID": 1,
"NodeIndex": 2,
"MapID": 3,
"X": 4,
"Y": 5,
"Z": 6
},
"TalentTab": {
"ID": 0,
"Name": 1,
"ClassMask": 12,
"OrderIndex": 14,
"BackgroundFile": 15
},
"Talent": {
"ID": 0,
"TabID": 1,
"Row": 2,
"Column": 3,
"RankSpell0": 4,
"PrereqTalent0": 9,
"PrereqRank0": 12
},
"SkillLineAbility": {
"SkillLineID": 1,
"SpellID": 2
},
"SkillLine": {
"ID": 0,
"Category": 1,
"Name": 3
},
"Map": {
"ID": 0,
"InternalName": 1
"RaceID": 1,
"SexID": 2,
"SkinID": 3
},
"CreatureModelData": {
"ID": 0,
"ModelPath": 2
},
"CharHairGeosets": {
"RaceID": 1,
"SexID": 2,
"Variation": 3,
"GeosetID": 4
},
"CharacterFacialHairStyles": {
"RaceID": 0,
"SexID": 1,
"Variation": 2,
"Geoset100": 3,
"Geoset300": 4,
"Geoset200": 5
},
"GameObjectDisplayInfo": {
"ID": 0,
"ModelName": 1
},
"Emotes": {
"ID": 0,
"AnimID": 2
"AnimID": 2,
"ID": 0
},
"EmotesText": {
"ID": 0,
"Command": 1,
"EmoteRef": 2,
"OthersTargetTextID": 3,
"SenderTargetTextID": 5,
"ID": 0,
"OthersNoTargetTextID": 7,
"SenderNoTargetTextID": 9
"OthersTargetTextID": 3,
"SenderNoTargetTextID": 9,
"SenderTargetTextID": 5
},
"EmotesTextData": {
"ID": 0,
"Text": 1
},
"Light": {
"Faction": {
"ID": 0,
"MapID": 1,
"X": 2,
"Z": 3,
"Y": 4,
"InnerRadius": 5,
"OuterRadius": 6,
"LightParamsID": 7,
"LightParamsIDRain": 8,
"LightParamsIDUnderwater": 9
"ReputationBase0": 10,
"ReputationBase1": 11,
"ReputationBase2": 12,
"ReputationBase3": 13,
"ReputationRaceMask0": 2,
"ReputationRaceMask1": 3,
"ReputationRaceMask2": 4,
"ReputationRaceMask3": 5
},
"LightParams": {
"LightParamsID": 0
"FactionTemplate": {
"Enemy0": 6,
"Enemy1": 7,
"Enemy2": 8,
"Enemy3": 9,
"EnemyGroup": 5,
"Faction": 1,
"FactionGroup": 3,
"FriendGroup": 4,
"ID": 0
},
"LightIntBand": {
"BlockIndex": 1,
"NumKeyframes": 2,
"TimeKey0": 3,
"Value0": 19
},
"LightFloatBand": {
"BlockIndex": 1,
"NumKeyframes": 2,
"TimeKey0": 3,
"Value0": 19
},
"WorldMapArea": {
"GameObjectDisplayInfo": {
"ID": 0,
"MapID": 1,
"AreaID": 2,
"AreaName": 3,
"LocLeft": 4,
"LocRight": 5,
"LocTop": 6,
"LocBottom": 7,
"DisplayMapID": 8,
"ParentWorldMapID": 10
"ModelName": 1
},
"SpellItemEnchantment": {
"ItemDisplayInfo": {
"GeosetGroup1": 7,
"GeosetGroup3": 9,
"ID": 0,
"Name": 8
"InventoryIcon": 5,
"LeftModel": 1,
"LeftModelTexture": 3,
"TextureArmLower": 15,
"TextureArmUpper": 14,
"TextureFoot": 21,
"TextureHand": 16,
"TextureLegLower": 20,
"TextureLegUpper": 19,
"TextureTorsoLower": 18,
"TextureTorsoUpper": 17
},
"ItemSet": {
"ID": 0,
"Name": 1,
"Item0": 18,
"Item1": 19,
"Item2": 20,
@ -262,6 +140,7 @@
"Item7": 25,
"Item8": 26,
"Item9": 27,
"Name": 1,
"Spell0": 28,
"Spell1": 29,
"Spell2": 30,
@ -283,21 +162,153 @@
"Threshold8": 46,
"Threshold9": 47
},
"SpellVisual": {
"Light": {
"ID": 0,
"InnerRadius": 5,
"LightParamsID": 7,
"LightParamsIDRain": 8,
"LightParamsIDUnderwater": 9,
"MapID": 1,
"OuterRadius": 6,
"X": 2,
"Y": 4,
"Z": 3
},
"LightFloatBand": {
"BlockIndex": 1,
"NumKeyframes": 2,
"TimeKey0": 3,
"Value0": 19
},
"LightIntBand": {
"BlockIndex": 1,
"NumKeyframes": 2,
"TimeKey0": 3,
"Value0": 19
},
"LightParams": {
"LightParamsID": 0
},
"Map": {
"ID": 0,
"InternalName": 1
},
"SkillLine": {
"Category": 1,
"ID": 0,
"Name": 3
},
"SkillLineAbility": {
"SkillLineID": 1,
"SpellID": 2
},
"Spell": {
"Attributes": 5,
"AttributesEx": 6,
"CastingTimeIndex": 22,
"DispelType": 3,
"DurationIndex": 40,
"EffectBasePoints0": 80,
"EffectBasePoints1": 81,
"EffectBasePoints2": 82,
"ID": 0,
"IconID": 124,
"ManaCost": 36,
"Name": 127,
"PowerType": 35,
"RangeIndex": 40,
"Rank": 136,
"SchoolMask": 215,
"SpellVisualID": 122,
"Tooltip": 154
},
"SpellIcon": {
"ID": 0,
"Path": 1
},
"SpellItemEnchantment": {
"ID": 0,
"Name": 8
},
"SpellRange": {
"MaxRange": 4
},
"SpellVisual": {
"CastKit": 2,
"ID": 0,
"ImpactKit": 3,
"MissileModel": 8
"MissileModel": 8,
"PrecastKit": 1
},
"SpellVisualEffectName": {
"FilePath": 2,
"ID": 0
},
"SpellVisualKit": {
"ID": 0,
"BaseEffect": 5,
"BreathEffect": 8,
"ChestEffect": 4,
"HeadEffect": 3,
"ID": 0,
"LeftHandEffect": 6,
"RightHandEffect": 7,
"SpecialEffect0": 11,
"SpecialEffect1": 12,
"SpecialEffect2": 13
},
"SpellVisualEffectName": {
"Talent": {
"Column": 3,
"ID": 0,
"FilePath": 2
"PrereqRank0": 12,
"PrereqTalent0": 9,
"RankSpell0": 4,
"Row": 2,
"TabID": 1
},
"TalentTab": {
"BackgroundFile": 15,
"ClassMask": 12,
"ID": 0,
"Name": 1,
"OrderIndex": 14
},
"TaxiNodes": {
"ID": 0,
"MapID": 1,
"MountDisplayIdAlliance": 14,
"MountDisplayIdAllianceFallback": 12,
"MountDisplayIdHorde": 15,
"MountDisplayIdHordeFallback": 13,
"Name": 5,
"X": 2,
"Y": 3,
"Z": 4
},
"TaxiPath": {
"Cost": 3,
"FromNode": 1,
"ID": 0,
"ToNode": 2
},
"TaxiPathNode": {
"ID": 0,
"MapID": 3,
"NodeIndex": 2,
"PathID": 1,
"X": 4,
"Y": 5,
"Z": 6
},
"WorldMapArea": {
"AreaID": 2,
"AreaName": 3,
"DisplayMapID": 8,
"ID": 0,
"LocBottom": 7,
"LocLeft": 4,
"LocRight": 5,
"LocTop": 6,
"MapID": 1,
"ParentWorldMapID": 10
}
}

View file

@ -1,48 +1,50 @@
{
"CONTAINER_FIELD_NUM_SLOTS": 64,
"CONTAINER_FIELD_SLOT_1": 66,
"GAMEOBJECT_DISPLAYID": 8,
"GAMEOBJECT_BYTES_1": 17,
"ITEM_FIELD_DURABILITY": 60,
"ITEM_FIELD_MAXDURABILITY": 61,
"ITEM_FIELD_STACK_COUNT": 14,
"OBJECT_FIELD_ENTRY": 3,
"OBJECT_FIELD_SCALE_X": 4,
"UNIT_FIELD_TARGET_LO": 16,
"UNIT_FIELD_TARGET_HI": 17,
"PLAYER_BYTES": 237,
"PLAYER_BYTES_2": 238,
"PLAYER_EXPLORED_ZONES_START": 1312,
"PLAYER_FIELD_ARENA_CURRENCY": 1506,
"PLAYER_FIELD_BANKBAG_SLOT_1": 784,
"PLAYER_FIELD_BANK_SLOT_1": 728,
"PLAYER_FIELD_COINAGE": 1441,
"PLAYER_FIELD_HONOR_CURRENCY": 1505,
"PLAYER_FIELD_INV_SLOT_HEAD": 650,
"PLAYER_FIELD_PACK_SLOT_1": 696,
"PLAYER_FLAGS": 236,
"PLAYER_NEXT_LEVEL_XP": 927,
"PLAYER_QUEST_LOG_START": 244,
"PLAYER_REST_STATE_EXPERIENCE": 1440,
"PLAYER_SKILL_INFO_START": 928,
"PLAYER_XP": 926,
"UNIT_DYNAMIC_FLAGS": 164,
"UNIT_END": 234,
"UNIT_FIELD_BYTES_0": 36,
"UNIT_FIELD_HEALTH": 22,
"UNIT_FIELD_POWER1": 23,
"UNIT_FIELD_MAXHEALTH": 28,
"UNIT_FIELD_MAXPOWER1": 29,
"UNIT_FIELD_LEVEL": 34,
"UNIT_FIELD_BYTES_1": 137,
"UNIT_FIELD_DISPLAYID": 152,
"UNIT_FIELD_FACTIONTEMPLATE": 35,
"UNIT_FIELD_FLAGS": 46,
"UNIT_FIELD_FLAGS_2": 47,
"UNIT_FIELD_DISPLAYID": 152,
"UNIT_FIELD_HEALTH": 22,
"UNIT_FIELD_LEVEL": 34,
"UNIT_FIELD_MAXHEALTH": 28,
"UNIT_FIELD_MAXPOWER1": 29,
"UNIT_FIELD_MOUNTDISPLAYID": 154,
"UNIT_NPC_FLAGS": 168,
"UNIT_DYNAMIC_FLAGS": 164,
"UNIT_FIELD_POWER1": 23,
"UNIT_FIELD_RESISTANCES": 185,
"UNIT_FIELD_STAT0": 159,
"UNIT_FIELD_STAT1": 160,
"UNIT_FIELD_STAT2": 161,
"UNIT_FIELD_STAT3": 162,
"UNIT_FIELD_STAT4": 163,
"UNIT_END": 234,
"PLAYER_FLAGS": 236,
"PLAYER_BYTES": 237,
"PLAYER_BYTES_2": 238,
"PLAYER_XP": 926,
"PLAYER_NEXT_LEVEL_XP": 927,
"PLAYER_REST_STATE_EXPERIENCE": 1440,
"PLAYER_FIELD_COINAGE": 1441,
"PLAYER_QUEST_LOG_START": 244,
"PLAYER_FIELD_INV_SLOT_HEAD": 650,
"PLAYER_FIELD_PACK_SLOT_1": 696,
"PLAYER_FIELD_BANK_SLOT_1": 728,
"PLAYER_FIELD_BANKBAG_SLOT_1": 784,
"PLAYER_SKILL_INFO_START": 928,
"PLAYER_EXPLORED_ZONES_START": 1312,
"PLAYER_FIELD_HONOR_CURRENCY": 1505,
"PLAYER_FIELD_ARENA_CURRENCY": 1506,
"GAMEOBJECT_DISPLAYID": 8,
"ITEM_FIELD_STACK_COUNT": 14,
"ITEM_FIELD_DURABILITY": 60,
"ITEM_FIELD_MAXDURABILITY": 61,
"CONTAINER_FIELD_NUM_SLOTS": 64,
"CONTAINER_FIELD_SLOT_1": 66
"UNIT_FIELD_TARGET_HI": 17,
"UNIT_FIELD_TARGET_LO": 16,
"UNIT_NPC_FLAGS": 168
}

View file

@ -1,90 +1,45 @@
{
"Spell": {
"AreaTable": {
"ExploreFlag": 3,
"ID": 0,
"Attributes": 5,
"AttributesEx": 6,
"IconID": 117,
"Name": 120,
"Tooltip": 147,
"Rank": 129,
"SchoolEnum": 1,
"CastingTimeIndex": 15,
"PowerType": 28,
"ManaCost": 29,
"RangeIndex": 33,
"DispelType": 4
"MapID": 1,
"ParentAreaNum": 2
},
"SpellRange": {
"MaxRange": 2
},
"ItemDisplayInfo": {
"ID": 0,
"LeftModel": 1,
"LeftModelTexture": 3,
"InventoryIcon": 5,
"GeosetGroup1": 7,
"GeosetGroup3": 9,
"TextureArmUpper": 14,
"TextureArmLower": 15,
"TextureHand": 16,
"TextureTorsoUpper": 17,
"TextureTorsoLower": 18,
"TextureLegUpper": 19,
"TextureLegLower": 20,
"TextureFoot": 21
},
"CharSections": {
"CharHairGeosets": {
"GeosetID": 4,
"RaceID": 1,
"SexID": 2,
"Variation": 3
},
"CharSections": {
"BaseSection": 3,
"VariationIndex": 4,
"ColorIndex": 5,
"Flags": 9,
"RaceID": 1,
"SexID": 2,
"Texture1": 6,
"Texture2": 7,
"Texture3": 8,
"Flags": 9
"VariationIndex": 4
},
"SpellIcon": {
"ID": 0,
"Path": 1
"CharacterFacialHairStyles": {
"Geoset100": 3,
"Geoset200": 5,
"Geoset300": 4,
"RaceID": 0,
"SexID": 1,
"Variation": 2
},
"FactionTemplate": {
"CreatureDisplayInfo": {
"ExtraDisplayId": 3,
"ID": 0,
"Faction": 1,
"FactionGroup": 3,
"FriendGroup": 4,
"EnemyGroup": 5,
"Enemy0": 6,
"Enemy1": 7,
"Enemy2": 8,
"Enemy3": 9
},
"Faction": {
"ID": 0,
"ReputationRaceMask0": 2,
"ReputationRaceMask1": 3,
"ReputationRaceMask2": 4,
"ReputationRaceMask3": 5,
"ReputationBase0": 10,
"ReputationBase1": 11,
"ReputationBase2": 12,
"ReputationBase3": 13
},
"AreaTable": {
"ID": 0,
"MapID": 1,
"ParentAreaNum": 2,
"ExploreFlag": 3
"ModelID": 1,
"Skin1": 6,
"Skin2": 7,
"Skin3": 8
},
"CreatureDisplayInfoExtra": {
"ID": 0,
"RaceID": 1,
"SexID": 2,
"SkinID": 3,
"FaceID": 4,
"HairStyleID": 5,
"HairColorID": 6,
"FacialHairID": 7,
"BakeName": 18,
"EquipDisplay0": 8,
"EquipDisplay1": 9,
"EquipDisplay2": 10,
@ -95,153 +50,80 @@
"EquipDisplay7": 15,
"EquipDisplay8": 16,
"EquipDisplay9": 17,
"BakeName": 18
},
"CreatureDisplayInfo": {
"FaceID": 4,
"FacialHairID": 7,
"HairColorID": 6,
"HairStyleID": 5,
"ID": 0,
"ModelID": 1,
"ExtraDisplayId": 3,
"Skin1": 6,
"Skin2": 7,
"Skin3": 8
},
"TaxiNodes": {
"ID": 0,
"MapID": 1,
"X": 2,
"Y": 3,
"Z": 4,
"Name": 5
},
"TaxiPath": {
"ID": 0,
"FromNode": 1,
"ToNode": 2,
"Cost": 3
},
"TaxiPathNode": {
"ID": 0,
"PathID": 1,
"NodeIndex": 2,
"MapID": 3,
"X": 4,
"Y": 5,
"Z": 6
},
"TalentTab": {
"ID": 0,
"Name": 1,
"ClassMask": 12,
"OrderIndex": 14,
"BackgroundFile": 15
},
"Talent": {
"ID": 0,
"TabID": 1,
"Row": 2,
"Column": 3,
"RankSpell0": 4,
"PrereqTalent0": 9,
"PrereqRank0": 12
},
"SkillLineAbility": {
"SkillLineID": 1,
"SpellID": 2
},
"SkillLine": {
"ID": 0,
"Category": 1,
"Name": 3
},
"Map": {
"ID": 0,
"InternalName": 1
"RaceID": 1,
"SexID": 2,
"SkinID": 3
},
"CreatureModelData": {
"ID": 0,
"ModelPath": 2
},
"CharHairGeosets": {
"RaceID": 1,
"SexID": 2,
"Variation": 3,
"GeosetID": 4
},
"CharacterFacialHairStyles": {
"RaceID": 0,
"SexID": 1,
"Variation": 2,
"Geoset100": 3,
"Geoset300": 4,
"Geoset200": 5
},
"GameObjectDisplayInfo": {
"ID": 0,
"ModelName": 1
},
"Emotes": {
"ID": 0,
"AnimID": 2
"AnimID": 2,
"ID": 0
},
"EmotesText": {
"ID": 0,
"Command": 1,
"EmoteRef": 2,
"OthersTargetTextID": 3,
"SenderTargetTextID": 5,
"ID": 0,
"OthersNoTargetTextID": 7,
"SenderNoTargetTextID": 9
"OthersTargetTextID": 3,
"SenderNoTargetTextID": 9,
"SenderTargetTextID": 5
},
"EmotesTextData": {
"ID": 0,
"Text": 1
},
"Light": {
"Faction": {
"ID": 0,
"MapID": 1,
"X": 2,
"Z": 3,
"Y": 4,
"InnerRadius": 5,
"OuterRadius": 6,
"LightParamsID": 7,
"LightParamsIDRain": 8,
"LightParamsIDUnderwater": 9
"ReputationBase0": 10,
"ReputationBase1": 11,
"ReputationBase2": 12,
"ReputationBase3": 13,
"ReputationRaceMask0": 2,
"ReputationRaceMask1": 3,
"ReputationRaceMask2": 4,
"ReputationRaceMask3": 5
},
"LightParams": {
"LightParamsID": 0
"FactionTemplate": {
"Enemy0": 6,
"Enemy1": 7,
"Enemy2": 8,
"Enemy3": 9,
"EnemyGroup": 5,
"Faction": 1,
"FactionGroup": 3,
"FriendGroup": 4,
"ID": 0
},
"LightIntBand": {
"BlockIndex": 1,
"NumKeyframes": 2,
"TimeKey0": 3,
"Value0": 19
},
"LightFloatBand": {
"BlockIndex": 1,
"NumKeyframes": 2,
"TimeKey0": 3,
"Value0": 19
},
"WorldMapArea": {
"GameObjectDisplayInfo": {
"ID": 0,
"MapID": 1,
"AreaID": 2,
"AreaName": 3,
"LocLeft": 4,
"LocRight": 5,
"LocTop": 6,
"LocBottom": 7,
"DisplayMapID": 8,
"ParentWorldMapID": 10
"ModelName": 1
},
"SpellItemEnchantment": {
"ItemDisplayInfo": {
"GeosetGroup1": 7,
"GeosetGroup3": 9,
"ID": 0,
"Name": 8
"InventoryIcon": 5,
"LeftModel": 1,
"LeftModelTexture": 3,
"TextureArmLower": 15,
"TextureArmUpper": 14,
"TextureFoot": 21,
"TextureHand": 16,
"TextureLegLower": 20,
"TextureLegUpper": 19,
"TextureTorsoLower": 18,
"TextureTorsoUpper": 17
},
"ItemSet": {
"ID": 0,
"Name": 1,
"Item0": 10,
"Item1": 11,
"Item2": 12,
@ -252,6 +134,7 @@
"Item7": 17,
"Item8": 18,
"Item9": 19,
"Name": 1,
"Spell0": 20,
"Spell1": 21,
"Spell2": 22,
@ -273,21 +156,149 @@
"Threshold8": 38,
"Threshold9": 39
},
"SpellVisual": {
"Light": {
"ID": 0,
"InnerRadius": 5,
"LightParamsID": 7,
"LightParamsIDRain": 8,
"LightParamsIDUnderwater": 9,
"MapID": 1,
"OuterRadius": 6,
"X": 2,
"Y": 4,
"Z": 3
},
"LightFloatBand": {
"BlockIndex": 1,
"NumKeyframes": 2,
"TimeKey0": 3,
"Value0": 19
},
"LightIntBand": {
"BlockIndex": 1,
"NumKeyframes": 2,
"TimeKey0": 3,
"Value0": 19
},
"LightParams": {
"LightParamsID": 0
},
"Map": {
"ID": 0,
"InternalName": 1
},
"SkillLine": {
"Category": 1,
"ID": 0,
"Name": 3
},
"SkillLineAbility": {
"SkillLineID": 1,
"SpellID": 2
},
"Spell": {
"Attributes": 5,
"AttributesEx": 6,
"CastingTimeIndex": 15,
"DispelType": 4,
"DurationIndex": 40,
"EffectBasePoints0": 80,
"EffectBasePoints1": 81,
"EffectBasePoints2": 82,
"ID": 0,
"IconID": 117,
"ManaCost": 29,
"Name": 120,
"PowerType": 28,
"RangeIndex": 33,
"Rank": 129,
"SchoolEnum": 1,
"SpellVisualID": 115,
"Tooltip": 147
},
"SpellIcon": {
"ID": 0,
"Path": 1
},
"SpellItemEnchantment": {
"ID": 0,
"Name": 8
},
"SpellRange": {
"MaxRange": 2
},
"SpellVisual": {
"CastKit": 2,
"ID": 0,
"ImpactKit": 3,
"MissileModel": 8
"MissileModel": 8,
"PrecastKit": 1
},
"SpellVisualEffectName": {
"FilePath": 2,
"ID": 0
},
"SpellVisualKit": {
"ID": 0,
"BaseEffect": 5,
"BreathEffect": 8,
"ChestEffect": 4,
"HeadEffect": 3,
"ID": 0,
"LeftHandEffect": 6,
"RightHandEffect": 7,
"SpecialEffect0": 11,
"SpecialEffect1": 12,
"SpecialEffect2": 13
},
"SpellVisualEffectName": {
"Talent": {
"Column": 3,
"ID": 0,
"FilePath": 2
"PrereqRank0": 12,
"PrereqTalent0": 9,
"RankSpell0": 4,
"Row": 2,
"TabID": 1
},
"TalentTab": {
"BackgroundFile": 15,
"ClassMask": 12,
"ID": 0,
"Name": 1,
"OrderIndex": 14
},
"TaxiNodes": {
"ID": 0,
"MapID": 1,
"Name": 5,
"X": 2,
"Y": 3,
"Z": 4
},
"TaxiPath": {
"Cost": 3,
"FromNode": 1,
"ID": 0,
"ToNode": 2
},
"TaxiPathNode": {
"ID": 0,
"MapID": 3,
"NodeIndex": 2,
"PathID": 1,
"X": 4,
"Y": 5,
"Z": 6
},
"WorldMapArea": {
"AreaID": 2,
"AreaName": 3,
"DisplayMapID": 8,
"ID": 0,
"LocBottom": 7,
"LocLeft": 4,
"LocRight": 5,
"LocTop": 6,
"MapID": 1,
"ParentWorldMapID": 10
}
}

View file

@ -1,48 +1,50 @@
{
"CONTAINER_FIELD_NUM_SLOTS": 48,
"CONTAINER_FIELD_SLOT_1": 50,
"GAMEOBJECT_DISPLAYID": 8,
"GAMEOBJECT_BYTES_1": 14,
"ITEM_FIELD_DURABILITY": 48,
"ITEM_FIELD_MAXDURABILITY": 49,
"ITEM_FIELD_STACK_COUNT": 14,
"OBJECT_FIELD_ENTRY": 3,
"OBJECT_FIELD_SCALE_X": 4,
"UNIT_FIELD_TARGET_LO": 16,
"UNIT_FIELD_TARGET_HI": 17,
"PLAYER_BYTES": 191,
"PLAYER_BYTES_2": 192,
"PLAYER_END": 1282,
"PLAYER_EXPLORED_ZONES_START": 1111,
"PLAYER_FIELD_BANKBAG_SLOT_1": 612,
"PLAYER_FIELD_BANK_SLOT_1": 564,
"PLAYER_FIELD_COINAGE": 1176,
"PLAYER_FIELD_INV_SLOT_HEAD": 486,
"PLAYER_FIELD_PACK_SLOT_1": 532,
"PLAYER_FLAGS": 190,
"PLAYER_NEXT_LEVEL_XP": 717,
"PLAYER_QUEST_LOG_START": 198,
"PLAYER_REST_STATE_EXPERIENCE": 1175,
"PLAYER_SKILL_INFO_START": 718,
"PLAYER_XP": 716,
"UNIT_DYNAMIC_FLAGS": 143,
"UNIT_END": 188,
"UNIT_FIELD_AURAFLAGS": 98,
"UNIT_FIELD_AURAS": 50,
"UNIT_FIELD_BYTES_0": 36,
"UNIT_FIELD_HEALTH": 22,
"UNIT_FIELD_POWER1": 23,
"UNIT_FIELD_MAXHEALTH": 28,
"UNIT_FIELD_MAXPOWER1": 29,
"UNIT_FIELD_LEVEL": 34,
"UNIT_FIELD_BYTES_1": 133,
"UNIT_FIELD_DISPLAYID": 131,
"UNIT_FIELD_FACTIONTEMPLATE": 35,
"UNIT_FIELD_FLAGS": 46,
"UNIT_FIELD_DISPLAYID": 131,
"UNIT_FIELD_HEALTH": 22,
"UNIT_FIELD_LEVEL": 34,
"UNIT_FIELD_MAXHEALTH": 28,
"UNIT_FIELD_MAXPOWER1": 29,
"UNIT_FIELD_MOUNTDISPLAYID": 133,
"UNIT_FIELD_AURAS": 50,
"UNIT_FIELD_AURAFLAGS": 98,
"UNIT_NPC_FLAGS": 147,
"UNIT_DYNAMIC_FLAGS": 143,
"UNIT_FIELD_POWER1": 23,
"UNIT_FIELD_RESISTANCES": 154,
"UNIT_FIELD_STAT0": 138,
"UNIT_FIELD_STAT1": 139,
"UNIT_FIELD_STAT2": 140,
"UNIT_FIELD_STAT3": 141,
"UNIT_FIELD_STAT4": 142,
"UNIT_END": 188,
"PLAYER_FLAGS": 190,
"PLAYER_BYTES": 191,
"PLAYER_BYTES_2": 192,
"PLAYER_XP": 716,
"PLAYER_NEXT_LEVEL_XP": 717,
"PLAYER_REST_STATE_EXPERIENCE": 1175,
"PLAYER_FIELD_COINAGE": 1176,
"PLAYER_QUEST_LOG_START": 198,
"PLAYER_FIELD_INV_SLOT_HEAD": 486,
"PLAYER_FIELD_PACK_SLOT_1": 532,
"PLAYER_FIELD_BANK_SLOT_1": 564,
"PLAYER_FIELD_BANKBAG_SLOT_1": 612,
"PLAYER_SKILL_INFO_START": 718,
"PLAYER_EXPLORED_ZONES_START": 1111,
"PLAYER_END": 1282,
"GAMEOBJECT_DISPLAYID": 8,
"ITEM_FIELD_STACK_COUNT": 14,
"ITEM_FIELD_DURABILITY": 48,
"ITEM_FIELD_MAXDURABILITY": 49,
"CONTAINER_FIELD_NUM_SLOTS": 48,
"CONTAINER_FIELD_SLOT_1": 50
}
"UNIT_FIELD_TARGET_HI": 17,
"UNIT_FIELD_TARGET_LO": 16,
"UNIT_NPC_FLAGS": 147
}

View file

@ -1,109 +1,65 @@
{
"Spell": {
"Achievement": {
"Description": 21,
"ID": 0,
"Attributes": 4,
"AttributesEx": 5,
"IconID": 133,
"Name": 136,
"Tooltip": 139,
"Rank": 153,
"SchoolMask": 225,
"PowerType": 14,
"ManaCost": 39,
"CastingTimeIndex": 47,
"RangeIndex": 49,
"DispelType": 2
"Points": 39,
"Title": 4
},
"SpellRange": {
"MaxRange": 4
},
"ItemDisplayInfo": {
"AchievementCriteria": {
"AchievementID": 1,
"Description": 9,
"ID": 0,
"LeftModel": 1,
"LeftModelTexture": 3,
"InventoryIcon": 5,
"GeosetGroup1": 7,
"GeosetGroup3": 9,
"TextureArmUpper": 14,
"TextureArmLower": 15,
"TextureHand": 16,
"TextureTorsoUpper": 17,
"TextureTorsoLower": 18,
"TextureLegUpper": 19,
"TextureLegLower": 20,
"TextureFoot": 21
"Quantity": 4
},
"CharSections": {
"AreaTable": {
"ExploreFlag": 3,
"ID": 0,
"MapID": 1,
"ParentAreaNum": 2
},
"CharHairGeosets": {
"GeosetID": 4,
"RaceID": 1,
"SexID": 2,
"Variation": 3
},
"CharSections": {
"BaseSection": 3,
"VariationIndex": 4,
"ColorIndex": 5,
"Flags": 9,
"RaceID": 1,
"SexID": 2,
"Texture1": 6,
"Texture2": 7,
"Texture3": 8,
"Flags": 9
},
"SpellIcon": {
"ID": 0,
"Path": 1
},
"FactionTemplate": {
"ID": 0,
"Faction": 1,
"FactionGroup": 3,
"FriendGroup": 4,
"EnemyGroup": 5,
"Enemy0": 6,
"Enemy1": 7,
"Enemy2": 8,
"Enemy3": 9
},
"Faction": {
"ID": 0,
"ReputationRaceMask0": 2,
"ReputationRaceMask1": 3,
"ReputationRaceMask2": 4,
"ReputationRaceMask3": 5,
"ReputationBase0": 10,
"ReputationBase1": 11,
"ReputationBase2": 12,
"ReputationBase3": 13
"VariationIndex": 4
},
"CharTitles": {
"ID": 0,
"Title": 2,
"TitleBit": 36
},
"Achievement": {
"ID": 0,
"Title": 4,
"Description": 21,
"Points": 39
"CharacterFacialHairStyles": {
"Geoset100": 3,
"Geoset200": 5,
"Geoset300": 4,
"RaceID": 0,
"SexID": 1,
"Variation": 2
},
"AchievementCriteria": {
"CreatureDisplayInfo": {
"ExtraDisplayId": 3,
"ID": 0,
"AchievementID": 1,
"Quantity": 4,
"Description": 9
},
"AreaTable": {
"ID": 0,
"MapID": 1,
"ParentAreaNum": 2,
"ExploreFlag": 3
"ModelID": 1,
"Skin1": 6,
"Skin2": 7,
"Skin3": 8
},
"CreatureDisplayInfoExtra": {
"ID": 0,
"RaceID": 1,
"SexID": 2,
"SkinID": 3,
"FaceID": 4,
"HairStyleID": 5,
"HairColorID": 6,
"FacialHairID": 7,
"BakeName": 20,
"EquipDisplay0": 8,
"EquipDisplay1": 9,
"EquipDisplay10": 18,
"EquipDisplay2": 10,
"EquipDisplay3": 11,
"EquipDisplay4": 12,
@ -112,158 +68,80 @@
"EquipDisplay7": 15,
"EquipDisplay8": 16,
"EquipDisplay9": 17,
"EquipDisplay10": 18,
"BakeName": 20
},
"CreatureDisplayInfo": {
"FaceID": 4,
"FacialHairID": 7,
"HairColorID": 6,
"HairStyleID": 5,
"ID": 0,
"ModelID": 1,
"ExtraDisplayId": 3,
"Skin1": 6,
"Skin2": 7,
"Skin3": 8
},
"TaxiNodes": {
"ID": 0,
"MapID": 1,
"X": 2,
"Y": 3,
"Z": 4,
"Name": 5,
"MountDisplayIdAllianceFallback": 20,
"MountDisplayIdHordeFallback": 21,
"MountDisplayIdAlliance": 22,
"MountDisplayIdHorde": 23
},
"TaxiPath": {
"ID": 0,
"FromNode": 1,
"ToNode": 2,
"Cost": 3
},
"TaxiPathNode": {
"ID": 0,
"PathID": 1,
"NodeIndex": 2,
"MapID": 3,
"X": 4,
"Y": 5,
"Z": 6
},
"TalentTab": {
"ID": 0,
"Name": 1,
"ClassMask": 20,
"OrderIndex": 22,
"BackgroundFile": 23
},
"Talent": {
"ID": 0,
"TabID": 1,
"Row": 2,
"Column": 3,
"RankSpell0": 4,
"PrereqTalent0": 9,
"PrereqRank0": 12
},
"SkillLineAbility": {
"SkillLineID": 1,
"SpellID": 2
},
"SkillLine": {
"ID": 0,
"Category": 1,
"Name": 3
},
"Map": {
"ID": 0,
"InternalName": 1
"RaceID": 1,
"SexID": 2,
"SkinID": 3
},
"CreatureModelData": {
"ID": 0,
"ModelPath": 2
},
"CharHairGeosets": {
"RaceID": 1,
"SexID": 2,
"Variation": 3,
"GeosetID": 4
},
"CharacterFacialHairStyles": {
"RaceID": 0,
"SexID": 1,
"Variation": 2,
"Geoset100": 3,
"Geoset300": 4,
"Geoset200": 5
},
"GameObjectDisplayInfo": {
"ID": 0,
"ModelName": 1
},
"Emotes": {
"ID": 0,
"AnimID": 2
"AnimID": 2,
"ID": 0
},
"EmotesText": {
"ID": 0,
"Command": 1,
"EmoteRef": 2,
"OthersTargetTextID": 3,
"SenderTargetTextID": 5,
"ID": 0,
"OthersNoTargetTextID": 7,
"SenderNoTargetTextID": 9
"OthersTargetTextID": 3,
"SenderNoTargetTextID": 9,
"SenderTargetTextID": 5
},
"EmotesTextData": {
"ID": 0,
"Text": 1
},
"Light": {
"Faction": {
"ID": 0,
"MapID": 1,
"X": 2,
"Z": 3,
"Y": 4,
"InnerRadius": 5,
"OuterRadius": 6,
"LightParamsID": 7,
"LightParamsIDRain": 8,
"LightParamsIDUnderwater": 9
"ReputationBase0": 10,
"ReputationBase1": 11,
"ReputationBase2": 12,
"ReputationBase3": 13,
"ReputationRaceMask0": 2,
"ReputationRaceMask1": 3,
"ReputationRaceMask2": 4,
"ReputationRaceMask3": 5
},
"LightParams": {
"LightParamsID": 0
"FactionTemplate": {
"Enemy0": 6,
"Enemy1": 7,
"Enemy2": 8,
"Enemy3": 9,
"EnemyGroup": 5,
"Faction": 1,
"FactionGroup": 3,
"FriendGroup": 4,
"ID": 0
},
"LightIntBand": {
"BlockIndex": 1,
"NumKeyframes": 2,
"TimeKey0": 3,
"Value0": 19
},
"LightFloatBand": {
"BlockIndex": 1,
"NumKeyframes": 2,
"TimeKey0": 3,
"Value0": 19
},
"WorldMapArea": {
"GameObjectDisplayInfo": {
"ID": 0,
"MapID": 1,
"AreaID": 2,
"AreaName": 3,
"LocLeft": 4,
"LocRight": 5,
"LocTop": 6,
"LocBottom": 7,
"DisplayMapID": 8,
"ParentWorldMapID": 10
"ModelName": 1
},
"SpellItemEnchantment": {
"ItemDisplayInfo": {
"GeosetGroup1": 7,
"GeosetGroup3": 9,
"ID": 0,
"Name": 8
"InventoryIcon": 5,
"LeftModel": 1,
"LeftModelTexture": 3,
"TextureArmLower": 15,
"TextureArmUpper": 14,
"TextureFoot": 21,
"TextureHand": 16,
"TextureLegLower": 20,
"TextureLegUpper": 19,
"TextureTorsoLower": 18,
"TextureTorsoUpper": 17
},
"ItemSet": {
"ID": 0,
"Name": 1,
"Item0": 18,
"Item1": 19,
"Item2": 20,
@ -274,6 +152,7 @@
"Item7": 25,
"Item8": 26,
"Item9": 27,
"Name": 1,
"Spell0": 28,
"Spell1": 29,
"Spell2": 30,
@ -299,21 +178,153 @@
"ID": 0,
"Name": 1
},
"SpellVisual": {
"Light": {
"ID": 0,
"InnerRadius": 5,
"LightParamsID": 7,
"LightParamsIDRain": 8,
"LightParamsIDUnderwater": 9,
"MapID": 1,
"OuterRadius": 6,
"X": 2,
"Y": 4,
"Z": 3
},
"LightFloatBand": {
"BlockIndex": 1,
"NumKeyframes": 2,
"TimeKey0": 3,
"Value0": 19
},
"LightIntBand": {
"BlockIndex": 1,
"NumKeyframes": 2,
"TimeKey0": 3,
"Value0": 19
},
"LightParams": {
"LightParamsID": 0
},
"Map": {
"ID": 0,
"InternalName": 1
},
"SkillLine": {
"Category": 1,
"ID": 0,
"Name": 3
},
"SkillLineAbility": {
"SkillLineID": 1,
"SpellID": 2
},
"Spell": {
"Attributes": 4,
"AttributesEx": 5,
"CastingTimeIndex": 47,
"DispelType": 2,
"DurationIndex": 40,
"EffectBasePoints0": 80,
"EffectBasePoints1": 81,
"EffectBasePoints2": 82,
"ID": 0,
"IconID": 133,
"ManaCost": 39,
"Name": 136,
"PowerType": 14,
"RangeIndex": 49,
"Rank": 153,
"SchoolMask": 225,
"SpellVisualID": 131,
"Tooltip": 139
},
"SpellIcon": {
"ID": 0,
"Path": 1
},
"SpellItemEnchantment": {
"ID": 0,
"Name": 8
},
"SpellRange": {
"MaxRange": 4
},
"SpellVisual": {
"CastKit": 2,
"ID": 0,
"ImpactKit": 3,
"MissileModel": 8
"MissileModel": 8,
"PrecastKit": 1
},
"SpellVisualEffectName": {
"FilePath": 2,
"ID": 0
},
"SpellVisualKit": {
"ID": 0,
"BaseEffect": 5,
"BreathEffect": 8,
"ChestEffect": 4,
"HeadEffect": 3,
"ID": 0,
"LeftHandEffect": 6,
"RightHandEffect": 7,
"SpecialEffect0": 11,
"SpecialEffect1": 12,
"SpecialEffect2": 13
},
"SpellVisualEffectName": {
"Talent": {
"Column": 3,
"ID": 0,
"FilePath": 2
"PrereqRank0": 12,
"PrereqTalent0": 9,
"RankSpell0": 4,
"Row": 2,
"TabID": 1
},
"TalentTab": {
"BackgroundFile": 23,
"ClassMask": 20,
"ID": 0,
"Name": 1,
"OrderIndex": 22
},
"TaxiNodes": {
"ID": 0,
"MapID": 1,
"MountDisplayIdAlliance": 22,
"MountDisplayIdAllianceFallback": 20,
"MountDisplayIdHorde": 23,
"MountDisplayIdHordeFallback": 21,
"Name": 5,
"X": 2,
"Y": 3,
"Z": 4
},
"TaxiPath": {
"Cost": 3,
"FromNode": 1,
"ID": 0,
"ToNode": 2
},
"TaxiPathNode": {
"ID": 0,
"MapID": 3,
"NodeIndex": 2,
"PathID": 1,
"X": 4,
"Y": 5,
"Z": 6
},
"WorldMapArea": {
"AreaID": 2,
"AreaName": 3,
"DisplayMapID": 8,
"ID": 0,
"LocBottom": 7,
"LocLeft": 4,
"LocRight": 5,
"LocTop": 6,
"MapID": 1,
"ParentWorldMapID": 10
}
}

View file

@ -1,60 +1,63 @@
{
"CONTAINER_FIELD_NUM_SLOTS": 64,
"CONTAINER_FIELD_SLOT_1": 66,
"GAMEOBJECT_DISPLAYID": 8,
"GAMEOBJECT_BYTES_1": 17,
"ITEM_FIELD_DURABILITY": 60,
"ITEM_FIELD_MAXDURABILITY": 61,
"ITEM_FIELD_STACK_COUNT": 14,
"OBJECT_FIELD_ENTRY": 3,
"OBJECT_FIELD_SCALE_X": 4,
"UNIT_FIELD_TARGET_LO": 6,
"UNIT_FIELD_TARGET_HI": 7,
"PLAYER_BLOCK_PERCENTAGE": 1024,
"PLAYER_BYTES": 153,
"PLAYER_BYTES_2": 154,
"PLAYER_CHOSEN_TITLE": 1349,
"PLAYER_CRIT_PERCENTAGE": 1029,
"PLAYER_DODGE_PERCENTAGE": 1025,
"PLAYER_EXPLORED_ZONES_START": 1041,
"PLAYER_FIELD_ARENA_CURRENCY": 1423,
"PLAYER_FIELD_BANKBAG_SLOT_1": 458,
"PLAYER_FIELD_BANK_SLOT_1": 402,
"PLAYER_FIELD_COINAGE": 1170,
"PLAYER_FIELD_COMBAT_RATING_1": 1231,
"PLAYER_FIELD_HONOR_CURRENCY": 1422,
"PLAYER_FIELD_INV_SLOT_HEAD": 324,
"PLAYER_FIELD_MOD_DAMAGE_DONE_POS": 1171,
"PLAYER_FIELD_MOD_HEALING_DONE_POS": 1192,
"PLAYER_FIELD_PACK_SLOT_1": 370,
"PLAYER_FLAGS": 150,
"PLAYER_NEXT_LEVEL_XP": 635,
"PLAYER_PARRY_PERCENTAGE": 1026,
"PLAYER_QUEST_LOG_START": 158,
"PLAYER_RANGED_CRIT_PERCENTAGE": 1030,
"PLAYER_REST_STATE_EXPERIENCE": 1169,
"PLAYER_SKILL_INFO_START": 636,
"PLAYER_SPELL_CRIT_PERCENTAGE1": 1032,
"PLAYER_XP": 634,
"UNIT_DYNAMIC_FLAGS": 147,
"UNIT_END": 148,
"UNIT_FIELD_ATTACK_POWER": 123,
"UNIT_FIELD_BYTES_0": 23,
"UNIT_FIELD_HEALTH": 24,
"UNIT_FIELD_POWER1": 25,
"UNIT_FIELD_MAXHEALTH": 32,
"UNIT_FIELD_MAXPOWER1": 33,
"UNIT_FIELD_LEVEL": 54,
"UNIT_FIELD_BYTES_1": 137,
"UNIT_FIELD_DISPLAYID": 67,
"UNIT_FIELD_FACTIONTEMPLATE": 55,
"UNIT_FIELD_FLAGS": 59,
"UNIT_FIELD_FLAGS_2": 60,
"UNIT_FIELD_DISPLAYID": 67,
"UNIT_FIELD_HEALTH": 24,
"UNIT_FIELD_LEVEL": 54,
"UNIT_FIELD_MAXHEALTH": 32,
"UNIT_FIELD_MAXPOWER1": 33,
"UNIT_FIELD_MOUNTDISPLAYID": 69,
"UNIT_NPC_FLAGS": 82,
"UNIT_DYNAMIC_FLAGS": 147,
"UNIT_FIELD_POWER1": 25,
"UNIT_FIELD_RANGED_ATTACK_POWER": 126,
"UNIT_FIELD_RESISTANCES": 99,
"UNIT_FIELD_STAT0": 84,
"UNIT_FIELD_STAT1": 85,
"UNIT_FIELD_STAT2": 86,
"UNIT_FIELD_STAT3": 87,
"UNIT_FIELD_STAT4": 88,
"UNIT_END": 148,
"UNIT_FIELD_ATTACK_POWER": 123,
"UNIT_FIELD_RANGED_ATTACK_POWER": 126,
"PLAYER_FLAGS": 150,
"PLAYER_BYTES": 153,
"PLAYER_BYTES_2": 154,
"PLAYER_XP": 634,
"PLAYER_NEXT_LEVEL_XP": 635,
"PLAYER_REST_STATE_EXPERIENCE": 1169,
"PLAYER_FIELD_COINAGE": 1170,
"PLAYER_QUEST_LOG_START": 158,
"PLAYER_FIELD_INV_SLOT_HEAD": 324,
"PLAYER_FIELD_PACK_SLOT_1": 370,
"PLAYER_FIELD_BANK_SLOT_1": 402,
"PLAYER_FIELD_BANKBAG_SLOT_1": 458,
"PLAYER_SKILL_INFO_START": 636,
"PLAYER_EXPLORED_ZONES_START": 1041,
"PLAYER_CHOSEN_TITLE": 1349,
"PLAYER_FIELD_MOD_DAMAGE_DONE_POS": 1171,
"PLAYER_FIELD_MOD_HEALING_DONE_POS": 1192,
"PLAYER_BLOCK_PERCENTAGE": 1024,
"PLAYER_DODGE_PERCENTAGE": 1025,
"PLAYER_PARRY_PERCENTAGE": 1026,
"PLAYER_CRIT_PERCENTAGE": 1029,
"PLAYER_RANGED_CRIT_PERCENTAGE": 1030,
"PLAYER_SPELL_CRIT_PERCENTAGE1": 1032,
"PLAYER_FIELD_COMBAT_RATING_1": 1231,
"PLAYER_FIELD_HONOR_CURRENCY": 1422,
"PLAYER_FIELD_ARENA_CURRENCY": 1423,
"GAMEOBJECT_DISPLAYID": 8,
"ITEM_FIELD_STACK_COUNT": 14,
"ITEM_FIELD_DURABILITY": 60,
"ITEM_FIELD_MAXDURABILITY": 61,
"CONTAINER_FIELD_NUM_SLOTS": 64,
"CONTAINER_FIELD_SLOT_1": 66
"UNIT_FIELD_TARGET_HI": 7,
"UNIT_FIELD_TARGET_LO": 6,
"UNIT_NPC_FLAGS": 82,
"UNIT_NPC_EMOTESTATE": 164
}

View file

@ -1,36 +0,0 @@
-- HelloWorld addon — demonstrates the WoWee addon system
-- Initialize saved variables (persisted across sessions)
if not HelloWorldDB then
HelloWorldDB = { loginCount = 0 }
end
HelloWorldDB.loginCount = (HelloWorldDB.loginCount or 0) + 1
-- Create a frame and register for events (standard WoW addon pattern)
local f = CreateFrame("Frame", "HelloWorldFrame")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("CHAT_MSG_SAY")
f:SetScript("OnEvent", function(self, event, ...)
if event == "PLAYER_ENTERING_WORLD" then
local name = UnitName("player")
local level = UnitLevel("player")
print("|cff00ff00[HelloWorld]|r Welcome, " .. name .. "! (Level " .. level .. ")")
print("|cff00ff00[HelloWorld]|r Login count: " .. HelloWorldDB.loginCount)
elseif event == "CHAT_MSG_SAY" then
local msg, sender = ...
if msg and sender then
print("|cff00ff00[HelloWorld]|r " .. sender .. " said: " .. msg)
end
end
end)
-- Register a custom slash command
SLASH_HELLOWORLD1 = "/hello"
SLASH_HELLOWORLD2 = "/hw"
SlashCmdList["HELLOWORLD"] = function(args)
print("|cff00ff00[HelloWorld]|r Hello! " .. (args ~= "" and args or "Type /hello <message>"))
print("|cff00ff00[HelloWorld]|r Sessions: " .. HelloWorldDB.loginCount)
end
print("|cff00ff00[HelloWorld]|r Addon loaded. Type /hello to test.")

View file

@ -1,5 +0,0 @@
## Interface: 30300
## Title: Hello World
## Notes: Test addon for the WoWee addon system
## SavedVariables: HelloWorldDB
HelloWorld.lua

View file

@ -7,6 +7,7 @@ WoWee supports three World of Warcraft expansions in a unified codebase using an
- **Vanilla (Classic) 1.12** - Original World of Warcraft
- **The Burning Crusade (TBC) 2.4.3** - First expansion
- **Wrath of the Lich King (WotLK) 3.3.5a** - Second expansion
- **Turtle WoW 1.17** - Custom Vanilla-based server with extended content
## Architecture Overview
@ -17,9 +18,9 @@ The multi-expansion support is built on the **Expansion Profile** system:
- Specifies which packet parsers to use
2. **Packet Parsers** - Expansion-specific message handling
- `packet_parsers_classic.cpp` - Vanilla 1.12 message parsing
- `packet_parsers_classic.cpp` - Vanilla 1.12 / Turtle WoW message parsing
- `packet_parsers_tbc.cpp` - TBC 2.4.3 message parsing
- `packet_parsers_wotlk.cpp` (default) - WotLK 3.3.5a message parsing
- Default (WotLK 3.3.5a) parsers in `game_handler.cpp` and domain handlers
3. **Update Fields** - Expansion-specific entity data layout
- Loaded from `update_fields.json` in expansion data directory
@ -78,17 +79,19 @@ WOWEE_EXPANSION=classic ./wowee # Force Classic
### Checking Current Expansion
```cpp
#include "game/expansion_profile.hpp"
#include "game/game_utils.hpp"
// Global helper
bool isClassicLikeExpansion() {
auto profile = ExpansionProfile::getActive();
return profile && (profile->name == "Classic" || profile->name == "Vanilla");
// Shared helpers (defined in game_utils.hpp)
if (isActiveExpansion("tbc")) {
// TBC-specific code
}
// Specific check
if (GameHandler::getInstance().isActiveExpansion("tbc")) {
// TBC-specific code
if (isClassicLikeExpansion()) {
// Classic or Turtle WoW
}
if (isPreWotlk()) {
// Classic, Turtle, or TBC (not WotLK)
}
```
@ -96,7 +99,7 @@ if (GameHandler::getInstance().isActiveExpansion("tbc")) {
```cpp
// In packet_parsers_*.cpp, implement expansion-specific logic
bool parseXxxPacket(BitStream& data, ...) {
bool TbcPacketParsers::parseXxx(network::Packet& packet, XxxData& data) {
// Custom logic for this expansion's packet format
}
```
@ -121,6 +124,7 @@ bool parseXxxPacket(BitStream& data, ...) {
## References
- `include/game/expansion_profile.hpp` - Expansion metadata
- `docs/status.md` - Current feature support by expansion
- `src/game/packet_parsers_*.cpp` - Format-specific parsing logic
- `include/game/game_utils.hpp` - `isActiveExpansion()`, `isClassicLikeExpansion()`, `isPreWotlk()`
- `src/game/packet_parsers_classic.cpp` / `packet_parsers_tbc.cpp` - Expansion-specific parsing
- `docs/status.md` - Current feature support
- `docs/` directory - Additional protocol documentation

View file

@ -39,20 +39,24 @@ WoWee needs game assets from your WoW installation:
**Using provided script (Windows)**:
```powershell
.\extract_assets.ps1 -WowDirectory "C:\Program Files\World of Warcraft"
.\extract_assets.ps1 "C:\Games\WoW-3.3.5a\Data"
```
**Manual extraction**:
1. Install [StormLib](https://github.com/ladislav-zezula/StormLib)
2. Extract to `./Data/`:
2. Use `asset_extract` or extract manually to `./Data/`:
```
Data/
├── dbc/ # DBC files
├── map/ # World map data
├── adt/ # Terrain chunks
├── wmo/ # Building models
├── m2/ # Character/creature models
└── blp/ # Textures
├── manifest.json # File index (generated by asset_extract)
├── expansions/<id>/ # Per-expansion config and DB
├── character/ # Character textures
├── creature/ # Creature models/textures
├── interface/ # UI textures and icons
├── item/ # Item model textures
├── spell/ # Spell effect models
├── terrain/ # ADT terrain, WMO, M2 doodads
├── world/ # World map images
└── sound/ # Audio files
```
### Step 3: Connect to a Server
@ -84,15 +88,19 @@ WoWee needs game assets from your WoW installation:
| Strafe Right | D |
| Jump | Space |
| Toggle Chat | Enter |
| Interact (talk to NPC, loot) | F |
| Open Inventory | B |
| Open Character Screen | C |
| Open Inventory | I |
| Open All Bags | B |
| Open Spellbook | P |
| Open Talent Tree | T |
| Open Quest Log | Q |
| Open World Map | W (when not typing) |
| Toggle Minimap | M |
| Open Talents | N |
| Open Quest Log | L |
| Open World Map | M |
| Toggle Nameplates | V |
| Toggle Party Frames | F |
| Toggle Raid Frames | F |
| Open Guild Roster | O |
| Open Dungeon Finder | J |
| Open Achievements | Y |
| Open Skills | K |
| Toggle Settings | Escape |
| Target Next Enemy | Tab |
| Target Previous Enemy | Shift+Tab |
@ -171,7 +179,7 @@ WOWEE_EXPANSION=tbc ./wowee # Force TBC
### General Issues
- Comprehensive troubleshooting: See [TROUBLESHOOTING.md](TROUBLESHOOTING.md)
- Check logs in `~/.wowee/logs/` for errors
- Check `logs/wowee.log` in the working directory for errors
- Verify expansion matches server requirements
## Server Configuration

View file

@ -9,28 +9,27 @@ arch=('x86_64')
url="https://github.com/Kelsidavis/WoWee"
license=('MIT')
depends=(
'sdl2'
'vulkan-icd-loader'
'openssl'
'zlib'
'ffmpeg'
'unicorn'
'glew'
'libx11'
'stormlib' # AUR — required at runtime by wowee-extract-assets (libstorm.so)
'sdl2' # Windowing and event loop
'vulkan-icd-loader' # Vulkan runtime (GPU driver communication)
'openssl' # SRP6a auth protocol (key exchange + RC4 encryption)
'zlib' # Network packet decompression and Warden module inflate
'ffmpeg' # Video playback (login cinematics)
'unicorn' # Warden anti-cheat x86 emulation (cross-platform, no Wine)
'libx11' # X11 windowing support
'stormlib' # AUR — MPQ extraction (wowee-extract-assets uses libstorm.so)
)
makedepends=(
'git'
'cmake'
'pkgconf'
'glm'
'vulkan-headers'
'shaderc'
'python'
'git' # Clone submodules (imgui, vk-bootstrap)
'cmake' # Build system
'pkgconf' # Dependency detection
'glm' # Header-only math library (vectors, matrices, quaternions)
'vulkan-headers' # Vulkan API definitions (build-time only)
'shaderc' # GLSL → SPIR-V shader compilation
'python' # Opcode registry generation and DBC validation scripts
)
provides=('wowee')
conflicts=('wowee')
source=("${pkgname}::git+https://github.com/Kelsidavis/WoWee.git#branch=main"
source=("${pkgname}::git+https://github.com/Kelsidavis/WoWee.git#branch=master"
"git+https://github.com/ocornut/imgui.git"
"git+https://github.com/charles-lunarg/vk-bootstrap.git")
sha256sums=('SKIP' 'SKIP' 'SKIP')

View file

@ -19,13 +19,39 @@ Protocol Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**.
> **Legal Disclaimer**: This is an educational/research project. It does not include any Blizzard Entertainment assets, data files, or proprietary code. World of Warcraft and all related assets are the property of Blizzard Entertainment, Inc. This project is not affiliated with or endorsed by Blizzard Entertainment. Users are responsible for supplying their own legally obtained game data files and for ensuring compliance with all applicable laws in their jurisdiction.
## Status & Direction (2026-03-07)
## Status & Direction (2026-04-14)
- **Compatibility**: **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a** are all supported via expansion profiles and per-expansion packet parsers (`src/game/packet_parsers_classic.cpp`, `src/game/packet_parsers_tbc.cpp`). All three expansions are roughly on par — no single one is significantly more complete than the others.
- **Tested against**: AzerothCore, TrinityCore, Mangos, and Turtle WoW (1.17).
- **Current focus**: instance dungeons, visual accuracy (lava/water, shadow mapping, WMO interiors), and multi-expansion coverage.
- **Compatibility**: **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a** are all supported via expansion profiles and per-expansion packet parsers. All three expansions are roughly on par.
- **Tested against**: AzerothCore/ChromieCraft, TrinityCore, Mangos, and Turtle WoW (1.17).
- **Current focus**: code quality (SOLID decomposition, documentation), rendering stability, and multi-expansion coverage.
- **Warden**: Full module execution via Unicorn Engine CPU emulation. Decrypts (RC4→RSA→zlib), parses and relocates the PE module, executes via x86 emulation with Windows API interception. Module cache at `~/.local/share/wowee/warden_cache/`.
- **CI**: GitHub Actions builds for Linux (x86-64, ARM64), Windows (MSYS2), and macOS (ARM64). Security scans via CodeQL, Semgrep, and sanitizers.
- **CI**: GitHub Actions builds for Linux (x86-64, ARM64), Windows (MSYS2 x86-64 + ARM64), and macOS (ARM64). Security scans via CodeQL, Semgrep, and sanitizers.
- **Container builds**: Multi-platform Docker build system for Linux, macOS (arm64/x86_64 via osxcross), and Windows (LLVM-MinGW) cross-compilation.
- **Release**: v1.8.9-preview — 530+ WoW API functions, 140+ events, 664 opcode handlers.
## World Editor
Standalone tool for creating custom WoW zones with novel open format exports.
```bash
# Build
cmake --build build --target wowee_editor
# Run
./build/bin/wowee_editor --data Data
# Batch convert assets
./build/bin/wowee_editor --convert-m2 Creature/Bear/Bear.m2 --data Data
./build/bin/wowee_editor --convert-wmo World/WMO/Stormwind/Stormwind.wmo --data Data
```
**6 editing modes** (Sculpt, Paint, Objects, Water, NPCs, Quests) with 30+ terrain tools, multi-select, time-of-day lighting, quest chains, and full undo/redo.
**7 novel open format replacements** for all Blizzard proprietary formats: WOT/WHM (terrain), WOC (collision), WOM1/WOM2 (static+animated models), WOB (buildings), zone.json (map def), PNG (textures), JSON (data tables). See `tools/editor/FORMAT_SPEC.md` for full specifications.
Exported zones auto-load in the wowee client from `custom_zones/` or `output/` directories.
**AzerothCore integration**: File > Generate Server Module creates a ready-to-import module with SQL spawn tables, map registration, teleport commands, zone flags, and a server admin README.
## Features
@ -52,7 +78,7 @@ Protocol Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**.
- **Movement** -- WASD movement, camera orbit, spline path following, transport riding (trams, ships, zeppelins), movement ACK responses
- **Combat** -- Auto-attack, spell casting with cooldowns, damage calculation, death handling, spirit healer resurrection
- **Targeting** -- Tab-cycling with hostility filtering, click-to-target, faction-based hostility (using Faction.dbc)
- **Inventory** -- 23 equipment slots, 16 backpack slots, drag-drop, auto-equip, item tooltips with weapon damage/speed
- **Inventory** -- 23 equipment slots, 16 backpack slots, drag-drop, auto-equip, item tooltips with weapon damage/speed, server-synced bag sort (quality/type/stack), independent bag windows
- **Bank** -- Full bank support for all expansions, bag slots, drag-drop, right-click deposit (non-equippable items)
- **Spells** -- Spellbook with specialty, general, profession, mount, and companion tabs; drag-drop to action bar; item use support
- **Talents** -- Talent tree UI with proper visuals and functionality
@ -67,7 +93,8 @@ Protocol Compatible with **Vanilla (Classic) 1.12 + TBC 2.4.3 + WotLK 3.3.5a**.
- **Chat** -- Tabs/channels, emotes, chat bubbles, clickable URLs, clickable item links with tooltips
- **Party** -- Group invites, party list, out-of-range member health via SMSG_PARTY_MEMBER_STATS
- **Pets** -- Pet tracking via SMSG_PET_SPELLS, action bar (10 slots with icon/autocast tinting/tooltips), dismiss button
- **Map Exploration** -- Subzone-level fog-of-war reveal matching retail behavior
- **Map Exploration** -- Subzone-level fog-of-war reveal, world map with continent/zone views, quest POI markers, taxi node markers, party member dots
- **NPC Voices** -- Race/gender-specific NPC greeting, farewell, vendor, pissed, aggro, and flee sounds for all playable races including Blood Elf and Draenei
- **Warden** -- Warden anti-cheat module execution via Unicorn Engine x86 emulation (cross-platform, no Wine)
- **UI** -- Loading screens with progress bar, settings window with graphics quality presets (LOW/MEDIUM/HIGH/ULTRA), shadow distance slider, minimap with zoom/rotation/square mode, top-right minimap mute speaker, separate bag windows with compact-empty mode (aggregate view)
@ -344,3 +371,13 @@ This project does not include any Blizzard Entertainment proprietary data, asset
## Known Issues
MANY issues this is actively under development
## Star History
<a href="https://www.star-history.com/?repos=Kelsidavis%2FWoWee&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=Kelsidavis/WoWee&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=Kelsidavis/WoWee&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=Kelsidavis/WoWee&type=date&legend=top-left" />
</picture>
</a>

415
TESTING.md Normal file
View file

@ -0,0 +1,415 @@
# WoWee Testing Guide
This document covers everything needed to build, run, lint, and extend the WoWee test suite.
---
## Table of Contents
1. [Overview](#overview)
2. [Prerequisites](#prerequisites)
3. [Test Suite Layout](#test-suite-layout)
4. [Building the Tests](#building-the-tests)
- [Release Build (normal)](#release-build-normal)
- [Debug + ASAN/UBSan Build](#debug--asanubsan-build)
5. [Running Tests](#running-tests)
- [test.sh — the unified entry point](#testsh--the-unified-entry-point)
- [Running directly with ctest](#running-directly-with-ctest)
6. [Lint (clang-tidy)](#lint-clang-tidy)
- [Running lint](#running-lint)
- [Applying auto-fixes](#applying-auto-fixes)
- [Configuration (.clang-tidy)](#configuration-clang-tidy)
7. [ASAN / UBSan](#asan--ubsan)
8. [Adding New Tests](#adding-new-tests)
9. [CI Reference](#ci-reference)
---
## Overview
WoWee uses **Catch2 v3** (amalgamated) for unit testing and **clang-tidy** for static analysis. The `test.sh` script is the single entry point for both.
| Command | What it does |
|---|---|
| `./test.sh` | Runs both unit tests (Release) and lint |
| `./test.sh --test` | Runs unit tests only (Release build) |
| `./test.sh --lint` | Runs clang-tidy only |
| `./test.sh --asan` | Runs unit tests under ASAN + UBSan (Debug build) |
| `FIX=1 ./test.sh --lint` | Applies clang-tidy auto-fixes in-place |
All commands exit non-zero on any failure.
---
## Prerequisites
The test suite requires the same base toolchain used to build the project. See [BUILD_INSTRUCTIONS.md](BUILD_INSTRUCTIONS.md) for platform-specific dependency installation.
### Linux (Ubuntu / Debian)
```bash
sudo apt update
sudo apt install -y \
build-essential cmake pkg-config git \
libssl-dev \
clang-tidy
```
### Linux (Arch)
```bash
sudo pacman -S --needed base-devel cmake pkgconf git openssl clang
```
### macOS
```bash
brew install cmake openssl@3 llvm
# Add LLVM tools to PATH so clang-tidy is found:
export PATH="$(brew --prefix llvm)/bin:$PATH"
```
### Windows (MSYS2)
Install the full toolchain as described in `BUILD_INSTRUCTIONS.md`, then add:
```bash
pacman -S --needed mingw-w64-x86_64-clang-tools-extra
```
---
## Test Suite Layout
```
tests/
CMakeLists.txt — CMake test configuration
# Core
test_packet.cpp — Network packet encode/decode
test_srp.cpp — SRP-6a authentication math (requires OpenSSL)
test_opcode_table.cpp — Opcode registry lookup
test_entity.cpp — ECS entity basics
test_dbc_loader.cpp — DBC binary file parsing
test_m2_structs.cpp — M2 model struct layout / alignment
test_blp_loader.cpp — BLP texture file parsing
test_frustum.cpp — View-frustum culling math
# Animation
test_animation_ids.cpp — Animation ID constants
test_locomotion_fsm.cpp — Locomotion state machine transitions
test_combat_fsm.cpp — Combat animation state machine
test_activity_fsm.cpp — Activity state machine
test_anim_capability.cpp — Animation capability queries
test_indoor_shadows.cpp — Indoor shadow rendering
# Transport & Spline
test_spline.cpp — CatmullRomSpline math (interpolation, binary search, looping)
test_transport_components.cpp — Transport clock sync and animator
test_transport_path_repo.cpp — TransportPathRepository (DBC loading, path inference)
# World Map
test_world_map.cpp — World map integration tests
test_world_map_coordinate_projection.cpp — UV projection, zone/continent spatial lookups
test_world_map_exploration_state.cpp — Server exploration mask, local tracking
test_world_map_map_resolver.cpp — Cross-map navigation (Outland, Northrend)
test_world_map_view_state_machine.cpp — COSMIC→WORLD→CONTINENT→ZONE transitions
test_world_map_zone_metadata.cpp — Zone level ranges and faction labels
# Chat
test_chat_markup_parser.cpp — Item link and markup parsing
test_chat_tab_completer.cpp — Tab-completion for names and commands
test_gm_commands.cpp — GM command data table and dispatch
test_macro_evaluator.cpp — Macro conditional evaluation
```
The Catch2 v3 amalgamated source lives at:
```
extern/catch2/
catch_amalgamated.hpp
catch_amalgamated.cpp
```
---
## Building the Tests
Tests are _not_ built by default. Enable them with `-DWOWEE_BUILD_TESTS=ON`.
### Release Build (normal)
> **Note:** Per project rules, always use `rebuild.sh` for a full clean build. Direct `cmake --build` is fine for test-only incremental builds.
```bash
# Configure (only needed once)
cmake -B build -DCMAKE_BUILD_TYPE=Release -DWOWEE_BUILD_TESTS=ON
# Build all test targets
cmake --build build --parallel $(nproc)
# Or build specific test targets
cmake --build build --target test_packet test_spline test_world_map
```
Or simply run a full rebuild (builds everything including the main binary):
```bash
./rebuild.sh # ~10 minutes — see BUILD_INSTRUCTIONS.md
```
### Debug + ASAN/UBSan Build
A separate CMake build directory is used so ASAN flags do not pollute the Release binary.
```bash
cmake -B build_asan \
-DCMAKE_BUILD_TYPE=Debug \
-DWOWEE_ENABLE_ASAN=ON \
-DWOWEE_BUILD_TESTS=ON
cmake --build build_asan --parallel $(nproc)
```
CMake will print: `Test targets: ASAN + UBSan ENABLED` when configured correctly.
---
## Running Tests
### test.sh — the unified entry point
`test.sh` is the recommended way to run tests and/or lint. It handles build-directory discovery, dependency checking, and exit-code aggregation across both steps.
```bash
# Run everything (tests + lint) — default when no flags are given
./test.sh
# Tests only (Release build)
./test.sh --test
# Tests only under ASAN+UBSan (Debug build — requires build_asan/)
./test.sh --asan
# Lint only
./test.sh --lint
# Both tests and lint explicitly
./test.sh --test --lint
# Usage summary
./test.sh --help
```
**Exit codes:**
| Outcome | Exit code |
|---|---|
| All tests passed, lint clean | `0` |
| Any test failed | `1` |
| Any lint diagnostic | `1` |
| Both test failure and lint issues | `1` |
### Running directly with ctest
```bash
# Release build
cd build
ctest --output-on-failure
# ASAN build
cd build_asan
ctest --output-on-failure
# Run one specific test suite by name
ctest --output-on-failure -R srp
# Verbose output (shows every SECTION and REQUIRE)
ctest --output-on-failure -V
```
You can also run a test binary directly for detailed Catch2 output:
```bash
./build/bin/test_srp
./build/bin/test_srp --reporter console
./build/bin/test_srp "[authentication]" # run only tests tagged [authentication]
```
---
## Lint (clang-tidy)
The project uses clang-tidy to enforce C++20 best practices on all first-party sources under `src/`. Third-party code (anything in `extern/`) and generated files are excluded.
### Running lint
```bash
./test.sh --lint
```
Under the hood the script:
1. Locates `clang-tidy` (tries versions 1418, then `clang-tidy`).
2. Uses `run-clang-tidy` for parallel execution when available; falls back to sequential.
3. Reads `build/compile_commands.json` (generated by CMake) for compiler flags.
4. Feeds GCC stdlib include paths as `-isystem` extras so clang-tidy can resolve `<vector>`, `<string>`, etc. when the compile-commands were generated with GCC.
`compile_commands.json` is regenerated automatically by any CMake configure step. If you only want to update it without rebuilding:
```bash
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
```
### Applying auto-fixes
Some clang-tidy checks can apply fixes automatically (e.g. `modernize-*`, `readability-*`):
```bash
FIX=1 ./test.sh --lint
```
> **Caution:** Review the diff before committing — automatic fixes occasionally produce non-idiomatic results in complex template code.
### Configuration (.clang-tidy)
The active check set is defined in [.clang-tidy](.clang-tidy) at the repository root.
**Enabled check categories:**
| Category | What it catches |
|---|---|
| `bugprone-*` | Common bug patterns (signed overflow, misplaced `=`, etc.) |
| `clang-analyzer-*` | Deep flow-analysis: null dereferences, memory leaks, dead stores |
| `performance-*` | Unnecessary copies, inefficient STL usage |
| `modernize-*` (subset) | Pre-C++11 patterns that should use modern equivalents |
| `readability-*` (subset) | Control-flow simplification, redundant code |
**Notable suppressions** (see `.clang-tidy` for details):
| Suppressed check | Reason |
|---|---|
| `bugprone-easily-swappable-parameters` | High false-positive rate in graphics/math APIs |
| `clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling` | Intentional low-level buffer code in rendering |
| `performance-avoid-endl` | `std::endl` is used intentionally for logger flushing |
To suppress a specific warning inline, use:
```cpp
// NOLINT(bugprone-narrowing-conversions)
uint8_t byte = static_cast<uint8_t>(value); // NOLINT
```
---
## ASAN / UBSan
AddressSanitizer (ASAN) and Undefined Behaviour Sanitizer (UBSan) are applied to all test targets when `WOWEE_ENABLE_ASAN=ON`.
Both the test executables **and** the `catch2_main` static library are recompiled with:
```
-fsanitize=address,undefined -fno-omit-frame-pointer
```
This means any heap overflow, stack buffer overflow, use-after-free, null dereference, signed integer overflow, or misaligned access detected during a test will abort the process and print a human-readable report to stderr.
### Workflow
```bash
# 1. Configure once (only needs to be re-run when CMakeLists.txt changes)
cmake -B build_asan \
-DCMAKE_BUILD_TYPE=Debug \
-DWOWEE_ENABLE_ASAN=ON \
-DWOWEE_BUILD_TESTS=ON
# 2. Build test binaries (fast incremental after the first build)
cmake --build build_asan --target test_packet test_srp # etc.
# 3. Run
./test.sh --asan
```
### Interpreting ASAN output
A failing ASAN report looks like:
```
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000010
READ of size 4 at 0x602000000010 thread T0
#0 0x... in PacketBuffer::read_uint32 src/network/packet.cpp:42
#1 0x... in test_packet tests/test_packet.cpp:88
```
Address the issue in the source file and re-run. Do **not** suppress ASAN reports without a code fix.
---
## Adding New Tests
1. **Create** `tests/test_<name>.cpp` with a standard Catch2 v3 structure:
```cpp
#include "catch_amalgamated.hpp"
TEST_CASE("SomeFeature does X", "[tag]") {
REQUIRE(1 + 1 == 2);
}
```
2. **Register** the test in `tests/CMakeLists.txt` following the existing pattern:
```cmake
# ── test_<name> ──────────────────────────────────────────────
add_executable(test_<name>
test_<name>.cpp
${TEST_COMMON_SOURCES}
${CMAKE_SOURCE_DIR}/src/<module>/<file>.cpp # source under test
)
target_include_directories(test_<name> PRIVATE ${TEST_INCLUDE_DIRS})
target_include_directories(test_<name> SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
target_link_libraries(test_<name> PRIVATE catch2_main)
add_test(NAME <name> COMMAND test_<name>)
register_test_target(test_<name>) # required — enables ASAN propagation
```
3. **Build** and verify:
```bash
cmake --build build --target test_<name>
./test.sh --test
```
The `register_test_target()` macro call is **mandatory** — without it the new test will not receive ASAN/UBSan flags when `WOWEE_ENABLE_ASAN=ON`.
---
## CI Reference
The following commands map to typical CI jobs:
| Job | Command |
|---|---|
| Unit tests (Release) | `./test.sh --test` |
| Unit tests (ASAN+UBSan) | `./test.sh --asan` |
| Lint | `./test.sh --lint` |
| Full check (tests + lint) | `./test.sh` |
**Configuring the ASAN job in CI:**
```yaml
- name: Configure ASAN build
run: |
cmake -B build_asan \
-DCMAKE_BUILD_TYPE=Debug \
-DWOWEE_ENABLE_ASAN=ON \
-DWOWEE_BUILD_TESTS=ON
- name: Build test targets
run: cmake --build build_asan --parallel $(nproc)
- name: Run ASAN tests
run: ./test.sh --asan
```
> See [BUILD_INSTRUCTIONS.md](BUILD_INSTRUCTIONS.md) for full platform dependency installation steps required before any CI job.

View file

@ -151,9 +151,7 @@ Graphics Preset: HIGH or ULTRA
## Getting Help
### Check Logs
Detailed logs are saved to:
- **Linux/macOS**: `~/.wowee/logs/`
- **Windows**: `%APPDATA%\wowee\logs\`
Detailed logs are saved to `logs/wowee.log` in the working directory (typically `build/bin/`).
Include relevant log entries when reporting issues.

View file

@ -1,38 +0,0 @@
#version 330 core
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;
out vec4 FragColor;
uniform vec3 uLightPos;
uniform vec3 uViewPos;
uniform vec4 uColor;
uniform sampler2D uTexture;
uniform bool uUseTexture;
void main() {
// Ambient
vec3 ambient = 0.3 * vec3(1.0);
// Diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(uLightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * vec3(1.0);
// Specular
vec3 viewDir = normalize(uViewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
vec3 specular = 0.5 * spec * vec3(1.0);
vec3 result = (ambient + diffuse + specular);
if (uUseTexture) {
FragColor = texture(uTexture, TexCoord) * vec4(result, 1.0);
} else {
FragColor = uColor * vec4(result, 1.0);
}
}

View file

@ -1,22 +0,0 @@
#version 330 core
layout (location = 0) in vec3 aPosition;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoord;
out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoord;
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;
void main() {
FragPos = vec3(uModel * vec4(aPosition, 1.0));
// Use mat3(uModel) directly - avoids expensive inverse() per vertex
Normal = mat3(uModel) * aNormal;
TexCoord = aTexCoord;
gl_Position = uProjection * uView * vec4(FragPos, 1.0);
}

View file

@ -126,7 +126,14 @@ void main() {
vec4 texColor = texture(uTexture, finalUV);
if (alphaTest != 0 && texColor.a < 0.5) discard;
if (alphaTest != 0) {
// Screen-space sharpened alpha for alpha-to-coverage anti-aliasing.
// Rescales alpha so the 0.5 cutoff maps to exactly the texel boundary,
// giving smooth edges when MSAA + alpha-to-coverage is active.
float aGrad = fwidth(texColor.a);
texColor.a = clamp((texColor.a - 0.5) / max(aGrad, 0.001) * 0.5 + 0.5, 0.0, 1.0);
if (texColor.a < 1.0 / 255.0) discard;
}
if (colorKeyBlack != 0) {
float lum = dot(texColor.rgb, vec3(0.299, 0.587, 0.114));
float ck = smoothstep(0.12, 0.30, lum);
@ -169,7 +176,7 @@ void main() {
if (proj.x >= 0.0 && proj.x <= 1.0 &&
proj.y >= 0.0 && proj.y <= 1.0 &&
proj.z >= 0.0 && proj.z <= 1.0) {
float bias = max(0.0005 * (1.0 - dot(norm, ldir)), 0.00005);
float bias = max(0.0005 * (1.0 - abs(dot(norm, ldir))), 0.00005);
shadow = sampleShadowPCF(uShadowMap, vec3(proj.xy, proj.z - bias));
}
shadow = mix(1.0, shadow, shadowParams.y);

Binary file not shown.

View file

@ -0,0 +1,8 @@
#version 450
layout(location = 0) in vec4 vColor;
layout(location = 0) out vec4 outColor;
void main() {
outColor = vColor;
}

View file

@ -0,0 +1,24 @@
#version 450
layout(set = 0, binding = 0) uniform PerFrame {
mat4 view;
mat4 projection;
mat4 lightSpaceMatrix;
vec4 lightDir;
vec4 lightColor;
vec4 ambientColor;
vec4 viewPos;
vec4 fogColor;
vec4 fogParams;
vec4 shadowParams;
};
layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec4 aColor;
layout(location = 0) out vec4 vColor;
void main() {
gl_Position = projection * view * vec4(aPosition, 1.0);
vColor = aColor;
}

View file

@ -0,0 +1,57 @@
#version 450
// Hierarchical-Z depth pyramid builder.
// Builds successive mip levels from the scene depth buffer.
// Each 2×2 block is reduced to its MAXIMUM depth (farthest/largest value).
// This is conservative for occlusion: an object is only culled when its nearest
// depth exceeds the farthest occluder depth in the pyramid region.
//
// Two modes controlled by push constant:
// mipLevel == 0: Sample from the source depth texture (mip 0 of the full-res depth).
// mipLevel > 0: Sample from the previous HiZ mip level.
layout(local_size_x = 8, local_size_y = 8) in;
// Source depth texture (full-resolution scene depth, or previous mip via same image)
layout(set = 0, binding = 0) uniform sampler2D srcDepth;
// Destination mip level (written as storage image)
layout(r32f, set = 0, binding = 1) uniform writeonly image2D dstMip;
layout(push_constant) uniform PushConstants {
ivec2 dstSize; // Width and height of the destination mip level
int mipLevel; // Current mip level being built (0 = from scene depth)
};
void main() {
ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
if (pos.x >= dstSize.x || pos.y >= dstSize.y) return;
// Each output texel covers a 2×2 block of the source.
// Use texelFetch for precise texel access (no filtering).
ivec2 srcPos = pos * 2;
float d00, d10, d01, d11;
if (mipLevel == 0) {
// Sample from full-res scene depth (sampler2D, lod 0)
d00 = texelFetch(srcDepth, srcPos + ivec2(0, 0), 0).r;
d10 = texelFetch(srcDepth, srcPos + ivec2(1, 0), 0).r;
d01 = texelFetch(srcDepth, srcPos + ivec2(0, 1), 0).r;
d11 = texelFetch(srcDepth, srcPos + ivec2(1, 1), 0).r;
} else {
// Sample from previous HiZ mip level (mipLevel - 1)
d00 = texelFetch(srcDepth, srcPos + ivec2(0, 0), mipLevel - 1).r;
d10 = texelFetch(srcDepth, srcPos + ivec2(1, 0), mipLevel - 1).r;
d01 = texelFetch(srcDepth, srcPos + ivec2(0, 1), mipLevel - 1).r;
d11 = texelFetch(srcDepth, srcPos + ivec2(1, 1), mipLevel - 1).r;
}
// Conservative maximum (standard depth buffer: 0=near, 1=far).
// We store the farthest (largest) depth in each 2×2 block.
// An object is occluded only when its nearest depth > the farthest occluder
// depth in the covered screen region — guaranteeing it's behind EVERYTHING.
float maxDepth = max(max(d00, d10), max(d01, d11));
imageStore(dstMip, pos, vec4(maxDepth));
}

View file

@ -13,19 +13,29 @@ layout(set = 0, binding = 0) uniform PerFrame {
vec4 shadowParams;
};
// Per-draw push constants (batch-level data only)
layout(push_constant) uniform Push {
mat4 model;
vec2 uvOffset;
int texCoordSet;
int useBones;
int isFoliage;
float fadeAlpha;
int texCoordSet; // UV set index (0 or 1)
int isFoliage; // Foliage wind animation flag
int instanceDataOffset; // Base index into InstanceSSBO for this draw group
} push;
layout(set = 2, binding = 0) readonly buffer BoneSSBO {
mat4 bones[];
};
// Per-instance data read via gl_InstanceIndex (GPU instancing)
struct InstanceData {
mat4 model;
vec2 uvOffset;
float fadeAlpha;
int useBones;
int boneBase;
};
layout(set = 3, binding = 0) readonly buffer InstanceSSBO {
InstanceData instanceData[];
};
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoord;
@ -41,15 +51,23 @@ layout(location = 4) out float ModelHeight;
layout(location = 5) out float vFadeAlpha;
void main() {
// Fetch per-instance data from SSBO
int instIdx = push.instanceDataOffset + gl_InstanceIndex;
mat4 model = instanceData[instIdx].model;
vec2 uvOff = instanceData[instIdx].uvOffset;
float fade = instanceData[instIdx].fadeAlpha;
int uBones = instanceData[instIdx].useBones;
int bBase = instanceData[instIdx].boneBase;
vec4 pos = vec4(aPos, 1.0);
vec4 norm = vec4(aNormal, 0.0);
if (push.useBones != 0) {
if (uBones != 0) {
ivec4 bi = ivec4(aBoneIndicesF);
mat4 skinMat = bones[bi.x] * aBoneWeights.x
+ bones[bi.y] * aBoneWeights.y
+ bones[bi.z] * aBoneWeights.z
+ bones[bi.w] * aBoneWeights.w;
mat4 skinMat = bones[bBase + bi.x] * aBoneWeights.x
+ bones[bBase + bi.y] * aBoneWeights.y
+ bones[bBase + bi.z] * aBoneWeights.z
+ bones[bBase + bi.w] * aBoneWeights.w;
pos = skinMat * pos;
norm = skinMat * norm;
}
@ -57,7 +75,7 @@ void main() {
// Wind animation for foliage
if (push.isFoliage != 0) {
float windTime = fogParams.z;
vec3 worldRef = push.model[3].xyz;
vec3 worldRef = model[3].xyz;
float heightFactor = clamp(pos.z / 20.0, 0.0, 1.0);
heightFactor *= heightFactor; // quadratic — base stays grounded
@ -80,15 +98,15 @@ void main() {
pos.y += trunkSwayY + branchSwayY + leafFlutterY;
}
vec4 worldPos = push.model * pos;
vec4 worldPos = model * pos;
FragPos = worldPos.xyz;
Normal = mat3(push.model) * norm.xyz;
Normal = mat3(model) * norm.xyz;
TexCoord = (push.texCoordSet == 1 ? aTexCoord2 : aTexCoord) + push.uvOffset;
TexCoord = (push.texCoordSet == 1 ? aTexCoord2 : aTexCoord) + uvOff;
InstanceOrigin = push.model[3].xyz;
InstanceOrigin = model[3].xyz;
ModelHeight = pos.z;
vFadeAlpha = push.fadeAlpha;
vFadeAlpha = fade;
gl_Position = projection * view * worldPos;
}

Binary file not shown.

View file

@ -0,0 +1,76 @@
#version 450
// GPU Frustum Culling for M2 doodads
// Each compute thread tests one M2 instance against 6 frustum planes.
// Input: per-instance bounding sphere + flags.
// Output: uint visibility array (1 = visible, 0 = culled).
layout(local_size_x = 64) in;
// Per-instance cull data (uploaded from CPU each frame)
struct CullInstance {
vec4 sphere; // xyz = world position, w = padded radius
float effectiveMaxDistSq; // adaptive distance cull threshold
uint flags; // bit 0 = valid, bit 1 = smoke, bit 2 = invisibleTrap
float _pad0;
float _pad1;
};
layout(std140, set = 0, binding = 0) uniform CullUniforms {
vec4 frustumPlanes[6]; // xyz = normal, w = distance
vec4 cameraPos; // xyz = camera position, w = maxPossibleDistSq
uint instanceCount;
uint _pad0;
uint _pad1;
uint _pad2;
};
layout(std430, set = 0, binding = 1) readonly buffer CullInput {
CullInstance cullInstances[];
};
layout(std430, set = 0, binding = 2) writeonly buffer CullOutput {
uint visibility[];
};
void main() {
uint id = gl_GlobalInvocationID.x;
if (id >= instanceCount) return;
CullInstance inst = cullInstances[id];
// Flag check: must be valid, not smoke, not invisible trap
uint f = inst.flags;
if ((f & 1u) == 0u || (f & 6u) != 0u) {
visibility[id] = 0u;
return;
}
// Early distance rejection (loose upper bound)
vec3 toCam = inst.sphere.xyz - cameraPos.xyz;
float distSq = dot(toCam, toCam);
if (distSq > cameraPos.w) {
visibility[id] = 0u;
return;
}
// Accurate per-instance distance cull
if (distSq > inst.effectiveMaxDistSq) {
visibility[id] = 0u;
return;
}
// Frustum cull: sphere vs 6 planes
float radius = inst.sphere.w;
if (radius > 0.0) {
for (int i = 0; i < 6; i++) {
float d = dot(frustumPlanes[i].xyz, inst.sphere.xyz) + frustumPlanes[i].w;
if (d < -radius) {
visibility[id] = 0u;
return;
}
}
}
visibility[id] = 1u;
}

Binary file not shown.

View file

@ -0,0 +1,184 @@
#version 450
// GPU Frustum + HiZ Occlusion Culling for M2 doodads (Phase 6.3).
//
// Two-level culling:
// 1. Frustum — current-frame planes from viewProj.
// 2. HiZ occlusion — projects bounding sphere into the PREVIOUS frame's
// screen space via prevViewProj and samples the Hierarchical-Z pyramid
// (built from said previous depth). Conservative safeguards:
// • Only objects that were visible last frame get the HiZ test.
// • AABB must be fully inside the screen (no border sampling).
// • Bounding sphere is inflated by 50 % for the HiZ AABB.
// • A depth bias is applied before the occlusion comparison.
// • Nearest depth is projected via prevViewProj from sphere center
// (avoids toCam mismatch between current and previous cameras).
//
// Falls back gracefully: if hizEnabled == 0, behaves identically to frustum-only.
layout(local_size_x = 64) in;
struct CullInstance {
vec4 sphere; // xyz = world position, w = padded radius
float effectiveMaxDistSq;
uint flags; // bit 0 = valid, bit 1 = smoke, bit 2 = invisibleTrap,
// bit 3 = previouslyVisible
float _pad0;
float _pad1;
};
layout(std140, set = 0, binding = 0) uniform CullUniforms {
vec4 frustumPlanes[6];
vec4 cameraPos; // xyz = camera position, w = maxPossibleDistSq
uint instanceCount;
uint hizEnabled;
uint hizMipLevels;
uint _pad2;
vec4 hizParams; // x = pyramidWidth, y = pyramidHeight, z = nearPlane, w = unused
mat4 viewProj; // current frame view-projection
mat4 prevViewProj; // PREVIOUS frame's view-projection for HiZ reprojection
};
layout(std430, set = 0, binding = 1) readonly buffer CullInput {
CullInstance cullInstances[];
};
layout(std430, set = 0, binding = 2) buffer CullOutput {
uint visibility[];
};
layout(set = 1, binding = 0) uniform sampler2D hizPyramid;
// Screen-edge margin — skip HiZ if the AABB touches this border.
// Depth data at screen edges is from unrelated geometry → false culls.
const float SCREEN_EDGE_MARGIN = 0.02;
// Sphere inflation factor for HiZ screen AABB (50 % larger → very conservative).
const float HIZ_SPHERE_INFLATE = 1.5;
// Depth bias — push nearest depth closer to camera so only objects
// significantly behind occluders are culled.
const float HIZ_DEPTH_BIAS = 0.02;
// Minimum screen-space size (pixels) for HiZ to engage.
const float HIZ_MIN_SCREEN_PX = 6.0;
void main() {
uint id = gl_GlobalInvocationID.x;
if (id >= instanceCount) return;
CullInstance inst = cullInstances[id];
// Flag check: must be valid, not smoke, not invisible trap
uint f = inst.flags;
if ((f & 1u) == 0u || (f & 6u) != 0u) {
visibility[id] = 0u;
return;
}
// Early distance rejection (loose upper bound)
vec3 toCam = inst.sphere.xyz - cameraPos.xyz;
float distSq = dot(toCam, toCam);
if (distSq > cameraPos.w) {
visibility[id] = 0u;
return;
}
// Accurate per-instance distance cull
if (distSq > inst.effectiveMaxDistSq) {
visibility[id] = 0u;
return;
}
// Frustum cull: sphere vs 6 planes (current frame)
float radius = inst.sphere.w;
if (radius > 0.0) {
for (int i = 0; i < 6; i++) {
float d = dot(frustumPlanes[i].xyz, inst.sphere.xyz) + frustumPlanes[i].w;
if (d < -radius) {
visibility[id] = 0u;
return;
}
}
}
// --- HiZ Occlusion Test ---
// Skip for objects not rendered last frame (bit 3 = previouslyVisible).
bool previouslyVisible = (f & 8u) != 0u;
if (hizEnabled != 0u && radius > 0.0 && previouslyVisible) {
// Inflate sphere for conservative screen-space AABB
float hizRadius = radius * HIZ_SPHERE_INFLATE;
// Project sphere center into previous frame's clip space
vec4 clipCenter = prevViewProj * vec4(inst.sphere.xyz, 1.0);
if (clipCenter.w > 0.0) {
vec3 ndc = clipCenter.xyz / clipCenter.w;
// --- Correct sphere → screen AABB using VP row-vector lengths ---
// The maximum screen-space extent of a world-space sphere is
// maxDeltaNdcX = R * ‖row_x(VP)‖ / w
// where row_x = (VP[0][0], VP[1][0], VP[2][0]) maps world XYZ
// offsets to clip-X. Using only the diagonal element (VP[0][0])
// underestimates the footprint when the camera is rotated,
// causing false culls at certain view angles.
float rowLenX = length(vec3(prevViewProj[0][0],
prevViewProj[1][0],
prevViewProj[2][0]));
float rowLenY = length(vec3(prevViewProj[0][1],
prevViewProj[1][1],
prevViewProj[2][1]));
float projRadX = hizRadius * rowLenX / clipCenter.w;
float projRadY = hizRadius * rowLenY / clipCenter.w;
float projRad = max(projRadX, projRadY);
vec2 uvCenter = ndc.xy * 0.5 + 0.5;
float uvRad = projRad * 0.5;
vec2 uvMin = uvCenter - uvRad;
vec2 uvMax = uvCenter + uvRad;
// **Screen-edge guard**: skip if AABB extends outside safe area.
// Depth data at borders is from unrelated geometry.
if (uvMin.x >= SCREEN_EDGE_MARGIN && uvMin.y >= SCREEN_EDGE_MARGIN &&
uvMax.x <= (1.0 - SCREEN_EDGE_MARGIN) && uvMax.y <= (1.0 - SCREEN_EDGE_MARGIN) &&
uvMax.x > uvMin.x && uvMax.y > uvMin.y)
{
float aabbW = (uvMax.x - uvMin.x) * hizParams.x;
float aabbH = (uvMax.y - uvMin.y) * hizParams.y;
float screenSize = max(aabbW, aabbH);
if (screenSize >= HIZ_MIN_SCREEN_PX) {
// Mip level: +1 for conservatism (coarser = bigger depth footprint)
float mipLevel = ceil(log2(max(screenSize, 1.0))) + 1.0;
mipLevel = clamp(mipLevel, 0.0, float(hizMipLevels - 1u));
// Sample HiZ at 4 corners — take MAX (farthest occluder)
float pz0 = textureLod(hizPyramid, uvMin, mipLevel).r;
float pz1 = textureLod(hizPyramid, vec2(uvMax.x, uvMin.y), mipLevel).r;
float pz2 = textureLod(hizPyramid, vec2(uvMin.x, uvMax.y), mipLevel).r;
float pz3 = textureLod(hizPyramid, uvMax, mipLevel).r;
float pyramidDepth = max(max(pz0, pz1), max(pz2, pz3));
// Nearest depth: project sphere center's NDC-Z then subtract
// the sphere's depth range. The depth span uses the Z-row
// length of VP (same Cauchy-Schwarz reasoning as X/Y), giving
// the correct NDC-Z extent regardless of camera orientation.
float rowLenZ = length(vec3(prevViewProj[0][2],
prevViewProj[1][2],
prevViewProj[2][2]));
float depthSpan = hizRadius * rowLenZ / clipCenter.w;
float centerDepth = ndc.z;
float nearestDepth = centerDepth - depthSpan - HIZ_DEPTH_BIAS;
if (nearestDepth > pyramidDepth && pyramidDepth < 1.0) {
visibility[id] = 0u;
return;
}
}
}
}
// fallthrough: conservatively visible
}
visibility[id] = 1u;
}

View file

@ -1,146 +0,0 @@
#version 330 core
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;
in vec2 LayerUV;
out vec4 FragColor;
// Texture layers (up to 4)
uniform sampler2D uBaseTexture;
uniform sampler2D uLayer1Texture;
uniform sampler2D uLayer2Texture;
uniform sampler2D uLayer3Texture;
// Alpha maps for blending
uniform sampler2D uLayer1Alpha;
uniform sampler2D uLayer2Alpha;
uniform sampler2D uLayer3Alpha;
// Layer control
uniform int uLayerCount;
uniform bool uHasLayer1;
uniform bool uHasLayer2;
uniform bool uHasLayer3;
// Lighting
uniform vec3 uLightDir;
uniform vec3 uLightColor;
uniform vec3 uAmbientColor;
// Camera
uniform vec3 uViewPos;
// Fog
uniform vec3 uFogColor;
uniform float uFogStart;
uniform float uFogEnd;
// Shadow mapping
uniform sampler2DShadow uShadowMap;
uniform mat4 uLightSpaceMatrix;
uniform bool uShadowEnabled;
uniform float uShadowStrength;
float calcShadow() {
vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0);
vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5;
if (proj.z > 1.0) return 1.0;
float edgeDist = max(abs(proj.x - 0.5), abs(proj.y - 0.5));
float coverageFade = 1.0 - smoothstep(0.40, 0.49, edgeDist);
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(-uLightDir);
float bias = max(0.005 * (1.0 - dot(norm, lightDir)), 0.001);
// 5-tap PCF tuned for slightly sharper detail while keeping stability.
vec2 texel = vec2(1.0 / 2048.0);
float ref = proj.z - bias;
vec2 off = texel * 0.7;
float shadow = 0.0;
shadow += texture(uShadowMap, vec3(proj.xy, ref)) * 0.55;
shadow += texture(uShadowMap, vec3(proj.xy + vec2(off.x, 0.0), ref)) * 0.1125;
shadow += texture(uShadowMap, vec3(proj.xy - vec2(off.x, 0.0), ref)) * 0.1125;
shadow += texture(uShadowMap, vec3(proj.xy + vec2(0.0, off.y), ref)) * 0.1125;
shadow += texture(uShadowMap, vec3(proj.xy - vec2(0.0, off.y), ref)) * 0.1125;
return mix(1.0, shadow, coverageFade);
}
float sampleAlpha(sampler2D tex, vec2 uv) {
// Slight blur near alpha-map borders to hide seams between chunks.
vec2 edge = min(uv, 1.0 - uv);
float border = min(edge.x, edge.y);
float doBlur = step(border, 2.0 / 64.0); // within ~2 texels of edge
if (doBlur < 0.5) {
return texture(tex, uv).r;
}
vec2 texel = vec2(1.0 / 64.0);
float a = 0.0;
a += texture(tex, uv + vec2(-texel.x, 0.0)).r;
a += texture(tex, uv + vec2(texel.x, 0.0)).r;
a += texture(tex, uv + vec2(0.0, -texel.y)).r;
a += texture(tex, uv + vec2(0.0, texel.y)).r;
return a * 0.25;
}
void main() {
// Sample base texture
vec4 baseColor = texture(uBaseTexture, TexCoord);
vec4 finalColor = baseColor;
// Apply texture layers with alpha blending
// TexCoord = tiling UVs for texture sampling (repeats across chunk)
// LayerUV = 0-1 per-chunk UVs for alpha map sampling
float a1 = uHasLayer1 ? sampleAlpha(uLayer1Alpha, LayerUV) : 0.0;
float a2 = uHasLayer2 ? sampleAlpha(uLayer2Alpha, LayerUV) : 0.0;
float a3 = uHasLayer3 ? sampleAlpha(uLayer3Alpha, LayerUV) : 0.0;
// Normalize weights to reduce quilting seams at chunk borders.
float w0 = 1.0;
float w1 = a1;
float w2 = a2;
float w3 = a3;
float sum = w0 + w1 + w2 + w3;
if (sum > 0.0) {
w0 /= sum; w1 /= sum; w2 /= sum; w3 /= sum;
}
finalColor = baseColor * w0;
if (uHasLayer1) {
vec4 layer1Color = texture(uLayer1Texture, TexCoord);
finalColor += layer1Color * w1;
}
if (uHasLayer2) {
vec4 layer2Color = texture(uLayer2Texture, TexCoord);
finalColor += layer2Color * w2;
}
if (uHasLayer3) {
vec4 layer3Color = texture(uLayer3Texture, TexCoord);
finalColor += layer3Color * w3;
}
// Normalize normal
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(-uLightDir);
// Ambient lighting
vec3 ambient = uAmbientColor * finalColor.rgb;
// Diffuse lighting (two-sided for terrain hills)
float diff = abs(dot(norm, lightDir));
diff = max(diff, 0.2); // Minimum light to prevent completely dark faces
vec3 diffuse = diff * uLightColor * finalColor.rgb;
// Shadow
float shadow = uShadowEnabled ? calcShadow() : 1.0;
shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0));
// Combine lighting (terrain is purely diffuse — no specular on ground)
vec3 result = ambient + shadow * diffuse;
// Apply fog
float distance = length(uViewPos - FragPos);
float fogFactor = clamp((uFogEnd - distance) / (uFogEnd - uFogStart), 0.0, 1.0);
result = mix(uFogColor, result, fogFactor);
FragColor = vec4(result, 1.0);
}

View file

@ -50,19 +50,21 @@ float sampleShadowPCF(sampler2DShadow smap, vec3 coords) {
}
float sampleAlpha(sampler2D tex, vec2 uv) {
// Smooth 5-tap box near chunk edges to hide alpha-map seams;
// blends gradually to avoid a visible ring at the transition.
vec2 edge = min(uv, 1.0 - uv);
float border = min(edge.x, edge.y);
float doBlur = step(border, 2.0 / 64.0);
if (doBlur < 0.5) {
return texture(tex, uv).r;
}
float blurWeight = 1.0 - smoothstep(0.5 / 64.0, 3.0 / 64.0, border);
float center = texture(tex, uv).r;
if (blurWeight < 0.001) return center;
vec2 texel = vec2(1.0 / 64.0);
float a = 0.0;
a += texture(tex, uv + vec2(-texel.x, 0.0)).r;
a += texture(tex, uv + vec2(texel.x, 0.0)).r;
a += texture(tex, uv + vec2(0.0, -texel.y)).r;
a += texture(tex, uv + vec2(0.0, texel.y)).r;
return a * 0.25;
float avg = center;
avg += texture(tex, uv + vec2(-texel.x, 0.0)).r;
avg += texture(tex, uv + vec2( texel.x, 0.0)).r;
avg += texture(tex, uv + vec2(0.0, -texel.y)).r;
avg += texture(tex, uv + vec2(0.0, texel.y)).r;
avg *= 0.2;
return mix(center, avg, blurWeight);
}
void main() {
@ -87,9 +89,12 @@ void main() {
vec3 norm = normalize(Normal);
// Derivative-based normal mapping: perturb vertex normal using texture detail.
// Fade out with distance — looks noisy/harsh beyond ~100 units.
// Fade out with distance and near chunk edges (dFdx/dFdy are invalid across
// chunk draw-call boundaries, producing visible seams if not faded).
float fragDist = length(viewPos.xyz - FragPos);
float bumpFade = 1.0 - smoothstep(50.0, 125.0, fragDist);
float edgeDist = min(min(LayerUV.x, 1.0 - LayerUV.x), min(LayerUV.y, 1.0 - LayerUV.y));
bumpFade *= smoothstep(0.0, 0.06, edgeDist);
if (bumpFade > 0.001) {
float lum = dot(finalColor.rgb, vec3(0.299, 0.587, 0.114));
float dLdx = dFdx(lum);
@ -116,8 +121,8 @@ void main() {
vec4 lsPos = lightSpaceMatrix * vec4(biasedPos, 1.0);
vec3 proj = lsPos.xyz / lsPos.w;
proj.xy = proj.xy * 0.5 + 0.5;
if (proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0 && proj.z <= 1.0) {
float bias = 0.0002;
if (proj.x >= 0.0 && proj.x <= 1.0 && proj.y >= 0.0 && proj.y <= 1.0 && proj.z >= 0.0 && proj.z <= 1.0) {
float bias = max(0.0005 * (1.0 - abs(dot(norm, ldir))), 0.00005);
shadow = sampleShadowPCF(uShadowMap, vec3(proj.xy, proj.z - bias));
shadow = mix(1.0, shadow, shadowParams.y);
}

View file

@ -1,28 +0,0 @@
#version 330 core
layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoord;
layout(location = 3) in vec2 aLayerUV;
out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoord;
out vec2 LayerUV;
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;
void main() {
vec4 worldPos = uModel * vec4(aPosition, 1.0);
FragPos = worldPos.xyz;
// Terrain uses identity model matrix, so normal passes through directly
Normal = aNormal;
TexCoord = aTexCoord;
LayerUV = aLayerUV;
gl_Position = uProjection * uView * worldPos;
}

View file

@ -47,12 +47,16 @@ layout(location = 0) out vec4 outColor;
// Dual-scroll detail normals (multi-octave ripple overlay)
// ============================================================
vec3 dualScrollWaveNormal(vec2 p, float time) {
vec2 d1 = normalize(vec2(0.86, 0.51));
vec2 d2 = normalize(vec2(-0.47, 0.88));
vec2 d3 = normalize(vec2(0.32, -0.95));
float f1 = 0.19, f2 = 0.43, f3 = 0.72;
float s1 = 0.95, s2 = 1.73, s3 = 2.40;
float a1 = 0.22, a2 = 0.10, a3 = 0.05;
// Three wave octaves at different angles, frequencies, and speeds.
// Directions are non-axis-aligned to prevent visible tiling patterns.
// Frequency increases and amplitude decreases per octave (standard
// multi-octave noise layering for natural water appearance).
vec2 d1 = normalize(vec2(0.86, 0.51)); // ~30° from +X
vec2 d2 = normalize(vec2(-0.47, 0.88)); // ~118° (opposing cross-wave)
vec2 d3 = normalize(vec2(0.32, -0.95)); // ~-71° (third axis for variety)
float f1 = 0.19, f2 = 0.43, f3 = 0.72; // spatial frequency (higher = tighter ripples)
float s1 = 0.95, s2 = 1.73, s3 = 2.40; // scroll speed (higher octaves move faster)
float a1 = 0.22, a2 = 0.10, a3 = 0.05; // amplitude (decreasing for natural falloff)
vec2 p1 = p + d1 * (time * s1 * 4.0);
vec2 p2 = p + d2 * (time * s2 * 4.0);

View file

@ -163,8 +163,9 @@ void main() {
vec3 result;
// Sample shadow map for all WMO groups (interior groups with 0x2000 flag
// include covered outdoor areas like archways/streets that should receive shadows)
// Sample shadow map for all groups. Interior groups receive attenuated
// shadow (30%) so they get subtle light/shadow variation without the full
// outdoor darkening that makes them look wrong.
float shadow = 1.0;
if (shadowParams.x > 0.5) {
vec3 ldir = normalize(-lightDir.xyz);
@ -176,7 +177,7 @@ void main() {
if (proj.x >= 0.0 && proj.x <= 1.0 &&
proj.y >= 0.0 && proj.y <= 1.0 &&
proj.z >= 0.0 && proj.z <= 1.0) {
float bias = max(0.0005 * (1.0 - dot(norm, ldir)), 0.00005);
float bias = max(0.0005 * (1.0 - abs(dot(norm, ldir))), 0.00005);
shadow = sampleShadowPCF(uShadowMap, vec3(proj.xy, proj.z - bias));
}
shadow = mix(1.0, shadow, shadowParams.y);
@ -185,17 +186,19 @@ void main() {
if (isLava != 0) {
// Lava is self-luminous — bright emissive, no shadows
result = texColor.rgb * 1.5;
} else if (unlit != 0) {
result = texColor.rgb * shadow;
} else if (isInterior != 0) {
// WMO interior: vertex colors (MOCV) are pre-baked lighting from the artist.
// The MOHD ambient color tints/floors the vertex colors so dark spots don't
// go completely black, matching the WoW client's interior shading.
// The MOHD ambient color floors the vertex colors so dark spots don't go
// completely black. Full shadow strength is applied but clamped so
// interiors never go darker than a minimum brightness.
vec3 wmoAmbient = vec3(wmoAmbientR, wmoAmbientG, wmoAmbientB);
// Clamp ambient to at least 0.3 to avoid total darkness when MOHD color is zero
wmoAmbient = max(wmoAmbient, vec3(0.3));
wmoAmbient = max(wmoAmbient, vec3(0.35));
vec3 mocv = max(VertColor.rgb, wmoAmbient);
result = texColor.rgb * mocv * shadow;
float clampedShadow = max(shadow, 0.45);
result = texColor.rgb * mocv * clampedShadow;
} else if (unlit != 0) {
// Outdoor unlit surface — still receives directional shadows
result = texColor.rgb * shadow;
} else {
vec3 ldir = normalize(-lightDir.xyz);
float diff = max(dot(norm, ldir), 0.0);

Binary file not shown.

View file

@ -0,0 +1,16 @@
#version 450
layout(set = 0, binding = 0) uniform sampler2D uTileTexture;
layout(push_constant) uniform PushConstants {
layout(offset = 16) vec4 tintColor;
};
layout(location = 0) in vec2 TexCoord;
layout(location = 0) out vec4 outColor;
void main() {
vec4 texel = texture(uTileTexture, TexCoord);
outColor = texel * tintColor;
}

33
assets/textures/README.md Normal file
View file

@ -0,0 +1,33 @@
# HD Texture Assets
**Source**: TurtleHD Texture Pack (Turtle WoW)
**Imported**: 2026-01-27
**Total Files**: 298 BLP textures
**Total Size**: 10MB
## Directory Structure
```
textures/
├── character/
│ └── human/ # 274 human male textures
├── creature/ # 15 creature textures
├── item/ # (reserved for future)
└── world/
├── generic/ # 1 generic world texture
└── stormwind/ # 8 Stormwind building textures
```
## Usage
These HD BLP textures are ready for integration with:
- **WMO Renderer**: Building texture mapping
- **Character Renderer**: M2 model skin/face textures
- **Creature Renderer**: NPC texture application
## Integration Status
Textures are loaded via the BLP pipeline and applied to WMO/M2 renderers.
HD texture overrides (e.g. TurtleHD packs) can be placed as PNG files
alongside the original BLP paths — the asset manager checks for `.png`
overrides before loading the `.blp` version.

283
container/FLOW.md Normal file
View file

@ -0,0 +1,283 @@
# Container Build Flow
Comprehensive documentation of the Docker-based build pipeline for each target platform.
---
## Architecture Overview
Each platform follows the same two-phase pattern:
1. **Image Build** (one-time, cached by Docker) — installs compilers, toolchains, and pre-builds vcpkg dependencies.
2. **Container Run** (each build) — copies source into the container, runs CMake configure + build, outputs artifacts to the host.
```
Host Docker
─────────────────────────────────────────────────────────────
run-{platform}.sh/.ps1
├─ docker build builder-{platform}.Dockerfile
│ (cached after first run) ├─ install compilers
│ ├─ install vcpkg + packages
│ └─ COPY build-{platform}.sh
└─ docker run build-{platform}.sh (entrypoint)
├─ bind /src (readonly) ├─ tar copy source → /wowee-build-src
└─ bind /out (writable) ├─ git clone FidelityFX SDKs
├─ cmake -S . -B /out
├─ cmake --build /out
└─ artifacts appear in /out
```
---
## Linux Build Flow
**Image:** `wowee-builder-linux`
**Dockerfile:** `builder-linux.Dockerfile`
**Toolchain:** GCC + Ninja (native amd64)
**Base:** Ubuntu 24.04
### Docker Image Build Steps
| Step | What | Why |
|------|------|-----|
| 1 | `apt-get install` cmake, ninja-build, build-essential, pkg-config, git, python3 | Core build tools |
| 2 | `apt-get install` glslang-tools, spirv-tools | Vulkan shader compilation |
| 3 | `apt-get install` libsdl2-dev, libglew-dev, libglm-dev, libssl-dev, zlib1g-dev | Runtime dependencies (system packages) |
| 4 | `apt-get install` libavformat-dev, libavcodec-dev, libswscale-dev, libavutil-dev | FFmpeg libraries |
| 5 | `apt-get install` libvulkan-dev, vulkan-tools | Vulkan SDK |
| 6 | `apt-get install` libstorm-dev, libunicorn-dev | MPQ archive + CPU emulation |
| 7 | COPY `build-linux.sh``/build-platform.sh` | Container entrypoint |
### Container Run Steps (build-linux.sh)
```
1. tar copy /src → /wowee-build-src (excludes build/, .git/, large Data/ dirs)
2. git clone FidelityFX-FSR2 (if missing)
3. git clone FidelityFX-SDK (if missing)
4. cmake configure:
-G Ninja
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_C_COMPILER=gcc
-DCMAKE_CXX_COMPILER=g++
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON
5. cmake --build (parallel)
6. Create Data symlink: build/linux/bin/Data → ../../../Data
```
### Output
- `build/linux/bin/wowee` — ELF 64-bit x86-64 executable
- `build/linux/bin/Data` — symlink to project Data/ directory
---
## macOS Build Flow
**Image:** `wowee-builder-macos`
**Dockerfile:** `builder-macos.Dockerfile` (multi-stage)
**Toolchain:** osxcross (Clang 18 + Apple ld64)
**Base:** Ubuntu 24.04
**Targets:** arm64-apple-darwin24.5 (default), x86_64-apple-darwin24.5
### Docker Image Build — Stage 1: SDK Fetcher
The macOS SDK is fetched automatically from Apple's public software update catalog.
No manual download required.
| Step | What | Why |
|------|------|-----|
| 1 | `FROM ubuntu:24.04 AS sdk-fetcher` | Lightweight stage for SDK download |
| 2 | `apt-get install` ca-certificates, python3, cpio, tar, gzip, xz-utils | SDK extraction tools |
| 3 | COPY `macos/sdk-fetcher.py``/opt/sdk-fetcher.py` | Python script that scrapes Apple's SUCATALOG |
| 4 | `python3 /opt/sdk-fetcher.py /opt/sdk` | Downloads, extracts, and packages MacOSX15.5.sdk.tar.gz |
**SDK Fetcher internals** (`macos/sdk-fetcher.py`):
1. Queries Apple SUCATALOG URLs for the latest macOS package
2. Downloads the `CLTools_macOSNMOS_SDK.pkg` package
3. Extracts the XAR archive (using `bsdtar` or pure-Python fallback)
4. Decompresses the PBZX payload stream
5. Extracts via `cpio` to get the SDK directory
6. Packages as `MacOSX<version>.sdk.tar.gz`
### Docker Image Build — Stage 2: Builder
| Step | What | Why |
|------|------|-----|
| 1 | `FROM ubuntu:24.04 AS builder` | Full build environment |
| 2 | `apt-get install` cmake, ninja-build, git, python3, curl, wget, xz-utils, zip, unzip, tar, make, patch, libssl-dev, zlib1g-dev, pkg-config, libbz2-dev, libxml2-dev, uuid-dev | Build tools + osxcross build deps |
| 3 | Install Clang 18 from LLVM apt repo (`llvm-toolchain-jammy-18`) | Cross-compiler backend |
| 4 | Symlink clang-18 → clang, clang++-18 → clang++, etc. | osxcross expects unversioned names |
| 5 | `git clone osxcross``/opt/osxcross` | Apple cross-compile toolchain wrapper |
| 6 | `COPY --from=sdk-fetcher /opt/sdk/ → /opt/osxcross/tarballs/` | SDK from stage 1 |
| 7 | `UNATTENDED=1 ./build.sh` | Builds osxcross (LLVM wrappers + cctools + ld64) |
| 8 | Create unprefixed symlinks (install_name_tool, otool, lipo, codesign) | vcpkg/CMake need these without arch prefix |
| 9 | COPY `macos/osxcross-toolchain.cmake``/opt/osxcross-toolchain.cmake` | Auto-detecting CMake toolchain |
| 10 | COPY `macos/triplets/``/opt/vcpkg-triplets/` | vcpkg cross-compile triplet definitions |
| 11 | `apt-get install` file, nasm | Mach-O detection + ffmpeg x86 asm |
| 12 | Bootstrap vcpkg → `/opt/vcpkg` | Package manager |
| 13 | `vcpkg install` sdl2, openssl, glew, glm, zlib, ffmpeg `--triplet arm64-osx-cross` | arm64 dependencies |
| 14 | `vcpkg install` same packages `--triplet x64-osx-cross` | x86_64 dependencies |
| 15 | `apt-get install` libvulkan-dev, glslang-tools | Vulkan headers (for compilation, not runtime) |
| 16 | COPY `build-macos.sh``/build-platform.sh` | Container entrypoint |
### Custom Toolchain Files
**`macos/osxcross-toolchain.cmake`** — Auto-detecting CMake toolchain:
- Detects SDK path via `file(GLOB)` in `/opt/osxcross/target/SDK/MacOSX*.sdk`
- Detects darwin version from compiler binary names (e.g., `arm64-apple-darwin24.5-clang`)
- Picks architecture from `CMAKE_OSX_ARCHITECTURES`
- Sets `CMAKE_C_COMPILER`, `CMAKE_CXX_COMPILER`, `CMAKE_AR`, `CMAKE_RANLIB`, `CMAKE_STRIP`
**`macos/triplets/arm64-osx-cross.cmake`**:
```cmake
set(VCPKG_TARGET_ARCHITECTURE arm64)
set(VCPKG_LIBRARY_LINKAGE static)
set(VCPKG_CMAKE_SYSTEM_NAME Darwin)
set(VCPKG_CHAINLOAD_TOOLCHAIN_FILE /opt/osxcross-toolchain.cmake)
```
### Container Run Steps (build-macos.sh)
```
1. Determine arch from MACOS_ARCH env (default: arm64)
2. Pick vcpkg triplet: arm64-osx-cross or x64-osx-cross
3. Auto-detect darwin target from osxcross binaries
4. tar copy /src → /wowee-build-src
5. git clone FidelityFX-FSR2 + FidelityFX-SDK (if missing)
6. cmake configure:
-G Ninja
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_SYSTEM_NAME=Darwin
-DCMAKE_OSX_ARCHITECTURES=${ARCH}
-DCMAKE_C_COMPILER=osxcross clang
-DCMAKE_CXX_COMPILER=osxcross clang++
-DCMAKE_TOOLCHAIN_FILE=vcpkg.cmake
-DVCPKG_TARGET_TRIPLET=arm64-osx-cross
-DVCPKG_OVERLAY_TRIPLETS=/opt/vcpkg-triplets
7. cmake --build (parallel)
```
### CMakeLists.txt Integration
The main CMakeLists.txt has a macOS cross-compile branch that:
- Finds Vulkan headers via vcpkg (`VulkanHeaders` package) instead of the Vulkan SDK
- Adds `-undefined dynamic_lookup` linker flag for Vulkan loader symbols (resolved at runtime via MoltenVK)
### Output
- `build/macos/bin/wowee` — Mach-O 64-bit arm64 (or x86_64) executable (~40 MB)
---
## Windows Build Flow
**Image:** `wowee-builder-windows`
**Dockerfile:** `builder-windows.Dockerfile`
**Toolchain:** LLVM-MinGW (Clang + LLD) targeting x86_64-w64-mingw32-ucrt
**Base:** Ubuntu 24.04
### Docker Image Build Steps
| Step | What | Why |
|------|------|-----|
| 1 | `apt-get install` ca-certificates, build-essential, cmake, ninja-build, git, python3, curl, zip, unzip, tar, xz-utils, pkg-config, nasm, libssl-dev, zlib1g-dev | Build tools |
| 2 | Download + extract LLVM-MinGW (v20240619 ucrt) → `/opt/llvm-mingw` | Clang/LLD cross-compiler for Windows |
| 3 | Add `/opt/llvm-mingw/bin` to PATH | Makes `x86_64-w64-mingw32-clang` etc. available |
| 4 | Bootstrap vcpkg → `/opt/vcpkg` | Package manager |
| 5 | `vcpkg install` sdl2, openssl, glew, glm, zlib, ffmpeg `--triplet x64-mingw-static` | Static Windows dependencies |
| 6 | `apt-get install` libvulkan-dev, glslang-tools | Vulkan headers + shader tools |
| 7 | Create no-op `powershell.exe` stub | vcpkg MinGW post-build hook needs it |
| 8 | COPY `build-windows.sh``/build-platform.sh` | Container entrypoint |
### Container Run Steps (build-windows.sh)
```
1. Set up no-op powershell.exe (if not already present)
2. tar copy /src → /wowee-build-src
3. git clone FidelityFX-FSR2 + FidelityFX-SDK (if missing)
4. Generate Vulkan import library:
a. Extract vk* symbols from vulkan_core.h
b. Create vulkan-1.def file
c. Run dlltool to create libvulkan-1.a
5. Lock PKG_CONFIG_LIBDIR to vcpkg packages only
6. cmake configure:
-G Ninja
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_SYSTEM_NAME=Windows
-DCMAKE_C_COMPILER=x86_64-w64-mingw32-clang
-DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-clang++
-DCMAKE_RC_COMPILER=x86_64-w64-mingw32-windres
-DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=lld
-DCMAKE_TOOLCHAIN_FILE=vcpkg.cmake
-DVCPKG_TARGET_TRIPLET=x64-mingw-static
-DVCPKG_APPLOCAL_DEPS=OFF
7. cmake --build (parallel)
```
### Vulkan Import Library Generation
Windows applications link against `vulkan-1.dll` (the Khronos Vulkan loader). Since the LLVM-MinGW toolchain doesn't ship a Vulkan import library, the build script generates one:
1. Parses `vulkan_core.h` for `VKAPI_CALL vk*` function names
2. Creates a `.def` file mapping symbols to `vulkan-1.dll`
3. Uses `dlltool` to produce `libvulkan-1.a` (PE import library)
This allows the linker to resolve Vulkan symbols at build time, while deferring actual loading to the runtime DLL.
### Output
- `build/windows/bin/wowee.exe` — PE32+ x86-64 executable (~135 MB)
---
## Shared Patterns
### Source Tree Copy
All three platforms use the same tar-based copy with exclusions:
```bash
tar -C /src \
--exclude='./build' --exclude='./logs' --exclude='./cache' \
--exclude='./container' --exclude='./.git' \
--exclude='./Data/character' --exclude='./Data/creature' \
--exclude='./Data/db' --exclude='./Data/environment' \
--exclude='./Data/interface' --exclude='./Data/item' \
--exclude='./Data/misc' --exclude='./Data/sound' \
--exclude='./Data/spell' --exclude='./Data/terrain' \
--exclude='./Data/world' \
-cf - . | tar -C /wowee-build-src -xf -
```
**Kept:** `Data/opcodes/`, `Data/expansions/` (small, needed at build time for configuration).
**Excluded:** Large game asset directories (character, creature, environment, etc.) not needed for compilation.
### FidelityFX SDK Fetch
All platforms clone the same two repos at build time:
1. **FidelityFX-FSR2** — FSR 2.0 upscaling
2. **FidelityFX-SDK** — FSR 3.0 frame generation (repo URL/ref configurable via env vars)
### .dockerignore
The `.dockerignore` at the project root minimizes the Docker build context by excluding:
- `build/`, `cache/`, `logs/`, `.git/`
- Large external dirs (`extern/FidelityFX-*`)
- IDE files, documentation, host-only scripts
- SDK tarballs (`*.tar.xz`, `*.tar.gz`, etc.)
---
## Timing Estimates
These are approximate times on a 4-core machine with 16 GB RAM:
| Phase | Linux | macOS | Windows |
|-------|-------|-------|---------|
| Docker image build (first time) | ~5 min | ~25 min | ~15 min |
| Docker image build (cached) | seconds | seconds | seconds |
| Source copy + SDK fetch | ~10 sec | ~10 sec | ~10 sec |
| CMake configure | ~20 sec | ~30 sec | ~30 sec |
| Compilation | ~8 min | ~8 min | ~8 min |
| **Total (first build)** | **~14 min** | **~34 min** | **~24 min** |
| **Total (subsequent)** | **~9 min** | **~9 min** | **~9 min** |
macOS image is slowest because osxcross builds a subset of LLVM + cctools, and vcpkg packages are compiled for two architectures (arm64 + x64).

119
container/README.md Normal file
View file

@ -0,0 +1,119 @@
# Container Builds
Build WoWee for **Linux**, **macOS**, or **Windows** with a single command.
All builds run inside Docker — no toolchains to install on your host.
## Prerequisites
- [Docker](https://docs.docker.com/get-docker/) (Docker Desktop on Windows/macOS, or Docker Engine on Linux)
- ~20 GB free disk space (toolchains + vcpkg packages are cached in the Docker image)
## Quick Start
Run **from the project root directory**.
### Linux (native amd64)
```bash
# Bash / Linux / macOS terminal
./container/run-linux.sh
```
```powershell
# PowerShell (Windows)
.\container\run-linux.ps1
```
Output: `build/linux/bin/wowee`
### macOS (cross-compile, arm64 default)
```bash
./container/run-macos.sh
```
```powershell
.\container\run-macos.ps1
```
Output: `build/macos/bin/wowee`
For Intel (x86_64):
```bash
MACOS_ARCH=x86_64 ./container/run-macos.sh
```
```powershell
.\container\run-macos.ps1 -Arch x86_64
```
### Windows (cross-compile, x86_64)
```bash
./container/run-windows.sh
```
```powershell
.\container\run-windows.ps1
```
Output: `build/windows/bin/wowee.exe`
## Options
| Option | Bash | PowerShell | Description |
|--------|------|------------|-------------|
| Rebuild image | `--rebuild-image` | `-RebuildImage` | Force a fresh Docker image build |
| macOS arch | `MACOS_ARCH=x86_64` | `-Arch x86_64` | Build for Intel instead of Apple Silicon |
| FidelityFX SDK repo | `WOWEE_FFX_SDK_REPO=<url>` | `$env:WOWEE_FFX_SDK_REPO="<url>"` | Custom FidelityFX SDK git URL |
| FidelityFX SDK ref | `WOWEE_FFX_SDK_REF=<ref>` | `$env:WOWEE_FFX_SDK_REF="<ref>"` | Custom FidelityFX SDK git ref/tag |
## Docker Image Caching
The first build takes longer because Docker builds the toolchain image (installing compilers, vcpkg packages, etc.). Subsequent builds reuse the cached image and only run the compilation step.
To force a full image rebuild:
```bash
./container/run-linux.sh --rebuild-image
```
## Output Locations
| Target | Binary | Size |
|--------|--------|------|
| Linux | `build/linux/bin/wowee` | ~135 MB |
| macOS | `build/macos/bin/wowee` | ~40 MB |
| Windows | `build/windows/bin/wowee.exe` | ~135 MB |
## File Structure
```
container/
├── run-linux.sh / .ps1 # Host launchers (bash / PowerShell)
├── run-macos.sh / .ps1
├── run-windows.sh / .ps1
├── build-linux.sh # Container entrypoints (run inside Docker)
├── build-macos.sh
├── build-windows.sh
├── builder-linux.Dockerfile # Docker image definitions
├── builder-macos.Dockerfile
├── builder-windows.Dockerfile
├── macos/
│ ├── sdk-fetcher.py # Auto-fetches macOS SDK from Apple's catalog
│ ├── osxcross-toolchain.cmake # CMake toolchain for osxcross
│ └── triplets/ # vcpkg cross-compile triplets
│ ├── arm64-osx-cross.cmake
│ └── x64-osx-cross.cmake
├── README.md # This file
└── FLOW.md # Detailed build flow documentation
```
## Troubleshooting
**"docker is not installed or not in PATH"**
Install Docker and ensure the `docker` command is available in your terminal.
**Build fails on first run**
Some vcpkg packages (ffmpeg, SDL2) take a while to compile. Ensure you have enough RAM (4 GB+) and disk space.
**macOS build: "could not find osxcross compiler"**
The Docker image may not have built correctly. Run with `--rebuild-image` to rebuild from scratch.
**Windows build: linker errors about vulkan-1.dll**
The build script auto-generates a Vulkan import library. If this fails, ensure the Docker image has `libvulkan-dev` installed (it should, by default).

View file

@ -1,19 +0,0 @@
#!/bin/bash
set -eu
set -o pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
podman build \
-f "${SCRIPT_DIR}/builder-ubuntu.Dockerfile" \
-t wowee-builder-ubuntu
BUILD_DIR="$(mktemp --tmpdir -d wowee.XXXXX \
--suffix=".$(cd "${PROJECT_ROOT}"; git rev-parse --short HEAD)")"
podman run \
--mount "type=bind,src=${PROJECT_ROOT},dst=/WoWee-src,ro=true" \
--mount "type=bind,src=${BUILD_DIR},dst=/build" \
localhost/wowee-builder-ubuntu \
./build-wowee.sh

63
container/build-linux.sh Executable file
View file

@ -0,0 +1,63 @@
#!/bin/bash
# Linux amd64 build entrypoint — runs INSIDE the linux container.
# Bind-mounts:
# /src (ro) — project source
# /out (rw) — host ./build/linux
set -euo pipefail
SRC=/src
OUT=/out
NPROC=$(nproc)
echo "==> [linux] Copying source tree..."
mkdir -p /wowee-build-src
tar -C "${SRC}" \
--exclude='./build' --exclude='./logs' --exclude='./cache' \
--exclude='./container' --exclude='./.git' \
--exclude='./Data/character' --exclude='./Data/creature' \
--exclude='./Data/db' --exclude='./Data/environment' \
--exclude='./Data/interface' --exclude='./Data/item' \
--exclude='./Data/misc' --exclude='./Data/sound' \
--exclude='./Data/spell' --exclude='./Data/terrain' \
--exclude='./Data/world' \
-cf - . | tar -C /wowee-build-src -xf -
cd /wowee-build-src
echo "==> [linux] Fetching external SDKs (if needed)..."
if [ ! -f extern/FidelityFX-FSR2/src/ffx-fsr2-api/ffx_fsr2.h ]; then
git clone --depth 1 \
https://github.com/GPUOpen-Effects/FidelityFX-FSR2.git \
extern/FidelityFX-FSR2 || echo "Warning: FSR2 clone failed — continuing without FSR2"
fi
SDK_REPO="${WOWEE_FFX_SDK_REPO:-https://github.com/Kelsidavis/FidelityFX-SDK.git}"
SDK_REF="${WOWEE_FFX_SDK_REF:-main}"
if [ ! -f "extern/FidelityFX-SDK/sdk/include/FidelityFX/host/ffx_frameinterpolation.h" ]; then
git clone --depth 1 --branch "${SDK_REF}" "${SDK_REPO}" extern/FidelityFX-SDK \
|| echo "Warning: FidelityFX-SDK clone failed — continuing without FSR3"
fi
echo "==> [linux] Configuring with CMake (Release, Ninja, amd64)..."
cmake -S . -B "${OUT}" \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=gcc \
-DCMAKE_CXX_COMPILER=g++ \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON
echo "==> [linux] Building with ${NPROC} cores..."
cmake --build "${OUT}" --parallel "${NPROC}"
echo "==> [linux] Creating Data symlink..."
mkdir -p "${OUT}/bin"
if [ ! -e "${OUT}/bin/Data" ]; then
# Relative symlink so it resolves correctly on the host:
# build/linux/bin/Data -> ../../../Data (project root)
ln -s ../../../Data "${OUT}/bin/Data"
fi
echo ""
echo "==> [linux] Build complete. Artifacts in: ./build/linux/"
echo " Binary: ./build/linux/bin/wowee"

83
container/build-macos.sh Executable file
View file

@ -0,0 +1,83 @@
#!/bin/bash
# macOS cross-compile entrypoint — runs INSIDE the macos container.
# Toolchain: osxcross + Apple Clang, target: arm64-apple-darwin (default) or
# x86_64-apple-darwin when MACOS_ARCH=x86_64.
# Bind-mounts:
# /src (ro) — project source
# /out (rw) — host ./build/macos
set -euo pipefail
SRC=/src
OUT=/out
NPROC=$(nproc)
# Arch selection: arm64 (Apple Silicon) is the default primary target.
ARCH="${MACOS_ARCH:-arm64}"
case "${ARCH}" in
arm64) VCPKG_TRIPLET=arm64-osx-cross ;;
x86_64) VCPKG_TRIPLET=x64-osx-cross ;;
*) echo "ERROR: unsupported MACOS_ARCH '${ARCH}'. Use arm64 or x86_64." ; exit 1 ;;
esac
# Auto-detect darwin target from osxcross binaries (e.g. arm64-apple-darwin24.5).
OSXCROSS_BIN=/opt/osxcross/target/bin
TARGET=$(basename "$(ls "${OSXCROSS_BIN}/${ARCH}-apple-darwin"*-clang 2>/dev/null | head -1)" | sed 's/-clang$//')
if [[ -z "${TARGET}" ]]; then
echo "ERROR: could not find osxcross ${ARCH} compiler in ${OSXCROSS_BIN}" >&2
exit 1
fi
echo "==> Detected osxcross target: ${TARGET}"
echo "==> [macos/${ARCH}] Copying source tree..."
mkdir -p /wowee-build-src
tar -C "${SRC}" \
--exclude='./build' --exclude='./logs' --exclude='./cache' \
--exclude='./container' --exclude='./.git' \
--exclude='./Data/character' --exclude='./Data/creature' \
--exclude='./Data/db' --exclude='./Data/environment' \
--exclude='./Data/interface' --exclude='./Data/item' \
--exclude='./Data/misc' --exclude='./Data/sound' \
--exclude='./Data/spell' --exclude='./Data/terrain' \
--exclude='./Data/world' \
-cf - . | tar -C /wowee-build-src -xf -
cd /wowee-build-src
echo "==> [macos/${ARCH}] Fetching external SDKs (if needed)..."
if [ ! -f extern/FidelityFX-FSR2/src/ffx-fsr2-api/ffx_fsr2.h ]; then
git clone --depth 1 \
https://github.com/GPUOpen-Effects/FidelityFX-FSR2.git \
extern/FidelityFX-FSR2 || echo "Warning: FSR2 clone failed"
fi
SDK_REPO="${WOWEE_FFX_SDK_REPO:-https://github.com/Kelsidavis/FidelityFX-SDK.git}"
SDK_REF="${WOWEE_FFX_SDK_REF:-main}"
if [ ! -f "extern/FidelityFX-SDK/sdk/include/FidelityFX/host/ffx_frameinterpolation.h" ]; then
git clone --depth 1 --branch "${SDK_REF}" "${SDK_REPO}" extern/FidelityFX-SDK \
|| echo "Warning: FidelityFX-SDK clone failed"
fi
echo "==> [macos/${ARCH}] Configuring with CMake (Release, Ninja, osxcross ${TARGET})..."
cmake -S . -B "${OUT}" \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_SYSTEM_NAME=Darwin \
-DCMAKE_OSX_ARCHITECTURES="${ARCH}" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-13.0}" \
-DCMAKE_C_COMPILER="${OSXCROSS_BIN}/${TARGET}-clang" \
-DCMAKE_CXX_COMPILER="${OSXCROSS_BIN}/${TARGET}-clang++" \
-DCMAKE_AR="${OSXCROSS_BIN}/${TARGET}-ar" \
-DCMAKE_RANLIB="${OSXCROSS_BIN}/${TARGET}-ranlib" \
-DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" \
-DVCPKG_TARGET_TRIPLET="${VCPKG_TRIPLET}" \
-DVCPKG_OVERLAY_TRIPLETS=/opt/vcpkg-triplets \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF \
-DWOWEE_ENABLE_ASAN=OFF
echo "==> [macos/${ARCH}] Building with ${NPROC} cores..."
cmake --build "${OUT}" --parallel "${NPROC}"
echo ""
echo "==> [macos/${ARCH}] Build complete. Artifacts in: ./build/macos/"
echo " Binary: ./build/macos/bin/wowee"

110
container/build-windows.sh Executable file
View file

@ -0,0 +1,110 @@
#!/bin/bash
# Windows cross-compile entrypoint — runs INSIDE the windows container.
# Toolchain: LLVM-MinGW (Clang + LLD), target: x86_64-w64-mingw32-ucrt
# Bind-mounts:
# /src (ro) — project source
# /out (rw) — host ./build/windows
set -euo pipefail
SRC=/src
OUT=/out
NPROC=$(nproc)
TARGET=x86_64-w64-mingw32
# vcpkg's MinGW applocal hook always appends a powershell.exe post-build step to
# copy DLLs next to each binary, even when VCPKG_APPLOCAL_DEPS=OFF. For the
# x64-mingw-static triplet the bin/ dir is empty (no DLLs) so the script does
# nothing — but it still needs to exit 0. Provide a no-op stub if the real
# PowerShell isn't available.
if ! command -v powershell.exe &>/dev/null; then
printf '#!/bin/sh\nexit 0\n' > /usr/local/bin/powershell.exe
chmod +x /usr/local/bin/powershell.exe
fi
echo "==> [windows] Copying source tree..."
mkdir -p /wowee-build-src
tar -C "${SRC}" \
--exclude='./build' --exclude='./logs' --exclude='./cache' \
--exclude='./container' --exclude='./.git' \
--exclude='./Data/character' --exclude='./Data/creature' \
--exclude='./Data/db' --exclude='./Data/environment' \
--exclude='./Data/interface' --exclude='./Data/item' \
--exclude='./Data/misc' --exclude='./Data/sound' \
--exclude='./Data/spell' --exclude='./Data/terrain' \
--exclude='./Data/world' \
-cf - . | tar -C /wowee-build-src -xf -
cd /wowee-build-src
echo "==> [windows] Fetching external SDKs (if needed)..."
if [ ! -f extern/FidelityFX-FSR2/src/ffx-fsr2-api/ffx_fsr2.h ]; then
git clone --depth 1 \
https://github.com/GPUOpen-Effects/FidelityFX-FSR2.git \
extern/FidelityFX-FSR2 || echo "Warning: FSR2 clone failed"
fi
SDK_REPO="${WOWEE_FFX_SDK_REPO:-https://github.com/Kelsidavis/FidelityFX-SDK.git}"
SDK_REF="${WOWEE_FFX_SDK_REF:-main}"
if [ ! -f "extern/FidelityFX-SDK/sdk/include/FidelityFX/host/ffx_frameinterpolation.h" ]; then
git clone --depth 1 --branch "${SDK_REF}" "${SDK_REPO}" extern/FidelityFX-SDK \
|| echo "Warning: FidelityFX-SDK clone failed"
fi
echo "==> [windows] Generating Vulkan import library for cross-compile..."
# Windows applications link against vulkan-1.dll (the Khronos Vulkan loader).
# The cross-compile toolchain only ships Vulkan *headers* (via vcpkg), not the
# import library. Generate a minimal libvulkan-1.a from the header prototypes
# so the linker can resolve vk* symbols → vulkan-1.dll at runtime.
# We use the host libvulkan-dev header for function name extraction — the Vulkan
# API prototypes are platform-independent.
VULKAN_IMP_DIR="${OUT}/vulkan-import"
if [ ! -f "${VULKAN_IMP_DIR}/libvulkan-1.a" ]; then
mkdir -p "${VULKAN_IMP_DIR}"
# Try vcpkg-installed header first (available on incremental builds),
# then fall back to the host libvulkan-dev header (always present in the image).
VK_HEADER="${OUT}/vcpkg_installed/x64-mingw-static/include/vulkan/vulkan_core.h"
if [ ! -f "${VK_HEADER}" ]; then
VK_HEADER="/usr/include/vulkan/vulkan_core.h"
fi
{
echo "LIBRARY vulkan-1.dll"
echo "EXPORTS"
grep -oP 'VKAPI_ATTR \S+ VKAPI_CALL \K(vk\w+)' "${VK_HEADER}" | sort -u | sed 's/^/ /'
} > "${VULKAN_IMP_DIR}/vulkan-1.def"
"${TARGET}-dlltool" -d "${VULKAN_IMP_DIR}/vulkan-1.def" \
-l "${VULKAN_IMP_DIR}/libvulkan-1.a" -m i386:x86-64
echo " Generated $(wc -l < "${VULKAN_IMP_DIR}/vulkan-1.def") export entries"
fi
echo "==> [windows] Configuring with CMake (Release, Ninja, LLVM-MinGW cross)..."
# Lock pkg-config to the cross-compiled vcpkg packages only.
# Without this, CMake's Vulkan pkg-config fallback finds the *Linux* libvulkan-dev
# and injects /usr/include into every MinGW compile command, which then fails
# because the glibc-specific bits/libc-header-start.h is not in the MinGW sysroot.
export PKG_CONFIG_LIBDIR="${OUT}/vcpkg_installed/x64-mingw-static/lib/pkgconfig"
export PKG_CONFIG_PATH=""
cmake -S . -B "${OUT}" \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_SYSTEM_NAME=Windows \
-DCMAKE_C_COMPILER="${TARGET}-clang" \
-DCMAKE_CXX_COMPILER="${TARGET}-clang++" \
-DCMAKE_RC_COMPILER="${TARGET}-windres" \
-DCMAKE_AR="/opt/llvm-mingw/bin/llvm-ar" \
-DCMAKE_RANLIB="/opt/llvm-mingw/bin/llvm-ranlib" \
-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld" \
-DCMAKE_SHARED_LINKER_FLAGS="-fuse-ld=lld" \
-DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" \
-DVCPKG_TARGET_TRIPLET=x64-mingw-static \
-DVCPKG_APPLOCAL_DEPS=OFF \
-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF \
-DWOWEE_ENABLE_ASAN=OFF
echo "==> [windows] Building with ${NPROC} cores..."
cmake --build "${OUT}" --parallel "${NPROC}"
echo ""
echo "==> [windows] Build complete. Artifacts in: ./build/windows/"
echo " Binary: ./build/windows/bin/wowee.exe"

View file

@ -1,14 +0,0 @@
#!/bin/bash
set -eu
set -o pipefail
cp -r /WoWee-src /WoWee
pushd /WoWee
./build.sh
popd
pushd /WoWee/build
cmake --install . --prefix=/build
popd

View file

@ -0,0 +1,33 @@
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y --no-install-recommends \
cmake \
ninja-build \
build-essential \
pkg-config \
git \
python3 \
glslang-tools \
spirv-tools \
libsdl2-dev \
libglew-dev \
libglm-dev \
libssl-dev \
zlib1g-dev \
libavformat-dev \
libavcodec-dev \
libswscale-dev \
libavutil-dev \
libvulkan-dev \
vulkan-tools \
libstorm-dev \
libunicorn-dev && \
rm -rf /var/lib/apt/lists/*
COPY build-linux.sh /build-platform.sh
RUN chmod +x /build-platform.sh
ENTRYPOINT ["/build-platform.sh"]

View file

@ -0,0 +1,142 @@
FROM ubuntu:24.04 AS sdk-fetcher
# Stage 1: Fetch macOS SDK from Apple's public software update catalog.
# This avoids requiring the user to supply the SDK tarball manually.
# The SDK is downloaded, extracted, and packaged as a .tar.gz.
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
python3 \
python3-defusedxml \
cpio \
tar \
gzip \
xz-utils && \
rm -rf /var/lib/apt/lists/*
COPY macos/sdk-fetcher.py /opt/sdk-fetcher.py
RUN python3 /opt/sdk-fetcher.py /opt/sdk
# ---------------------------------------------------------------------------
FROM ubuntu:24.04 AS builder
# Stage 2: macOS cross-compile image using osxcross + Clang 18.
#
# Target triplets (auto-detected from osxcross):
# arm64-apple-darwinNN (Apple Silicon)
# x86_64-apple-darwinNN (Intel)
# Default: arm64. Override with MACOS_ARCH=x86_64 env var at run time.
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
cmake \
ninja-build \
git \
python3 \
curl \
wget \
xz-utils \
zip \
unzip \
tar \
make \
patch \
libssl-dev \
zlib1g-dev \
pkg-config \
libbz2-dev \
libxml2-dev \
libz-dev \
liblzma-dev \
uuid-dev \
python3-lxml \
gnupg \
software-properties-common && \
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add - && \
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-18 main" > /etc/apt/sources.list.d/llvm-18.list && \
apt-get update && \
apt-get install -y --no-install-recommends \
clang-18 \
lld-18 \
llvm-18 && \
ln -sf /usr/bin/clang-18 /usr/bin/clang && \
ln -sf /usr/bin/clang++-18 /usr/bin/clang++ && \
ln -sf /usr/bin/lld-18 /usr/bin/lld && \
ln -sf /usr/bin/ld.lld-18 /usr/bin/ld.lld && \
ln -sf /usr/bin/llvm-ar-18 /usr/bin/llvm-ar && \
rm -rf /var/lib/apt/lists/*
# Build osxcross with SDK from stage 1
RUN git clone --depth 1 https://github.com/tpoechtrager/osxcross.git /opt/osxcross
COPY --from=sdk-fetcher /opt/sdk/ /opt/osxcross/tarballs/
ENV MACOSX_DEPLOYMENT_TARGET=13.0
RUN cd /opt/osxcross && \
UNATTENDED=1 ./build.sh && \
rm -rf /opt/osxcross/build /opt/osxcross/tarballs
ENV PATH="/opt/osxcross/target/bin:${PATH}"
ENV OSXCROSS_TARGET_DIR="/opt/osxcross/target"
ENV MACOSX_DEPLOYMENT_TARGET=13.0
# Create unprefixed symlinks for macOS tools that vcpkg/CMake expect
RUN cd /opt/osxcross/target/bin && \
for tool in install_name_tool otool lipo codesign; do \
src="$(ls *-apple-darwin*-"${tool}" 2>/dev/null | head -1)"; \
if [ -n "$src" ]; then \
ln -sf "$src" "$tool"; \
fi; \
done
# Custom osxcross toolchain + vcpkg triplets
COPY macos/osxcross-toolchain.cmake /opt/osxcross-toolchain.cmake
COPY macos/triplets/ /opt/vcpkg-triplets/
# Extra tools needed by vcpkg's Mach-O rpath fixup and ffmpeg x86 asm
RUN apt-get update && \
apt-get install -y --no-install-recommends file nasm && \
rm -rf /var/lib/apt/lists/*
# vcpkg — macOS cross triplets (arm64-osx-cross / x64-osx-cross)
ENV VCPKG_ROOT=/opt/vcpkg
RUN git clone --depth 1 https://github.com/microsoft/vcpkg.git "${VCPKG_ROOT}" && \
"${VCPKG_ROOT}/bootstrap-vcpkg.sh" -disableMetrics
# Pre-install deps for both arches; the launcher script picks the right one at run time.
RUN "${VCPKG_ROOT}/vcpkg" install \
sdl2[vulkan] \
openssl \
glew \
glm \
zlib \
ffmpeg \
--triplet arm64-osx-cross \
--overlay-triplets=/opt/vcpkg-triplets
RUN "${VCPKG_ROOT}/vcpkg" install \
sdl2[vulkan] \
openssl \
glew \
glm \
zlib \
ffmpeg \
--triplet x64-osx-cross \
--overlay-triplets=/opt/vcpkg-triplets
# Vulkan SDK headers (MoltenVK is the runtime — headers only needed to compile)
RUN apt-get update && \
apt-get install -y --no-install-recommends libvulkan-dev glslang-tools && \
rm -rf /var/lib/apt/lists/*
COPY build-macos.sh /build-platform.sh
RUN chmod +x /build-platform.sh
ENTRYPOINT ["/build-platform.sh"]

View file

@ -1,25 +0,0 @@
FROM ubuntu:24.04
RUN apt-get update && \
apt install -y \
cmake \
build-essential \
pkg-config \
git \
libsdl2-dev \
libglew-dev \
libglm-dev \
libssl-dev \
zlib1g-dev \
libavformat-dev \
libavcodec-dev \
libswscale-dev \
libavutil-dev \
libvulkan-dev \
vulkan-tools \
libstorm-dev && \
rm -rf /var/lib/apt/lists/*
COPY build-wowee.sh /
ENTRYPOINT ./build-wowee.sh

View file

@ -0,0 +1,67 @@
FROM ubuntu:24.04
# Windows cross-compile using LLVM-MinGW — best-in-class Clang/LLD toolchain
# targeting x86_64-w64-mingw32. Produces native .exe/.dll without MSVC or Wine.
# LLVM-MinGW ships: clang, clang++, lld, libc++ / libunwind headers, winpthreads.
ENV DEBIAN_FRONTEND=noninteractive
ENV LLVM_MINGW_VERSION=20240619
ENV LLVM_MINGW_URL=https://github.com/mstorsjo/llvm-mingw/releases/download/${LLVM_MINGW_VERSION}/llvm-mingw-${LLVM_MINGW_VERSION}-ucrt-ubuntu-20.04-x86_64.tar.xz
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
build-essential \
cmake \
ninja-build \
git \
python3 \
curl \
zip \
unzip \
tar \
xz-utils \
pkg-config \
nasm \
libssl-dev \
zlib1g-dev && \
rm -rf /var/lib/apt/lists/*
# Install LLVM-MinGW toolchain
RUN curl -fsSL "${LLVM_MINGW_URL}" -o /tmp/llvm-mingw.tar.xz && \
tar -xf /tmp/llvm-mingw.tar.xz -C /opt && \
mv /opt/llvm-mingw-${LLVM_MINGW_VERSION}-ucrt-ubuntu-20.04-x86_64 /opt/llvm-mingw && \
rm /tmp/llvm-mingw.tar.xz
ENV PATH="/opt/llvm-mingw/bin:${PATH}"
# Windows dependencies via vcpkg (static, x64-mingw-static triplet)
ENV VCPKG_ROOT=/opt/vcpkg
RUN git clone --depth 1 https://github.com/microsoft/vcpkg.git "${VCPKG_ROOT}" && \
"${VCPKG_ROOT}/bootstrap-vcpkg.sh" -disableMetrics
ENV VCPKG_DEFAULT_TRIPLET=x64-mingw-static
RUN "${VCPKG_ROOT}/vcpkg" install \
sdl2[vulkan] \
openssl \
glew \
glm \
zlib \
ffmpeg \
--triplet x64-mingw-static
# Vulkan SDK headers (loader is linked statically via SDL2's vulkan surface)
RUN apt-get update && \
apt-get install -y --no-install-recommends libvulkan-dev glslang-tools && \
rm -rf /var/lib/apt/lists/*
# Provide a no-op powershell.exe so vcpkg's MinGW applocal post-build hook
# exits cleanly. The x64-mingw-static triplet is fully static (no DLLs to
# copy), so the script has nothing to do — it just needs to not fail.
RUN printf '#!/bin/sh\nexit 0\n' > /usr/local/bin/powershell.exe && \
chmod +x /usr/local/bin/powershell.exe
COPY build-windows.sh /build-platform.sh
RUN chmod +x /build-platform.sh
ENTRYPOINT ["/build-platform.sh"]

View file

@ -0,0 +1,62 @@
# osxcross CMake toolchain file for cross-compiling to macOS from Linux.
# Used by vcpkg triplets and the WoWee build.
# Auto-detects SDK, darwin version, and arch from the osxcross installation
# and the VCPKG_OSX_ARCHITECTURES / CMAKE_OSX_ARCHITECTURES setting.
set(CMAKE_SYSTEM_NAME Darwin)
# osxcross paths
set(_target_dir "/opt/osxcross/target")
if(DEFINED ENV{OSXCROSS_TARGET_DIR})
set(_target_dir "$ENV{OSXCROSS_TARGET_DIR}")
endif()
# Auto-detect SDK (pick the newest if several are present)
file(GLOB _sdk_dirs "${_target_dir}/SDK/MacOSX*.sdk")
list(SORT _sdk_dirs)
list(GET _sdk_dirs -1 _sdk_dir)
set(CMAKE_OSX_SYSROOT "${_sdk_dir}" CACHE PATH "" FORCE)
# Deployment target
set(CMAKE_OSX_DEPLOYMENT_TARGET "13.0" CACHE STRING "" FORCE)
if(DEFINED ENV{MACOSX_DEPLOYMENT_TARGET})
set(CMAKE_OSX_DEPLOYMENT_TARGET "$ENV{MACOSX_DEPLOYMENT_TARGET}" CACHE STRING "" FORCE)
endif()
# auto-detect darwin version from compiler names
file(GLOB _darwin_compilers "${_target_dir}/bin/*-apple-darwin*-clang")
list(GET _darwin_compilers 0 _first_compiler)
get_filename_component(_compiler_name "${_first_compiler}" NAME)
string(REGEX MATCH "apple-darwin[0-9.]+" _darwin_part "${_compiler_name}")
# pick architecture
# CMAKE_OSX_ARCHITECTURES is set by vcpkg from VCPKG_OSX_ARCHITECTURES
if(CMAKE_OSX_ARCHITECTURES STREQUAL "arm64")
set(_arch "arm64")
elseif(CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64")
set(_arch "x86_64")
elseif(DEFINED ENV{OSXCROSS_ARCH})
set(_arch "$ENV{OSXCROSS_ARCH}")
else()
set(_arch "arm64")
endif()
set(_host "${_arch}-${_darwin_part}")
set(CMAKE_SYSTEM_PROCESSOR "${_arch}" CACHE STRING "" FORCE)
# compilers
set(CMAKE_C_COMPILER "${_target_dir}/bin/${_host}-clang" CACHE FILEPATH "" FORCE)
set(CMAKE_CXX_COMPILER "${_target_dir}/bin/${_host}-clang++" CACHE FILEPATH "" FORCE)
# tools
set(CMAKE_AR "${_target_dir}/bin/${_host}-ar" CACHE FILEPATH "" FORCE)
set(CMAKE_RANLIB "${_target_dir}/bin/${_host}-ranlib" CACHE FILEPATH "" FORCE)
set(CMAKE_STRIP "${_target_dir}/bin/${_host}-strip" CACHE FILEPATH "" FORCE)
set(CMAKE_INSTALL_NAME_TOOL "${_target_dir}/bin/${_host}-install_name_tool" CACHE FILEPATH "" FORCE)
# search paths
set(CMAKE_FIND_ROOT_PATH "${_sdk_dir}" "${_target_dir}")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

View file

@ -0,0 +1,380 @@
#!/usr/bin/env python3
"""Download and extract macOS SDK from Apple's Command Line Tools package.
Apple publishes Command Line Tools (CLT) packages via their publicly
accessible software update catalog. This script downloads the latest CLT,
extracts just the macOS SDK, and packages it as a .tar.gz tarball suitable
for osxcross.
No Apple ID or paid developer account required.
Usage:
python3 sdk-fetcher.py [output_dir]
The script prints the absolute path of the resulting tarball to stdout.
All progress / status messages go to stderr.
If a cached SDK tarball already exists in output_dir, it is reused.
Dependencies: python3 (>= 3.6), cpio, tar, gzip
Optional: bsdtar (libarchive-tools) or xar -- faster XAR extraction.
Falls back to a pure-Python XAR parser when neither is available.
"""
import glob
import gzip
import lzma
import os
import plistlib
import re
import shutil
import struct
import subprocess
import sys
import tempfile
import urllib.request
import zlib
try:
import defusedxml.ElementTree as ET
except ImportError as exc:
raise ImportError(
"defusedxml is required: pip install defusedxml"
) from exc
# -- Configuration -----------------------------------------------------------
CATALOG_URLS = [
# Try newest catalog first; first successful fetch wins.
"https://swscan.apple.com/content/catalogs/others/"
"index-16-15-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-"
"mountainlion-lion-snowleopard-leopard.merged-1.sucatalog.gz",
"https://swscan.apple.com/content/catalogs/others/"
"index-15-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-"
"mountainlion-lion-snowleopard-leopard.merged-1.sucatalog.gz",
"https://swscan.apple.com/content/catalogs/others/"
"index-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-"
"mountainlion-lion-snowleopard-leopard.merged-1.sucatalog.gz",
]
USER_AGENT = "Software%20Update"
# -- Helpers -----------------------------------------------------------------
def _validate_url(url):
"""Reject non-HTTPS URLs to prevent file:// and other scheme attacks."""
if not url.startswith("https://"):
raise ValueError(f"Refusing non-HTTPS URL: {url}")
def log(msg):
print(msg, file=sys.stderr, flush=True)
# -- 1) Catalog & URL discovery ----------------------------------------------
def find_sdk_pkg_url():
"""Search Apple catalogs for the latest CLTools_macOSNMOS_SDK.pkg URL."""
for cat_url in CATALOG_URLS:
short = cat_url.split("/index-")[1][:25] + "..."
log(f" Trying catalog: {short}")
try:
_validate_url(cat_url)
req = urllib.request.Request(cat_url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req, timeout=60) as resp: # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected
raw = gzip.decompress(resp.read())
catalog = plistlib.loads(raw)
except Exception as exc:
log(f" -> fetch failed: {exc}")
continue
products = catalog.get("Products", {})
candidates = []
for pid, product in products.items():
post_date = str(product.get("PostDate", ""))
for pkg in product.get("Packages", []):
url = pkg.get("URL", "")
size = pkg.get("Size", 0)
if "CLTools_macOSNMOS_SDK" in url and url.endswith(".pkg"):
candidates.append((post_date, url, size, pid))
if not candidates:
log(f" -> no CLTools SDK packages in this catalog, trying next...")
continue
candidates.sort(reverse=True)
_date, url, size, pid = candidates[0]
log(f"==> Found: CLTools_macOSNMOS_SDK (product {pid}, {size // 1048576} MB)")
return url
log("ERROR: No CLTools SDK packages found in any Apple catalog.")
sys.exit(1)
# -- 2) Download -------------------------------------------------------------
def download(url, dest):
"""Download *url* to *dest* with a basic progress indicator."""
_validate_url(url)
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req, timeout=600) as resp: # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected
total = int(resp.headers.get("Content-Length", 0))
done = 0
with open(dest, "wb") as f:
while True:
chunk = resp.read(1 << 20)
if not chunk:
break
f.write(chunk)
done += len(chunk)
if total:
pct = done * 100 // total
log(f"\r {done // 1048576} / {total // 1048576} MB ({pct}%)")
log("")
# -- 3) XAR extraction -------------------------------------------------------
def extract_xar(pkg_path, dest_dir):
"""Extract a XAR (.pkg) archive -- external tool or pure-Python fallback."""
for tool in ("bsdtar", "xar"):
if shutil.which(tool):
log(f"==> Extracting .pkg with {tool}...")
r = subprocess.run([tool, "-xf", pkg_path, "-C", dest_dir],
capture_output=True)
if r.returncode == 0:
return
log(f" {tool} exited {r.returncode}, trying next method...")
log("==> Extracting .pkg with built-in Python XAR parser...")
_extract_xar_python(pkg_path, dest_dir)
def _extract_xar_python(pkg_path, dest_dir):
"""Pure-Python XAR extractor (no external dependencies)."""
with open(pkg_path, "rb") as f:
raw = f.read(28)
if len(raw) < 28:
raise ValueError("File too small to be a valid XAR archive")
magic, hdr_size, _ver, toc_clen, _toc_ulen, _ck = struct.unpack(
">4sHHQQI", raw,
)
if magic != b"xar!":
raise ValueError(f"Not a XAR file (magic: {magic!r})")
f.seek(hdr_size)
toc_xml = zlib.decompress(f.read(toc_clen))
heap_off = hdr_size + toc_clen
root = ET.fromstring(toc_xml)
toc = root.find("toc")
if toc is None:
raise ValueError("Malformed XAR: no <toc> element")
def _walk(elem, base):
for fe in elem.findall("file"):
name = fe.findtext("name", "")
ftype = fe.findtext("type", "file")
path = os.path.join(base, name)
if ftype == "directory":
os.makedirs(path, exist_ok=True)
_walk(fe, path)
continue
de = fe.find("data")
if de is None:
continue
offset = int(de.findtext("offset", "0"))
size = int(de.findtext("size", "0"))
enc_el = de.find("encoding")
enc = enc_el.get("style", "") if enc_el is not None else ""
os.makedirs(os.path.dirname(path), exist_ok=True)
f.seek(heap_off + offset)
if "gzip" in enc:
with open(path, "wb") as out:
out.write(zlib.decompress(f.read(size), 15 + 32))
elif "bzip2" in enc:
import bz2
with open(path, "wb") as out:
out.write(bz2.decompress(f.read(size)))
else:
with open(path, "wb") as out:
rem = size
while rem > 0:
blk = f.read(min(rem, 1 << 20))
if not blk:
break
out.write(blk)
rem -= len(blk)
_walk(toc, dest_dir)
# -- 4) Payload extraction (pbzx / gzip cpio) --------------------------------
def _pbzx_stream(path):
"""Yield decompressed chunks from a pbzx-compressed file."""
with open(path, "rb") as f:
if f.read(4) != b"pbzx":
raise ValueError("Not a pbzx file")
f.read(8)
while True:
hdr = f.read(16)
if len(hdr) < 16:
break
_usize, csize = struct.unpack(">QQ", hdr)
data = f.read(csize)
if len(data) < csize:
break
if csize == _usize:
yield data
else:
yield lzma.decompress(data)
def _gzip_stream(path):
"""Yield decompressed chunks from a gzip file."""
with gzip.open(path, "rb") as f:
while True:
chunk = f.read(1 << 20)
if not chunk:
break
yield chunk
def _raw_stream(path):
"""Yield raw 1 MiB chunks (last resort)."""
with open(path, "rb") as f:
while True:
chunk = f.read(1 << 20)
if not chunk:
break
yield chunk
def extract_payload(payload_path, out_dir):
"""Decompress a CLT Payload (pbzx or gzip cpio) into *out_dir*."""
with open(payload_path, "rb") as pf:
magic = pf.read(4)
if magic == b"pbzx":
log(" Payload format: pbzx (LZMA chunks)")
stream = _pbzx_stream(payload_path)
elif magic[:2] == b"\x1f\x8b":
log(" Payload format: gzip")
stream = _gzip_stream(payload_path)
else:
log(f" Payload format: unknown (magic: {magic.hex()}), trying raw cpio...")
stream = _raw_stream(payload_path)
proc = subprocess.Popen(
["cpio", "-id", "--quiet"],
stdin=subprocess.PIPE,
cwd=out_dir,
stderr=subprocess.PIPE,
)
for chunk in stream:
try:
proc.stdin.write(chunk)
except BrokenPipeError:
break
proc.stdin.close()
proc.wait()
# -- Main --------------------------------------------------------------------
def main():
output_dir = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else os.getcwd()
os.makedirs(output_dir, exist_ok=True)
# Re-use a previously fetched SDK if present.
cached = glob.glob(os.path.join(output_dir, "MacOSX*.sdk.tar.*"))
if cached:
cached.sort()
result = os.path.realpath(cached[-1])
log(f"==> Using cached SDK: {os.path.basename(result)}")
print(result)
return
work = tempfile.mkdtemp(prefix="fetch-macos-sdk-")
try:
# 1 -- Locate SDK package URL from Apple's catalog
log("==> Searching Apple software-update catalogs...")
sdk_url = find_sdk_pkg_url()
# 2 -- Download (just the SDK component, ~55 MB)
pkg = os.path.join(work, "sdk.pkg")
log("==> Downloading CLTools SDK package...")
download(sdk_url, pkg)
# 3 -- Extract the flat .pkg (XAR format) to get the Payload
pkg_dir = os.path.join(work, "pkg")
os.makedirs(pkg_dir)
extract_xar(pkg, pkg_dir)
os.unlink(pkg)
# 4 -- Locate the Payload file
log("==> Locating SDK payload...")
sdk_payload = None
for dirpath, _dirs, files in os.walk(pkg_dir):
if "Payload" in files:
sdk_payload = os.path.join(dirpath, "Payload")
log(f" Found: {os.path.relpath(sdk_payload, pkg_dir)}")
break
if sdk_payload is None:
log("ERROR: No Payload found in extracted package")
sys.exit(1)
# 5 -- Decompress Payload -> cpio -> filesystem
sdk_root = os.path.join(work, "sdk")
os.makedirs(sdk_root)
log("==> Extracting SDK from payload (this may take a minute)...")
extract_payload(sdk_payload, sdk_root)
shutil.rmtree(pkg_dir)
# 6 -- Find MacOSX*.sdk directory
sdk_found = None
for dirpath, dirs, _files in os.walk(sdk_root):
for d in dirs:
if re.match(r"MacOSX\d+(\.\d+)?\.sdk$", d):
sdk_found = os.path.join(dirpath, d)
break
if sdk_found:
break
if not sdk_found:
log("ERROR: MacOSX*.sdk directory not found. Extracted contents:")
for dp, ds, fs in os.walk(sdk_root):
depth = dp.replace(sdk_root, "").count(os.sep)
if depth < 4:
log(f" {' ' * depth}{os.path.basename(dp)}/")
sys.exit(1)
sdk_name = os.path.basename(sdk_found)
log(f"==> Found: {sdk_name}")
# 7 -- Package as .tar.gz
tarball = os.path.join(output_dir, f"{sdk_name}.tar.gz")
log(f"==> Packaging: {sdk_name}.tar.gz ...")
subprocess.run(
["tar", "-czf", tarball, "-C", os.path.dirname(sdk_found), sdk_name],
check=True,
)
log(f"==> macOS SDK ready: {tarball}")
print(tarball)
finally:
shutil.rmtree(work, ignore_errors=True)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,10 @@
set(VCPKG_TARGET_ARCHITECTURE arm64)
set(VCPKG_CRT_LINKAGE dynamic)
set(VCPKG_LIBRARY_LINKAGE static)
set(VCPKG_CMAKE_SYSTEM_NAME Darwin)
set(VCPKG_OSX_ARCHITECTURES arm64)
set(VCPKG_OSX_DEPLOYMENT_TARGET 13.0)
# osxcross toolchain
set(VCPKG_CHAINLOAD_TOOLCHAIN_FILE /opt/osxcross-toolchain.cmake)

View file

@ -0,0 +1,10 @@
set(VCPKG_TARGET_ARCHITECTURE x64)
set(VCPKG_CRT_LINKAGE dynamic)
set(VCPKG_LIBRARY_LINKAGE static)
set(VCPKG_CMAKE_SYSTEM_NAME Darwin)
set(VCPKG_OSX_ARCHITECTURES x86_64)
set(VCPKG_OSX_DEPLOYMENT_TARGET 13.0)
# osxcross toolchain
set(VCPKG_CHAINLOAD_TOOLCHAIN_FILE /opt/osxcross-toolchain.cmake)

64
container/run-linux.ps1 Normal file
View file

@ -0,0 +1,64 @@
# run-linux.ps1 — Build WoWee for Linux (amd64) inside a Docker container.
#
# Usage (run from project root):
# .\container\run-linux.ps1 [-RebuildImage]
#
# Environment variables:
# WOWEE_FFX_SDK_REPO — FidelityFX SDK git repo URL (passed through to container)
# WOWEE_FFX_SDK_REF — FidelityFX SDK git ref / tag (passed through to container)
param(
[switch]$RebuildImage
)
$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$ProjectRoot = (Resolve-Path "$ScriptDir\..").Path
$ImageName = "wowee-builder-linux"
$BuildOutput = "$ProjectRoot\build\linux"
# Verify Docker is available
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
Write-Error "docker is not installed or not in PATH."
exit 1
}
# Build the image (skip if already present and -RebuildImage not given)
$imageExists = docker image inspect $ImageName 2>$null
if ($RebuildImage -or -not $imageExists) {
Write-Host "==> Building Docker image: $ImageName"
docker build `
-f "$ScriptDir\builder-linux.Dockerfile" `
-t $ImageName `
"$ScriptDir"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
} else {
Write-Host "==> Using existing Docker image: $ImageName"
}
# Create output directory on the host
New-Item -ItemType Directory -Force -Path $BuildOutput | Out-Null
Write-Host "==> Starting Linux build (output: $BuildOutput)"
$dockerArgs = @(
"run", "--rm",
"--mount", "type=bind,src=$ProjectRoot,dst=/src,readonly",
"--mount", "type=bind,src=$BuildOutput,dst=/out"
)
if ($env:WOWEE_FFX_SDK_REPO) {
$dockerArgs += @("--env", "WOWEE_FFX_SDK_REPO=$env:WOWEE_FFX_SDK_REPO")
}
if ($env:WOWEE_FFX_SDK_REF) {
$dockerArgs += @("--env", "WOWEE_FFX_SDK_REF=$env:WOWEE_FFX_SDK_REF")
}
$dockerArgs += $ImageName
& docker @dockerArgs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host "==> Linux build complete. Artifacts in: $BuildOutput"

58
container/run-linux.sh Executable file
View file

@ -0,0 +1,58 @@
#!/usr/bin/env bash
# run-linux.sh — Build WoWee for Linux (amd64) inside a Docker container.
#
# Usage (run from project root):
# ./container/run-linux.sh [--rebuild-image]
#
# Environment variables:
# WOWEE_FFX_SDK_REPO — FidelityFX SDK git repo URL (passed through to container)
# WOWEE_FFX_SDK_REF — FidelityFX SDK git ref / tag (passed through to container)
# REBUILD_IMAGE — Set to 1 to force a fresh docker build (same as --rebuild-image)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
IMAGE_NAME="wowee-builder-linux"
BUILD_OUTPUT="${PROJECT_ROOT}/build/linux"
# Parse arguments
REBUILD_IMAGE="${REBUILD_IMAGE:-0}"
for arg in "$@"; do
case "$arg" in
--rebuild-image) REBUILD_IMAGE=1 ;;
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
esac
done
# Verify Docker is available
if ! command -v docker &>/dev/null; then
echo "Error: docker is not installed or not in PATH." >&2
exit 1
fi
# Build the image (skip if already present and --rebuild-image not given)
if [[ "$REBUILD_IMAGE" == "1" ]] || ! docker image inspect "$IMAGE_NAME" &>/dev/null; then
echo "==> Building Docker image: ${IMAGE_NAME}"
docker build \
-f "${SCRIPT_DIR}/builder-linux.Dockerfile" \
-t "$IMAGE_NAME" \
"${SCRIPT_DIR}"
else
echo "==> Using existing Docker image: ${IMAGE_NAME}"
fi
# Create output directory on the host
mkdir -p "$BUILD_OUTPUT"
echo "==> Starting Linux build (output: ${BUILD_OUTPUT})"
docker run --rm \
--mount "type=bind,src=${PROJECT_ROOT},dst=/src,readonly" \
--mount "type=bind,src=${BUILD_OUTPUT},dst=/out" \
${WOWEE_FFX_SDK_REPO:+--env "WOWEE_FFX_SDK_REPO=${WOWEE_FFX_SDK_REPO}"} \
${WOWEE_FFX_SDK_REF:+--env "WOWEE_FFX_SDK_REF=${WOWEE_FFX_SDK_REF}"} \
"$IMAGE_NAME"
echo "==> Linux build complete. Artifacts in: ${BUILD_OUTPUT}"

71
container/run-macos.ps1 Normal file
View file

@ -0,0 +1,71 @@
# run-macos.ps1 — Cross-compile WoWee for macOS (arm64 or x86_64) inside a Docker container.
#
# Usage (run from project root):
# .\container\run-macos.ps1 [-RebuildImage] [-Arch arm64|x86_64]
#
# The macOS SDK is fetched automatically inside the Docker build from Apple's
# public software update catalog. No manual SDK download required.
#
# Environment variables:
# WOWEE_FFX_SDK_REPO — FidelityFX SDK git repo URL (passed through to container)
# WOWEE_FFX_SDK_REF — FidelityFX SDK git ref / tag (passed through to container)
param(
[switch]$RebuildImage,
[ValidateSet("arm64", "x86_64")]
[string]$Arch = "arm64"
)
$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$ProjectRoot = (Resolve-Path "$ScriptDir\..").Path
$ImageName = "wowee-builder-macos"
$BuildOutput = "$ProjectRoot\build\macos"
# Verify Docker is available
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
Write-Error "docker is not installed or not in PATH."
exit 1
}
# Build the image (skip if already present and -RebuildImage not given)
$imageExists = docker image inspect $ImageName 2>$null
if ($RebuildImage -or -not $imageExists) {
Write-Host "==> Building Docker image: $ImageName"
Write-Host " (SDK will be fetched automatically from Apple's catalog)"
docker build `
-f "$ScriptDir\builder-macos.Dockerfile" `
-t $ImageName `
"$ScriptDir"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
} else {
Write-Host "==> Using existing Docker image: $ImageName"
}
# Create output directory on the host
New-Item -ItemType Directory -Force -Path $BuildOutput | Out-Null
Write-Host "==> Starting macOS cross-compile build (arch=$Arch, output: $BuildOutput)"
$dockerArgs = @(
"run", "--rm",
"--mount", "type=bind,src=$ProjectRoot,dst=/src,readonly",
"--mount", "type=bind,src=$BuildOutput,dst=/out",
"--env", "MACOS_ARCH=$Arch"
)
if ($env:WOWEE_FFX_SDK_REPO) {
$dockerArgs += @("--env", "WOWEE_FFX_SDK_REPO=$env:WOWEE_FFX_SDK_REPO")
}
if ($env:WOWEE_FFX_SDK_REF) {
$dockerArgs += @("--env", "WOWEE_FFX_SDK_REF=$env:WOWEE_FFX_SDK_REF")
}
$dockerArgs += $ImageName
& docker @dockerArgs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host "==> macOS cross-compile build complete. Artifacts in: $BuildOutput"

74
container/run-macos.sh Executable file
View file

@ -0,0 +1,74 @@
#!/usr/bin/env bash
# run-macos.sh — Cross-compile WoWee for macOS (arm64 or x86_64) inside a Docker container.
#
# Usage (run from project root):
# ./container/run-macos.sh [--rebuild-image]
#
# The macOS SDK is fetched automatically inside the Docker build from Apple's
# public software update catalog. No manual SDK download required.
#
# Environment variables:
# MACOS_ARCH — Target arch: arm64 (default) or x86_64
# WOWEE_FFX_SDK_REPO — FidelityFX SDK git repo URL (passed through to container)
# WOWEE_FFX_SDK_REF — FidelityFX SDK git ref / tag (passed through to container)
# REBUILD_IMAGE — Set to 1 to force a fresh docker build (same as --rebuild-image)
#
# Toolchain: osxcross (Clang + Apple ld)
# vcpkg triplets: arm64-osx-cross (arm64) / x64-osx-cross (x86_64)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
IMAGE_NAME="wowee-builder-macos"
MACOS_ARCH="${MACOS_ARCH:-arm64}"
BUILD_OUTPUT="${PROJECT_ROOT}/build/macos"
# Parse arguments
REBUILD_IMAGE="${REBUILD_IMAGE:-0}"
for arg in "$@"; do
case "$arg" in
--rebuild-image) REBUILD_IMAGE=1 ;;
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
esac
done
# Validate arch
if [[ "$MACOS_ARCH" != "arm64" && "$MACOS_ARCH" != "x86_64" ]]; then
echo "Error: MACOS_ARCH must be 'arm64' or 'x86_64' (got: ${MACOS_ARCH})" >&2
exit 1
fi
# Verify Docker is available
if ! command -v docker &>/dev/null; then
echo "Error: docker is not installed or not in PATH." >&2
exit 1
fi
# Build the image (skip if already present and --rebuild-image not given)
if [[ "$REBUILD_IMAGE" == "1" ]] || ! docker image inspect "$IMAGE_NAME" &>/dev/null; then
echo "==> Building Docker image: ${IMAGE_NAME}"
echo " (SDK will be fetched automatically from Apple's catalog)"
docker build \
-f "${SCRIPT_DIR}/builder-macos.Dockerfile" \
-t "$IMAGE_NAME" \
"${SCRIPT_DIR}"
else
echo "==> Using existing Docker image: ${IMAGE_NAME}"
fi
# Create output directory on the host
mkdir -p "$BUILD_OUTPUT"
echo "==> Starting macOS cross-compile build (arch=${MACOS_ARCH}, output: ${BUILD_OUTPUT})"
docker run --rm \
--mount "type=bind,src=${PROJECT_ROOT},dst=/src,readonly" \
--mount "type=bind,src=${BUILD_OUTPUT},dst=/out" \
--env "MACOS_ARCH=${MACOS_ARCH}" \
${WOWEE_FFX_SDK_REPO:+--env "WOWEE_FFX_SDK_REPO=${WOWEE_FFX_SDK_REPO}"} \
${WOWEE_FFX_SDK_REF:+--env "WOWEE_FFX_SDK_REF=${WOWEE_FFX_SDK_REF}"} \
"$IMAGE_NAME"
echo "==> macOS cross-compile build complete. Artifacts in: ${BUILD_OUTPUT}"

64
container/run-windows.ps1 Normal file
View file

@ -0,0 +1,64 @@
# run-windows.ps1 — Cross-compile WoWee for Windows (x86_64) inside a Docker container.
#
# Usage (run from project root):
# .\container\run-windows.ps1 [-RebuildImage]
#
# Environment variables:
# WOWEE_FFX_SDK_REPO — FidelityFX SDK git repo URL (passed through to container)
# WOWEE_FFX_SDK_REF — FidelityFX SDK git ref / tag (passed through to container)
param(
[switch]$RebuildImage
)
$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$ProjectRoot = (Resolve-Path "$ScriptDir\..").Path
$ImageName = "wowee-builder-windows"
$BuildOutput = "$ProjectRoot\build\windows"
# Verify Docker is available
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
Write-Error "docker is not installed or not in PATH."
exit 1
}
# Build the image (skip if already present and -RebuildImage not given)
$imageExists = docker image inspect $ImageName 2>$null
if ($RebuildImage -or -not $imageExists) {
Write-Host "==> Building Docker image: $ImageName"
docker build `
-f "$ScriptDir\builder-windows.Dockerfile" `
-t $ImageName `
"$ScriptDir"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
} else {
Write-Host "==> Using existing Docker image: $ImageName"
}
# Create output directory on the host
New-Item -ItemType Directory -Force -Path $BuildOutput | Out-Null
Write-Host "==> Starting Windows cross-compile build (output: $BuildOutput)"
$dockerArgs = @(
"run", "--rm",
"--mount", "type=bind,src=$ProjectRoot,dst=/src,readonly",
"--mount", "type=bind,src=$BuildOutput,dst=/out"
)
if ($env:WOWEE_FFX_SDK_REPO) {
$dockerArgs += @("--env", "WOWEE_FFX_SDK_REPO=$env:WOWEE_FFX_SDK_REPO")
}
if ($env:WOWEE_FFX_SDK_REF) {
$dockerArgs += @("--env", "WOWEE_FFX_SDK_REF=$env:WOWEE_FFX_SDK_REF")
}
$dockerArgs += $ImageName
& docker @dockerArgs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host "==> Windows cross-compile build complete. Artifacts in: $BuildOutput"

61
container/run-windows.sh Executable file
View file

@ -0,0 +1,61 @@
#!/usr/bin/env bash
# run-windows.sh — Cross-compile WoWee for Windows (x86_64) inside a Docker container.
#
# Usage (run from project root):
# ./container/run-windows.sh [--rebuild-image]
#
# Environment variables:
# WOWEE_FFX_SDK_REPO — FidelityFX SDK git repo URL (passed through to container)
# WOWEE_FFX_SDK_REF — FidelityFX SDK git ref / tag (passed through to container)
# REBUILD_IMAGE — Set to 1 to force a fresh docker build (same as --rebuild-image)
#
# Toolchain: LLVM-MinGW (Clang + LLD) targeting x86_64-w64-mingw32-ucrt
# vcpkg triplet: x64-mingw-static
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
IMAGE_NAME="wowee-builder-windows"
BUILD_OUTPUT="${PROJECT_ROOT}/build/windows"
# Parse arguments
REBUILD_IMAGE="${REBUILD_IMAGE:-0}"
for arg in "$@"; do
case "$arg" in
--rebuild-image) REBUILD_IMAGE=1 ;;
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
esac
done
# Verify Docker is available
if ! command -v docker &>/dev/null; then
echo "Error: docker is not installed or not in PATH." >&2
exit 1
fi
# Build the image (skip if already present and --rebuild-image not given)
if [[ "$REBUILD_IMAGE" == "1" ]] || ! docker image inspect "$IMAGE_NAME" &>/dev/null; then
echo "==> Building Docker image: ${IMAGE_NAME}"
docker build \
-f "${SCRIPT_DIR}/builder-windows.Dockerfile" \
-t "$IMAGE_NAME" \
"${SCRIPT_DIR}"
else
echo "==> Using existing Docker image: ${IMAGE_NAME}"
fi
# Create output directory on the host
mkdir -p "$BUILD_OUTPUT"
echo "==> Starting Windows cross-compile build (output: ${BUILD_OUTPUT})"
docker run --rm \
--mount "type=bind,src=${PROJECT_ROOT},dst=/src,readonly" \
--mount "type=bind,src=${BUILD_OUTPUT},dst=/out" \
${WOWEE_FFX_SDK_REPO:+--env "WOWEE_FFX_SDK_REPO=${WOWEE_FFX_SDK_REPO}"} \
${WOWEE_FFX_SDK_REF:+--env "WOWEE_FFX_SDK_REF=${WOWEE_FFX_SDK_REF}"} \
"$IMAGE_NAME"
echo "==> Windows cross-compile build complete. Artifacts in: ${BUILD_OUTPUT}"

110
docs/ANIMATION_SYSTEM.md Normal file
View file

@ -0,0 +1,110 @@
# Animation System
Unified, FSM-based animation system for all characters (players, NPCs, companions).
Every character uses the same `CharacterAnimator` — there is no separate NPC/Mob animator.
## Architecture
```
AnimationController (thin adapter — bridges Renderer ↔ CharacterAnimator)
└─ CharacterAnimator (FSM composer — implements ICharacterAnimator)
├─ CombatFSM (stun, hit reaction, spell cast, melee, ranged, charge)
├─ ActivityFSM (emote, loot, sit/stand/kneel/sleep)
├─ LocomotionFSM (idle, walk, run, sprint, jump, swim, strafe)
└─ MountFSM (mount idle, mount run, flight)
AnimationManager (registry of CharacterAnimator instances by ID)
AnimCapabilitySet (probed once per model — cached resolved anim IDs)
AnimCapabilityProbe (queries which animations a model supports)
```
### Priority Resolution
`CharacterAnimator::resolveAnimation()` runs every frame. The first FSM to
return a valid `AnimOutput` wins:
1. **Mount** — if mounted, return `MOUNT` (overrides everything)
2. **Combat** — stun > hit reaction > spell > charge > melee/ranged > combat idle
3. **Activity** — emote > loot > sit/stand transitions
4. **Locomotion** — run/walk/sprint/jump/swim/strafe/idle
If no FSM produces a valid output, the last animation continues (STAY policy).
### Overlay Layer
After resolution, `applyOverlays()` substitutes stealth animation variants
(stealth idle, stealth walk, stealth run) without changing sub-FSM state.
## File Map
### Headers (`include/rendering/animation/`)
| File | Purpose |
|---|---|
| `i_animator.hpp` | Base interface: `onEvent()`, `update()` |
| `i_character_animator.hpp` | 20 virtual methods (combat, spells, emotes, mounts, etc.) |
| `character_animator.hpp` | FSM composer — the single animator class |
| `locomotion_fsm.hpp` | Movement states: idle, walk, run, sprint, jump, swim |
| `combat_fsm.hpp` | Combat states: melee, ranged, spell cast, stun, hit reaction |
| `activity_fsm.hpp` | Activity states: emote, loot, sit/stand/kneel |
| `mount_fsm.hpp` | Mount states: idle, run, flight, taxi |
| `anim_capability_set.hpp` | Probed capability flags + resolved animation IDs |
| `anim_capability_probe.hpp` | Probes a model for available animations |
| `anim_event.hpp` | `AnimEvent` enum (MOVE_START, MOVE_STOP, JUMP, etc.) |
| `animation_manager.hpp` | Central registry of CharacterAnimator instances |
| `weapon_type.hpp` | WeaponLoadout, RangedWeaponType enums |
| `emote_registry.hpp` | Emote name → animation ID lookup |
| `footstep_driver.hpp` | Footstep sound event driver |
| `sfx_state_driver.hpp` | State-transition SFX (jump, land, swim enter/exit) |
| `i_anim_renderer.hpp` | Interface for renderer animation queries |
### Sources (`src/rendering/animation/`)
| File | Purpose |
|---|---|
| `character_animator.cpp` | ICharacterAnimator implementation + priority resolver |
| `locomotion_fsm.cpp` | Locomotion state transitions + resolve logic |
| `combat_fsm.cpp` | Combat state transitions + resolve logic |
| `activity_fsm.cpp` | Activity state transitions + resolve logic |
| `mount_fsm.cpp` | Mount state transitions + resolve logic |
| `anim_capability_probe.cpp` | Model animation probing |
| `animation_manager.cpp` | Registry CRUD + bulk update |
| `emote_registry.cpp` | Emote database |
| `footstep_driver.cpp` | Footstep timing logic |
| `sfx_state_driver.cpp` | SFX transition detection |
### Controller (`include/rendering/animation_controller.hpp` + `src/rendering/animation_controller.cpp`)
Thin adapter that:
- Collects per-frame input from camera/renderer → `CharacterAnimator::FrameInput`
- Forwards state changes (combat, emote, spell, mount, etc.) → `CharacterAnimator`
- Reads `AnimOutput` → applies via `CharacterRenderer`
- Owns footstep and SFX drivers
## Key Types
- **`AnimEvent`** — discrete events: `MOVE_START`, `MOVE_STOP`, `JUMP`, `LAND`, `MOUNT`, `DISMOUNT`, etc.
- **`AnimOutput`** — result of FSM resolution: `{animId, loop, valid}`. `valid=false` means STAY.
- **`AnimCapabilitySet`** — probed once per model load. Caches resolved IDs and capability flags.
- **`CharacterAnimator::FrameInput`** — per-frame input struct (movement flags, timers, animation state queries).
## Adding a New Animation State
1. Decide which FSM owns the state (combat, activity, locomotion, or mount).
2. Add the state enum to the FSM's `State` enum.
3. Add transitions in the FSM's `resolve()` method.
4. Add resolved ID fields to `AnimCapabilitySet` if the animation needs model probing.
5. If the state needs external triggering, add a method to `ICharacterAnimator` and implement in `CharacterAnimator`.
## Tests
Each FSM has its own test file in `tests/`:
- `test_locomotion_fsm.cpp`
- `test_combat_fsm.cpp`
- `test_activity_fsm.cpp`
- `test_anim_capability.cpp`
Run all tests:
```bash
cd build && ctest --output-on-failure
```

View file

@ -93,13 +93,16 @@ The RSA public modulus is extracted from WoW.exe (`.rdata` section at offset 0x0
## Key Files
```
include/game/warden_handler.hpp - Packet handler interface
src/game/warden_handler.cpp - handleWardenData + module manager init
include/game/warden_module.hpp - Module loader interface
src/game/warden_module.cpp - 8-step pipeline
include/game/warden_emulator.hpp - Emulator interface
src/game/warden_emulator.cpp - Unicorn Engine executor + API hooks
include/game/warden_crypto.hpp - Crypto interface
src/game/warden_crypto.cpp - RC4 / key derivation
src/game/game_handler.cpp - Packet handler (handleWardenData)
include/game/warden_memory.hpp - PE image + memory patch interface
src/game/warden_memory.cpp - PE loader, runtime globals patching
```
---

View file

@ -58,10 +58,11 @@ strict Warden enforcement in that mode.
## Key Files
```
src/game/warden_module.hpp/cpp - Module loader (8-step pipeline)
src/game/warden_emulator.hpp/cpp - Unicorn Engine executor
src/game/warden_crypto.hpp/cpp - RC4/MD5/SHA1/RSA crypto
src/game/game_handler.cpp - Packet handler (handleWardenData)
include/game/warden_handler.hpp + src/game/warden_handler.cpp - Packet handler
include/game/warden_module.hpp + src/game/warden_module.cpp - Module loader (8-step pipeline)
include/game/warden_emulator.hpp + src/game/warden_emulator.cpp - Unicorn Engine executor
include/game/warden_crypto.hpp + src/game/warden_crypto.cpp - RC4/MD5/SHA1/RSA crypto
include/game/warden_memory.hpp + src/game/warden_memory.cpp - PE image + memory patching
```
---

View file

@ -8,7 +8,7 @@ Wowee follows a modular architecture with clear separation of concerns:
┌─────────────────────────────────────────────┐
│ Application (main loop) │
│ - State management (auth/realms/game) │
│ - Update cycle (60 FPS)
│ - Update cycle
│ - Event dispatch │
└──────────────┬──────────────────────────────┘
@ -16,8 +16,8 @@ Wowee follows a modular architecture with clear separation of concerns:
│ │
┌──────▼──────┐ ┌─────▼──────┐
│ Window │ │ Input │
│ (SDL2) │ │ (Keyboard/ │
│ │ Mouse) │
│ (SDL2 + │ │ (Keyboard/ │
Vulkan) │ │ Mouse) │
└──────┬──────┘ └─────┬──────┘
│ │
└───────┬────────┘
@ -26,517 +26,341 @@ Wowee follows a modular architecture with clear separation of concerns:
│ │
┌───▼────────┐ ┌───────▼──────┐
│ Renderer │ │ UI Manager │
(OpenGL) │ │ (ImGui) │
(Vulkan) │ │ (ImGui) │
└───┬────────┘ └──────────────┘
├─ Camera
├─ Scene Graph
├─ Shaders
├─ Meshes
└─ Textures
├─ Camera + CameraController
├─ TerrainRenderer (ADT streaming)
├─ WMORenderer (buildings, collision)
├─ M2Renderer (models, particles, ribbons)
├─ CharacterRenderer (skeletal animation)
├─ WaterRenderer (refraction, lava, slime)
├─ SkyBox + StarField + Weather
├─ LightingManager (Light.dbc volumes)
└─ SwimEffects, ChargeEffect, Lightning
```
## Core Systems
### 1. Application Layer (`src/core/`)
**Application** - Main controller
- Owns all subsystems
- Manages application state
**Application** (`application.hpp/cpp`) - Main controller
- Owns all subsystems (renderer, game handler, asset manager, UI)
- Manages application state (AUTH → REALM_SELECT → CHAR_SELECT → IN_WORLD)
- Runs update/render loop
- Handles lifecycle (init/shutdown)
- Populates `GameServices` struct and passes to `GameHandler` at construction
**Window** - SDL2 wrapper
- Creates window and OpenGL context
**Window** (`window.hpp/cpp`) - SDL2 + Vulkan wrapper
- Creates SDL2 window with Vulkan surface
- Owns `VkContext` (Vulkan device, swapchain, render passes)
- Handles resize events
- Manages VSync and fullscreen
**Input** - Input management
- Keyboard state tracking
- Mouse position and buttons
- Mouse locking for camera control
**Input** (`input.hpp/cpp`) - Input management
- Keyboard state tracking (SDL scancodes)
- Mouse position, buttons (1-based SDL indices), wheel delta
- Per-frame delta calculation
**Logger** - Logging system
- Thread-safe logging
**Logger** (`logger.hpp/cpp`) - Thread-safe logging
- Multiple log levels (DEBUG, INFO, WARNING, ERROR, FATAL)
- Timestamp formatting
- File output to `logs/wowee.log`
- Configurable via `WOWEE_LOG_LEVEL` env var
### 2. Rendering System (`src/rendering/`)
**Renderer** - Main rendering coordinator
- Manages OpenGL state
- Coordinates frame rendering
- Owns camera and scene
**Renderer** (`renderer.hpp/cpp`) - Main rendering coordinator
- Manages Vulkan pipeline state
- Coordinates frame rendering across all sub-renderers
- Owns camera, sky, weather, lighting, and all sub-renderers
- Shadow mapping with PCF filtering
**Camera** - View/projection matrices
**VkContext** (`vk_context.hpp/cpp`) - Vulkan infrastructure
- Device selection, queue families, swapchain
- Render passes, framebuffers, command pools
- Sampler cache (FNV-1a hashed dedup)
- Pipeline cache persistence for fast startup
**Camera** (`camera.hpp/cpp`) - View/projection matrices
- Position and orientation
- FOV and aspect ratio
- View frustum (for culling)
- FOV, aspect ratio, near/far planes
- Sub-pixel jitter for TAA/FSR2 (column 2 NDC offset)
- Frustum extraction for culling
**Scene** - Scene graph
- Mesh collection
- Spatial organization
- Visibility determination
**TerrainRenderer** - ADT terrain streaming
- Async chunk loading within configurable radius
- 4-layer texture splatting with alpha blending
- Frustum + distance culling
- Vegetation/foliage placement via deterministic RNG
**Shader** - GLSL program wrapper
- Loads vertex/fragment shaders
- Uniform management
- Compilation and linking
**WMORenderer** - World Map Objects (buildings)
- Multi-material batch rendering
- Portal-based visibility culling
- Floor/wall collision (normal-based classification)
- Interior glass transparency, doodad placement
**Mesh** - Geometry container
- Vertex buffer (position, normal, texcoord)
- Index buffer
- VAO/VBO/EBO management
**M2Renderer** - Models (creatures, doodads, spell effects)
- Skeletal animation with GPU bone transforms
- Particle emitters (WotLK FBlock format)
- Ribbon emitters (charge trails, enchant glows)
- Portal spin effects, foliage wind displacement
- Per-instance animation state
**Texture** - Texture management
- Loading (BLP via `AssetManager`, optional PNG overrides for development)
- OpenGL texture object
- Mipmap generation
**CharacterRenderer** - Player/NPC character models
- GPU vertex skinning (256 bones)
- Race/gender-aware textures via CharSections.dbc
- Equipment rendering (geoset visibility per slot)
- Fallback textures (white/transparent/flat-normal) for missing assets
**Material** - Surface properties
- Shader assignment
- Texture binding
- Color/properties
**WaterRenderer** - Terrain and WMO water
- Refraction/reflection rendering
- Magma/slime with multi-octave FBM noise flow
- Beer-Lambert absorption
**Skybox + StarField + Weather**
- Procedural sky dome with time-of-day lighting
- Star field with day/night fade (dusk 18:0020:00, dawn 04:0006:00)
- Rain/snow particle systems per zone (via zone weather table)
**LightingManager** - Light.dbc volume sampling
- Time-of-day color bands (half-minutes, 02879)
- Distance-weighted light volume blending
- Fog color/distance parameters
**World Map System** (`src/rendering/world_map/`) - Modular map architecture:
- `WorldMapFacade` - Public API (PIMPL pattern), composes all components
- `CompositeRenderer` - Vulkan tile pipeline + off-screen FBO compositing (1024×768 FBO, 1002×668 visible)
- `DataRepository` - DBC zone loading, ZMP pixel map, POI/overlay storage
- `CoordinateProjection` - UV projection, zone/continent spatial lookups
- `ExplorationState` - Server exploration mask + local fog-of-war tracking
- `ViewStateMachine` - COSMIC → WORLD → CONTINENT → ZONE navigation with transitions
- `InputHandler` - Keyboard/mouse input → `InputAction` mapping
- `OverlayRenderer` - Layer-based ImGui overlay system (Open/Closed Principle)
- `MapResolver` - Cross-map navigation (Outland, Northrend detection)
- `ZoneMetadata` - Zone level ranges and faction data for labels
- 9 overlay layers (each implements `IOverlayLayer`): player marker, party dot, taxi node, POI marker, quest POI, corpse marker, zone highlight, coordinate display, subzone tooltip
### 3. Networking (`src/network/`)
**Socket** (Abstract base class)
- Connection interface
- Packet send/receive
- Callback system
**TCPSocket** (`tcp_socket.hpp/cpp`) - Platform TCP
- Non-blocking I/O with per-frame recv budgets
- 4 KB recv buffer per call
- Portable across Linux/macOS/Windows
**TCPSocket** - Linux TCP sockets
- Non-blocking I/O
- Raw TCP (replaces WebSocket)
- Packet framing
**WorldSocket** (`world_socket.hpp/cpp`) - WoW world connection
- RC4 header encryption (derived from SRP session key)
- Packet parsing with configurable per-frame budgets
- Compressed move packet handling
**Packet** - Binary data container
- Read/write primitives
- Byte order handling
- Opcode management
**Packet** (`packet.hpp/cpp`) - Binary data container
- Read/write primitives (uint8uint64, float, string, packed GUID)
- Bounds-checked reads (return 0 past end)
### 4. Authentication (`src/auth/`)
**AuthHandler** - Auth server protocol
- Connects to port 3724
- SRP authentication flow
- Session key generation
**AuthHandler** - Auth server protocol (port 3724)
- SRP6a challenge/proof flow
- Security flags: PIN (0x01), Matrix (0x02), Authenticator (0x04)
- Realm list retrieval
**SRP** - Secure Remote Password
- SRP6a algorithm
- Big integer math
- Salt and verifier generation
**SRP** (`srp.hpp/cpp`) - Secure Remote Password
- SRP6a with 19-byte (152-bit) ephemeral
- OpenSSL BIGNUM math
- Session key generation (40 bytes)
**Crypto** - Cryptographic functions
- SHA1 hashing (OpenSSL)
- Random number generation
- Encryption helpers
**Integrity** - Client integrity verification
- Checksum computation for Warden compatibility
### 5. Game Logic (`src/game/`)
**GameHandler** - World server protocol
- Connects to port 8085 (configurable)
- Packet handlers for 100+ opcodes
- Session management with RC4 encryption
- Character enumeration and login flow
**GameHandler** (`game_handler.hpp/cpp`) - Central game state
- Dispatch table routing 664+ opcodes to domain handlers
- Owns all domain handlers via composition
- Receives dependencies via `GameServices` struct (no singleton access)
**World** - Game world state
- Map loading with async terrain streaming
- Entity management (players, NPCs, creatures)
- Zone management and exploration
- Time-of-day synchronization
**Domain Handlers** (SOLID decomposition from GameHandler):
- `EntityController` - UPDATE_OBJECT parsing, entity spawn/despawn
- `MovementHandler` - Movement packets, speed, taxi, swimming, flying
- `CombatHandler` - Damage, healing, death, auto-attack, threat
- `SpellHandler` - Spell casting, cooldowns, auras, talents, pet spells
- `InventoryHandler` - Equipment, bags, bank, mail, auction, vendors
- `QuestHandler` - Quest accept/complete, objectives, progress tracking
- `SocialHandler` - Party, guild, LFG, friends, who, duel, trade
- `ChatHandler` - Chat messages, channels, emotes, system messages
- `WardenHandler` - Anti-cheat module management
**Player** - Player character
- Position and movement (WASD + spline movement)
- Stats tracking (health, mana, XP, level)
- Equipment and inventory (23 + 16 slots)
- Action queue and spell casting
- Death and resurrection handling
**OpcodeTable** - Expansion-agnostic opcode mapping
- `LogicalOpcode` enum → wire opcode via JSON config per expansion
- Runtime remapping for Classic/TBC/WotLK/Turtle protocol differences
**Character** - Character data
- Race, class, gender, appearance
- Creation and customization
- 3D model preview
- Online character lifecycle and state synchronization
**Entity / EntityManager** - Entity lifecycle
- Shared entity base class with update fields (uint32 array)
- Player, Unit, GameObject subtypes
- GUID-based lookup, field extraction (health, level, display ID, etc.)
**Entity** - Game entities
- NPCs and creatures with display info
- Animation state (idle, combat, walk, run)
- GUID management (player, creature, item, gameobject)
- Targeting and selection
**TransportManager** - Transport lifecycle and server sync
- Delegates path data to `TransportPathRepository`
- Delegates spline math to `math::CatmullRomSpline`
- Clock-based motion with `TransportClockSync`
- Reduced from ~1,200 to ~500 lines after decomposition
**Inventory** - Item management
- Equipment slots (head, shoulders, chest, etc.)
- Backpack storage (16 slots)
- Item metadata (icons, stats, durability)
- Drag-drop system
- Auto-equip and unequip
**TransportPathRepository** - Transport path data
- DBC loading (TransportAnimation.dbc, TaxiPathNode.dbc)
- Path inference heuristics for server spawn→DBC mapping
- Z-only elevator detection vs XY transport paths
**NPC Interactions** - handled through `GameHandler`
- Gossip system
- Quest givers with markers (! and ?)
- Vendors (buy/sell)
- Trainers (placeholder)
- Combat animations
**math::CatmullRomSpline** (`src/math/`) - Reusable spline module
- Catmull-Rom interpolation with O(log n) binary search segment lookup
- Fused position+tangent evaluation (single call per frame per transport)
- Time-closed (looping) and clamped (non-looping) path modes
- `orientationFromTangent()` for smooth transport/entity facing
**ZoneManager** - Zone and area tracking
- Map exploration
- Area discovery
- Zone change detection
**SplineBlockData** (`src/game/spline_packet.hpp/cpp`) - Unified spline parsing
- Consolidates 7 duplicated spline parsers into shared functions
- `parseMonsterMoveSplineBody()` (WotLK/TBC), `parseMonsterMoveSplineBodyVanilla()`
- `parseWotlkMoveUpdateSpline()`, `parseClassicMoveUpdateSpline()`
- Packed delta decoding (11+11+10-bit signed, ×0.25 scale)
**Opcodes** - Protocol definitions
- 100+ Client→Server opcodes (CMSG_*)
- 100+ Server→Client opcodes (SMSG_*)
- WoW 3.3.5a (build 12340) specific
**Expansion Helpers** (`game_utils.hpp`):
- `isActiveExpansion("classic")` / `isActiveExpansion("tbc")` / `isActiveExpansion("wotlk")`
- `isClassicLikeExpansion()` (Classic or Turtle WoW)
- `isPreWotlk()` (Classic, Turtle, or TBC)
### 6. Asset Pipeline (`src/pipeline/`)
**AssetManager** - Runtime asset access
- Loads an extracted loose-file tree indexed by `Data/manifest.json`
- Extracted loose-file tree indexed by `Data/manifest.json`
- Layered resolution via optional overlay manifests (multi-expansion dedup)
- File cache + path normalization
- File cache with configurable budget (256 MB min, 12 GB max)
- PNG override support (checks for .png before .blp)
**asset_extract (tool)** - MPQ extraction
- Uses StormLib to extract MPQs into `Data/` and generate `manifest.json`
- Driven by `extract_assets.sh`
- Driven by `extract_assets.sh` / `extract_assets.ps1`
**BLPLoader** - Texture parser
- BLP format (Blizzard texture format)
- DXT1/3/5 compression support
- Mipmap extraction and generation
- OpenGL texture object creation
**BLPLoader** - Texture decompression
- DXT1/3/5 block compression (RGB565 color endpoints)
- Palette mode with 1/4/8-bit alpha
- Mipmap extraction
**M2Loader** - Model parser
- Character/creature models with materials
- Skeletal animation data (256 bones max)
- Bone hierarchies and transforms
- Animation sequences (idle, walk, run, attack, etc.)
- Particle emitters (WotLK FBlock format)
- Attachment points (weapons, mounts, etc.)
- Geoset support (hide/show body parts)
- Multiple texture units and render batches
**M2Loader** - Model binary parsing
- Version-aware header (Classic v256 vs WotLK v264)
- Skeletal animation tracks (embedded vs external .anim files, flag 0x20)
- Compressed quaternions (int16 offset mapping)
- Particle emitters, ribbon emitters, attachment points
- Geoset support (group × 100 + variant encoding)
**WMOLoader** - World object parser
- Buildings and structures
- Multi-material batches
- Portal system (visibility culling)
- Doodad placement (decorations)
- Group-based rendering
- Liquid data (indoor water)
**WMOLoader** - World object parsing
- Multi-group rendering with portal visibility
- Doodad placement (24-bit name index + 8-bit flags packing)
- Liquid data, collision geometry
**ADTLoader** - Terrain parser
- 64x64 tiles per map (map_XX_YY.adt)
- 16x16 chunks per tile (MCNK)
- Height map data (9x9 outer + 8x8 inner vertices)
- Texture layers (up to 4 per chunk with alpha blending)
- Liquid data (water/lava/slime with height and flags)
- Object placement (M2 and WMO references)
- Terrain holes
**ADTLoader** - Terrain parsing
- 64×64 tiles per map, 16×16 chunks per tile (MCNK)
- MCVT height grid (145 vertices: 9 outer + 8 inner per row × 9 rows)
- Texture layers (up to 4 with alpha blending, RLE-compressed alpha maps)
- Async loading to prevent frame stalls
**DBCLoader** - Database parser
- 20+ DBC files loaded (Spell, Item, Creature, SkillLine, Faction, etc.)
- Type-safe record access
- String block parsing
- Memory-efficient caching
- Used for:
- Spell icons and tooltips (Spell.dbc, SpellIcon.dbc)
- Item data (Item.dbc, ItemDisplayInfo.dbc)
- Creature display info (CreatureDisplayInfo.dbc, CreatureModelData.dbc)
- Class and race info (ChrClasses.dbc, ChrRaces.dbc)
- Skill lines (SkillLine.dbc, SkillLineAbility.dbc)
- Faction and reputation (Faction.dbc)
- Map and area names (Map.dbc, AreaTable.dbc)
**DBCLoader** - Database table parsing
- Binary DBC format (fixed 4-byte uint32 fields + string block)
- CSV fallback for pre-extracted data
- Expansion-aware field layout via `dbc_layouts.json`
- 20+ DBC files: Spell, Item, Creature, Faction, Map, AreaTable, etc.
### 7. UI System (`src/ui/`)
**UIManager** - ImGui coordinator
- ImGui initialization with SDL2/OpenGL backend
- ImGui initialization with SDL2/Vulkan backend
- Screen state management and transitions
- Event handling and input routing
- Render dispatch with opacity control
- Screen state management
**AuthScreen** - Login interface
- Username/password input fields
- Server address configuration
- Connection status and error messages
**Screens:**
- `AuthScreen` - Login with username/password, server address, security code
- `RealmScreen` - Realm list with population and type indicators
- `CharacterScreen` - Character selection with 3D animated preview, keyboard navigation
- `CharacterCreateScreen` - Race/class/gender/appearance customization
- `GameScreen` - Main HUD: chat, action bar, target frame, minimap, nameplates, combat text, tooltips
- `InventoryScreen` - Equipment paper doll, backpack, bag windows, item tooltips with stats
- `SpellbookScreen` - Tabbed spell list with icons, drag-drop to action bar
- `QuestLogScreen` - Quest list with objectives, details, and rewards
- `TalentScreen` - Talent tree UI with point allocation
- `SettingsScreen` - Graphics presets (LOW/MEDIUM/HIGH/ULTRA), audio, keybindings
**RealmScreen** - Server selection
- Realm list display with names and types
- Population info (Low/Medium/High/Full)
- Realm type indicators (PvP/PvE/RP/RPPvP)
- Auto-select for single realm
**Chat System** (`src/ui/chat/`) - Modular chat architecture:
- `ChatPanel` - Main chat UI (tabs, input, message display)
- `ChatInput` - Input handling and history
- `ChatTabManager` - Tab creation, switching, per-tab filters
- `ChatTabCompleter` - Tab-completion for player names, commands, channels
- `ChatCommandRegistry` - Slash command dispatch with `IChatCommand` interface
- `ChatMarkupParser` / `ChatMarkupRenderer` - Item link parsing and colored rich-text rendering
- `ChatBubbleManager` - Floating chat bubbles above entities
- `ChatSettings` - Per-channel color, font size, timestamp options
- `MacroEvaluator` - WoW-style macro conditional evaluation (`[mod:shift]`, `[target=focus]`, etc.)
- `GameStateAdapter` / `InputModifierAdapter` - Testable abstractions over game state
- `ItemTooltipRenderer` - Chat-embedded item tooltip rendering (510 LOC)
- `CastSequenceTracker` - `/castsequence` state tracking
- 11 command modules under `commands/`: channel, combat, emote, GM, group, guild, help, misc, social, system, target
- 190-command GM data table with dot-prefix interception and `/gmhelp`
**CharacterScreen** - Character selection
- Character list with 3D animated preview
- Stats panel (level, race, class, location)
- Create/delete character buttons
- Enter world button
- Auto-select for single character
### 8. Audio System (`src/audio/`)
**CharacterCreateScreen** - Character creation
- Race selection (all Alliance and Horde races)
- Class selection (class availability by race)
- Gender selection
- Appearance customization (face, skin, hair, color, features)
- Name input with validation
- 3D character preview
**AudioEngine** - miniaudio-based playback
- WAV decode cache (256 entries, LRU eviction)
- 2D and 3D positional audio
- Sample rate preservation (explicit to avoid miniaudio pitch distortion)
**GameScreen** - In-game HUD
- Chat window with message history and formatting
- Action bar (12 slots with icons, cooldowns, keybindings)
- Target frame (name, level, health, hostile/friendly coloring)
- Player stats (health, mana/rage/energy)
- Minimap with quest markers
- Experience bar
**Sound Managers:**
- `AmbientSoundManager` - Wind, water, fire, birds, crickets, city ambience, bell tolls
- `ActivitySoundManager` - Swimming strokes, jumping, landing
- `MovementSoundManager` - Footsteps (terrain-aware), mount movement
- `MountSoundManager` - Mount-specific movement audio
- `MusicManager` - Zone music with day/night variants
**InventoryScreen** - Inventory management
- Equipment paper doll (23 slots: head, shoulders, chest, etc.)
- Backpack grid (16 slots)
- Item icons with tooltips
- Drag-drop to equip/unequip
- Item stats and durability
- Gold display
### 9. Warden Anti-Cheat (`src/game/`)
**SpellbookScreen** - Spells and abilities
- Tabbed interface (class specialties + General)
- Spell icons organized by SkillLine
- Spell tooltips (name, rank, cost, cooldown, description)
- Drag-drop to action bar
- Known spell tracking
**QuestLogScreen** - Quest tracking
- Active quest list
- Quest objectives and progress
- Quest details (description, objectives, rewards)
- Abandon quest button
- Quest level and recommended party size
**TalentScreen** - Talent trees
- Placeholder for talent system
- Tree visualization (TODO)
- Talent point allocation (TODO)
**Settings Window** - Configuration
- UI opacity slider
- Graphics options (TODO)
- Audio controls (TODO)
- Keybinding customization (TODO)
**Loading Screen** - Map loading progress
- Progress bar with percentage
- Background image (map-specific, TODO)
- Loading tips (TODO)
- Shown during world entry and map transitions
## Data Flow Examples
### Authentication Flow
```
User Input (username/password)
AuthHandler::authenticate()
SRP::calculateVerifier()
TCPSocket::send(LOGON_CHALLENGE)
Server Response (LOGON_CHALLENGE)
AuthHandler receives packet
SRP::calculateProof()
TCPSocket::send(LOGON_PROOF)
Server Response (LOGON_PROOF) → Success
Application::setState(REALM_SELECTION)
```
### Rendering Flow
```
Application::render()
Renderer::beginFrame()
├─ glClearColor() - Clear screen
└─ glClear() - Clear buffers
Renderer::renderWorld(world)
├─ Update camera matrices
├─ Frustum culling
├─ For each visible chunk:
│ ├─ Bind shader
│ ├─ Set uniforms (matrices, lighting)
│ ├─ Bind textures
│ └─ Mesh::draw() → glDrawElements()
└─ For each entity:
├─ Calculate bone transforms
└─ Render skinned mesh
UIManager::render()
├─ ImGui::NewFrame()
├─ Render current UI screen
└─ ImGui::Render()
Renderer::endFrame()
Window::swapBuffers()
```
### Asset Loading Flow
```
World::loadMap(mapId)
AssetManager::readFile("World/Maps/{map}/map.adt")
ADTLoader::load(adtData)
├─ Parse MCNK chunks (terrain)
├─ Parse MCLY chunks (textures)
├─ Parse MCVT chunks (vertices)
└─ Parse MCNR chunks (normals)
For each texture reference:
AssetManager::readFile(texturePath)
BLPLoader::load(blpData)
Texture::loadFromMemory(imageData)
Create Mesh from vertices/normals/texcoords
Add to Scene
Renderer draws in next frame
```
4-layer architecture:
- `WardenHandler` - Packet handling (SMSG/CMSG_WARDEN_DATA)
- `WardenModuleManager` - Module lifecycle and caching
- `WardenModule` - 8-step pipeline: decrypt (RC4), strip RSA-2048 signature, decompress (zlib), parse PE headers, relocate, resolve imports, execute
- `WardenEmulator` - Unicorn Engine x86 CPU emulation with Windows API interception
- `WardenMemory` - PE image loading with bounds-checked reads, runtime global patching
## Threading Model
Currently **single-threaded** with async operations:
- Main thread: Window events, update, render
- Network I/O: Non-blocking in main thread (event-driven)
- Asset loading: Async terrain streaming (non-blocking chunk loads)
**Async Systems Implemented:**
- Terrain streaming loads ADT chunks asynchronously to prevent frame stalls
- Network packets processed in batches per frame
- UI rendering deferred until after world rendering
**Future multi-threading opportunities:**
- Asset loading thread pool (background texture/model decompression)
- Network thread (dedicated for socket I/O)
- Physics thread (if collision detection is added)
- Audio streaming thread
- **Main thread**: Window events, game logic update, rendering
- **Async terrain**: Non-blocking chunk loading (std::async)
- **Network I/O**: Non-blocking recv in main thread with per-frame budgets
- **Normal maps**: Background CPU generation with mutex-protected result queue
- **GPU uploads**: Second Vulkan queue for parallel texture/buffer transfers
## Memory Management
- **Smart pointers:** Used throughout (std::unique_ptr, std::shared_ptr)
- **RAII:** All resources (OpenGL, SDL) cleaned up automatically
- **No manual memory management:** No raw new/delete
- **OpenGL resources:** Wrapped in classes with proper destructors
## Performance Considerations
### Rendering
- **Frustum culling:** Only render visible chunks (terrain and WMO groups)
- **Distance culling:** WMO groups culled beyond 160 units
- **Batching:** Group draw calls by material and shader
- **LOD:** Distance-based level of detail (TODO)
- **Occlusion:** Portal-based visibility (WMO system)
- **GPU skinning:** Character animation computed on GPU (256 bones)
- **Instancing:** Future optimization for repeated models
### Asset Streaming
- **Async loading:** Terrain chunks load asynchronously (prevents frame stalls)
- **Lazy loading:** Load chunks as player moves within streaming radius
- **Unloading:** Free distant chunks automatically
- **Caching:** Keep frequently used assets in memory (textures, models)
- **Priority queue:** Load visible chunks first
### Network
- **Non-blocking I/O:** Never stall main thread
- **Packet buffering:** Handle multiple packets per frame
- **Batch processing:** Process received packets in batches
- **RC4 encryption:** Efficient header encryption (minimal overhead)
- **Compression:** Some packets are compressed (TODO)
### Memory Management
- **Smart pointers:** Automatic cleanup, no memory leaks
- **Object pooling:** Reuse particle objects (weather system)
- **DBC caching:** Load once, access fast
- **Texture sharing:** Same texture used by multiple models
## Error Handling
- **Logging:** All errors logged with context
- **Graceful degradation:** Missing assets show placeholder
- **State recovery:** Network disconnect → back to auth screen
- **No crashes:** Exceptions caught at application level
## Configuration
Currently hardcoded, future config system:
- Window size and fullscreen
- Graphics quality settings
- Server addresses
- Keybindings
- Audio volume
## Testing Strategy
**Unit Testing** (TODO):
- Packet serialization/deserialization
- SRP math functions
- Asset parsers with sample files
- DBC record parsing
- Inventory slot calculations
**Integration Testing** (TODO):
- Full auth flow against test server
- Realm list retrieval
- Character creation and selection
- Quest turn-in flow
- Vendor transactions
**Manual Testing:**
- Visual verification of rendering (terrain, water, models, particles)
- Performance profiling (F1 performance HUD)
- Memory leak checking (valgrind)
- Online gameplay against AzerothCore/TrinityCore/MaNGOS servers
- UI interactions (drag-drop, click events)
**Current Test Coverage:**
- Full authentication flow tested against live servers
- Character creation and selection verified
- Quest system tested (accept, track, turn-in)
- Vendor system tested (buy, sell)
- Combat system tested (targeting, auto-attack, spells)
- Inventory system tested (equip, unequip, drag-drop)
- **Smart pointers**: `std::unique_ptr` / `std::shared_ptr` throughout
- **RAII**: All Vulkan resources wrapped with proper destructors
- **VMA**: Vulkan Memory Allocator for GPU memory
- **Object pooling**: Weather particles, combat text entries
- **DBC caching**: Lazy-loaded mutable caches in const getters
## Build System
**CMake:**
- Modular target structure
- Automatic dependency discovery
- Cross-platform (Linux focus, but portable)
- Out-of-source builds
**CMake** with modular targets:
- `wowee` - Main executable
- `asset_extract` - MPQ extraction tool (requires StormLib)
- `dbc_to_csv` / `auth_probe` / `blp_convert` - Utility tools
**Dependencies:**
- SDL2 (system)
- OpenGL/GLEW (system)
- OpenSSL (system)
- GLM (system or header-only)
- SDL2, Vulkan SDK, OpenSSL, GLM, zlib (system)
- ImGui (submodule in extern/)
- StormLib (system, optional)
- VMA, vk-bootstrap, stb_image (vendored in extern/)
- StormLib (system, optional — only for asset_extract)
- Unicorn Engine (system, optional — only for Warden emulation)
- FFmpeg (system, optional — for video playback)
**CI**: GitHub Actions for Linux (x86-64, ARM64), Windows (MSYS2), macOS (ARM64)
**Container builds**: Docker cross-compilation for Linux, macOS (osxcross), Windows (LLVM-MinGW)
## Code Style
- **C++20 standard**
- **Namespaces:** wowee::core, wowee::rendering, etc.
- **Naming:** PascalCase for classes, camelCase for functions/variables
- **Headers:** .hpp extension
- **Includes:** Relative to project root
---
This architecture provides a solid foundation for a full-featured native WoW client!
- **Namespaces**: `wowee::core`, `wowee::rendering`, `wowee::rendering::world_map`, `wowee::game`, `wowee::ui`, `wowee::ui::chat`, `wowee::math`, `wowee::network`, `wowee::auth`, `wowee::audio`, `wowee::pipeline`
- **Naming**: PascalCase for classes, camelCase for functions/variables, kPascalCase for constants
- **Headers**: `.hpp` extension, `#pragma once`
- **Commits**: Conventional style (`feat:`, `fix:`, `refactor:`, `docs:`, `perf:`)

View file

@ -563,5 +563,4 @@ The client is now ready for character operations and world entry! 🎮
---
**Implementation Status:** 100% Complete for authentication
**Next Milestone:** Character enumeration and world entry
**Implementation Status:** Complete — authentication, character enumeration, and world entry all working.

View file

@ -397,6 +397,4 @@ The authentication system can now reliably communicate with WoW 3.3.5a servers!
---
**Status:** ✅ Complete and tested
**Next Steps:** Test with live server and implement realm list protocol.
**Status:** ✅ Complete and tested against AzerothCore, TrinityCore, Mangos, and Turtle WoW.

79
docs/perf_baseline.md Normal file
View file

@ -0,0 +1,79 @@
# Performance Baseline — WoWee
> Phase 0.3 deliverable. Measurements taken before any optimization work.
> Re-run after each phase to quantify improvement.
## Tracy Profiler Integration
Tracy v0.11.1 integrated under `WOWEE_ENABLE_TRACY` CMake option (default: OFF).
When enabled, zero-cost zone markers instrument the following critical paths:
### Instrumented Zones
| Zone Name | File | Purpose |
|-----------|------|---------|
| `Application::run` | src/core/application.cpp | Main loop entry |
| `Application::update` | src/core/application.cpp | Per-frame game logic |
| `Renderer::beginFrame` | src/rendering/renderer.cpp | Vulkan frame begin |
| `Renderer::endFrame` | src/rendering/renderer.cpp | Post-process + present |
| `Renderer::update` | src/rendering/renderer.cpp | Renderer per-frame update |
| `Renderer::renderWorld` | src/rendering/renderer.cpp | Main world draw call |
| `Renderer::renderShadowPass` | src/rendering/renderer.cpp | Shadow depth pass |
| `PostProcess::execute` | src/rendering/post_process_pipeline.cpp | FSR/FXAA post-process |
| `M2::computeBoneMatrices` | src/rendering/m2_renderer.cpp | CPU skeletal animation |
| `M2Renderer::update` | src/rendering/m2_renderer.cpp | M2 instance update + culling |
| `TerrainManager::update` | src/rendering/terrain_manager.cpp | Terrain streaming logic |
| `TerrainManager::processReadyTiles` | src/rendering/terrain_manager.cpp | GPU tile uploads |
| `ADTLoader::load` | src/pipeline/adt_loader.cpp | ADT binary parsing |
| `AssetManager::loadTexture` | src/pipeline/asset_manager.cpp | BLP texture loading |
| `AssetManager::loadDBC` | src/pipeline/asset_manager.cpp | DBC data file loading |
| `WorldSocket::update` | src/network/world_socket.cpp | Network packet dispatch |
`FrameMark` placed at frame boundary in Application::update to track FPS.
### How to Profile
```bash
# Build with Tracy enabled
mkdir -p build_tracy && cd build_tracy
cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo -DWOWEE_ENABLE_TRACY=ON
cmake --build . --parallel $(nproc)
# Run the client — Tracy will broadcast on default port (8086)
cd bin && ./wowee
# Connect with Tracy profiler GUI (separate download from https://github.com/wolfpld/tracy/releases)
# Or capture from CLI: tracy-capture -o trace.tracy
```
## Baseline Scenarios
> **TODO:** Record measurements once profiler is connected to a running instance.
> Each scenario should record: avg FPS, frame time (p50/p95/p99), and per-zone timings.
### Scenario 1: Stormwind (Heavy M2/WMO)
- **Location:** Stormwind City center
- **Load:** Dense M2 models (NPCs, doodads), multiple WMO interiors
- **Avg FPS:** _pending_
- **Frame time (p50/p95/p99):** _pending_
- **Top zones:** _pending_
### Scenario 2: The Barrens (Heavy Terrain)
- **Location:** Central Barrens
- **Load:** Many terrain tiles loaded, sparse M2, large draw distance
- **Avg FPS:** _pending_
- **Frame time (p50/p95/p99):** _pending_
- **Top zones:** _pending_
### Scenario 3: Dungeon Instance (WMO-only)
- **Location:** Any dungeon instance (e.g., Deadmines entrance)
- **Load:** WMO interior rendering, no terrain
- **Avg FPS:** _pending_
- **Frame time (p50/p95/p99):** _pending_
- **Top zones:** _pending_
## Notes
- When `WOWEE_ENABLE_TRACY` is OFF (default), all `ZoneScopedN` / `FrameMark` macros expand to nothing — zero runtime overhead.
- Tracy requires a network connection to capture traces. Run the Tracy profiler GUI or `tracy-capture` CLI alongside the client.
- Debug builds are significantly slower due to -Og and no LTO; use RelWithDebInfo for representative measurements.

View file

@ -19,17 +19,11 @@ For a more honest snapshot of gaps and current direction, see `docs/status.md`.
### 1. Clone
```bash
git clone https://github.com/Kelsidavis/WoWee.git
cd wowee
git clone --recurse-submodules https://github.com/Kelsidavis/WoWee.git
cd WoWee
```
### 2. Install ImGui
```bash
git clone https://github.com/ocornut/imgui.git extern/imgui
```
### 3. Build
### 2. Build
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
@ -96,7 +90,7 @@ Use `BUILD_INSTRUCTIONS.md` for distro-specific package lists.
- Verify auth/world server is running
- Check host/port settings
- Check server logs and client logs in `build/bin/logs/`
- Check server logs and client logs in `logs/wowee.log`
### Missing assets (models/textures/terrain)

View file

@ -609,6 +609,6 @@ Once you have a working local server connection:
---
**Status**: Ready for local server testing
**Last Updated**: 2026-01-27
**Client Version**: 1.0.3
**Server Compatibility**: WoW 3.3.5a (12340)
**Last Updated**: 2026-03-30
**Client Version**: v1.8.9-preview
**Server Compatibility**: Vanilla 1.12, TBC 2.4.3, WotLK 3.3.5a (12340), Turtle WoW 1.17

View file

@ -351,13 +351,13 @@ The expensive operation (session key computation) only happens once per login.
2. **No Plaintext Storage:** Password is immediately hashed, never stored
3. **Forward Secrecy:** Ephemeral keys (a, A) are generated per session
4. **Mutual Authentication:** Both client and server prove knowledge of password
5. **Secure Channel:** Session key K can be used for encryption (not implemented yet)
5. **Secure Channel:** Session key K is used for RC4 header encryption after auth completes
## References
- [SRP Protocol](http://srp.stanford.edu/)
- [WoWDev Wiki - SRP](https://wowdev.wiki/SRP)
- Original wowee: `/wowee/src/lib/crypto/srp.js`
- Implementation: `src/auth/srp.cpp`, `include/auth/srp.hpp`
- OpenSSL BIGNUM: https://www.openssl.org/docs/man1.1.1/man3/BN_new.html
---

View file

@ -1,6 +1,6 @@
# Project Status
**Last updated**: 2026-03-18
**Last updated**: 2026-04-14
## What This Repo Is
@ -30,16 +30,28 @@ Implemented (working in normal use):
- Target/focus frames: guild name, creature type, rank badges, combo points, cast bars
- Map exploration: subzone-level fog-of-war reveal
- Warden anti-cheat: full module execution via Unicorn Engine x86 emulation; module caching
- Audio: ambient, movement, combat, spell, and UI sound systems
- Bag UI: separate bag windows, open-bag indicator on bag bar, optional collapse-empty mode in aggregate bag view
- Audio: ambient, movement, combat, spell, and UI sound systems; NPC voice lines for all playable races (greeting/farewell/vendor/pissed/aggro/flee)
- Bag UI: independent bag windows (any bag closable independently), open-bag indicator on bag bar, server-synced bag sort, off-screen position reset, optional collapse-empty mode in aggregate view
- DBC auto-detection: CharSections.dbc field layout auto-detected at runtime (handles stock WotLK vs HD-textured clients)
- Multi-expansion: Classic/Vanilla, TBC, WotLK, and Turtle WoW (1.17) protocol and asset variants
- CI: GitHub Actions for Linux (x86-64, ARM64), Windows (MSYS2), macOS (ARM64); container builds via Podman
- CI: GitHub Actions for Linux (x86-64, ARM64), Windows (MSYS2 x86-64 + ARM64), macOS (ARM64); container builds via Podman
Recent refactors (PRs #59-63, April 2026):
- Chat system decomposed into 15+ modules under `src/ui/chat/` with 11 command modules, GM command support, macro evaluator, and tab completion
- World map decomposed into 16 modules under `src/rendering/world_map/` with overlay layer system, view state machine, and ZMP-based hover detection
- TransportManager decomposed: spline math extracted to `src/math/`, path data to TransportPathRepository, 7 duplicated spline parsers consolidated into `spline_packet.cpp`
- Spell visual effects system with bone-tracked ribbons and particles
- Entity movement improvements: multi-segment path interpolation, terrain height clamping, walk/run animation fix
- 27 unit tests (up from 8), covering chat, world map, spline math, transport, and animation systems
- Code quality fix pass: 7 issues resolved across hover detection, null safety, buffer bounds, and coordinate validation
In progress / known gaps:
- World map: zone hover detection has edge cases with some zone boundaries; cosmic highlight sizing is approximate
- Transports: M2 transports (trams) working with position-delta riding; WMO transports (ships, zeppelins) working with path following; some edge cases remain
- Quest GO interaction: CMSG_GAMEOBJ_USE + CMSG_LOOT sent correctly, but some AzerothCore/ChromieCraft servers don't grant quest credit for chest-type GOs (server-side limitation)
- Visual edge cases: some M2/WMO rendering gaps (some particle effects)
- Lava steam particles: sparse in some areas (tuning opportunity)
- Water refraction: enabled by default; srcAccessMask barrier fix (2026-03-18) resolved prior VK_ERROR_DEVICE_LOST on AMD/Mali GPUs
## Where To Look

156
docs/threading.md Normal file
View file

@ -0,0 +1,156 @@
# Threading Model
This document describes the threading architecture of WoWee, the synchronisation
primitives that protect shared state, and the conventions that new code must
follow.
---
## Thread Inventory
| # | Name / Role | Created At | Lifetime |
|----|------------------------|-------------------------------------------------|-------------------------------|
| 1 | **Main thread** | `Application::run()` (`main.cpp`) | Entire session |
| 2 | **Async network pump** | `WorldSocket::connectAsync()` (`world_socket.cpp`) | Connect → disconnect |
| 3 | **Terrain workers** | `TerrainManager::startWorkers()` (`terrain_manager.cpp`) | Map load → map unload |
| 4 | **Watchdog** | `Application::startWatchdog()` (`application.cpp`) | After first frame → shutdown |
| 5 | **Fire-and-forget** | `std::async` / `std::thread(...).detach()` (various) | Task-scoped (bone anim, normal-map gen, warden crypto, world preload, entity model loading) |
### Thread Responsibilities
* **Main thread** — SDL event pumping, game logic (entity update, camera, UI),
GPU resource upload/finalization, render command recording, Vulkan present.
* **Network pump**`recv()` loop, header decryption, packet parsing. Pushes
parsed packets into `pendingPacketCallbacks_` (locked by `callbackMutex_`).
The main thread drains this queue via `dispatchQueuedPackets()`.
* **Terrain workers** — background ADT/WMO/M2 file I/O, mesh decoding, texture
decompression. Workers push completed `PendingTile` objects into `readyQueue`
(locked by `queueMutex`). The main thread finalizes (GPU upload) via
`processReadyTiles()`.
* **Watchdog** — periodic frame-stall detection. Reads `watchdogHeartbeatMs`
(atomic) and optionally requests a Vulkan device reset via
`watchdogRequestRelease` (atomic).
* **Fire-and-forget** — short-lived tasks. Each captures only the data it
needs or uses a dedicated result channel (e.g. `std::future`,
`completedNormalMaps_` with `normalMapResultsMutex_`).
---
## Shared State Map
### Legend
| Annotation | Meaning |
|-------------------------|---------|
| `THREAD-SAFE: <mutex>` | Protected by the named mutex/atomic. |
| `MAIN-THREAD-ONLY` | Accessed exclusively by the main thread. No lock needed. |
### Asset Manager (`include/pipeline/asset_manager.hpp`)
| Variable | Guard | Notes |
|-------------------------|------------------|-------|
| `fileCache` | `cacheMutex` (shared_mutex) | `shared_lock` for reads, `lock_guard` for writes/eviction |
| `dbcCache` | `cacheMutex` | Same mutex as fileCache |
| `fileCacheTotalBytes` | `cacheMutex` | Written under exclusive lock only |
| `fileCacheAccessCounter`| `cacheMutex` | Written under exclusive lock only |
| `fileCacheHits` | `std::atomic` | Incremented after releasing cacheMutex |
| `fileCacheMisses` | `std::atomic` | Incremented after releasing cacheMutex |
### Audio Engine (`src/audio/audio_engine.cpp`)
| Variable | Guard | Notes |
|------------------------|---------------------------|-------|
| `gDecodedWavCache` | `gDecodedWavCacheMutex` (shared_mutex) | `shared_lock` for cache hits, `lock_guard` for miss+eviction. Double-check after decoding. |
### World Socket (`include/network/world_socket.hpp`)
| Variable | Guard | Notes |
|---------------------------|------------------|-------|
| `sockfd`, `connected`, `encryptionEnabled`, `receiveBuffer`, `receiveReadOffset_`, `headerBytesDecrypted`, cipher state, `recentPacketHistory_` | `ioMutex_` | Consistent `lock_guard` in `send()` and `pumpNetworkIO()` |
| `pendingPacketCallbacks_` | `callbackMutex_` | Pump thread produces, main thread consumes in `dispatchQueuedPackets()` |
| `asyncPumpStop_`, `asyncPumpRunning_` | `std::atomic<bool>` | Memory-order acquire/release |
| `packetCallback` | *implicit* | Set once before `connectAsync()` starts the pump thread |
### Terrain Manager (`include/rendering/terrain_manager.hpp`)
| Variable | Guard | Notes |
|-----------------------|--------------------------|-------|
| `loadQueue`, `readyQueue`, `pendingTiles` | `queueMutex` + `queueCV` | Workers wait; main signals on enqueue/finalize |
| `tileCache_`, `tileCacheLru_`, `tileCacheBytes_` | `tileCacheMutex_` | Read/write by both main and workers |
| `uploadedM2Ids_` | `uploadedM2IdsMutex_` | Workers check, main inserts on finalize |
| `preparedWmoUniqueIds_`| `preparedWmoUniqueIdsMutex_` | Workers only |
| `missingAdtWarnings_` | `missingAdtWarningsMutex_` | Workers only |
| `workerRunning` | `std::atomic<bool>` | — |
| `placedDoodadIds`, `placedWmoIds`, `loadedTiles`, `failedTiles` | MAIN-THREAD-ONLY | Only touched in processReadyTiles / unloadDistantTiles |
### Entity Manager (`include/game/entity.hpp`)
| Variable | Guard | Notes |
|------------|------------------|-------|
| `entities` | MAIN-THREAD-ONLY | All mutations via `dispatchQueuedPackets()` on main thread |
### Character Renderer (`include/rendering/character_renderer.hpp`)
| Variable | Guard | Notes |
|------------------------|---------------------------|-------|
| `completedNormalMaps_` | `normalMapResultsMutex_` | Detached threads push, main thread drains |
| `pendingNormalMapCount_`| `std::atomic<int>` | acq_rel ordering |
### Logger (`include/core/logger.hpp`)
| Variable | Guard | Notes |
|-------------|-----------------|-------|
| `minLevel_` | `std::atomic<int>` | Fast path check in `shouldLog()` |
| `fileStream`, `lastMessage_`, suppression state | `mutex` | Locked in `log()` |
### Application (`src/core/application.cpp`)
| Variable | Guard | Notes |
|-------------------------|--------------------|-------|
| `watchdogHeartbeatMs` | `std::atomic<int64_t>` | Main stores, watchdog loads |
| `watchdogRequestRelease`| `std::atomic<bool>` | Watchdog stores, main exchanges |
| `watchdogRunning` | `std::atomic<bool>` | — |
---
## Conventions for New Code
1. **Prefer `std::shared_mutex`** for read-heavy caches. Use `std::shared_lock`
for lookups and `std::lock_guard<std::shared_mutex>` for mutations.
2. **Annotate shared state** at the declaration site with either
`// THREAD-SAFE: protected by <mutex_name>` or `// MAIN-THREAD-ONLY`.
3. **Keep lock scope minimal.** Copy data under the lock, then process outside.
4. **Avoid detaching threads** when possible. Prefer `std::async` with a
`std::future` stored on the owning object so shutdown can wait for completion.
5. **Use `std::atomic` for counters and flags** that are read/written without
other invariants (e.g. cache hit stats, boolean run flags).
6. **No lock-order inversions.** Current order (most-outer first):
`ioMutex_``callbackMutex_``queueMutex``cacheMutex`.
7. **ThreadSanitizer** — run periodically with `-fsanitize=thread` to catch
regressions:
```bash
cmake -DCMAKE_CXX_FLAGS="-fsanitize=thread" .. && make -j$(nproc)
```
---
## Known Limitations
* `EntityManager::entities` relies on the convention that all entity mutations
happen on the main thread through `dispatchQueuedPackets()`. There is no
compile-time enforcement. If a future change introduces direct entity
modification from the network pump thread, a mutex must be added.
* `packetCallback` in `WorldSocket` is set once before `connectAsync()` and
never modified afterwards. This is safe in practice but not formally
synchronized — do not change the callback after `connectAsync()`.
* `fileCacheMisses` is declared as `std::atomic<size_t>` for consistency but is
currently never incremented; the actual miss count must be inferred from
`fileCacheAccessCounter - fileCacheHits`.

1
extern/FidelityFX-FSR2 vendored Submodule

@ -0,0 +1 @@
Subproject commit 3d22aefd90fd861e5cee1c3cde18ff185e221f2d

1
extern/FidelityFX-SDK vendored Submodule

@ -0,0 +1 @@
Subproject commit ce81c674d92d81ad1253841f39a359811dd738cf

14
extern/VERSIONS.md vendored Normal file
View file

@ -0,0 +1,14 @@
# Vendored Library Versions
Versions of third-party libraries vendored in `extern/`. Update this file
when upgrading any dependency so maintainers can track drift.
| Library | Version | Source | Notes |
|---------|---------|--------|-------|
| Dear ImGui | 1.92.6 WIP | https://github.com/ocornut/imgui | Git submodule |
| vk-bootstrap | latest | https://github.com/charles-lunarg/vk-bootstrap | Git submodule |
| Vulkan Memory Allocator | 3.4.0 | https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator | Single header |
| miniaudio | 0.11.24 | https://miniaud.io/ | Single header |
| stb_image | 2.30 | https://github.com/nothings/stb | Single header |
| stb_image_write | 1.16 | https://github.com/nothings/stb | Single header |
| Lua | 5.1.5 | https://www.lua.org/ | Intentionally 5.1 for WoW addon API compatibility |

11811
extern/catch2/catch_amalgamated.cpp vendored Normal file

File diff suppressed because it is too large Load diff

14106
extern/catch2/catch_amalgamated.hpp vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@ public:
AddonManager();
~AddonManager();
bool initialize(game::GameHandler* gameHandler);
bool initialize(game::GameHandler* gameHandler, const LuaServices& services = {});
void scanAddons(const std::string& addonsPath);
void loadAllAddons();
bool runScript(const std::string& code);
@ -35,6 +35,7 @@ private:
LuaEngine luaEngine_;
std::vector<TocFile> addons_;
game::GameHandler* gameHandler_ = nullptr;
LuaServices luaServices_;
std::string addonsPath_;
bool loadAddon(const TocFile& addon);

View file

@ -0,0 +1,166 @@
// lua_api_helpers.hpp — Shared helpers, lookup tables, and utility functions
// used by all lua_*_api.cpp domain files.
// Extracted from lua_engine.cpp as part of §5.1 (Tame LuaEngine).
#pragma once
#include <string>
#include <chrono>
#include <cstring>
#include <algorithm>
#include "addons/lua_services.hpp"
#include "game/game_handler.hpp"
#include "game/entity.hpp"
#include "game/update_field_table.hpp"
#include "core/logger.hpp"
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}
namespace wowee::addons {
// ---- String helper ----
inline void toLowerInPlace(std::string& s) {
for (char& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
// ---- Lua return helpers — used 200+ times as guard/fallback returns ----
inline int luaReturnNil(lua_State* L) { lua_pushnil(L); return 1; }
inline int luaReturnZero(lua_State* L) { lua_pushnumber(L, 0); return 1; }
inline int luaReturnFalse(lua_State* L){ lua_pushboolean(L, 0); return 1; }
// ---- Shared GetTime() epoch ----
// All time-returning functions must use this same origin
// so that addon calculations like (start + duration - GetTime()) are consistent.
inline const auto& luaTimeEpoch() {
static const auto epoch = std::chrono::steady_clock::now();
return epoch;
}
inline double luaGetTimeNow() {
return std::chrono::duration<double>(std::chrono::steady_clock::now() - luaTimeEpoch()).count();
}
// ---- Shared WoW class/race/power name tables (indexed by ID, element 0 = unknown) ----
inline constexpr const char* kLuaClasses[] = {
"","Warrior","Paladin","Hunter","Rogue","Priest",
"Death Knight","Shaman","Mage","Warlock","","Druid"
};
inline constexpr const char* kLuaRaces[] = {
"","Human","Orc","Dwarf","Night Elf","Undead",
"Tauren","Gnome","Troll","","Blood Elf","Draenei"
};
inline constexpr const char* kLuaPowerNames[] = {
"MANA","RAGE","FOCUS","ENERGY","HAPPINESS","","RUNIC_POWER"
};
// ---- Quality hex strings ----
// No alpha prefix — for item links
inline constexpr const char* kQualHexNoAlpha[] = {
"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000","e6cc80","e6cc80"
};
// With ff alpha prefix — for Lua color returns
inline constexpr const char* kQualHexAlpha[] = {
"ff9d9d9d","ffffffff","ff1eff00","ff0070dd","ffa335ee","ffff8000","ffe6cc80","ff00ccff"
};
// ---- Retrieve GameHandler pointer stored in Lua registry ----
inline game::GameHandler* getGameHandler(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, "wowee_game_handler");
auto* gh = static_cast<game::GameHandler*>(lua_touserdata(L, -1));
lua_pop(L, 1);
return gh;
}
// ---- Retrieve LuaServices pointer stored in Lua registry ----
inline LuaServices* getLuaServices(lua_State* L) {
lua_getfield(L, LUA_REGISTRYINDEX, "wowee_lua_services");
auto* svc = static_cast<LuaServices*>(lua_touserdata(L, -1));
lua_pop(L, 1);
return svc;
}
// ---- Unit resolution helpers ----
// Read UNIT_FIELD_TARGET_LO/HI from an entity's update fields to get what it's targeting
inline uint64_t getEntityTargetGuid(game::GameHandler* gh, uint64_t guid) {
if (guid == 0) return 0;
// If asking for the player's target, use direct accessor
if (guid == gh->getPlayerGuid()) return gh->getTargetGuid();
auto entity = gh->getEntityManager().getEntity(guid);
if (!entity) return 0;
const auto& fields = entity->getFields();
auto loIt = fields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_LO));
if (loIt == fields.end()) return 0;
uint64_t targetGuid = loIt->second;
auto hiIt = fields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_HI));
if (hiIt != fields.end())
targetGuid |= (static_cast<uint64_t>(hiIt->second) << 32);
return targetGuid;
}
// Resolve WoW unit IDs to GUID
inline uint64_t resolveUnitGuid(game::GameHandler* gh, const std::string& uid) {
if (uid == "player") return gh->getPlayerGuid();
if (uid == "target") return gh->getTargetGuid();
if (uid == "focus") return gh->getFocusGuid();
if (uid == "mouseover") return gh->getMouseoverGuid();
if (uid == "pet") return gh->getPetGuid();
// Compound unit IDs: targettarget, focustarget, pettarget, mouseovertarget
if (uid == "targettarget") return getEntityTargetGuid(gh, gh->getTargetGuid());
if (uid == "focustarget") return getEntityTargetGuid(gh, gh->getFocusGuid());
if (uid == "pettarget") return getEntityTargetGuid(gh, gh->getPetGuid());
if (uid == "mouseovertarget") return getEntityTargetGuid(gh, gh->getMouseoverGuid());
// party1-party4, raid1-raid40
if (uid.rfind("party", 0) == 0 && uid.size() > 5) {
int idx = 0;
try { idx = std::stoi(uid.substr(5)); } catch (...) { return 0; }
if (idx < 1 || idx > 4) return 0;
const auto& pd = gh->getPartyData();
// party members exclude self; index 1-based
int found = 0;
for (const auto& m : pd.members) {
if (m.guid == gh->getPlayerGuid()) continue;
if (++found == idx) return m.guid;
}
return 0;
}
if (uid.rfind("raid", 0) == 0 && uid.size() > 4 && uid[4] != 'p') {
int idx = 0;
try { idx = std::stoi(uid.substr(4)); } catch (...) { return 0; }
if (idx < 1 || idx > 40) return 0;
const auto& pd = gh->getPartyData();
if (idx <= static_cast<int>(pd.members.size()))
return pd.members[idx - 1].guid;
return 0;
}
return 0;
}
// Resolve unit IDs (player, target, focus, mouseover, pet, targettarget, etc.) to entity
inline game::Unit* resolveUnit(lua_State* L, const char* unitId) {
auto* gh = getGameHandler(L);
if (!gh || !unitId) return nullptr;
std::string uid(unitId);
toLowerInPlace(uid);
uint64_t guid = resolveUnitGuid(gh, uid);
if (guid == 0) return nullptr;
auto entity = gh->getEntityManager().getEntity(guid);
if (!entity) return nullptr;
return dynamic_cast<game::Unit*>(entity.get());
}
// Find GroupMember data for a GUID (for party members out of entity range)
inline const game::GroupMember* findPartyMember(game::GameHandler* gh, uint64_t guid) {
if (!gh || guid == 0) return nullptr;
for (const auto& m : gh->getPartyData().members) {
if (m.guid == guid && m.hasPartyStats) return &m;
}
return nullptr;
}
} // namespace wowee::addons

View file

@ -0,0 +1,18 @@
// lua_api_registrations.hpp — Forward declarations for per-domain Lua API
// registration functions. Called from LuaEngine::registerCoreAPI().
// Extracted from lua_engine.cpp as part of §5.1 (Tame LuaEngine).
#pragma once
struct lua_State;
namespace wowee::addons {
void registerUnitLuaAPI(lua_State* L);
void registerSpellLuaAPI(lua_State* L);
void registerInventoryLuaAPI(lua_State* L);
void registerQuestLuaAPI(lua_State* L);
void registerSocialLuaAPI(lua_State* L);
void registerSystemLuaAPI(lua_State* L);
void registerActionLuaAPI(lua_State* L);
} // namespace wowee::addons

View file

@ -1,5 +1,6 @@
#pragma once
#include "addons/lua_services.hpp"
#include <functional>
#include <string>
#include <vector>
@ -27,6 +28,7 @@ public:
bool executeString(const std::string& code);
void setGameHandler(game::GameHandler* handler);
void setLuaServices(const LuaServices& services);
// Fire a WoW event to all registered Lua handlers.
void fireEvent(const std::string& eventName,
@ -55,6 +57,7 @@ public:
private:
lua_State* L_ = nullptr;
game::GameHandler* gameHandler_ = nullptr;
LuaServices luaServices_;
LuaErrorCallback luaErrorCallback_;
void registerCoreAPI();

View file

@ -0,0 +1,17 @@
// lua_services.hpp — Dependency-injected services for Lua bindings.
// Replaces Application::getInstance() calls in domain API files (§5.2).
#pragma once
namespace wowee::core { class Window; }
namespace wowee::audio { class AudioCoordinator; }
namespace wowee::game { class ExpansionRegistry; }
namespace wowee::addons {
struct LuaServices {
core::Window* window = nullptr;
audio::AudioCoordinator* audioCoordinator = nullptr;
game::ExpansionRegistry* expansionRegistry = nullptr;
};
} // namespace wowee::addons

View file

@ -0,0 +1,106 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include <glm/vec3.hpp>
namespace wowee {
namespace pipeline { class AssetManager; }
namespace game { class ZoneManager; }
namespace audio {
class MusicManager;
class FootstepManager;
class ActivitySoundManager;
class MountSoundManager;
class NpcVoiceManager;
class AmbientSoundManager;
class UiSoundManager;
class CombatSoundManager;
class SpellSoundManager;
class MovementSoundManager;
/// Flat context passed from Renderer into updateZoneAudio() each frame.
/// All values are pre-queried so AudioCoordinator needs no rendering pointers.
struct ZoneAudioContext {
float deltaTime = 0.0f;
glm::vec3 cameraPosition{0.0f};
bool isSwimming = false;
bool insideWmo = false;
uint32_t insideWmoId = 0;
// Visual weather state for ambient audio sync
int weatherType = 0; // 0=none, 1=rain, 2=snow, 3=storm
float weatherIntensity = 0.0f;
// Terrain tile for offline zone lookup
int tileX = 0, tileY = 0;
bool hasTile = false;
// Server-authoritative zone (from SMSG_INIT_WORLD_STATES); 0 = offline
uint32_t serverZoneId = 0;
// Zone manager pointer (for zone info and music queries)
game::ZoneManager* zoneManager = nullptr;
};
/// Coordinates all audio subsystems.
/// Extracted from Renderer to separate audio lifecycle from rendering.
/// Owned by Application; Renderer and UI components access through Application.
class AudioCoordinator {
public:
AudioCoordinator();
~AudioCoordinator();
/// Initialize the audio engine and all managers.
/// @return true if audio is available (engine initialized successfully)
[[nodiscard]] bool initialize();
/// Initialize managers that need AssetManager (music lookups, sound banks).
void initializeWithAssets(pipeline::AssetManager* assetManager);
/// Shutdown all audio managers and engine.
void shutdown();
/// Per-frame zone detection, music transitions, and ambient weather sync.
/// Called from Renderer::update() with a pre-filled context.
void updateZoneAudio(const ZoneAudioContext& ctx);
const std::string& getCurrentZoneName() const { return currentZoneName_; }
uint32_t getCurrentZoneId() const { return currentZoneId_; }
// Accessors for all audio managers (same interface as Renderer had)
MusicManager* getMusicManager() { return musicManager_.get(); }
FootstepManager* getFootstepManager() { return footstepManager_.get(); }
ActivitySoundManager* getActivitySoundManager() { return activitySoundManager_.get(); }
MountSoundManager* getMountSoundManager() { return mountSoundManager_.get(); }
NpcVoiceManager* getNpcVoiceManager() { return npcVoiceManager_.get(); }
AmbientSoundManager* getAmbientSoundManager() { return ambientSoundManager_.get(); }
UiSoundManager* getUiSoundManager() { return uiSoundManager_.get(); }
CombatSoundManager* getCombatSoundManager() { return combatSoundManager_.get(); }
SpellSoundManager* getSpellSoundManager() { return spellSoundManager_.get(); }
MovementSoundManager* getMovementSoundManager() { return movementSoundManager_.get(); }
private:
void playZoneMusic(const std::string& music);
std::unique_ptr<MusicManager> musicManager_;
std::unique_ptr<FootstepManager> footstepManager_;
std::unique_ptr<ActivitySoundManager> activitySoundManager_;
std::unique_ptr<MountSoundManager> mountSoundManager_;
std::unique_ptr<NpcVoiceManager> npcVoiceManager_;
std::unique_ptr<AmbientSoundManager> ambientSoundManager_;
std::unique_ptr<UiSoundManager> uiSoundManager_;
std::unique_ptr<CombatSoundManager> combatSoundManager_;
std::unique_ptr<SpellSoundManager> spellSoundManager_;
std::unique_ptr<MovementSoundManager> movementSoundManager_;
bool audioAvailable_ = false;
// Zone/music state — moved from Renderer
uint32_t currentZoneId_ = 0;
std::string currentZoneName_;
bool inTavern_ = false;
bool inBlacksmith_ = false;
float musicSwitchCooldown_ = 0.0f;
};
} // namespace audio
} // namespace wowee

View file

@ -25,7 +25,7 @@ public:
~AudioEngine();
// Initialization
bool initialize();
[[nodiscard]] bool initialize();
void shutdown();
bool isInitialized() const { return initialized_; }

View file

@ -38,6 +38,10 @@ enum class VoiceType {
GNOME_FEMALE,
GOBLIN_MALE,
GOBLIN_FEMALE,
BLOODELF_MALE,
BLOODELF_FEMALE,
DRAENEI_MALE,
DRAENEI_FEMALE,
GENERIC, // Fallback
};

View file

@ -40,7 +40,7 @@ public:
~AuthHandler();
// Connection
bool connect(const std::string& host, uint16_t port = 3724);
[[nodiscard]] bool connect(const std::string& host, uint16_t port = 3724);
void disconnect();
bool isConnected() const;

View file

@ -53,7 +53,7 @@ struct LogonChallengeResponse {
// LOGON_CHALLENGE response parser
class LogonChallengeResponseParser {
public:
static bool parse(network::Packet& packet, LogonChallengeResponse& response);
[[nodiscard]] static bool parse(network::Packet& packet, LogonChallengeResponse& response);
};
// LOGON_PROOF packet builder
@ -92,7 +92,7 @@ struct LogonProofResponse {
// LOGON_PROOF response parser
class LogonProofResponseParser {
public:
static bool parse(network::Packet& packet, LogonProofResponse& response);
[[nodiscard]] static bool parse(network::Packet& packet, LogonProofResponse& response);
};
// Realm data structure
@ -131,7 +131,7 @@ struct RealmListResponse {
class RealmListResponseParser {
public:
// protocolVersion: 3 = vanilla (uint8 realmCount, uint32 icon), 8 = WotLK (uint16 realmCount, uint8 icon)
static bool parse(network::Packet& packet, RealmListResponse& response, uint8_t protocolVersion = 8);
[[nodiscard]] static bool parse(network::Packet& packet, RealmListResponse& response, uint8_t protocolVersion = 8);
};
} // namespace auth

View file

@ -2,6 +2,7 @@
#include <array>
#include <cstdint>
#include <optional>
#include <string>
namespace wowee {
@ -19,9 +20,11 @@ struct PinProof {
// - Compute: pin_hash = SHA1(client_salt || SHA1(server_salt || randomized_pin_ascii))
//
// PIN must be 4-10 ASCII digits.
PinProof computePinProof(const std::string& pinDigits,
uint32_t pinGridSeed,
const std::array<uint8_t, 16>& serverSalt);
// Returns std::nullopt on invalid input (bad length, non-digit chars, or grid corruption).
[[nodiscard]] std::optional<PinProof> computePinProof(
const std::string& pinDigits,
uint32_t pinGridSeed,
const std::array<uint8_t, 16>& serverSalt);
} // namespace auth
} // namespace wowee

View file

@ -48,6 +48,10 @@ public:
// Get session key (K) - used for encryption
std::vector<uint8_t> getSessionKey() const;
// Securely erase stored plaintext credentials from memory.
// Called automatically at the end of feed() once the SRP values are computed.
void clearCredentials();
private:
// WoW-specific SRP multiplier (k = 3)
static constexpr uint32_t K_VALUE = 3;

View file

@ -0,0 +1,50 @@
#pragma once
#include <cstdint>
#include <glm/glm.hpp>
namespace wowee {
namespace rendering { class Renderer; }
namespace game { class GameHandler; }
namespace core { class EntitySpawner; }
namespace core {
/// Handles animation callbacks: death, respawn, swing, hit reaction, spell cast, emote,
/// stun, stealth, health, ghost, stand state, loot, sprint, vehicle, charge.
/// Owns charge rush state (interpolated in update).
class AnimationCallbackHandler {
public:
AnimationCallbackHandler(EntitySpawner& entitySpawner,
rendering::Renderer& renderer,
game::GameHandler& gameHandler);
void setupCallbacks();
/// Called each frame from Application::update() to drive charge interpolation.
/// Returns true if charge is active (player is externally driven).
bool updateCharge(float deltaTime);
// Charge state queries (used by Application::update for externallyDrivenMotion)
bool isCharging() const { return chargeActive_; }
// Reset charge state (logout/disconnect)
void resetChargeState();
private:
EntitySpawner& entitySpawner_;
rendering::Renderer& renderer_;
game::GameHandler& gameHandler_;
// Charge rush state (moved from Application)
bool chargeActive_ = false;
float chargeTimer_ = 0.0f;
float chargeDuration_ = 0.0f;
glm::vec3 chargeStartPos_{0.0f}; // Render coordinates
glm::vec3 chargeEndPos_{0.0f}; // Render coordinates
uint64_t chargeTargetGuid_ = 0;
};
} // namespace core
} // namespace wowee

View file

@ -0,0 +1,107 @@
#pragma once
#include "game/character.hpp"
#include <string>
#include <vector>
#include <unordered_set>
#include <cstdint>
namespace wowee {
namespace rendering { class Renderer; }
namespace pipeline { class AssetManager; class DBCLayout; struct M2Model; }
namespace game { class GameHandler; }
namespace core {
class EntitySpawner;
// Default (bare) geoset IDs per equipment group.
// Each group's base is groupNumber * 100; variant 01 is typically bare/default.
constexpr uint16_t kGeosetDefaultConnector = 101; // Group 1: default hair connector
constexpr uint16_t kGeosetBareForearms = 401; // Group 4: no gloves
constexpr uint16_t kGeosetBareShins = 503; // Group 5: no boots
constexpr uint16_t kGeosetDefaultEars = 702; // Group 7: ears
constexpr uint16_t kGeosetBareSleeves = 801; // Group 8: no chest armor sleeves
constexpr uint16_t kGeosetDefaultKneepads = 902; // Group 9: kneepads
constexpr uint16_t kGeosetDefaultTabard = 1201; // Group 12: tabard base
constexpr uint16_t kGeosetBarePants = 1301; // Group 13: no leggings
constexpr uint16_t kGeosetNoCape = 1501; // Group 15: no cape
constexpr uint16_t kGeosetWithCape = 1502; // Group 15: with cape
constexpr uint16_t kGeosetBareFeet = 2002; // Group 20: bare feet
/// Resolved texture paths from CharSections.dbc for player character compositing.
struct PlayerTextureInfo {
std::string bodySkinPath;
std::string faceLowerPath;
std::string faceUpperPath;
std::string hairTexturePath;
std::vector<std::string> underwearPaths;
};
/// Handles player character visual appearance: skin compositing, geoset selection,
/// texture path lookups, and equipment weapon rendering.
class AppearanceComposer {
public:
AppearanceComposer(rendering::Renderer* renderer,
pipeline::AssetManager* assetManager,
game::GameHandler* gameHandler,
pipeline::DBCLayout* dbcLayout,
EntitySpawner* entitySpawner);
// Player model path resolution
std::string getPlayerModelPath(game::Race race, game::Gender gender) const;
// Resolve texture paths from CharSections.dbc and fill model texture slots.
// Call BEFORE charRenderer->loadModel().
PlayerTextureInfo resolvePlayerTextures(pipeline::M2Model& model,
game::Race race, game::Gender gender,
uint32_t appearanceBytes);
// Apply composited textures to loaded model instance.
// Call AFTER charRenderer->loadModel(). Saves skin state for re-compositing.
void compositePlayerSkin(uint32_t modelSlotId, const PlayerTextureInfo& texInfo);
// Build default active geosets for player character
std::unordered_set<uint16_t> buildDefaultPlayerGeosets(uint8_t raceId, uint8_t sexId,
uint8_t hairStyleId, uint8_t facialId);
// Equipment weapon loading (reads inventory, attaches weapon M2 models)
void loadEquippedWeapons();
// Weapon sheathe state
void setWeaponsSheathed(bool sheathed) { weaponsSheathed_ = sheathed; }
bool isWeaponsSheathed() const { return weaponsSheathed_; }
void toggleWeaponsSheathed() { weaponsSheathed_ = !weaponsSheathed_; }
// Ranged weapon swap: temporarily show ranged weapon in right hand
void showRangedWeapon(bool show);
bool isShowingRanged() const { return showingRanged_; }
// Saved skin state accessors (used by game_screen.cpp for equipment re-compositing)
const std::string& getBodySkinPath() const { return bodySkinPath_; }
const std::vector<std::string>& getUnderwearPaths() const { return underwearPaths_; }
uint32_t getSkinTextureSlotIndex() const { return skinTextureSlotIndex_; }
uint32_t getCloakTextureSlotIndex() const { return cloakTextureSlotIndex_; }
private:
bool loadWeaponM2(const std::string& m2Path, pipeline::M2Model& outModel);
rendering::Renderer* renderer_;
pipeline::AssetManager* assetManager_;
game::GameHandler* gameHandler_;
pipeline::DBCLayout* dbcLayout_;
EntitySpawner* entitySpawner_;
// Saved at spawn for skin re-compositing on equipment changes
std::string bodySkinPath_;
std::vector<std::string> underwearPaths_;
uint32_t skinTextureSlotIndex_ = 0;
uint32_t cloakTextureSlotIndex_ = 0;
bool weaponsSheathed_ = false;
bool showingRanged_ = false;
};
} // namespace core
} // namespace wowee

View file

@ -2,7 +2,11 @@
#include "core/window.hpp"
#include "core/input.hpp"
#include "core/entity_spawner.hpp"
#include "core/appearance_composer.hpp"
#include "core/world_loader.hpp"
#include "game/character.hpp"
#include "game/game_services.hpp"
#include "pipeline/blp_loader.hpp"
#include <memory>
#include <string>
@ -25,11 +29,20 @@ namespace ui { class UIManager; }
namespace auth { class AuthHandler; }
namespace game { class GameHandler; class World; class ExpansionRegistry; }
namespace pipeline { class AssetManager; class DBCLayout; struct M2Model; struct WMOModel; }
namespace audio { enum class VoiceType; }
namespace audio { enum class VoiceType; class AudioCoordinator; }
namespace addons { class AddonManager; }
namespace core {
// Handler forward declarations
class NPCInteractionCallbackHandler;
class AudioCallbackHandler;
class EntitySpawnCallbackHandler;
class AnimationCallbackHandler;
class TransportCallbackHandler;
class WorldEntryCallbackHandler;
class UIScreenCallbackHandler;
enum class AppState {
AUTHENTICATION,
REALM_SELECTION,
@ -40,6 +53,8 @@ enum class AppState {
};
class Application {
friend class WorldLoader;
public:
Application();
~Application();
@ -71,59 +86,47 @@ public:
// Singleton access
static Application& getInstance() { return *instance; }
// Weapon loading (called at spawn and on equipment change)
void loadEquippedWeapons();
// Logout to login screen
void logoutToLogin();
// Render bounds lookup (for click targeting / selection)
// Render bounds lookup (for click targeting / selection) — delegates to EntitySpawner
bool getRenderBoundsForGuid(uint64_t guid, glm::vec3& outCenter, float& outRadius) const;
bool getRenderFootZForGuid(uint64_t guid, float& outFootZ) const;
bool getRenderPositionForGuid(uint64_t guid, glm::vec3& outPos) const;
// Character skin composite state (saved at spawn for re-compositing on equipment change)
const std::string& getBodySkinPath() const { return bodySkinPath_; }
const std::vector<std::string>& getUnderwearPaths() const { return underwearPaths_; }
uint32_t getSkinTextureSlotIndex() const { return skinTextureSlotIndex_; }
uint32_t getCloakTextureSlotIndex() const { return cloakTextureSlotIndex_; }
uint32_t getGryphonDisplayId() const { return gryphonDisplayId_; }
uint32_t getWyvernDisplayId() const { return wyvernDisplayId_; }
// Character skin composite state — delegated to AppearanceComposer
const std::string& getBodySkinPath() const { return appearanceComposer_ ? appearanceComposer_->getBodySkinPath() : emptyString_; }
const std::vector<std::string>& getUnderwearPaths() const { return appearanceComposer_ ? appearanceComposer_->getUnderwearPaths() : emptyStringVec_; }
uint32_t getSkinTextureSlotIndex() const { return appearanceComposer_ ? appearanceComposer_->getSkinTextureSlotIndex() : 0; }
uint32_t getCloakTextureSlotIndex() const { return appearanceComposer_ ? appearanceComposer_->getCloakTextureSlotIndex() : 0; }
uint32_t getGryphonDisplayId() const { return entitySpawner_ ? entitySpawner_->getGryphonDisplayId() : 0; }
uint32_t getWyvernDisplayId() const { return entitySpawner_ ? entitySpawner_->getWyvernDisplayId() : 0; }
// Entity spawner access
EntitySpawner* getEntitySpawner() { return entitySpawner_.get(); }
// Appearance composer access
AppearanceComposer* getAppearanceComposer() { return appearanceComposer_.get(); }
// World loader access
WorldLoader* getWorldLoader() { return worldLoader_.get(); }
// Audio coordinator access
audio::AudioCoordinator* getAudioCoordinator() { return audioCoordinator_.get(); }
private:
void update(float deltaTime);
void render();
void setupUICallbacks();
void spawnPlayerCharacter();
std::string getPlayerModelPath() const;
static const char* mapIdToName(uint32_t mapId);
void loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z);
void buildFactionHostilityMap(uint8_t playerRace);
pipeline::M2Model loadCreatureM2Sync(const std::string& m2Path);
void spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation, float scale = 1.0f);
void despawnOnlineCreature(uint64_t guid);
bool tryAttachCreatureVirtualWeapons(uint64_t guid, uint32_t instanceId);
void spawnOnlinePlayer(uint64_t guid,
uint8_t raceId,
uint8_t genderId,
uint32_t appearanceBytes,
uint8_t facialFeatures,
float x, float y, float z, float orientation);
void setOnlinePlayerEquipment(uint64_t guid,
const std::array<uint32_t, 19>& displayInfoIds,
const std::array<uint8_t, 19>& inventoryTypes);
void despawnOnlinePlayer(uint64_t guid);
void buildCreatureDisplayLookups();
std::string getModelPathForDisplayId(uint32_t displayId) const;
void spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation, float scale = 1.0f);
void despawnOnlineGameObject(uint64_t guid);
void buildGameObjectDisplayLookups();
std::string getGameObjectModelPathForDisplayId(uint32_t displayId) const;
audio::VoiceType detectVoiceTypeFromDisplayId(uint32_t displayId) const;
void setupTestTransport(); // Test transport boat for development
static Application* instance;
game::GameServices gameServices_;
std::unique_ptr<Window> window;
std::unique_ptr<rendering::Renderer> renderer;
std::unique_ptr<ui::UIManager> uiManager;
@ -135,10 +138,22 @@ private:
bool addonsLoaded_ = false;
std::unique_ptr<game::ExpansionRegistry> expansionRegistry_;
std::unique_ptr<pipeline::DBCLayout> dbcLayout_;
std::unique_ptr<EntitySpawner> entitySpawner_;
std::unique_ptr<AppearanceComposer> appearanceComposer_;
std::unique_ptr<WorldLoader> worldLoader_;
std::unique_ptr<audio::AudioCoordinator> audioCoordinator_;
// Callback handlers (extracted from setupUICallbacks)
std::unique_ptr<NPCInteractionCallbackHandler> npcInteractionCallbacks_;
std::unique_ptr<AudioCallbackHandler> audioCallbacks_;
std::unique_ptr<EntitySpawnCallbackHandler> entitySpawnCallbacks_;
std::unique_ptr<AnimationCallbackHandler> animationCallbacks_;
std::unique_ptr<TransportCallbackHandler> transportCallbacks_;
std::unique_ptr<WorldEntryCallbackHandler> worldEntryCallbacks_;
std::unique_ptr<UIScreenCallbackHandler> uiScreenCallbacks_;
AppState state = AppState::AUTHENTICATION;
bool running = false;
std::string pendingCreatedCharacterName_; // Auto-select after character creation
bool playerCharacterSpawned = false;
bool npcsSpawned = false;
bool spawnSnapToGround = true;
@ -152,325 +167,20 @@ private:
uint32_t spawnedAppearanceBytes_ = 0;
uint8_t spawnedFacialFeatures_ = 0;
// Weapon model ID counter (starting high to avoid collision with character model IDs)
uint32_t nextWeaponModelId_ = 1000;
// Static empty values for null-safe delegation
static inline const std::string emptyString_;
static inline const std::vector<std::string> emptyStringVec_;
// Saved at spawn for skin re-compositing
std::string bodySkinPath_;
std::vector<std::string> underwearPaths_;
uint32_t skinTextureSlotIndex_ = 0;
uint32_t cloakTextureSlotIndex_ = 0;
// Online creature model spawning
struct CreatureDisplayData {
uint32_t modelId = 0;
std::string skin1, skin2, skin3; // Texture names from CreatureDisplayInfo.dbc
uint32_t extraDisplayId = 0; // Link to CreatureDisplayInfoExtra.dbc
};
struct HumanoidDisplayExtra {
uint8_t raceId = 0;
uint8_t sexId = 0;
uint8_t skinId = 0;
uint8_t faceId = 0;
uint8_t hairStyleId = 0;
uint8_t hairColorId = 0;
uint8_t facialHairId = 0;
std::string bakeName; // Pre-baked texture path if available
// Equipment display IDs (from columns 8-18)
// 0=helm, 1=shoulder, 2=shirt, 3=chest, 4=belt, 5=legs, 6=feet, 7=wrist, 8=hands, 9=tabard, 10=cape
uint32_t equipDisplayId[11] = {0};
};
std::unordered_map<uint32_t, CreatureDisplayData> displayDataMap_; // displayId → display data
std::unordered_map<uint32_t, HumanoidDisplayExtra> humanoidExtraMap_; // extraDisplayId → humanoid data
std::unordered_map<uint32_t, std::string> modelIdToPath_; // modelId → M2 path (from CreatureModelData.dbc)
// CharHairGeosets.dbc: key = (raceId<<16)|(sexId<<8)|variationId → geosetId (skinSectionId)
std::unordered_map<uint32_t, uint16_t> hairGeosetMap_;
// CharFacialHairStyles.dbc: key = (raceId<<16)|(sexId<<8)|variationId → {geoset100, geoset300, geoset200}
struct FacialHairGeosets { uint16_t geoset100 = 0; uint16_t geoset300 = 0; uint16_t geoset200 = 0; };
std::unordered_map<uint32_t, FacialHairGeosets> facialHairGeosetMap_;
std::unordered_map<uint64_t, uint32_t> creatureInstances_; // guid → render instanceId
std::unordered_map<uint64_t, uint32_t> creatureModelIds_; // guid → loaded modelId
std::unordered_map<uint64_t, glm::vec3> creatureRenderPosCache_; // guid -> last synced render position
std::unordered_map<uint64_t, bool> creatureWasMoving_; // guid -> previous-frame movement state
std::unordered_map<uint64_t, bool> creatureWasSwimming_; // guid -> previous-frame swim state (for anim transition detection)
std::unordered_map<uint64_t, bool> creatureWasFlying_; // guid -> previous-frame flying state (for anim transition detection)
std::unordered_map<uint64_t, bool> creatureWasWalking_; // guid -> previous-frame walking state (walk vs run transition detection)
std::unordered_map<uint64_t, bool> creatureSwimmingState_; // guid -> currently in swim mode (SWIMMING flag)
std::unordered_map<uint64_t, bool> creatureWalkingState_; // guid -> walking (WALKING flag, selects Walk(4) vs Run(5))
std::unordered_map<uint64_t, bool> creatureFlyingState_; // guid -> currently flying (FLYING flag)
std::unordered_set<uint64_t> creatureWeaponsAttached_; // guid set when NPC virtual weapons attached
std::unordered_map<uint64_t, uint8_t> creatureWeaponAttachAttempts_; // guid -> attach attempts
std::unordered_map<uint32_t, bool> modelIdIsWolfLike_; // modelId → cached wolf/worg check
static constexpr int MAX_WEAPON_ATTACHES_PER_TICK = 2; // limit weapon attach work per 1s tick
// CharSections.dbc lookup cache to avoid O(N) DBC scan per NPC spawn.
// Key: (race<<24)|(sex<<16)|(section<<12)|(variation<<8)|color → texture path
std::unordered_map<uint64_t, std::string> charSectionsCache_;
bool charSectionsCacheBuilt_ = false;
void buildCharSectionsCache();
std::string lookupCharSection(uint8_t race, uint8_t sex, uint8_t section,
uint8_t variation, uint8_t color, int texIndex = 0) const;
// Async creature model loading: file I/O + M2 parsing on background thread,
// GPU upload + instance creation on main thread.
struct PreparedCreatureModel {
uint64_t guid;
uint32_t displayId;
uint32_t modelId;
float x, y, z, orientation;
float scale = 1.0f;
std::shared_ptr<pipeline::M2Model> model; // parsed on background thread
std::unordered_map<std::string, pipeline::BLPImage> predecodedTextures; // decoded on bg thread
bool valid = false;
bool permanent_failure = false;
};
struct AsyncCreatureLoad {
std::future<PreparedCreatureModel> future;
};
std::vector<AsyncCreatureLoad> asyncCreatureLoads_;
std::unordered_set<uint32_t> asyncCreatureDisplayLoads_; // displayIds currently loading in background
void processAsyncCreatureResults(bool unlimited = false);
static constexpr int MAX_ASYNC_CREATURE_LOADS = 4; // concurrent background loads
std::unordered_set<uint64_t> deadCreatureGuids_; // GUIDs that should spawn in corpse/death pose
std::unordered_map<uint32_t, uint32_t> displayIdModelCache_; // displayId → modelId (model caching)
std::unordered_set<uint32_t> displayIdTexturesApplied_; // displayIds with per-model textures applied
std::unordered_map<uint32_t, std::unordered_map<std::string, pipeline::BLPImage>> displayIdPredecodedTextures_; // displayId → pre-decoded skin textures
mutable std::unordered_set<uint32_t> warnedMissingDisplayDataIds_; // displayIds already warned
mutable std::unordered_set<uint32_t> warnedMissingModelPathIds_; // modelIds/displayIds already warned
uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures
uint32_t gryphonDisplayId_ = 0;
uint32_t wyvernDisplayId_ = 0;
bool lastTaxiFlight_ = false;
uint32_t loadedMapId_ = 0xFFFFFFFF; // Map ID of currently loaded terrain (0xFFFFFFFF = none)
uint32_t worldLoadGeneration_ = 0; // Incremented on each world entry to detect re-entrant loads
bool loadingWorld_ = false; // True while loadOnlineWorldTerrain is running
struct PendingWorldEntry {
uint32_t mapId; float x, y, z;
};
std::optional<PendingWorldEntry> pendingWorldEntry_; // Deferred world entry during loading
float taxiLandingClampTimer_ = 0.0f;
float worldEntryMovementGraceTimer_ = 0.0f;
// Hearth teleport: freeze player until terrain loads at destination
bool hearthTeleportPending_ = false;
glm::vec3 hearthTeleportPos_{0.0f}; // render coords
float hearthTeleportTimer_ = 0.0f; // timeout safety
float facingSendCooldown_ = 0.0f; // Rate-limits MSG_MOVE_SET_FACING
float lastSentCanonicalYaw_ = 1000.0f; // Sentinel — triggers first send
float taxiStreamCooldown_ = 0.0f;
bool idleYawned_ = false;
// Charge rush state
bool chargeActive_ = false;
float chargeTimer_ = 0.0f;
float chargeDuration_ = 0.0f;
glm::vec3 chargeStartPos_{0.0f}; // Render coordinates
glm::vec3 chargeEndPos_{0.0f}; // Render coordinates
uint64_t chargeTargetGuid_ = 0;
// Online gameobject model spawning
struct GameObjectInstanceInfo {
uint32_t modelId = 0;
uint32_t instanceId = 0;
bool isWmo = false;
};
std::unordered_map<uint32_t, std::string> gameObjectDisplayIdToPath_;
std::unordered_map<uint32_t, uint32_t> gameObjectDisplayIdModelCache_; // displayId → M2 modelId
std::unordered_set<uint32_t> gameObjectDisplayIdFailedCache_; // displayIds that permanently fail to load
std::unordered_map<uint32_t, uint32_t> gameObjectDisplayIdWmoCache_; // displayId → WMO modelId
std::unordered_map<uint64_t, GameObjectInstanceInfo> gameObjectInstances_; // guid → instance info
struct PendingTransportMove {
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
float orientation = 0.0f;
};
struct PendingTransportRegistration {
uint64_t guid = 0;
uint32_t entry = 0;
uint32_t displayId = 0;
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
float orientation = 0.0f;
};
std::unordered_map<uint64_t, PendingTransportMove> pendingTransportMoves_; // guid -> latest pre-registration move
std::deque<PendingTransportRegistration> pendingTransportRegistrations_;
uint32_t nextGameObjectModelId_ = 20000;
uint32_t nextGameObjectWmoModelId_ = 40000;
bool testTransportSetup_ = false;
bool gameObjectLookupsBuilt_ = false;
// Mount model tracking
uint32_t mountInstanceId_ = 0;
uint32_t mountModelId_ = 0;
uint32_t pendingMountDisplayId_ = 0; // Deferred mount load (0 = none pending)
bool weaponsSheathed_ = false;
bool wasAutoAttacking_ = false;
void processPendingMount();
bool creatureLookupsBuilt_ = false;
bool mapNameCacheLoaded_ = false;
std::unordered_map<uint32_t, std::string> mapNameById_;
// Deferred creature spawn queue (throttles spawning to avoid hangs)
struct PendingCreatureSpawn {
uint64_t guid;
uint32_t displayId;
float x, y, z, orientation;
float scale = 1.0f;
};
std::deque<PendingCreatureSpawn> pendingCreatureSpawns_;
static constexpr int MAX_SPAWNS_PER_FRAME = 3;
static constexpr int MAX_NEW_CREATURE_MODELS_PER_FRAME = 1;
static constexpr uint16_t MAX_CREATURE_SPAWN_RETRIES = 300;
std::unordered_set<uint64_t> pendingCreatureSpawnGuids_;
std::unordered_map<uint64_t, uint16_t> creatureSpawnRetryCounts_;
std::unordered_set<uint32_t> nonRenderableCreatureDisplayIds_;
// Online player instances (separate from creatures so we can apply per-player skin/hair textures).
std::unordered_map<uint64_t, uint32_t> playerInstances_; // guid → render instanceId
struct OnlinePlayerAppearanceState {
uint32_t instanceId = 0;
uint32_t modelId = 0;
uint8_t raceId = 0;
uint8_t genderId = 0;
uint32_t appearanceBytes = 0;
uint8_t facialFeatures = 0;
std::string bodySkinPath;
std::vector<std::string> underwearPaths;
};
std::unordered_map<uint64_t, OnlinePlayerAppearanceState> onlinePlayerAppearance_;
std::unordered_map<uint64_t, std::pair<std::array<uint32_t, 19>, std::array<uint8_t, 19>>> pendingOnlinePlayerEquipment_;
// Deferred equipment compositing queue — processes max 1 per frame to avoid stutter
std::vector<std::pair<uint64_t, std::pair<std::array<uint32_t, 19>, std::array<uint8_t, 19>>>> deferredEquipmentQueue_;
void processDeferredEquipmentQueue();
// Async equipment texture pre-decode: BLP decode on background thread, composite on main thread
struct PreparedEquipmentUpdate {
uint64_t guid;
std::array<uint32_t, 19> displayInfoIds;
std::array<uint8_t, 19> inventoryTypes;
std::unordered_map<std::string, pipeline::BLPImage> predecodedTextures;
};
struct AsyncEquipmentLoad {
std::future<PreparedEquipmentUpdate> future;
};
std::vector<AsyncEquipmentLoad> asyncEquipmentLoads_;
void processAsyncEquipmentResults();
std::vector<std::string> resolveEquipmentTexturePaths(uint64_t guid,
const std::array<uint32_t, 19>& displayInfoIds,
const std::array<uint8_t, 19>& inventoryTypes) const;
// Deferred NPC texture setup — async DBC lookups + BLP pre-decode to avoid main-thread stalls
struct DeferredNpcComposite {
uint32_t modelId;
uint32_t displayId;
// Skin compositing (type-1 slots)
std::string basePath; // CharSections skin base texture
std::vector<std::string> overlayPaths; // face + underwear overlays
std::vector<std::pair<int, std::string>> regionLayers; // equipment region overlays
std::vector<uint32_t> skinTextureSlots; // model texture slots needing skin composite
bool hasComposite = false; // needs compositing (overlays or equipment regions)
bool hasSimpleSkin = false; // just base skin, no compositing needed
// Baked skin (type-1 slots)
std::string bakedSkinPath; // baked texture path (if available)
bool hasBakedSkin = false; // baked skin resolved successfully
// Hair (type-6 slots)
std::vector<uint32_t> hairTextureSlots; // model texture slots needing hair texture
std::string hairTexturePath; // resolved hair texture path
bool useBakedForHair = false; // bald NPC: use baked skin for type-6
};
struct PreparedNpcComposite {
DeferredNpcComposite info;
std::unordered_map<std::string, pipeline::BLPImage> predecodedTextures;
};
struct AsyncNpcCompositeLoad {
std::future<PreparedNpcComposite> future;
};
std::vector<AsyncNpcCompositeLoad> asyncNpcCompositeLoads_;
void processAsyncNpcCompositeResults(bool unlimited = false);
// Cache base player model geometry by (raceId, genderId)
std::unordered_map<uint32_t, uint32_t> playerModelCache_; // key=(race<<8)|gender → modelId
struct PlayerTextureSlots { int skin = -1; int hair = -1; int underwear = -1; };
std::unordered_map<uint32_t, PlayerTextureSlots> playerTextureSlotsByModelId_;
uint32_t nextPlayerModelId_ = 60000;
struct PendingPlayerSpawn {
uint64_t guid;
uint8_t raceId;
uint8_t genderId;
uint32_t appearanceBytes;
uint8_t facialFeatures;
float x, y, z, orientation;
};
std::vector<PendingPlayerSpawn> pendingPlayerSpawns_;
std::unordered_set<uint64_t> pendingPlayerSpawnGuids_;
void processPlayerSpawnQueue();
std::unordered_set<uint64_t> creaturePermanentFailureGuids_;
void processCreatureSpawnQueue(bool unlimited = false);
struct PendingGameObjectSpawn {
uint64_t guid;
uint32_t entry;
uint32_t displayId;
float x, y, z, orientation;
float scale = 1.0f;
};
std::vector<PendingGameObjectSpawn> pendingGameObjectSpawns_;
void processGameObjectSpawnQueue();
// Async WMO loading for game objects (file I/O + parse on background thread)
struct PreparedGameObjectWMO {
uint64_t guid;
uint32_t entry;
uint32_t displayId;
float x, y, z, orientation;
float scale = 1.0f;
std::shared_ptr<pipeline::WMOModel> wmoModel;
std::unordered_map<std::string, pipeline::BLPImage> predecodedTextures; // decoded on bg thread
bool valid = false;
bool isWmo = false;
std::string modelPath;
};
struct AsyncGameObjectLoad {
std::future<PreparedGameObjectWMO> future;
};
std::vector<AsyncGameObjectLoad> asyncGameObjectLoads_;
void processAsyncGameObjectResults();
struct PendingTransportDoodadBatch {
uint64_t guid = 0;
uint32_t modelId = 0;
uint32_t instanceId = 0;
size_t nextIndex = 0;
size_t doodadBudget = 0;
size_t spawnedDoodads = 0;
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
float orientation = 0.0f;
};
std::vector<PendingTransportDoodadBatch> pendingTransportDoodadBatches_;
static constexpr size_t MAX_TRANSPORT_DOODADS_PER_FRAME = 4;
void processPendingTransportRegistrations();
void processPendingTransportDoodads();
// Quest marker billboard sprites (above NPCs)
void loadQuestMarkerModels(); // Now loads BLP textures
void updateQuestMarkers(); // Updates billboard positions
// Background world preloader — warms AssetManager file cache for the
// expected world before the user clicks Enter World.
struct WorldPreload {
uint32_t mapId = 0;
std::string mapName;
int centerTileX = 0;
int centerTileY = 0;
std::atomic<bool> cancel{false};
std::vector<std::thread> workers;
};
std::unique_ptr<WorldPreload> worldPreload_;
void startWorldPreload(uint32_t mapId, const std::string& mapName, float serverX, float serverY);
void cancelWorldPreload();
void saveLastWorldInfo(uint32_t mapId, const std::string& mapName, float serverX, float serverY);
struct LastWorldInfo { uint32_t mapId = 0; std::string mapName; float x = 0, y = 0; bool valid = false; };
LastWorldInfo loadLastWorldInfo() const;
};
} // namespace core

Some files were not shown because too many files have changed in this diff Show more