Compare commits

...

10 commits

Author SHA1 Message Date
Kelsi
60c26a17aa Fix audio playback not stopping when Stop button clicked
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
Use spawn context for clean subprocess isolation and add kill() fallback
after terminate() to ensure the audio process is reliably stopped.
2026-02-23 22:26:17 -08:00
Kelsi
55faacef96 Add M2/WMO 3D viewer with textured rendering, animation, and audio playback
- New tools/m2_viewer.py: Pygame/OpenGL viewer for M2 models (textured
  rendering, skeletal animation, orbit camera) and WMO buildings
- M2 viewer: per-batch texture mapping, CPU vertex skinning, animation
  playback with play/pause/speed controls, wireframe overlay toggle
- WMO viewer: root+group file parsing (MOTX/MOMT/MOVT/MOVI/MONR/MOTV/MOBA),
  per-batch material rendering with BLP textures
- Asset browser: "Open 3D Viewer" buttons for M2 and WMO previews,
  audio Play/Stop buttons using pygame.mixer in subprocess
- Handles both WotLK (v264) and Vanilla (v256) M2 formats
2026-02-23 22:22:39 -08:00
Kelsi
edf0a40759 Fix asset browser BLP errors, M2 wireframes, and add anim filter
- BLP: blp_convert takes one arg, not two; was passing an output path
  that caused conversion failures
- M2: vertex header offsets were wrong (used 80/100 instead of 60/68),
  producing garbage vertex counts that failed the sanity check
- Add "Hide .anim/.skin" checkbox (on by default) to filter ~30k
  companion files from the directory tree
2026-02-23 20:51:33 -08:00
Kelsi
6e2d51b325 Fix asset browser hanging on launch with large manifests
Manifest keys use backslashes but tree splitting used forward slashes,
causing all 241k entries to land at root level. Combined with O(N)
any(startswith) checks per entry, this produced an O(N^2) hang. Re-key
manifest by the forward-slash 'p' field and build a directory index in
a single O(N) pass so tree operations are O(1) lookups.
2026-02-23 20:45:19 -08:00
Kelsi
2a1bd12f5b Fix game objects rendering with player textures
When M2Renderer's descriptor pool was exhausted, batch.materialSet
would be VK_NULL_HANDLE and the bind was skipped, but the draw call
still executed using the previously bound descriptor set from
CharacterRenderer — causing game objects to render with the player's
skin/armor textures. Skip the entire batch instead.
2026-02-23 20:41:06 -08:00
Kelsi
739ae5b569 Add Asset Browser tab to pipeline GUI with inline previews
New tab lets users explore extracted assets visually: BLP textures
rendered via blp_convert+Pillow, M2/WMO wireframes with mouse-drag
rotation and zoom, DBC/CSV tables with named columns from
dbc_layouts.json, ADT heightmap grids, text file viewer, audio
metadata, and hex dumps. Directory tree lazy-loads from manifest.json
with search and file-type filtering.
2026-02-23 20:35:32 -08:00
Kelsi
ef04dde2fe Update asset pipeline GUI documentation
Cover all new features: cancel extraction, background override rebuild,
zip-slip protection, extractor auto-detection order, readonly dropdowns,
and selection preservation. Reorganize into per-tab sections with tables.
2026-02-23 20:09:26 -08:00
Kelsi
f95770720f Fix security, bugs, and UX in asset pipeline GUI
- Fix zip slip vulnerability: validate extracted paths stay within target
- Fix redundant mkdir before rmtree in rebuild_override()
- Add build/asset_extract and Windows .ps1 fallback to extractor search
- Preserve pack list selection across refreshes
- Add Cancel Extraction button with process.terminate()
- Run override rebuild in background thread to avoid UI freeze
- Fix locale combobox state to readonly
- Add asset_pipeline/ to .gitignore
- Make script executable
2026-02-23 20:06:41 -08:00
Kelsi
20dd5ed63b Add cross-platform Python asset pipeline GUI 2026-02-23 20:03:07 -08:00
Kelsi
aaab2115d1 Fix all remaining build warnings and eliminate UB in binary parsers
Resolve 57 compiler warnings (unused params/vars, ignored return values,
enum mismatch) and replace undefined-behavior reinterpret_cast with
memcpy in DBC, BLP, and Warden module loaders for ARM64 portability.
2026-02-23 19:58:38 -08:00
18 changed files with 4686 additions and 106 deletions

3
.gitignore vendored
View file

@ -89,6 +89,9 @@ Data/expansions/*/overlay/
Data/hd/
ingest/
# Asset pipeline state and texture packs
asset_pipeline/
# Local texture dumps / extracted art should never be committed
assets/textures/
node_modules/

View file

@ -106,6 +106,9 @@ This project requires WoW client data that you extract from your own legally obt
Wowee loads assets via an extracted loose-file tree indexed by `manifest.json` (it does not read MPQs at runtime).
For a cross-platform GUI workflow (extraction + texture pack management + active override state), see:
- [Asset Pipeline GUI](docs/asset-pipeline-gui.md)
#### 1) Extract MPQs into `./Data/`
```bash
@ -196,6 +199,7 @@ make -j$(nproc)
- [Project Status](docs/status.md) -- Current code state, limitations, and near-term direction
- [Quick Start](docs/quickstart.md) -- Installation and first steps
- [Build Instructions](BUILD_INSTRUCTIONS.md) -- Detailed dependency, build, and run guide
- [Asset Pipeline GUI](docs/asset-pipeline-gui.md) -- Python GUI for extraction, pack installs, ordering, and override rebuilds
### Technical Documentation
- [Architecture](docs/architecture.md) -- System design and module overview

194
docs/asset-pipeline-gui.md Normal file
View file

@ -0,0 +1,194 @@
# Asset Pipeline GUI
WoWee includes a Python GUI for extraction and texture-pack management:
```bash
python3 tools/asset_pipeline_gui.py
```
The script is also executable directly: `./tools/asset_pipeline_gui.py`
## Supported Platforms
- Linux
- macOS
- Windows
The app uses Python's built-in `tkinter` module. If `tkinter` is missing, install the platform package:
- Linux (Debian/Ubuntu): `sudo apt install python3-tk`
- Fedora: `sudo dnf install python3-tkinter`
- Arch: `sudo pacman -S tk`
- macOS: use the official Python.org installer (includes Tk)
- Windows: use the official Python installer and enable Tcl/Tk support
## What It Does
- Runs `asset_extract` (or shell/PowerShell script fallback) to extract MPQ data
- Saves extraction config in `asset_pipeline/state.json`
- Installs texture packs from ZIP or folders (with zip-slip protection)
- Lets users activate/deactivate packs and reorder active pack priority
- Rebuilds `Data/override` from active pack order (runs in background thread)
- Shows current data state (`manifest.json`, entry count, override file count, last runs)
- Browses extracted assets with inline previews (images, 3D wireframes, data tables, text, hex dumps)
## Configuration Tab
### Path Settings
| Field | Description |
|-------|-------------|
| **WoW Data (MPQ source)** | Path to your WoW client's `Data/` folder containing `.MPQ` files |
| **Output Data directory** | Where extracted assets land. Defaults to `<project root>/Data` |
| **Extractor binary/script** | Optional. Leave blank for auto-detection (see below) |
### Extractor Auto-Detection
When no extractor path is configured, the GUI searches in order:
1. `build/bin/asset_extract` — CMake build with bin subdirectory
2. `build/asset_extract` — CMake build without bin subdirectory
3. `bin/asset_extract` — standalone binary
4. **Windows only**: `extract_assets.ps1` — invoked via `powershell -ExecutionPolicy Bypass -File`
5. **Linux/macOS only**: `extract_assets.sh` — invoked via `bash`
On Windows, `.exe` is appended to binary candidates automatically.
### Extraction Options
| Option | Description |
|--------|-------------|
| **Expansion** | `auto`, `classic`, `turtle`, `tbc`, or `wotlk`. Read-only dropdown. |
| **Locale** | `auto`, `enUS`, `enGB`, `deDE`, `frFR`, etc. Read-only dropdown. |
| **Threads** | Worker thread count. 0 = auto (uses all cores). |
| **Skip DBC extraction** | Skip database client files (faster if you only want textures). |
| **Generate DBC CSV** | Output human-readable CSV alongside binary DBC files. |
| **Verify CRC** | Check file integrity during extraction (slower but safer). |
| **Verbose output** | More detail in the Logs tab. |
### Buttons
| Button | Action |
|--------|--------|
| **Save Configuration** | Writes all settings to `asset_pipeline/state.json`. |
| **Run Extraction** | Starts the extractor in a background thread. Output streams to the Logs tab. |
| **Cancel Extraction** | Terminates a running extraction. Grayed out when idle, active during extraction. |
| **Refresh State** | Reloads the Current State tab. |
## Texture Packs Tab
### Installing Packs
- **Install ZIP**: Opens a file picker for `.zip` archives. Each member path is validated against zip-slip attacks before extraction.
- **Install Folder**: Opens a folder picker and copies the entire folder into the pipeline's internal pack storage.
### Managing Packs
| Button | Action |
|--------|--------|
| **Activate** | Adds the selected pack to the active override list. |
| **Deactivate** | Removes the selected pack from the active list (stays installed). |
| **Move Up / Move Down** | Changes priority order. Pack #1 is the base layer; higher numbers override lower. |
| **Rebuild Override** | Merges all active packs into `Data/override/` in a background thread. UI stays responsive. |
| **Uninstall** | Removes the pack from disk after confirmation. |
Pack list selection is preserved across refreshes — you can activate a pack and immediately reorder it without re-selecting.
## Pack Format
Supported pack layouts:
1. `PackName/Data/...`
2. `PackName/data/...`
3. `PackName/...` where top folders include game folders (`Interface`, `World`, `Character`, `Textures`, `Sound`)
4. Single wrapper directory containing any of the above
When multiple active packs contain the same file path, **later packs in active order win**.
## Asset Browser Tab
Browse and preview every extracted asset visually. Requires a completed extraction with a `manifest.json` in the output directory.
### Layout
- **Top bar**: Search entry, file type filter dropdown, Search/Reset buttons, result count
- **Left panel** (~30%): Directory tree built lazily from `manifest.json`
- **Right panel** (~70%): Preview area that adapts to the selected file type
- **Bottom bar**: File path, size, and CRC from manifest
### Search and Filtering
Type a substring into the search box and/or pick a file type from the dropdown, then click **Search**. The tree repopulates with matching results (capped at 5000 entries). Click **Reset** to restore the full tree.
File type filters: All, BLP, M2, WMO, DBC, ADT, Audio, Text.
### Preview Types
| Type | What You See |
|------|--------------|
| **BLP** | Converted to PNG via `blp_convert --to-png`, displayed as an image. Cached in `asset_pipeline/preview_cache/`. |
| **M2** | Wireframe rendering of model vertices and triangles on a Canvas. Drag to rotate, scroll to zoom. |
| **WMO** | Wireframe of group geometry (MOVT/MOVI chunks). Root WMOs auto-load the `_000` group file. |
| **CSV** (DBC exports) | Scrollable table with column names from `dbc_layouts.json`. First 500 rows loaded, click "Load more" for the rest. |
| **ADT** | Colored heightmap grid parsed from MCNK chunks. |
| **Text** (XML, LUA, JSON, HTML, TOC) | Syntax-highlighted scrollable text view. |
| **Audio** (WAV, MP3, OGG) | Metadata display — format, channels, sample rate, duration (WAV). |
| **Other** | Hex dump of the first 512 bytes. |
### Wireframe Controls
- **Left-click drag**: Rotate the model (azimuth + elevation)
- **Scroll wheel**: Zoom in/out
- Depth coloring: closer geometry renders brighter
### Optional Dependencies
| Dependency | Required For | Fallback |
|------------|-------------|----------|
| [Pillow](https://pypi.org/project/Pillow/) (`pip install Pillow`) | BLP image preview | Shows install instructions |
| `blp_convert` (built with project) | BLP → PNG conversion | Shows "not found" message |
All other previews (wireframe, table, text, hex) work without any extra dependencies.
### Cache
BLP previews are cached as PNG files in `asset_pipeline/preview_cache/` keyed by path and file size. Delete this directory to clear the cache.
## Current State Tab
Shows a summary of pipeline state:
- Output directory existence and `manifest.json` entry count
- Override folder file count and last build timestamp
- Installed and active pack counts with priority order
- Last extraction time, success/failure, and the exact command used
- Paths to the state file and packs directory
Click **Refresh** to reload, or it auto-refreshes after operations.
## Logs Tab
All extraction output, override rebuild messages, cancellations, and errors stream here in real time via a log queue polled every 120ms. Click **Clear Logs** to reset.
## State Files and Folders
| Path | Description |
|------|-------------|
| `asset_pipeline/state.json` | All configuration, pack metadata, and extraction history |
| `asset_pipeline/packs/<pack-id>/` | Installed pack contents (one directory per pack) |
| `asset_pipeline/preview_cache/` | Cached BLP → PNG conversions for the Asset Browser |
| `<Output Data>/override/` | Merged output from active packs |
The `asset_pipeline/` directory is gitignored.
## Typical Workflow
1. Launch: `python3 tools/asset_pipeline_gui.py`
2. **Configuration tab**: Browse to your WoW `Data/` folder, pick expansion, click **Save Configuration**.
3. Click **Run Extraction** — watch progress in the **Logs** tab. Cancel with **Cancel Extraction** if needed.
4. Switch to **Texture Packs** tab. Click **Install ZIP** and pick a texture pack.
5. Select the pack and click **Activate**.
6. (Optional) Install more packs, activate them, and use **Move Up/Down** to set priority.
7. Click **Rebuild Override** — the status bar shows progress, and the result appears in Logs.
8. (Optional) Switch to **Asset Browser** to explore extracted files — preview textures, inspect models, browse DBC tables.
9. Run wowee — it loads override textures on top of the extracted base assets.

View file

@ -92,8 +92,8 @@ inline ProcessHandle spawnProcess(const std::vector<std::string>& args) {
if (pid == 0) {
// Child process
setpgid(0, 0);
freopen("/dev/null", "w", stdout);
freopen("/dev/null", "w", stderr);
if (!freopen("/dev/null", "w", stdout)) { _exit(1); }
if (!freopen("/dev/null", "w", stderr)) { _exit(1); }
// Build argv for exec
std::vector<const char*> argv;

View file

@ -91,7 +91,7 @@ void ActivitySoundManager::shutdown() {
assetManager = nullptr;
}
void ActivitySoundManager::update(float deltaTime) {
void ActivitySoundManager::update([[maybe_unused]] float deltaTime) {
reapProcesses();
// Play swimming stroke sounds periodically when swimming and moving
@ -168,7 +168,7 @@ void ActivitySoundManager::rebuildJumpClipsForProfile(const std::string& raceFol
}
}
void ActivitySoundManager::rebuildSwimLoopClipsForProfile(const std::string& raceFolder, const std::string& raceBase, bool male) {
void ActivitySoundManager::rebuildSwimLoopClipsForProfile([[maybe_unused]] const std::string& raceFolder, [[maybe_unused]] const std::string& raceBase, [[maybe_unused]] bool male) {
swimLoopClips.clear();
// WoW 3.3.5a doesn't have dedicated swim loop sounds

View file

@ -117,10 +117,10 @@ bool AmbientSoundManager::initialize(pipeline::AssetManager* assets) {
bool forestNightLoaded = loadSound("Sound\\Ambience\\ZoneAmbience\\ForestNormalNight.wav", forestNormalNightSounds_[0], assets);
forestSnowDaySounds_.resize(1);
bool forestSnowDayLoaded = loadSound("Sound\\Ambience\\ZoneAmbience\\ForestSnowDay.wav", forestSnowDaySounds_[0], assets);
loadSound("Sound\\Ambience\\ZoneAmbience\\ForestSnowDay.wav", forestSnowDaySounds_[0], assets);
forestSnowNightSounds_.resize(1);
bool forestSnowNightLoaded = loadSound("Sound\\Ambience\\ZoneAmbience\\ForestSnowNight.wav", forestSnowNightSounds_[0], assets);
loadSound("Sound\\Ambience\\ZoneAmbience\\ForestSnowNight.wav", forestSnowNightSounds_[0], assets);
beachDaySounds_.resize(1);
bool beachDayLoaded = loadSound("Sound\\Ambience\\ZoneAmbience\\BeachDay.wav", beachDaySounds_[0], assets);
@ -129,34 +129,34 @@ bool AmbientSoundManager::initialize(pipeline::AssetManager* assets) {
bool beachNightLoaded = loadSound("Sound\\Ambience\\ZoneAmbience\\BeachNight.wav", beachNightSounds_[0], assets);
grasslandsDaySounds_.resize(1);
bool grasslandsDayLoaded = loadSound("Sound\\Ambience\\ZoneAmbience\\GrasslandsDay.wav", grasslandsDaySounds_[0], assets);
loadSound("Sound\\Ambience\\ZoneAmbience\\GrasslandsDay.wav", grasslandsDaySounds_[0], assets);
grasslandsNightSounds_.resize(1);
bool grasslandsNightLoaded = loadSound("Sound\\Ambience\\ZoneAmbience\\GrassLandsNight.wav", grasslandsNightSounds_[0], assets);
loadSound("Sound\\Ambience\\ZoneAmbience\\GrassLandsNight.wav", grasslandsNightSounds_[0], assets);
jungleDaySounds_.resize(1);
bool jungleDayLoaded = loadSound("Sound\\Ambience\\ZoneAmbience\\JungleDay.wav", jungleDaySounds_[0], assets);
loadSound("Sound\\Ambience\\ZoneAmbience\\JungleDay.wav", jungleDaySounds_[0], assets);
jungleNightSounds_.resize(1);
bool jungleNightLoaded = loadSound("Sound\\Ambience\\ZoneAmbience\\JungleNight.wav", jungleNightSounds_[0], assets);
loadSound("Sound\\Ambience\\ZoneAmbience\\JungleNight.wav", jungleNightSounds_[0], assets);
marshDaySounds_.resize(1);
bool marshDayLoaded = loadSound("Sound\\Ambience\\ZoneAmbience\\MarshDay.wav", marshDaySounds_[0], assets);
loadSound("Sound\\Ambience\\ZoneAmbience\\MarshDay.wav", marshDaySounds_[0], assets);
marshNightSounds_.resize(1);
bool marshNightLoaded = loadSound("Sound\\Ambience\\ZoneAmbience\\MarshNight.wav", marshNightSounds_[0], assets);
loadSound("Sound\\Ambience\\ZoneAmbience\\MarshNight.wav", marshNightSounds_[0], assets);
desertCanyonDaySounds_.resize(1);
bool desertCanyonDayLoaded = loadSound("Sound\\Ambience\\ZoneAmbience\\CanyonDesertDay.wav", desertCanyonDaySounds_[0], assets);
desertCanyonNightSounds_.resize(1);
bool desertCanyonNightLoaded = loadSound("Sound\\Ambience\\ZoneAmbience\\CanyonDesertNight.wav", desertCanyonNightSounds_[0], assets);
loadSound("Sound\\Ambience\\ZoneAmbience\\CanyonDesertNight.wav", desertCanyonNightSounds_[0], assets);
desertPlainsDaySounds_.resize(1);
bool desertPlainsDayLoaded = loadSound("Sound\\Ambience\\ZoneAmbience\\PlainsDesertDay.wav", desertPlainsDaySounds_[0], assets);
desertPlainsNightSounds_.resize(1);
bool desertPlainsNightLoaded = loadSound("Sound\\Ambience\\ZoneAmbience\\PlainsDesertNight.wav", desertPlainsNightSounds_[0], assets);
loadSound("Sound\\Ambience\\ZoneAmbience\\PlainsDesertNight.wav", desertPlainsNightSounds_[0], assets);
// Load city ambience sounds (day and night where available)
stormwindDaySounds_.resize(1);
@ -169,10 +169,10 @@ bool AmbientSoundManager::initialize(pipeline::AssetManager* assets) {
bool ironforgeLoaded = loadSound("Sound\\Ambience\\WMOAmbience\\Ironforge.wav", ironforgeSounds_[0], assets);
darnassusDaySounds_.resize(1);
bool darnassusDayLoaded = loadSound("Sound\\Ambience\\WMOAmbience\\DarnassusDay.wav", darnassusDaySounds_[0], assets);
loadSound("Sound\\Ambience\\WMOAmbience\\DarnassusDay.wav", darnassusDaySounds_[0], assets);
darnassusNightSounds_.resize(1);
bool darnassusNightLoaded = loadSound("Sound\\Ambience\\WMOAmbience\\DarnassusNight.wav", darnassusNightSounds_[0], assets);
loadSound("Sound\\Ambience\\WMOAmbience\\DarnassusNight.wav", darnassusNightSounds_[0], assets);
orgrimmarDaySounds_.resize(1);
bool orgrimmarDayLoaded = loadSound("Sound\\Ambience\\WMOAmbience\\OrgrimmarDay.wav", orgrimmarDaySounds_[0], assets);
@ -181,13 +181,13 @@ bool AmbientSoundManager::initialize(pipeline::AssetManager* assets) {
bool orgrimmarNightLoaded = loadSound("Sound\\Ambience\\WMOAmbience\\OrgrimmarNight.wav", orgrimmarNightSounds_[0], assets);
undercitySounds_.resize(1);
bool undercityLoaded = loadSound("Sound\\Ambience\\WMOAmbience\\Undercity.wav", undercitySounds_[0], assets);
loadSound("Sound\\Ambience\\WMOAmbience\\Undercity.wav", undercitySounds_[0], assets);
thunderbluffDaySounds_.resize(1);
bool thunderbluffDayLoaded = loadSound("Sound\\Ambience\\WMOAmbience\\ThunderBluffDay.wav", thunderbluffDaySounds_[0], assets);
loadSound("Sound\\Ambience\\WMOAmbience\\ThunderBluffDay.wav", thunderbluffDaySounds_[0], assets);
thunderbluffNightSounds_.resize(1);
bool thunderbluffNightLoaded = loadSound("Sound\\Ambience\\WMOAmbience\\ThunderBluffNight.wav", thunderbluffNightSounds_[0], assets);
loadSound("Sound\\Ambience\\WMOAmbience\\ThunderBluffNight.wav", thunderbluffNightSounds_[0], assets);
// Load bell toll sounds
bellAllianceSounds_.resize(1);

View file

@ -295,7 +295,7 @@ void CombatSoundManager::playWeaponMiss(bool twoHanded) {
}
}
void CombatSoundManager::playImpact(WeaponSize weaponSize, ImpactType impactType, bool isCrit) {
void CombatSoundManager::playImpact([[maybe_unused]] WeaponSize weaponSize, ImpactType impactType, bool isCrit) {
// Select appropriate impact sound library
const std::vector<CombatSample>* normalLibrary = nullptr;
const std::vector<CombatSample>* critLibrary = nullptr;

View file

@ -34,16 +34,16 @@ bool UiSoundManager::initialize(pipeline::AssetManager* assets) {
bool charSheetCloseLoaded = loadSound("Sound\\Interface\\iAbilitiesCloseA.wav", characterSheetCloseSounds_[0], assets);
auctionOpenSounds_.resize(1);
bool auctionOpenLoaded = loadSound("Sound\\Interface\\AuctionWindowOpen.wav", auctionOpenSounds_[0], assets);
loadSound("Sound\\Interface\\AuctionWindowOpen.wav", auctionOpenSounds_[0], assets);
auctionCloseSounds_.resize(1);
bool auctionCloseLoaded = loadSound("Sound\\Interface\\AuctionWindowClose.wav", auctionCloseSounds_[0], assets);
loadSound("Sound\\Interface\\AuctionWindowClose.wav", auctionCloseSounds_[0], assets);
guildBankOpenSounds_.resize(1);
bool guildBankOpenLoaded = loadSound("Sound\\Interface\\GuildVaultOpen.wav", guildBankOpenSounds_[0], assets);
loadSound("Sound\\Interface\\GuildVaultOpen.wav", guildBankOpenSounds_[0], assets);
guildBankCloseSounds_.resize(1);
bool guildBankCloseLoaded = loadSound("Sound\\Interface\\GuildVaultClose.wav", guildBankCloseSounds_[0], assets);
loadSound("Sound\\Interface\\GuildVaultClose.wav", guildBankCloseSounds_[0], assets);
// Load button sounds
buttonClickSounds_.resize(1);
@ -63,7 +63,7 @@ bool UiSoundManager::initialize(pipeline::AssetManager* assets) {
bool questFailedLoaded = loadSound("Sound\\Interface\\igQuestFailed.wav", questFailedSounds_[0], assets);
questUpdateSounds_.resize(1);
bool questUpdateLoaded = loadSound("Sound\\Interface\\iQuestUpdate.wav", questUpdateSounds_[0], assets);
loadSound("Sound\\Interface\\iQuestUpdate.wav", questUpdateSounds_[0], assets);
// Load loot sounds
lootCoinSmallSounds_.resize(1);
@ -86,13 +86,13 @@ bool UiSoundManager::initialize(pipeline::AssetManager* assets) {
bool pickupBookLoaded = loadSound("Sound\\Interface\\PickUp\\PickUpBook.wav", pickupBookSounds_[0], assets);
pickupClothSounds_.resize(1);
bool pickupClothLoaded = loadSound("Sound\\Interface\\PickUp\\PickUpCloth_Leather01.wav", pickupClothSounds_[0], assets);
loadSound("Sound\\Interface\\PickUp\\PickUpCloth_Leather01.wav", pickupClothSounds_[0], assets);
pickupFoodSounds_.resize(1);
bool pickupFoodLoaded = loadSound("Sound\\Interface\\PickUp\\PickUpFoodGeneric.wav", pickupFoodSounds_[0], assets);
loadSound("Sound\\Interface\\PickUp\\PickUpFoodGeneric.wav", pickupFoodSounds_[0], assets);
pickupGemSounds_.resize(1);
bool pickupGemLoaded = loadSound("Sound\\Interface\\PickUp\\PickUpGems.wav", pickupGemSounds_[0], assets);
loadSound("Sound\\Interface\\PickUp\\PickUpGems.wav", pickupGemSounds_[0], assets);
// Load eating/drinking sounds
eatingSounds_.resize(1);
@ -107,13 +107,13 @@ bool UiSoundManager::initialize(pipeline::AssetManager* assets) {
// Load error/feedback sounds
errorSounds_.resize(1);
bool errorLoaded = loadSound("Sound\\Interface\\Error.wav", errorSounds_[0], assets);
loadSound("Sound\\Interface\\Error.wav", errorSounds_[0], assets);
selectTargetSounds_.resize(1);
bool selectTargetLoaded = loadSound("Sound\\Interface\\iSelectTarget.wav", selectTargetSounds_[0], assets);
loadSound("Sound\\Interface\\iSelectTarget.wav", selectTargetSounds_[0], assets);
deselectTargetSounds_.resize(1);
bool deselectTargetLoaded = loadSound("Sound\\Interface\\iDeselectTarget.wav", deselectTargetSounds_[0], assets);
loadSound("Sound\\Interface\\iDeselectTarget.wav", deselectTargetSounds_[0], assets);
LOG_INFO("UISoundManager: Window sounds - Bag: ", (bagOpenLoaded && bagCloseLoaded) ? "YES" : "NO",
", QuestLog: ", (questLogOpenLoaded && questLogCloseLoaded) ? "YES" : "NO",

View file

@ -122,7 +122,7 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
uint32_t WardenEmulator::hookAPI(const std::string& dllName,
const std::string& functionName,
std::function<uint32_t(WardenEmulator&, const std::vector<uint32_t>&)> handler) {
[[maybe_unused]] std::function<uint32_t(WardenEmulator&, const std::vector<uint32_t>&)> handler) {
// Allocate address for this API stub
static uint32_t nextStubAddr = API_STUB_BASE;
uint32_t stubAddr = nextStubAddr;
@ -239,7 +239,7 @@ std::string WardenEmulator::readString(uint32_t address, size_t maxLen) {
return std::string(buffer.data());
}
uint32_t WardenEmulator::allocateMemory(size_t size, uint32_t protection) {
uint32_t WardenEmulator::allocateMemory(size_t size, [[maybe_unused]] uint32_t protection) {
// Align to 4KB
size = (size + 0xFFF) & ~0xFFF;
@ -315,7 +315,7 @@ uint32_t WardenEmulator::apiVirtualFree(WardenEmulator& emu, const std::vector<u
return emu.freeMemory(lpAddress) ? 1 : 0;
}
uint32_t WardenEmulator::apiGetTickCount(WardenEmulator& emu, const std::vector<uint32_t>& args) {
uint32_t WardenEmulator::apiGetTickCount([[maybe_unused]] WardenEmulator& emu, [[maybe_unused]] const std::vector<uint32_t>& args) {
auto now = std::chrono::steady_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
uint32_t ticks = static_cast<uint32_t>(ms & 0xFFFFFFFF);
@ -324,7 +324,7 @@ uint32_t WardenEmulator::apiGetTickCount(WardenEmulator& emu, const std::vector<
return ticks;
}
uint32_t WardenEmulator::apiSleep(WardenEmulator& emu, const std::vector<uint32_t>& args) {
uint32_t WardenEmulator::apiSleep([[maybe_unused]] WardenEmulator& emu, const std::vector<uint32_t>& args) {
if (args.size() < 1) return 0;
uint32_t dwMilliseconds = args[0];
@ -333,12 +333,12 @@ uint32_t WardenEmulator::apiSleep(WardenEmulator& emu, const std::vector<uint32_
return 0;
}
uint32_t WardenEmulator::apiGetCurrentThreadId(WardenEmulator& emu, const std::vector<uint32_t>& args) {
uint32_t WardenEmulator::apiGetCurrentThreadId([[maybe_unused]] WardenEmulator& emu, [[maybe_unused]] const std::vector<uint32_t>& args) {
std::cout << "[WinAPI] GetCurrentThreadId() = 1234" << '\n';
return 1234; // Fake thread ID
}
uint32_t WardenEmulator::apiGetCurrentProcessId(WardenEmulator& emu, const std::vector<uint32_t>& args) {
uint32_t WardenEmulator::apiGetCurrentProcessId([[maybe_unused]] WardenEmulator& emu, [[maybe_unused]] const std::vector<uint32_t>& args) {
std::cout << "[WinAPI] GetCurrentProcessId() = 5678" << '\n';
return 5678; // Fake process ID
}
@ -347,7 +347,7 @@ uint32_t WardenEmulator::apiReadProcessMemory(WardenEmulator& emu, const std::ve
// ReadProcessMemory(hProcess, lpBaseAddress, lpBuffer, nSize, lpNumberOfBytesRead)
if (args.size() < 5) return 0;
uint32_t hProcess = args[0];
[[maybe_unused]] uint32_t hProcess = args[0];
uint32_t lpBaseAddress = args[1];
uint32_t lpBuffer = args[2];
uint32_t nSize = args[3];
@ -377,13 +377,11 @@ uint32_t WardenEmulator::apiReadProcessMemory(WardenEmulator& emu, const std::ve
// Unicorn Callbacks
// ============================================================================
void WardenEmulator::hookCode(uc_engine* uc, uint64_t address, uint32_t size, void* userData) {
WardenEmulator* emu = static_cast<WardenEmulator*>(userData);
void WardenEmulator::hookCode([[maybe_unused]] uc_engine* uc, uint64_t address, [[maybe_unused]] uint32_t size, [[maybe_unused]] void* userData) {
std::cout << "[Trace] 0x" << std::hex << address << std::dec << '\n';
}
void WardenEmulator::hookMemInvalid(uc_engine* uc, int type, uint64_t address, int size, int64_t value, void* userData) {
WardenEmulator* emu = static_cast<WardenEmulator*>(userData);
void WardenEmulator::hookMemInvalid([[maybe_unused]] uc_engine* uc, int type, uint64_t address, int size, [[maybe_unused]] int64_t value, [[maybe_unused]] void* userData) {
const char* typeStr = "UNKNOWN";
switch (type) {

View file

@ -129,7 +129,7 @@ bool WardenModule::load(const std::vector<uint8_t>& moduleData,
}
bool WardenModule::processCheckRequest(const std::vector<uint8_t>& checkData,
std::vector<uint8_t>& responseOut) {
[[maybe_unused]] std::vector<uint8_t>& responseOut) {
if (!loaded_) {
std::cerr << "[WardenModule] Module not loaded, cannot process checks" << '\n';
return false;
@ -198,7 +198,7 @@ bool WardenModule::processCheckRequest(const std::vector<uint8_t>& checkData,
return false;
}
uint32_t WardenModule::tick(uint32_t deltaMs) {
uint32_t WardenModule::tick([[maybe_unused]] uint32_t deltaMs) {
if (!loaded_ || !funcList_.tick) {
return 0; // No tick needed
}
@ -209,7 +209,7 @@ uint32_t WardenModule::tick(uint32_t deltaMs) {
return 0;
}
void WardenModule::generateRC4Keys(uint8_t* packet) {
void WardenModule::generateRC4Keys([[maybe_unused]] uint8_t* packet) {
if (!loaded_ || !funcList_.generateRC4Keys) {
return;
}
@ -633,9 +633,11 @@ bool WardenModule::applyRelocations() {
currentOffset += delta;
if (currentOffset + 4 <= moduleSize_) {
uint32_t* ptr = reinterpret_cast<uint32_t*>(
static_cast<uint8_t*>(moduleMemory_) + currentOffset);
*ptr += moduleBase_;
uint8_t* addr = static_cast<uint8_t*>(moduleMemory_) + currentOffset;
uint32_t val;
std::memcpy(&val, addr, sizeof(uint32_t));
val += moduleBase_;
std::memcpy(addr, &val, sizeof(uint32_t));
relocCount++;
} else {
std::cerr << "[WardenModule] Relocation offset " << currentOffset
@ -755,16 +757,16 @@ bool WardenModule::initializeModule() {
void (*logMessage)(const char* msg);
};
// Setup client callbacks
ClientCallbacks callbacks = {};
// Setup client callbacks (used when calling module entry point below)
[[maybe_unused]] ClientCallbacks callbacks = {};
// Stub callbacks (would need real implementations)
callbacks.sendPacket = [](uint8_t* data, size_t len) {
callbacks.sendPacket = []([[maybe_unused]] uint8_t* data, size_t len) {
std::cout << "[WardenModule Callback] sendPacket(" << len << " bytes)" << '\n';
// TODO: Send CMSG_WARDEN_DATA packet
};
callbacks.validateModule = [](uint8_t* hash) {
callbacks.validateModule = []([[maybe_unused]] uint8_t* hash) {
std::cout << "[WardenModule Callback] validateModule()" << '\n';
// TODO: Validate module hash
};
@ -779,7 +781,7 @@ bool WardenModule::initializeModule() {
free(ptr);
};
callbacks.generateRC4 = [](uint8_t* seed) {
callbacks.generateRC4 = []([[maybe_unused]] uint8_t* seed) {
std::cout << "[WardenModule Callback] generateRC4()" << '\n';
// TODO: Re-key RC4 cipher
};

View file

@ -30,34 +30,39 @@ BLPImage BLPLoader::load(const std::vector<uint8_t>& blpData) {
}
BLPImage BLPLoader::loadBLP1(const uint8_t* data, size_t size) {
// BLP1 header has all uint32 fields (different layout from BLP2)
const BLP1Header* header = reinterpret_cast<const BLP1Header*>(data);
// Copy header to stack to avoid unaligned reinterpret_cast (UB on strict platforms)
if (size < sizeof(BLP1Header)) {
LOG_ERROR("BLP1 data too small for header");
return BLPImage();
}
BLP1Header header;
std::memcpy(&header, data, sizeof(BLP1Header));
BLPImage image;
image.format = BLPFormat::BLP1;
image.width = header->width;
image.height = header->height;
image.width = header.width;
image.height = header.height;
image.channels = 4;
image.mipLevels = header->hasMips ? 16 : 1;
image.mipLevels = header.hasMips ? 16 : 1;
// BLP1 compression: 0=JPEG (not used in WoW), 1=palette/indexed
// BLP1 does NOT support DXT — only palette with optional alpha
if (header->compression == 1) {
if (header.compression == 1) {
image.compression = BLPCompression::PALETTE;
} else if (header->compression == 0) {
} else if (header.compression == 0) {
LOG_WARNING("BLP1 JPEG compression not supported");
return BLPImage();
} else {
LOG_WARNING("BLP1 unknown compression: ", header->compression);
LOG_WARNING("BLP1 unknown compression: ", header.compression);
return BLPImage();
}
LOG_DEBUG("Loading BLP1: ", image.width, "x", image.height, " ",
getCompressionName(image.compression), " alpha=", header->alphaBits);
getCompressionName(image.compression), " alpha=", header.alphaBits);
// Get first mipmap (full resolution)
uint32_t offset = header->mipOffsets[0];
uint32_t mipSize = header->mipSizes[0];
uint32_t offset = header.mipOffsets[0];
uint32_t mipSize = header.mipSizes[0];
if (offset + mipSize > size) {
LOG_ERROR("BLP1 mipmap data out of bounds (offset=", offset, " size=", mipSize, " fileSize=", size, ")");
@ -70,45 +75,50 @@ BLPImage BLPLoader::loadBLP1(const uint8_t* data, size_t size) {
int pixelCount = image.width * image.height;
image.data.resize(pixelCount * 4); // RGBA8
decompressPalette(mipData, image.data.data(), header->palette,
image.width, image.height, static_cast<uint8_t>(header->alphaBits));
decompressPalette(mipData, image.data.data(), header.palette,
image.width, image.height, static_cast<uint8_t>(header.alphaBits));
return image;
}
BLPImage BLPLoader::loadBLP2(const uint8_t* data, size_t size) {
// BLP2 header has uint8 fields for compression/alpha/encoding
const BLP2Header* header = reinterpret_cast<const BLP2Header*>(data);
// Copy header to stack to avoid unaligned reinterpret_cast (UB on strict platforms)
if (size < sizeof(BLP2Header)) {
LOG_ERROR("BLP2 data too small for header");
return BLPImage();
}
BLP2Header header;
std::memcpy(&header, data, sizeof(BLP2Header));
BLPImage image;
image.format = BLPFormat::BLP2;
image.width = header->width;
image.height = header->height;
image.width = header.width;
image.height = header.height;
image.channels = 4;
image.mipLevels = header->hasMips ? 16 : 1;
image.mipLevels = header.hasMips ? 16 : 1;
// BLP2 compression types:
// 1 = palette/uncompressed
// 2 = DXTC (DXT1/DXT3/DXT5 based on alphaDepth + alphaEncoding)
// 3 = plain A8R8G8B8
if (header->compression == 1) {
if (header.compression == 1) {
image.compression = BLPCompression::PALETTE;
} else if (header->compression == 2) {
} else if (header.compression == 2) {
// BLP2 DXTC format selection based on alphaDepth + alphaEncoding:
// alphaDepth=0 → DXT1 (no alpha)
// alphaDepth>0, alphaEncoding=0 → DXT1 (1-bit alpha)
// alphaDepth>0, alphaEncoding=1 → DXT3 (explicit 4-bit alpha)
// alphaDepth>0, alphaEncoding=7 → DXT5 (interpolated alpha)
if (header->alphaDepth == 0 || header->alphaEncoding == 0) {
if (header.alphaDepth == 0 || header.alphaEncoding == 0) {
image.compression = BLPCompression::DXT1;
} else if (header->alphaEncoding == 1) {
} else if (header.alphaEncoding == 1) {
image.compression = BLPCompression::DXT3;
} else if (header->alphaEncoding == 7) {
} else if (header.alphaEncoding == 7) {
image.compression = BLPCompression::DXT5;
} else {
image.compression = BLPCompression::DXT1;
}
} else if (header->compression == 3) {
} else if (header.compression == 3) {
image.compression = BLPCompression::ARGB8888;
} else {
image.compression = BLPCompression::ARGB8888;
@ -116,13 +126,13 @@ BLPImage BLPLoader::loadBLP2(const uint8_t* data, size_t size) {
LOG_DEBUG("Loading BLP2: ", image.width, "x", image.height, " ",
getCompressionName(image.compression),
" (comp=", (int)header->compression, " alphaDepth=", (int)header->alphaDepth,
" alphaEnc=", (int)header->alphaEncoding, " mipOfs=", header->mipOffsets[0],
" mipSize=", header->mipSizes[0], ")");
" (comp=", (int)header.compression, " alphaDepth=", (int)header.alphaDepth,
" alphaEnc=", (int)header.alphaEncoding, " mipOfs=", header.mipOffsets[0],
" mipSize=", header.mipSizes[0], ")");
// Get first mipmap (full resolution)
uint32_t offset = header->mipOffsets[0];
uint32_t mipSize = header->mipSizes[0];
uint32_t offset = header.mipOffsets[0];
uint32_t mipSize = header.mipSizes[0];
if (offset + mipSize > size) {
LOG_ERROR("BLP2 mipmap data out of bounds");
@ -149,8 +159,8 @@ BLPImage BLPLoader::loadBLP2(const uint8_t* data, size_t size) {
break;
case BLPCompression::PALETTE:
decompressPalette(mipData, image.data.data(), header->palette,
image.width, image.height, header->alphaDepth);
decompressPalette(mipData, image.data.data(), header.palette,
image.width, image.height, header.alphaDepth);
break;
case BLPCompression::ARGB8888:

View file

@ -42,19 +42,20 @@ bool DBCFile::load(const std::vector<uint8_t>& dbcData) {
return false;
}
// Read header
const DBCHeader* header = reinterpret_cast<const DBCHeader*>(dbcData.data());
// Read header safely (avoid unaligned reinterpret_cast — UB on strict platforms)
DBCHeader header;
std::memcpy(&header, dbcData.data(), sizeof(DBCHeader));
// Verify magic
if (std::memcmp(header->magic, "WDBC", 4) != 0) {
LOG_ERROR("Invalid DBC magic: ", std::string(header->magic, 4));
if (std::memcmp(header.magic, "WDBC", 4) != 0) {
LOG_ERROR("Invalid DBC magic: ", std::string(header.magic, 4));
return false;
}
recordCount = header->recordCount;
fieldCount = header->fieldCount;
recordSize = header->recordSize;
stringBlockSize = header->stringBlockSize;
recordCount = header.recordCount;
fieldCount = header.fieldCount;
recordSize = header.recordSize;
stringBlockSize = header.stringBlockSize;
// Validate sizes
uint32_t expectedSize = sizeof(DBCHeader) + (recordCount * recordSize) + stringBlockSize;
@ -111,8 +112,9 @@ uint32_t DBCFile::getUInt32(uint32_t recordIndex, uint32_t fieldIndex) const {
return 0;
}
const uint32_t* field = reinterpret_cast<const uint32_t*>(record + (fieldIndex * 4));
return *field;
uint32_t value;
std::memcpy(&value, record + (fieldIndex * 4), sizeof(uint32_t));
return value;
}
int32_t DBCFile::getInt32(uint32_t recordIndex, uint32_t fieldIndex) const {
@ -129,8 +131,9 @@ float DBCFile::getFloat(uint32_t recordIndex, uint32_t fieldIndex) const {
return 0.0f;
}
const float* field = reinterpret_cast<const float*>(record + (fieldIndex * 4));
return *field;
float value;
std::memcpy(&value, record + (fieldIndex * 4), sizeof(float));
return value;
}
std::string DBCFile::getString(uint32_t recordIndex, uint32_t fieldIndex) const {

View file

@ -1456,9 +1456,7 @@ bool M2Loader::loadSkin(const std::vector<uint8_t>& skinData, M2Model& model) {
if (header.nSubmeshes > 0 && header.ofsSubmeshes > 0) {
submeshes = readArray<M2SkinSubmesh>(skinData, header.ofsSubmeshes, header.nSubmeshes);
core::Logger::getInstance().debug(" Submeshes: ", submeshes.size());
for (size_t i = 0; i < submeshes.size(); i++) {
const auto& sm = submeshes[i];
}
(void)submeshes;
}
// Read batches with proper submesh references

View file

@ -1610,7 +1610,7 @@ glm::mat4 CharacterRenderer::getBoneTransform(const pipeline::M2Bone& bone, floa
// --- Rendering ---
void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera) {
void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, [[maybe_unused]] const Camera& camera) {
if (instances.empty() || !opaquePipeline_) {
return;
}

View file

@ -2381,11 +2381,11 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
}
}
// Bind material descriptor set (set 1)
if (batch.materialSet) {
// Bind material descriptor set (set 1) — skip batch if missing
// to avoid inheriting a stale descriptor set from a prior renderer
if (!batch.materialSet) continue;
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
pipelineLayout_, 1, 1, &batch.materialSet, 0, nullptr);
}
// Push constants
M2PushConstants pc;

View file

@ -19,7 +19,7 @@ bool VkRenderTarget::create(VkContext& ctx, uint32_t width, uint32_t height,
// Create color image (multisampled if MSAA)
colorImage_ = createImage(device, allocator, width, height, format,
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | (useMSAA ? VkImageUsageFlags(0) : VK_IMAGE_USAGE_SAMPLED_BIT),
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | (useMSAA ? static_cast<VkImageUsageFlags>(0) : static_cast<VkImageUsageFlags>(VK_IMAGE_USAGE_SAMPLED_BIT)),
msaaSamples);
if (!colorImage_.image) {

2198
tools/asset_pipeline_gui.py Executable file

File diff suppressed because it is too large Load diff

2170
tools/m2_viewer.py Normal file

File diff suppressed because it is too large Load diff