Merge commit '32bb0becc8' into chore/game-screen-extract

This commit is contained in:
Paul 2026-03-31 19:51:37 +03:00
commit 43aecab1ef
145 changed files with 3237 additions and 2849 deletions

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

15
.gitignore vendored
View file

@ -46,6 +46,20 @@ 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
@ -53,6 +67,7 @@ extern/*
!extern/vk-bootstrap
!extern/vk_mem_alloc.h
!extern/lua-5.1.5
!extern/VERSIONS.md
# ImGui state
imgui.ini

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

View file

@ -1,6 +1,38 @@
# Changelog
## [Unreleased] — changes since v1.8.1-preview (2026-03-23)
## [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)
### 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

View file

@ -248,6 +248,70 @@ endif()
find_package(SDL2 REQUIRED)
find_package(Vulkan QUIET)
if(NOT Vulkan_FOUND)
# 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_IMPORT_DIR}")
set_property(TARGET Vulkan::Vulkan APPEND PROPERTY
INTERFACE_LINK_LIBRARIES vulkan-1)
endif()
set(Vulkan_FOUND TRUE)
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)
@ -266,16 +330,16 @@ if(NOT Vulkan_FOUND)
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)
@ -514,12 +578,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
@ -547,7 +608,6 @@ set(WOWEE_SOURCES
src/rendering/levelup_effect.cpp
src/rendering/charge_effect.cpp
src/rendering/loading_screen.cpp
$<$<BOOL:${HAVE_FFMPEG}>:${CMAKE_CURRENT_SOURCE_DIR}/src/rendering/video_player.cpp>
# UI
src/ui/ui_manager.cpp
@ -642,12 +702,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
@ -666,7 +723,6 @@ set(WOWEE_HEADERS
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
@ -682,12 +738,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()
@ -717,6 +777,11 @@ add_executable(wowee ${WOWEE_SOURCES} ${WOWEE_HEADERS} ${WOWEE_PLATFORM_SOURCES}
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.
@ -757,14 +822,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})

View file

@ -12,7 +12,7 @@ make -C build -j$(nproc)
## Code Style
- **C++17**. Use `#pragma once` for include guards.
- **C++20**. Use `#pragma once` for include guards.
- Namespaces: `wowee::game`, `wowee::rendering`, `wowee::ui`, `wowee::core`, `wowee::network`.
- Conventional commit messages in imperative mood:
- `feat:` new feature

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,14 +19,15 @@ 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-24)
## Status & Direction (2026-03-30)
- **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**: gameplay correctness (quest/GO interaction, NPC visibility), rendering stability, and multi-expansion coverage.
- **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 x86-64 + ARM64), and macOS (ARM64). Security scans via CodeQL, Semgrep, and sanitizers.
- **Release**: v1.8.2-preview — 530+ WoW API functions, 140+ events, 664 opcode handlers.
- **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.
## Features

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

@ -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

@ -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);

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}"

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,294 @@ 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
### 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 path evaluation
- Catmull-Rom spline interpolation from TransportAnimation.dbc
- Clock-based motion with server time synchronization
- Time-closed looping paths (wrap point duplicated, no index wrapping)
**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
**NPC Interactions** - handled through `GameHandler`
- Gossip system
- Quest givers with markers (! and ?)
- Vendors (buy/sell)
- Trainers (placeholder)
- Combat animations
**ZoneManager** - Zone and area tracking
- Map exploration
- Area discovery
- Zone change detection
**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
- `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
### 8. Audio System (`src/audio/`)
**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
**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)
**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
**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
**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
### 9. Warden Anti-Cheat (`src/game/`)
**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
**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::game`, `wowee::ui`, `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.

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-24
**Last updated**: 2026-03-30
## What This Repo Is

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 |

View file

@ -3,6 +3,7 @@
#include "core/window.hpp"
#include "core/input.hpp"
#include "game/character.hpp"
#include "game/game_services.hpp"
#include "pipeline/blp_loader.hpp"
#include <memory>
#include <string>
@ -126,6 +127,7 @@ private:
static Application* instance;
game::GameServices gameServices_;
std::unique_ptr<Window> window;
std::unique_ptr<rendering::Renderer> renderer;
std::unique_ptr<ui::UIManager> uiManager;

View file

@ -93,7 +93,9 @@ public:
float impliedVX = (destX - fromX) / durationSec;
float impliedVY = (destY - fromY) / durationSec;
float impliedVZ = (destZ - fromZ) / durationSec;
// Exponentially smooth velocity so jittery packet timing doesn't snap speed.
// Exponential moving average on velocity — 65% new sample, 35% previous.
// Smooths out jitter from irregular server update intervals (~200-600ms)
// without introducing visible lag on direction changes.
const float alpha = 0.65f;
velX_ = alpha * impliedVX + (1.0f - alpha) * velX_;
velY_ = alpha * impliedVY + (1.0f - alpha) * velY_;

View file

@ -13,6 +13,7 @@
#include "game/quest_handler.hpp"
#include "game/movement_handler.hpp"
#include "game/entity_controller.hpp"
#include "game/game_services.hpp"
#include "network/packet.hpp"
#include <glm/glm.hpp>
#include <memory>
@ -130,9 +131,11 @@ public:
using TalentEntry = game::TalentEntry;
using TalentTabEntry = game::TalentTabEntry;
GameHandler();
explicit GameHandler(GameServices& services);
~GameHandler();
const GameServices& services() const { return services_; }
/** Access the active opcode table (wire ↔ logical mapping). */
const OpcodeTable& getOpcodeTable() const { return opcodeTable_; }
OpcodeTable& getOpcodeTable() { return opcodeTable_; }
@ -2298,6 +2301,9 @@ private:
float localOrientation);
void clearTransportAttachment(uint64_t childGuid);
// Explicit service dependencies (owned by Application)
GameServices& services_;
// Domain handlers — each manages a specific concern extracted from GameHandler
std::unique_ptr<ChatHandler> chatHandler_;
std::unique_ptr<MovementHandler> movementHandler_;

View file

@ -0,0 +1,23 @@
#pragma once
#include <cstdint>
namespace wowee {
namespace rendering { class Renderer; }
namespace pipeline { class AssetManager; }
namespace game { class ExpansionRegistry; }
namespace game {
// Explicit service dependencies for game handlers.
// Owned by Application, passed by reference to GameHandler at construction.
// Replaces hidden Application::getInstance() singleton access.
struct GameServices {
rendering::Renderer* renderer = nullptr;
pipeline::AssetManager* assetManager = nullptr;
ExpansionRegistry* expansionRegistry = nullptr;
uint32_t gryphonDisplayId = 0;
uint32_t wyvernDisplayId = 0;
};
} // namespace game
} // namespace wowee

View file

@ -70,7 +70,12 @@ class Inventory {
public:
static constexpr int BACKPACK_SLOTS = 16;
static constexpr int KEYRING_SLOTS = 32;
// WoW slot layout: 0-22 are equipment (head, neck, ... tabard, mainhand, offhand, ranged, ammo).
// Backpack inventory starts at slot 23 in bag 0xFF, so packet slot = NUM_EQUIP_SLOTS + backpackIndex.
static constexpr int NUM_EQUIP_SLOTS = 23;
// Bag containers occupy equipment slots 19-22 (bag1, bag2, bag3, bag4).
// Packet bag byte = FIRST_BAG_EQUIP_SLOT + bagIndex.
static constexpr int FIRST_BAG_EQUIP_SLOT = 19;
static constexpr int NUM_BAG_SLOTS = 4;
static constexpr int MAX_BAG_SIZE = 36;
static constexpr int BANK_SLOTS = 28;

View file

@ -122,8 +122,11 @@ public:
};
const std::unordered_map<uint32_t, TaxiNode>& getTaxiNodes() const { return taxiNodes_; }
// WotLK 3.3.5a TaxiNodes.dbc has 384 entries; the known-taxi bitmask
// is 12 × uint32 = 384 bits. Node IDs outside this range are invalid.
static constexpr uint32_t kMaxTaxiNodeId = 384;
bool isKnownTaxiNode(uint32_t nodeId) const {
if (nodeId == 0 || nodeId > 384) return false;
if (nodeId == 0 || nodeId > kMaxTaxiNodeId) return false;
uint32_t idx = nodeId - 1;
return (knownTaxiMask_[idx / 32] & (1u << (idx % 32))) != 0;
}

View file

@ -249,6 +249,9 @@ private:
void handleChannelUpdate(network::Packet& packet);
// --- Internal helpers ---
// Find the on-use spell for an item (trigger=0 Use or trigger=5 NoDelay).
// CMSG_USE_ITEM requires a valid spellId or the server silently ignores it.
uint32_t findOnUseSpellId(uint32_t itemId) const;
void loadSpellNameCache() const;
void loadSkillLineAbilityDbc();
void categorizeTrainerSpells();

View file

@ -138,6 +138,10 @@ public:
*/
std::vector<uint8_t> readData(uint32_t address, size_t size);
// Look up an already-registered API stub address by DLL and function name.
// Returns 0 if not found. Used by WardenModule::bindAPIs() for IAT patching.
uint32_t getAPIAddress(const std::string& dllName, const std::string& funcName) const;
private:
uc_engine* uc_; // Unicorn engine instance
uint32_t moduleBase_; // Module base address

View file

@ -13,6 +13,7 @@ namespace game {
// Forward declarations
class WardenEmulator;
class WardenCrypto;
/**
* Represents Warden callback functions exported by loaded module
@ -36,18 +37,19 @@ struct WardenFuncList {
* Warden module loader and executor
*
* IMPLEMENTATION STATUS:
* Module metadata parsing
* Basic validation framework
* RC4 decryption (uses existing WardenCrypto)
* RSA signature verification (TODO - requires OpenSSL RSA)
* zlib decompression (TODO - requires zlib library)
* Custom executable format parsing (TODO - major reverse engineering)
* Address relocation (TODO - x86 address fixups)
* API binding (TODO - kernel32/user32 function resolution)
* Native code execution (TODO - execute loaded x86 code)
* Module metadata parsing and validation
* RC4 decryption (WardenCrypto)
* RSA-2048 signature verification (OpenSSL EVP real Blizzard modulus)
* zlib decompression
* Custom executable format parsing (3 pair-format variants)
* Address relocation (delta-encoded fixups)
* x86 emulation via Unicorn Engine (cross-platform)
* Client callbacks (sendPacket, validateModule, generateRC4)
* API binding / IAT patching (parses import table, auto-stubs unknown APIs)
* RSA modulus verified (Blizzard key, same across 1.12.1/2.4.3/3.3.5a)
*
* For strict servers like Warmane, ALL TODOs must be implemented.
* For permissive servers, fake responses in GameHandler work.
* Non-fatal verification: RSA mismatch logs warning but continues loading,
* so private-server modules signed with custom keys still work.
*/
class WardenModule {
public:
@ -126,6 +128,12 @@ public:
size_t getModuleSize() const { return moduleSize_; }
const std::vector<uint8_t>& getDecompressedData() const { return decompressedData_; }
// Inject dependencies for module callbacks (sendPacket, generateRC4).
// Must be called before initializeModule() so callbacks can reach the
// network layer and crypto state.
using SendPacketFunc = std::function<void(const uint8_t*, size_t)>;
void setCallbackDependencies(WardenCrypto* crypto, SendPacketFunc sendFunc);
private:
bool loaded_; // Module successfully loaded
std::vector<uint8_t> md5Hash_; // Module identifier
@ -142,6 +150,11 @@ private:
std::unique_ptr<WardenEmulator> emulator_; // Cross-platform x86 emulator
uint32_t emulatedPacketHandlerAddr_ = 0; // Raw emulated VA for 4-arg PacketHandler call
// Dependencies injected via setCallbackDependencies() for module callbacks.
// These are NOT owned — the handler owns the crypto and socket lifetime.
WardenCrypto* callbackCrypto_ = nullptr;
SendPacketFunc callbackSendPacket_;
// Validation and loading steps
bool verifyMD5(const std::vector<uint8_t>& data,
const std::vector<uint8_t>& expectedHash);

View file

@ -1,129 +0,0 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
#include <memory>
#include <map>
#include <unordered_map>
#include <unordered_set>
#include <mutex>
#include <shared_mutex>
// Forward declare StormLib handle
typedef void* HANDLE;
namespace wowee {
namespace pipeline {
/**
* MPQManager - Manages MPQ archive loading and file reading
*
* WoW 3.3.5a stores all game assets in MPQ archives.
* This manager loads multiple archives and provides unified file access.
*/
class MPQManager {
public:
MPQManager();
~MPQManager();
/**
* Initialize the MPQ system
* @param dataPath Path to WoW Data directory
* @return true if initialization succeeded
*/
bool initialize(const std::string& dataPath);
/**
* Shutdown and close all archives
*/
void shutdown();
/**
* Load a single MPQ archive
* @param path Full path to MPQ file
* @param priority Priority for file resolution (higher = checked first)
* @return true if archive loaded successfully
*/
bool loadArchive(const std::string& path, int priority = 0);
/**
* Check if a file exists in any loaded archive
* @param filename Virtual file path (e.g., "World\\Maps\\Azeroth\\Azeroth.wdt")
* @return true if file exists
*/
bool fileExists(const std::string& filename) const;
/**
* Read a file from MPQ archives
* @param filename Virtual file path
* @return File contents as byte vector (empty if not found)
*/
std::vector<uint8_t> readFile(const std::string& filename) const;
/**
* Get file size without reading it
* @param filename Virtual file path
* @return File size in bytes (0 if not found)
*/
uint32_t getFileSize(const std::string& filename) const;
/**
* Check if MPQ system is initialized
*/
bool isInitialized() const { return initialized; }
/**
* Get list of loaded archives
*/
const std::vector<std::string>& getLoadedArchives() const { return archiveNames; }
private:
struct ArchiveEntry {
HANDLE handle;
std::string path;
int priority;
};
bool initialized = false;
std::string dataPath;
std::vector<ArchiveEntry> archives;
std::vector<std::string> archiveNames;
/**
* Find archive containing a file
* @param filename File to search for
* @return Archive handle or nullptr if not found
*/
HANDLE findFileArchive(const std::string& filename) const;
/**
* Load patch archives (e.g., patch.MPQ, patch-2.MPQ, etc.)
*/
bool loadPatchArchives();
/**
* Load locale-specific archives
* @param locale Locale string (e.g., "enUS")
*/
bool loadLocaleArchives(const std::string& locale);
void logMissingFileOnce(const std::string& filename) const;
// Cache for mapping "virtual filename" -> archive handle (or INVALID_HANDLE_VALUE for not found).
// This avoids scanning every archive for repeated lookups, which can otherwise appear as a hang
// on screens that trigger many asset probes (character select, character preview, etc.).
//
// Important: caching misses can blow up memory if the game probes many unique non-existent filenames.
// Miss caching is disabled by default and must be explicitly enabled.
mutable std::shared_mutex fileArchiveCacheMutex_;
mutable std::unordered_map<std::string, HANDLE> fileArchiveCache_;
size_t fileArchiveCacheMaxEntries_ = 500000;
bool fileArchiveCacheMisses_ = false;
mutable std::mutex missingFileMutex_;
mutable std::unordered_set<std::string> missingFileWarnings_;
};
} // namespace pipeline
} // namespace wowee

View file

@ -81,6 +81,8 @@ private:
// ImGui texture handle for displaying the preview (VkDescriptorSet in Vulkan backend)
VkDescriptorSet imguiTextureId_ = VK_NULL_HANDLE;
// 4:5 portrait aspect ratio — taller than wide to show full character body
// from head to feet in the character creation/selection screen
static constexpr int fboWidth_ = 400;
static constexpr int fboHeight_ = 500;

View file

@ -251,6 +251,10 @@ public:
private:
// Create 1×1 fallback textures used when real textures are missing or still loading.
// Called during both init and clear to ensure valid descriptor bindings at all times.
void createFallbackTextures(VkDevice device);
VkContext* vkCtx_ = nullptr;
VkRenderPass renderPassOverride_ = VK_NULL_HANDLE;
VkSampleCountFlagBits msaaSamplesOverride_ = VK_SAMPLE_COUNT_1_BIT;

View file

@ -1,33 +0,0 @@
#pragma once
#include <vector>
#include <GL/glew.h>
#include <glm/glm.hpp>
namespace wowee {
namespace rendering {
struct Vertex {
glm::vec3 position;
glm::vec3 normal;
glm::vec2 texCoord;
};
class Mesh {
public:
Mesh() = default;
~Mesh();
void create(const std::vector<Vertex>& vertices, const std::vector<uint32_t>& indices);
void destroy();
void draw() const;
private:
GLuint VAO = 0;
GLuint VBO = 0;
GLuint EBO = 0;
size_t indexCount = 0;
};
} // namespace rendering
} // namespace wowee

View file

@ -30,7 +30,6 @@ namespace rendering {
class Camera;
class CameraController;
class Scene;
class TerrainRenderer;
class TerrainManager;
class PerformanceHUD;
@ -54,7 +53,6 @@ class Minimap;
class WorldMap;
class QuestMarkerRenderer;
class CharacterPreview;
class Shader;
class AmdFsr3Runtime;
class Renderer {
@ -119,7 +117,6 @@ public:
Camera* getCamera() { return camera.get(); }
CameraController* getCameraController() { return cameraController.get(); }
Scene* getScene() { return scene.get(); }
TerrainRenderer* getTerrainRenderer() const { return terrainRenderer.get(); }
TerrainManager* getTerrainManager() const { return terrainManager.get(); }
PerformanceHUD* getPerformanceHUD() { return performanceHUD.get(); }
@ -219,7 +216,6 @@ private:
core::Window* window = nullptr;
std::unique_ptr<Camera> camera;
std::unique_ptr<CameraController> cameraController;
std::unique_ptr<Scene> scene;
std::unique_ptr<TerrainRenderer> terrainRenderer;
std::unique_ptr<TerrainManager> terrainManager;
std::unique_ptr<PerformanceHUD> performanceHUD;

View file

@ -1,27 +0,0 @@
#pragma once
#include <vector>
#include <memory>
namespace wowee {
namespace rendering {
class Mesh;
class Scene {
public:
Scene() = default;
~Scene() = default;
void addMesh(std::shared_ptr<Mesh> mesh);
void removeMesh(const std::shared_ptr<Mesh>& mesh);
void clear();
const std::vector<std::shared_ptr<Mesh>>& getMeshes() const { return meshes; }
private:
std::vector<std::shared_ptr<Mesh>> meshes;
};
} // namespace rendering
} // namespace wowee

View file

@ -1,51 +0,0 @@
#pragma once
#include <string>
#include <unordered_map>
#include <GL/glew.h>
#include <glm/glm.hpp>
namespace wowee {
namespace rendering {
class Shader {
public:
Shader() = default;
~Shader();
[[nodiscard]] bool loadFromFile(const std::string& vertexPath, const std::string& fragmentPath);
[[nodiscard]] bool loadFromSource(const std::string& vertexSource, const std::string& fragmentSource);
void use() const;
void unuse() const;
void setUniform(const std::string& name, int value);
void setUniform(const std::string& name, float value);
void setUniform(const std::string& name, const glm::vec2& value);
void setUniform(const std::string& name, const glm::vec3& value);
void setUniform(const std::string& name, const glm::vec4& value);
void setUniform(const std::string& name, const glm::mat3& value);
void setUniform(const std::string& name, const glm::mat4& value);
void setUniformMatrixArray(const std::string& name, const glm::mat4* matrices, int count);
GLuint getProgram() const { return program; }
// Adopt an externally-created program (no ownership of individual shaders)
void setProgram(GLuint prog) { program = prog; }
// Release ownership without deleting (caller retains the GL program)
void releaseProgram() { program = 0; vertexShader = 0; fragmentShader = 0; }
private:
bool compile(const std::string& vertexSource, const std::string& fragmentSource);
GLint getUniformLocation(const std::string& name) const;
GLuint program = 0;
GLuint vertexShader = 0;
GLuint fragmentShader = 0;
// Cache uniform locations to avoid expensive glGetUniformLocation calls
mutable std::unordered_map<std::string, GLint> uniformLocationCache;
};
} // namespace rendering
} // namespace wowee

View file

@ -1,51 +0,0 @@
#pragma once
#include <string>
#include <cstdint>
#include <vector>
typedef unsigned int GLuint;
namespace wowee {
namespace rendering {
class VideoPlayer {
public:
VideoPlayer();
~VideoPlayer();
bool open(const std::string& path);
void update(float deltaTime);
void close();
bool isReady() const { return textureReady; }
GLuint getTextureId() const { return textureId; }
int getWidth() const { return width; }
int getHeight() const { return height; }
private:
bool decodeNextFrame();
void uploadFrame();
void* formatCtx = nullptr;
void* codecCtx = nullptr;
void* frame = nullptr;
void* rgbFrame = nullptr;
void* packet = nullptr;
void* swsCtx = nullptr;
int videoStreamIndex = -1;
int width = 0;
int height = 0;
double frameTime = 1.0 / 30.0;
double accumulator = 0.0;
bool eof = false;
GLuint textureId = 0;
bool textureReady = false;
std::string sourcePath;
std::vector<uint8_t> rgbBuffer;
};
} // namespace rendering
} // namespace wowee

View file

@ -68,6 +68,8 @@ public:
private:
void generateMipmaps(VkContext& ctx, VkFormat format, uint32_t width, uint32_t height);
// Shared sampler finalization: prefer the global cache, fall back to direct creation
bool finalizeSampler(VkDevice device, const VkSamplerCreateInfo& samplerInfo);
AllocatedImage image_{};
VkSampler sampler_ = VK_NULL_HANDLE;

View file

@ -1 +1 @@
IDI_APP_ICON ICON "assets\\wowee.ico"
IDI_APP_ICON ICON "assets/wowee.ico"

0
restart-worldserver.sh Normal file → Executable file
View file

View file

@ -696,10 +696,14 @@ static int lua_UnitGroupRolesAssigned(lua_State* L) {
const auto& pd = gh->getPartyData();
for (const auto& m : pd.members) {
if (m.guid == guid) {
// WotLK roles bitmask: 0x02=Tank, 0x04=Healer, 0x08=DPS
if (m.roles & 0x02) { lua_pushstring(L, "TANK"); return 1; }
if (m.roles & 0x04) { lua_pushstring(L, "HEALER"); return 1; }
if (m.roles & 0x08) { lua_pushstring(L, "DAMAGER"); return 1; }
// WotLK LFG roles bitmask (from SMSG_GROUP_LIST / SMSG_LFG_ROLE_CHECK_UPDATE).
// Bit 0x01 = Leader (not a combat role), 0x02 = Tank, 0x04 = Healer, 0x08 = DPS.
constexpr uint8_t kRoleTank = 0x02;
constexpr uint8_t kRoleHealer = 0x04;
constexpr uint8_t kRoleDamager = 0x08;
if (m.roles & kRoleTank) { lua_pushstring(L, "TANK"); return 1; }
if (m.roles & kRoleHealer) { lua_pushstring(L, "HEALER"); return 1; }
if (m.roles & kRoleDamager) { lua_pushstring(L, "DAMAGER"); return 1; }
break;
}
}

View file

@ -921,7 +921,8 @@ void AmbientSoundManager::updateBellTolls(float deltaTime) {
static_cast<int>(currentCity_));
}
// Play remaining tolls with 1.5 second delay between each
// Play remaining tolls with 1.5s spacing — matches retail WoW bell cadence
// (long enough for each toll to ring out before the next begins)
if (remainingTolls_ > 0) {
bellTollDelay_ += deltaTime;

View file

@ -8,6 +8,7 @@
#include <cstring>
#include <cstdlib>
#include <iterator>
#include <memory>
#include <unordered_map>
@ -98,10 +99,13 @@ static bool decodeWavCached(const std::vector<uint8_t>& wavData, DecodedWavCache
entry.sampleRate = sampleRate;
entry.frames = framesRead;
entry.pcmData = pcmData;
// Evict oldest half when cache grows too large (keeps ~128 most-recent sounds)
if (gDecodedWavCache.size() >= 256) {
// Evict oldest half when cache grows too large. 256 entries ≈ 50-100 MB of decoded
// PCM data depending on file lengths; halving keeps memory bounded while retaining
// recently-heard sounds (footsteps, UI clicks, combat hits) for instant replay.
constexpr size_t kMaxCachedSounds = 256;
if (gDecodedWavCache.size() >= kMaxCachedSounds) {
auto it = gDecodedWavCache.begin();
for (size_t n = gDecodedWavCache.size() / 2; n > 0; --n, ++it) {}
std::advance(it, gDecodedWavCache.size() / 2);
gDecodedWavCache.erase(gDecodedWavCache.begin(), it);
}
gDecodedWavCache.emplace(key, entry);
@ -239,7 +243,9 @@ bool AudioEngine::playSound2D(const std::vector<uint8_t>& wavData, float volume,
decoded.pcmData->data(),
nullptr // No custom allocator
);
bufferConfig.sampleRate = decoded.sampleRate; // Critical: preserve original sample rate!
// Must set explicitly — miniaudio defaults to device sample rate, which causes
// pitch distortion if it differs from the file's native rate (e.g. 22050 vs 44100 Hz).
bufferConfig.sampleRate = decoded.sampleRate;
ma_audio_buffer* audioBuffer = static_cast<ma_audio_buffer*>(std::malloc(sizeof(ma_audio_buffer)));
if (!audioBuffer) return false;
@ -394,7 +400,9 @@ bool AudioEngine::playSound3D(const std::vector<uint8_t>& wavData, const glm::ve
decoded.pcmData->data(),
nullptr
);
bufferConfig.sampleRate = decoded.sampleRate; // Critical: preserve original sample rate!
// Must set explicitly — miniaudio defaults to device sample rate, which causes
// pitch distortion if it differs from the file's native rate (e.g. 22050 vs 44100 Hz).
bufferConfig.sampleRate = decoded.sampleRate;
ma_audio_buffer* audioBuffer = static_cast<ma_audio_buffer*>(std::malloc(sizeof(ma_audio_buffer)));
if (!audioBuffer) return false;

View file

@ -13,6 +13,12 @@
namespace wowee {
namespace auth {
// WoW login security flags (CMD_AUTH_LOGON_CHALLENGE response, securityFlags byte).
// Multiple flags can be set simultaneously; the client must satisfy all of them.
constexpr uint8_t kSecurityFlagPin = 0x01; // PIN grid challenge
constexpr uint8_t kSecurityFlagMatrixCard = 0x02; // Matrix card (unused by most servers)
constexpr uint8_t kSecurityFlagAuthenticator = 0x04; // TOTP authenticator token
AuthHandler::AuthHandler() {
LOG_DEBUG("AuthHandler created");
}
@ -196,9 +202,9 @@ void AuthHandler::handleLogonChallengeResponse(network::Packet& packet) {
if (response.securityFlags != 0) {
LOG_WARNING("Server sent security flags: 0x", std::hex, static_cast<int>(response.securityFlags), std::dec);
if (response.securityFlags & 0x01) LOG_WARNING(" PIN required");
if (response.securityFlags & 0x02) LOG_WARNING(" Matrix card required (not supported)");
if (response.securityFlags & 0x04) LOG_WARNING(" Authenticator required (not supported)");
if (response.securityFlags & kSecurityFlagPin) LOG_WARNING(" PIN required");
if (response.securityFlags & kSecurityFlagMatrixCard) LOG_WARNING(" Matrix card required (not supported)");
if (response.securityFlags & kSecurityFlagAuthenticator) LOG_WARNING(" Authenticator required (not supported)");
}
LOG_INFO("Challenge: N=", response.N.size(), "B g=", response.g.size(), "B salt=",
@ -209,7 +215,7 @@ void AuthHandler::handleLogonChallengeResponse(network::Packet& packet) {
securityFlags_ = response.securityFlags;
checksumSalt_ = response.checksumSalt;
if (securityFlags_ & 0x01) {
if (securityFlags_ & kSecurityFlagPin) {
pinGridSeed_ = response.pinGridSeed;
pinServerSalt_ = response.pinSalt;
}
@ -217,8 +223,8 @@ void AuthHandler::handleLogonChallengeResponse(network::Packet& packet) {
setState(AuthState::CHALLENGE_RECEIVED);
// If a security code is required, wait for user input.
if (((securityFlags_ & 0x04) || (securityFlags_ & 0x01)) && pendingSecurityCode_.empty()) {
setState((securityFlags_ & 0x04) ? AuthState::AUTHENTICATOR_REQUIRED : AuthState::PIN_REQUIRED);
if (((securityFlags_ & kSecurityFlagAuthenticator) || (securityFlags_ & kSecurityFlagPin)) && pendingSecurityCode_.empty()) {
setState((securityFlags_ & kSecurityFlagAuthenticator) ? AuthState::AUTHENTICATOR_REQUIRED : AuthState::PIN_REQUIRED);
return;
}
@ -238,7 +244,7 @@ void AuthHandler::sendLogonProof() {
std::array<uint8_t, 20> crcHash{};
const std::array<uint8_t, 20>* crcHashPtr = nullptr;
if (securityFlags_ & 0x01) {
if (securityFlags_ & kSecurityFlagPin) {
try {
PinProof proof = computePinProof(pendingSecurityCode_, pinGridSeed_, pinServerSalt_);
pinClientSalt = proof.clientSalt;
@ -299,7 +305,7 @@ void AuthHandler::sendLogonProof() {
auto packet = LogonProofPacket::build(A, M1, securityFlags_, crcHashPtr, pinClientSaltPtr, pinHashPtr);
socket->send(packet);
if (securityFlags_ & 0x04) {
if (securityFlags_ & kSecurityFlagAuthenticator) {
// TrinityCore-style Google Authenticator token: send immediately after proof.
const std::string token = pendingSecurityCode_;
auto tokPkt = AuthenticatorTokenPacket::build(token);

View file

@ -136,6 +136,8 @@ std::vector<uint8_t> BigNum::toArray(bool littleEndian, int minSize) const {
std::string BigNum::toHex() const {
char* hex = BN_bn2hex(bn);
// BN_bn2hex returns nullptr on allocation failure
if (!hex) return "(null)";
std::string result(hex);
OPENSSL_free(hex);
return result;
@ -143,6 +145,7 @@ std::string BigNum::toHex() const {
std::string BigNum::toDecimal() const {
char* dec = BN_bn2dec(bn);
if (!dec) return "(null)";
std::string result(dec);
OPENSSL_free(dec);
return result;

View file

@ -63,7 +63,9 @@ static std::vector<uint8_t> randomizePinDigits(const std::string& pinDigits,
if (idx == 0xFF) {
throw std::runtime_error("PIN digit not found in remapped grid");
}
out.push_back(static_cast<uint8_t>(idx + 0x30)); // ASCII '0'+idx
// PIN grid encodes each digit as its ASCII character ('0'..'9') for the
// server-side HMAC computation — this matches Blizzard's auth protocol.
out.push_back(static_cast<uint8_t>(idx + '0'));
}
return out;

View file

@ -129,11 +129,15 @@ std::vector<uint8_t> SRP::computeAuthHash(const std::string& username,
void SRP::computeClientEphemeral() {
LOG_DEBUG("Computing client ephemeral");
// Generate random private ephemeral a (19 bytes = 152 bits)
// Keep trying until we get a valid A
// Generate random private ephemeral a (19 bytes = 152 bits).
// WoW SRP-6a requires A != 0 mod N; in practice this almost never fails
// (probability ≈ 2^-152), but we retry to be safe. 100 attempts is far more
// than needed — if it fails, the RNG is broken.
static constexpr int kMaxEphemeralAttempts = 100;
static constexpr int kEphemeralBytes = 19; // 152 bits — matches Blizzard client
int attempts = 0;
while (attempts < 100) {
a = BigNum::fromRandom(19);
while (attempts < kMaxEphemeralAttempts) {
a = BigNum::fromRandom(kEphemeralBytes);
// A = g^a mod N
A = g.modPow(a, N);
@ -146,8 +150,8 @@ void SRP::computeClientEphemeral() {
attempts++;
}
if (attempts >= 100) {
LOG_ERROR("Failed to generate valid client ephemeral after 100 attempts!");
if (attempts >= kMaxEphemeralAttempts) {
LOG_ERROR("Failed to generate valid client ephemeral after ", kMaxEphemeralAttempts, " attempts!");
}
}

View file

@ -48,7 +48,6 @@
#include "pipeline/dbc_layout.hpp"
#include <SDL2/SDL.h>
// GL/glew.h removed — Vulkan migration Phase 1
#include <cstdlib>
#include <climits>
#include <algorithm>
@ -256,7 +255,6 @@ bool Application::initialize() {
// Create subsystems
authHandler = std::make_unique<auth::AuthHandler>();
gameHandler = std::make_unique<game::GameHandler>();
world = std::make_unique<game::World>();
// Create and initialize expansion registry
@ -268,6 +266,14 @@ bool Application::initialize() {
// Create asset manager
assetManager = std::make_unique<pipeline::AssetManager>();
// Populate game services — all subsystems now available
gameServices_.renderer = renderer.get();
gameServices_.assetManager = assetManager.get();
gameServices_.expansionRegistry = expansionRegistry_.get();
// Create game handler with explicit service dependencies
gameHandler = std::make_unique<game::GameHandler>(gameServices_);
// Try to get WoW data path from environment variable
const char* dataPathEnv = std::getenv("WOW_DATA_PATH");
std::string dataPath = dataPathEnv ? dataPathEnv : "./Data";
@ -914,6 +920,7 @@ void Application::shutdown() {
world.reset();
LOG_WARNING("Resetting gameHandler...");
gameHandler.reset();
gameServices_ = {};
LOG_WARNING("Resetting authHandler...");
authHandler.reset();
LOG_WARNING("Resetting assetManager...");
@ -5657,6 +5664,8 @@ void Application::buildCreatureDisplayLookups() {
gryphonDisplayId_ = resolveDisplayIdForExactPath("Creature\\Gryphon\\Gryphon.m2");
wyvernDisplayId_ = resolveDisplayIdForExactPath("Creature\\Wyvern\\Wyvern.m2");
gameServices_.gryphonDisplayId = gryphonDisplayId_;
gameServices_.wyvernDisplayId = wyvernDisplayId_;
LOG_INFO("Taxi mount displayIds: gryphon=", gryphonDisplayId_, " wyvern=", wyvernDisplayId_);
// CharHairGeosets.dbc: maps (race, sex, hairStyleId) → skinSectionId for hair mesh

View file

@ -25,7 +25,10 @@ void Input::update() {
Uint32 mouseState = SDL_GetMouseState(&mouseX, &mouseY);
mousePosition = glm::vec2(static_cast<float>(mouseX), static_cast<float>(mouseY));
for (int i = 0; i < NUM_MOUSE_BUTTONS; ++i) {
// SDL_BUTTON(x) is defined as (1 << (x-1)), so button indices are 1-based.
// SDL_BUTTON(0) is undefined behavior (negative shift). Start at 1.
currentMouseState[0] = false;
for (int i = 1; i < NUM_MOUSE_BUTTONS; ++i) {
currentMouseState[i] = (mouseState & SDL_BUTTON(i)) != 0;
}

View file

@ -23,9 +23,10 @@ size_t readMemAvailableBytesFromProc() {
std::string line;
while (std::getline(meminfo, line)) {
// Format: "MemAvailable: 123456789 kB"
// /proc/meminfo format: "MemAvailable: 123456789 kB"
static constexpr size_t kFieldPrefixLen = 13; // strlen("MemAvailable:")
if (line.rfind("MemAvailable:", 0) != 0) continue;
std::istringstream iss(line.substr(13));
std::istringstream iss(line.substr(kFieldPrefixLen));
size_t kb = 0;
iss >> kb;
if (kb > 0) return kb * 1024ull;
@ -42,13 +43,18 @@ MemoryMonitor& MemoryMonitor::getInstance() {
}
void MemoryMonitor::initialize() {
constexpr size_t kOneGB = 1024ull * 1024 * 1024;
// Fallback if OS API unavailable — 16 GB is a safe conservative estimate
// that prevents over-aggressive asset caching on unknown hardware.
constexpr size_t kFallbackRAM = 16 * kOneGB;
#ifdef _WIN32
ULONGLONG totalKB = 0;
if (GetPhysicallyInstalledSystemMemory(&totalKB)) {
totalRAM_ = static_cast<size_t>(totalKB) * 1024ull;
LOG_INFO("System RAM detected: ", totalRAM_ / (1024 * 1024 * 1024), " GB");
LOG_INFO("System RAM detected: ", totalRAM_ / kOneGB, " GB");
} else {
totalRAM_ = 16ull * 1024 * 1024 * 1024;
totalRAM_ = kFallbackRAM;
LOG_WARNING("Could not detect system RAM, assuming 16GB");
}
#elif defined(__APPLE__)
@ -56,19 +62,18 @@ void MemoryMonitor::initialize() {
size_t len = sizeof(physmem);
if (sysctlbyname("hw.memsize", &physmem, &len, nullptr, 0) == 0) {
totalRAM_ = static_cast<size_t>(physmem);
LOG_INFO("System RAM detected: ", totalRAM_ / (1024 * 1024 * 1024), " GB");
LOG_INFO("System RAM detected: ", totalRAM_ / kOneGB, " GB");
} else {
totalRAM_ = 16ull * 1024 * 1024 * 1024;
totalRAM_ = kFallbackRAM;
LOG_WARNING("Could not detect system RAM, assuming 16GB");
}
#else
struct sysinfo info;
if (sysinfo(&info) == 0) {
totalRAM_ = static_cast<size_t>(info.totalram) * info.mem_unit;
LOG_INFO("System RAM detected: ", totalRAM_ / (1024 * 1024 * 1024), " GB");
LOG_INFO("System RAM detected: ", totalRAM_ / kOneGB, " GB");
} else {
// Fallback: assume 16GB
totalRAM_ = 16ull * 1024 * 1024 * 1024;
totalRAM_ = kFallbackRAM;
LOG_WARNING("Could not detect system RAM, assuming 16GB");
}
#endif

View file

@ -103,6 +103,8 @@ bool Window::initialize() {
return true;
}
// Shutdown progress uses LOG_WARNING so these messages are always visible even at
// default log levels — useful for diagnosing hangs or crashes during teardown.
void Window::shutdown() {
LOG_WARNING("Window::shutdown - vkContext...");
if (vkContext) {

View file

@ -435,11 +435,7 @@ void ChatHandler::handleChannelNotify(network::Packet& packet) {
switch (data.notifyType) {
case ChannelNotifyType::YOU_JOINED: {
bool found = false;
for (const auto& ch : joinedChannels_) {
if (ch == data.channelName) { found = true; break; }
}
if (!found) {
if (std::find(joinedChannels_.begin(), joinedChannels_.end(), data.channelName) == joinedChannels_.end()) {
joinedChannels_.push_back(data.channelName);
}
MessageChatData msg;
@ -461,11 +457,9 @@ void ChatHandler::handleChannelNotify(network::Packet& packet) {
break;
}
case ChannelNotifyType::PLAYER_ALREADY_MEMBER: {
bool found = false;
for (const auto& ch : joinedChannels_) {
if (ch == data.channelName) { found = true; break; }
}
if (!found) {
// Server confirms we're in this channel but our local list doesn't have it yet —
// can happen after reconnect or if the join notification was missed.
if (std::find(joinedChannels_.begin(), joinedChannels_.end(), data.channelName) == joinedChannels_.end()) {
joinedChannels_.push_back(data.channelName);
LOG_INFO("Already in channel: ", data.channelName);
}

View file

@ -451,7 +451,7 @@ void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) {
}
// Play combat sounds via CombatSoundManager + character vocalizations
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* csm = renderer->getCombatSoundManager()) {
auto weaponSize = audio::CombatSoundManager::WeaponSize::MEDIUM;
if (data.isMiss()) {
@ -1362,6 +1362,7 @@ void CombatHandler::togglePvp() {
auto entity = owner_.getEntityManager().getEntity(owner_.playerGuid);
bool currentlyPvp = false;
if (entity) {
// UNIT_FIELD_FLAGS (index 59), bit 0x1000 = UNIT_FLAG_PVP
currentlyPvp = (entity->getField(59) & 0x00001000) != 0;
}
if (currentlyPvp) {

View file

@ -1015,11 +1015,14 @@ bool EntityController::applyPlayerStatFields(const std::map<uint16_t, uint32_t>&
owner_.playerSpellDmgBonus_[key - pfi.spDmg1] = static_cast<int32_t>(val);
}
else if (pfi.healBonus != 0xFFFF && key == pfi.healBonus) { owner_.playerHealBonus_ = static_cast<int32_t>(val); }
else if (pfi.blockPct != 0xFFFF && key == pfi.blockPct) { std::memcpy(&owner_.playerBlockPct_, &val, 4); }
else if (pfi.dodgePct != 0xFFFF && key == pfi.dodgePct) { std::memcpy(&owner_.playerDodgePct_, &val, 4); }
else if (pfi.parryPct != 0xFFFF && key == pfi.parryPct) { std::memcpy(&owner_.playerParryPct_, &val, 4); }
else if (pfi.critPct != 0xFFFF && key == pfi.critPct) { std::memcpy(&owner_.playerCritPct_, &val, 4); }
else if (pfi.rangedCritPct != 0xFFFF && key == pfi.rangedCritPct) { std::memcpy(&owner_.playerRangedCritPct_, &val, 4); }
// Percentage stats are stored as IEEE 754 floats packed into uint32 update fields.
// memcpy reinterprets the bits; clamp to [0..100] to guard against NaN/Inf from
// corrupted packets reaching the UI (display-only, no gameplay logic depends on these).
else if (pfi.blockPct != 0xFFFF && key == pfi.blockPct) { std::memcpy(&owner_.playerBlockPct_, &val, 4); owner_.playerBlockPct_ = std::clamp(owner_.playerBlockPct_, 0.0f, 100.0f); }
else if (pfi.dodgePct != 0xFFFF && key == pfi.dodgePct) { std::memcpy(&owner_.playerDodgePct_, &val, 4); owner_.playerDodgePct_ = std::clamp(owner_.playerDodgePct_, 0.0f, 100.0f); }
else if (pfi.parryPct != 0xFFFF && key == pfi.parryPct) { std::memcpy(&owner_.playerParryPct_, &val, 4); owner_.playerParryPct_ = std::clamp(owner_.playerParryPct_, 0.0f, 100.0f); }
else if (pfi.critPct != 0xFFFF && key == pfi.critPct) { std::memcpy(&owner_.playerCritPct_, &val, 4); owner_.playerCritPct_ = std::clamp(owner_.playerCritPct_, 0.0f, 100.0f); }
else if (pfi.rangedCritPct != 0xFFFF && key == pfi.rangedCritPct) { std::memcpy(&owner_.playerRangedCritPct_, &val, 4); owner_.playerRangedCritPct_ = std::clamp(owner_.playerRangedCritPct_, 0.0f, 100.0f); }
else if (pfi.sCrit1 != 0xFFFF && key >= pfi.sCrit1 && key < pfi.sCrit1 + 7) {
std::memcpy(&owner_.playerSpellCritPct_[key - pfi.sCrit1], &val, 4);
}
@ -1072,6 +1075,8 @@ void EntityController::dispatchEntitySpawn(uint64_t guid, ObjectType objectType,
float unitScale = 1.0f;
uint16_t scaleIdx = fieldIndex(UF::OBJECT_FIELD_SCALE_X);
if (scaleIdx != 0xFFFF) {
// raw == 0 means the field was never populated (IEEE 754 0.0f is all-zero bits).
// Keep the default 1.0f rather than setting scale to 0 and making the entity invisible.
uint32_t raw = entity->getField(scaleIdx);
if (raw != 0) {
std::memcpy(&unitScale, &raw, sizeof(float));

View file

@ -58,7 +58,14 @@ std::string jsonValue(const std::string& json, const std::string& key) {
int jsonInt(const std::string& json, const std::string& key, int def = 0) {
std::string v = jsonValue(json, key);
if (v.empty()) return def;
try { return std::stoi(v); } catch (...) { return def; }
try {
return std::stoi(v);
} catch (...) {
// Non-numeric value for an integer field — fall back to default rather than
// crashing, but log it so malformed expansion.json files are diagnosable.
wowee::core::Logger::getInstance().warning("jsonInt: failed to parse '", key, "' value '", v, "', using default ", def);
return def;
}
}
std::vector<uint32_t> jsonUintArray(const std::string& json, const std::string& key) {

View file

@ -1,4 +1,5 @@
#include "game/game_handler.hpp"
#include "game/game_utils.hpp"
#include "game/chat_handler.hpp"
#include "game/movement_handler.hpp"
#include "game/combat_handler.hpp"
@ -92,23 +93,6 @@ bool isAuthCharPipelineOpcode(LogicalOpcode op) {
namespace {
bool isActiveExpansion(const char* expansionId) {
auto& app = core::Application::getInstance();
auto* registry = app.getExpansionRegistry();
if (!registry) return false;
auto* profile = registry->getActive();
if (!profile) return false;
return profile->id == expansionId;
}
bool isClassicLikeExpansion() {
return isActiveExpansion("classic") || isActiveExpansion("turtle");
}
bool isPreWotlk() {
return isClassicLikeExpansion() || isActiveExpansion("tbc");
}
bool envFlagEnabled(const char* key, bool defaultValue = false) {
const char* raw = std::getenv(key);
if (!raw || !*raw) return defaultValue;
@ -615,7 +599,7 @@ static QuestQueryRewards tryParseQuestRewards(const std::vector<uint8_t>& data,
template<typename ManagerGetter, typename Callback>
void GameHandler::withSoundManager(ManagerGetter getter, Callback cb) {
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = services_.renderer) {
if (auto* mgr = (renderer->*getter)()) cb(mgr);
}
}
@ -639,7 +623,8 @@ void GameHandler::registerWorldHandler(LogicalOpcode op, void (GameHandler::*han
};
}
GameHandler::GameHandler() {
GameHandler::GameHandler(GameServices& services)
: services_(services) {
LOG_DEBUG("GameHandler created");
setActiveOpcodeTable(&opcodeTable_);
@ -819,7 +804,7 @@ void GameHandler::resetDbcCaches() {
// Clear the AssetManager DBC file cache so that expansion-specific DBCs
// (CharSections, ItemDisplayInfo, etc.) are reloaded from the new expansion's
// MPQ files instead of returning stale data from a previous session/expansion.
auto* am = core::Application::getInstance().getAssetManager();
auto* am = services_.assetManager;
if (am) {
am->clearDBCCache();
}
@ -1213,7 +1198,7 @@ void GameHandler::updateTimers(float deltaTime) {
}
if (!alreadyAnnounced && pendingLootMoneyAmount_ > 0) {
addSystemChatMessage("Looted: " + formatCopperAmount(pendingLootMoneyAmount_));
auto* renderer = core::Application::getInstance().getRenderer();
auto* renderer = services_.renderer;
if (renderer) {
if (auto* sfx = renderer->getUiSoundManager()) {
if (pendingLootMoneyAmount_ >= 10000) {
@ -1362,16 +1347,22 @@ void GameHandler::update(float deltaTime) {
addSystemChatMessage("Interrupted.");
}
// Check if client-side cast timer expired (tick-down is in SpellHandler::updateTimers).
// SMSG_SPELL_GO normally clears casting, but GO interaction casts are client-timed
// and need this fallback to trigger the loot/use action.
// Two paths depending on whether this is a GO interaction cast:
if (spellHandler_ && spellHandler_->casting_ && spellHandler_->castTimeRemaining_ <= 0.0f) {
if (pendingGameObjectInteractGuid_ != 0) {
uint64_t interactGuid = pendingGameObjectInteractGuid_;
// GO interaction cast: do NOT call resetCastState() here. The server
// sends SMSG_SPELL_GO when the cast completes server-side (~50-200ms
// after the client timer expires due to float precision/frame timing).
// handleSpellGo checks `wasInTimedCast = casting_ && spellId == currentCastSpellId_`
// — if we clear those fields now, wasInTimedCast is false and the loot
// path (CMSG_LOOT via lastInteractedGoGuid_) never fires.
// Let the cast bar sit at 100% until SMSG_SPELL_GO arrives to clean up.
pendingGameObjectInteractGuid_ = 0;
performGameObjectInteractionNow(interactGuid);
}
} else {
// Regular cast with no GO pending: clean up immediately.
spellHandler_->resetCastState();
}
}
// Unit cast states and spell cooldowns are ticked by SpellHandler::updateTimers()
// (called from GameHandler::updateTimers above). No duplicate tick-down here.
@ -3099,7 +3090,7 @@ void GameHandler::registerOpcodeHandlers() {
uint64_t impTargetGuid = packet.readUInt64();
uint32_t impVisualId = packet.readUInt32();
if (impVisualId == 0) return;
auto* renderer = core::Application::getInstance().getRenderer();
auto* renderer = services_.renderer;
if (!renderer) return;
glm::vec3 spawnPos;
if (impTargetGuid == playerGuid) {
@ -6108,14 +6099,24 @@ void GameHandler::interactWithNpc(uint64_t guid) {
}
void GameHandler::interactWithGameObject(uint64_t guid) {
if (guid == 0) return;
if (!isInWorld()) return;
LOG_WARNING("[GO-DIAG] interactWithGameObject called: guid=0x", std::hex, guid, std::dec);
if (guid == 0) { LOG_WARNING("[GO-DIAG] BLOCKED: guid==0"); return; }
if (!isInWorld()) { LOG_WARNING("[GO-DIAG] BLOCKED: not in world"); return; }
// Do not overlap an actual spell cast.
if (spellHandler_ && spellHandler_->casting_ && spellHandler_->currentCastSpellId_ != 0) return;
if (spellHandler_ && spellHandler_->casting_ && spellHandler_->currentCastSpellId_ != 0) {
LOG_WARNING("[GO-DIAG] BLOCKED: already casting spellId=", spellHandler_->currentCastSpellId_);
return;
}
// Always clear melee intent before GO interactions.
stopAutoAttack();
// Interact immediately; server drives any real cast/channel feedback.
pendingGameObjectInteractGuid_ = 0;
// Set the pending GO guid so that:
// 1. cancelCast() won't send CMSG_CANCEL_CAST for GO-triggered casts
// (e.g., "Opening" on a quest chest) — without this, any movement
// during the cast cancels it server-side and quest credit is lost.
// 2. The cast-completion fallback in update() can call
// performGameObjectInteractionNow after the cast timer expires.
// 3. isGameObjectInteractionCasting() returns true during GO casts.
pendingGameObjectInteractGuid_ = guid;
performGameObjectInteractionNow(guid);
}
@ -6220,10 +6221,13 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
lastInteractedGoGuid_ = guid;
if (chestLike) {
// Chest-like GOs also need a CMSG_LOOT to open the loot window.
// Sent in the same frame: USE transitions the GO to lootable state,
// then LOOT requests the contents.
lootTarget(guid);
// Don't send CMSG_LOOT immediately — the server may start a timed cast
// (e.g., "Opening") and the GO isn't lootable until the cast finishes.
// Sending LOOT prematurely gets an empty response or is silently dropped,
// which can interfere with the server's loot state machine.
// Instead, handleSpellGo will send LOOT after the cast completes
// (using lastInteractedGoGuid_ set above). For instant-open chests
// (no cast), the server sends SMSG_LOOT_RESPONSE directly after USE.
} else if (isMailbox) {
LOG_INFO("Mailbox interaction: opening mail UI and requesting mail list");
mailboxGuid_ = guid;
@ -6367,7 +6371,7 @@ void GameHandler::offerQuestFromItem(uint64_t itemGuid, uint32_t questId) {
uint64_t GameHandler::getBagItemGuid(int bagIndex, int slotIndex) const {
if (bagIndex < 0 || bagIndex >= inventory.NUM_BAG_SLOTS) return 0;
if (slotIndex < 0) return 0;
uint64_t bagGuid = equipSlotGuids_[19 + bagIndex];
uint64_t bagGuid = equipSlotGuids_[Inventory::FIRST_BAG_EQUIP_SLOT + bagIndex];
if (bagGuid == 0) return 0;
auto it = containerContents_.find(bagGuid);
if (it == containerContents_.end()) return 0;
@ -7249,7 +7253,7 @@ void GameHandler::loadTitleNameCache() const {
if (titleNameCacheLoaded_) return;
titleNameCacheLoaded_ = true;
auto* am = core::Application::getInstance().getAssetManager();
auto* am = services_.assetManager;
if (!am || !am->isInitialized()) return;
auto dbc = am->loadDBC("CharTitles.dbc");
@ -7301,7 +7305,7 @@ void GameHandler::loadAchievementNameCache() {
if (achievementNameCacheLoaded_) return;
achievementNameCacheLoaded_ = true;
auto* am = core::Application::getInstance().getAssetManager();
auto* am = services_.assetManager;
if (!am || !am->isInitialized()) return;
auto dbc = am->loadDBC("Achievement.dbc");
@ -7386,7 +7390,7 @@ void GameHandler::loadFactionNameCache() const {
if (factionNameCacheLoaded_) return;
factionNameCacheLoaded_ = true;
auto* am = core::Application::getInstance().getAssetManager();
auto* am = services_.assetManager;
if (!am || !am->isInitialized()) return;
auto dbc = am->loadDBC("Faction.dbc");
@ -7487,7 +7491,7 @@ void GameHandler::loadAreaNameCache() const {
if (areaNameCacheLoaded_) return;
areaNameCacheLoaded_ = true;
auto* am = core::Application::getInstance().getAssetManager();
auto* am = services_.assetManager;
if (!am || !am->isInitialized()) return;
auto dbc = am->loadDBC("WorldMapArea.dbc");
@ -7522,7 +7526,7 @@ void GameHandler::loadMapNameCache() const {
if (mapNameCacheLoaded_) return;
mapNameCacheLoaded_ = true;
auto* am = core::Application::getInstance().getAssetManager();
auto* am = services_.assetManager;
if (!am || !am->isInitialized()) return;
auto dbc = am->loadDBC("Map.dbc");
@ -7555,7 +7559,7 @@ void GameHandler::loadLfgDungeonDbc() const {
if (lfgDungeonNameCacheLoaded_) return;
lfgDungeonNameCacheLoaded_ = true;
auto* am = core::Application::getInstance().getAssetManager();
auto* am = services_.assetManager;
if (!am || !am->isInitialized()) return;
auto dbc = am->loadDBC("LFGDungeons.dbc");

View file

@ -239,13 +239,13 @@ std::vector<Inventory::SwapOp> Inventory::computeSortSwaps() const {
entries.reserve(BACKPACK_SLOTS + NUM_BAG_SLOTS * MAX_BAG_SIZE);
for (int i = 0; i < BACKPACK_SLOTS; ++i) {
entries.push_back({0xFF, static_cast<uint8_t>(23 + i),
entries.push_back({0xFF, static_cast<uint8_t>(NUM_EQUIP_SLOTS + i),
backpack[i].item.itemId, backpack[i].item.quality,
backpack[i].item.stackCount});
}
for (int b = 0; b < NUM_BAG_SLOTS; ++b) {
for (int s = 0; s < bags[b].size; ++s) {
entries.push_back({static_cast<uint8_t>(19 + b), static_cast<uint8_t>(s),
entries.push_back({static_cast<uint8_t>(FIRST_BAG_EQUIP_SLOT + b), static_cast<uint8_t>(s),
bags[b].slots[s].item.itemId, bags[b].slots[s].item.quality,
bags[b].slots[s].item.stackCount});
}

View file

@ -70,7 +70,7 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) {
}
if (!alreadyAnnounced) {
owner_.addSystemChatMessage("Looted: " + formatCopperAmount(amount));
auto* renderer = core::Application::getInstance().getRenderer();
auto* renderer = owner_.services().renderer;
if (renderer) {
if (auto* sfx = renderer->getUiSoundManager()) {
if (amount >= 10000) sfx->playLootCoinLarge();
@ -222,7 +222,7 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) {
std::string msg = "Received item: " + link;
if (count > 1) msg += " x" + std::to_string(count);
owner_.addSystemChatMessage(msg);
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
sfx->playLootItem();
}
@ -253,7 +253,7 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) {
" result=", static_cast<int>(result));
if (result == 0) {
pendingSellToBuyback_.erase(itemGuid);
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
sfx->playDropOnGround();
}
@ -295,7 +295,7 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) {
const char* msg = (result < 7) ? sellErrors[result] : "Unknown sell error";
owner_.addUIError(std::string("Sell failed: ") + msg);
owner_.addSystemChatMessage(std::string("Sell failed: ") + msg);
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
sfx->playError();
}
@ -392,7 +392,7 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) {
std::string msg = errMsg ? errMsg : "Inventory error (" + std::to_string(error) + ").";
owner_.addUIError(msg);
owner_.addSystemChatMessage(msg);
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
sfx->playError();
}
@ -450,7 +450,7 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) {
}
owner_.addUIError(msg);
owner_.addSystemChatMessage(msg);
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
sfx->playError();
}
@ -474,7 +474,7 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) {
std::string msg = "Purchased: " + buildItemLink(pendingBuyItemId_, buyQuality, itemLabel);
if (itemCount > 1) msg += " x" + std::to_string(itemCount);
owner_.addSystemChatMessage(msg);
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
sfx->playPickupBag();
}
@ -695,6 +695,9 @@ void InventoryHandler::handleLootResponse(network::Packet& packet) {
const bool wotlkLoot = isActiveExpansion("wotlk");
if (!LootResponseParser::parse(packet, currentLoot_, wotlkLoot)) return;
const bool hasLoot = !currentLoot_.items.empty() || currentLoot_.gold > 0;
LOG_WARNING("[GO-DIAG] SMSG_LOOT_RESPONSE: guid=0x", std::hex, currentLoot_.lootGuid, std::dec,
" items=", currentLoot_.items.size(), " gold=", currentLoot_.gold,
" hasLoot=", hasLoot);
if (!hasLoot && owner_.isCasting() && owner_.getCurrentCastSpellId() != 0 && lastInteractedGoGuid_ != 0) {
LOG_DEBUG("Ignoring empty SMSG_LOOT_RESPONSE during gather cast");
return;
@ -763,7 +766,7 @@ void InventoryHandler::handleLootRemoved(network::Packet& packet) {
std::string msgStr = "Looted: " + link;
if (it->count > 1) msgStr += " x" + std::to_string(it->count);
owner_.addSystemChatMessage(msgStr);
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
sfx->playLootItem();
}
@ -978,7 +981,7 @@ void InventoryHandler::sellItemInBag(int bagIndex, int slotIndex) {
}
uint64_t itemGuid = 0;
uint64_t bagGuid = owner_.equipSlotGuids_[19 + bagIndex];
uint64_t bagGuid = owner_.equipSlotGuids_[Inventory::FIRST_BAG_EQUIP_SLOT + bagIndex];
if (bagGuid != 0) {
auto it = owner_.containerContents_.find(bagGuid);
if (it != owner_.containerContents_.end() && slotIndex < static_cast<int>(it->second.numSlots)) {
@ -1044,7 +1047,7 @@ void InventoryHandler::autoEquipItemBySlot(int backpackIndex) {
if (slot.empty()) return;
if (owner_.state == WorldState::IN_WORLD && owner_.socket) {
auto packet = AutoEquipItemPacket::build(0xFF, static_cast<uint8_t>(23 + backpackIndex));
auto packet = AutoEquipItemPacket::build(0xFF, static_cast<uint8_t>(Inventory::NUM_EQUIP_SLOTS + backpackIndex));
owner_.socket->send(packet);
}
}
@ -1055,7 +1058,7 @@ void InventoryHandler::autoEquipItemInBag(int bagIndex, int slotIndex) {
if (owner_.state == WorldState::IN_WORLD && owner_.socket) {
auto packet = AutoEquipItemPacket::build(
static_cast<uint8_t>(19 + bagIndex), static_cast<uint8_t>(slotIndex));
static_cast<uint8_t>(Inventory::FIRST_BAG_EQUIP_SLOT + bagIndex), static_cast<uint8_t>(slotIndex));
owner_.socket->send(packet);
}
}
@ -1084,8 +1087,8 @@ void InventoryHandler::useItemBySlot(int backpackIndex) {
" spellId=", useSpellId, " spellCount=", info->spells.size());
}
auto packet = owner_.packetParsers_
? owner_.packetParsers_->buildUseItem(0xFF, static_cast<uint8_t>(23 + backpackIndex), itemGuid, useSpellId)
: UseItemPacket::build(0xFF, static_cast<uint8_t>(23 + backpackIndex), itemGuid, useSpellId);
? owner_.packetParsers_->buildUseItem(0xFF, static_cast<uint8_t>(Inventory::NUM_EQUIP_SLOTS + backpackIndex), itemGuid, useSpellId)
: UseItemPacket::build(0xFF, static_cast<uint8_t>(Inventory::NUM_EQUIP_SLOTS + backpackIndex), itemGuid, useSpellId);
owner_.socket->send(packet);
} else if (itemGuid == 0) {
LOG_WARNING("useItemBySlot: itemGuid=0 for item='", slot.item.name,
@ -1101,7 +1104,7 @@ void InventoryHandler::useItemInBag(int bagIndex, int slotIndex) {
if (slot.empty()) return;
uint64_t itemGuid = 0;
uint64_t bagGuid = owner_.equipSlotGuids_[19 + bagIndex];
uint64_t bagGuid = owner_.equipSlotGuids_[Inventory::FIRST_BAG_EQUIP_SLOT + bagIndex];
if (bagGuid != 0) {
auto it = owner_.containerContents_.find(bagGuid);
if (it != owner_.containerContents_.end() && slotIndex < static_cast<int>(it->second.numSlots)) {
@ -1125,7 +1128,7 @@ void InventoryHandler::useItemInBag(int bagIndex, int slotIndex) {
}
}
}
uint8_t wowBag = static_cast<uint8_t>(19 + bagIndex);
uint8_t wowBag = static_cast<uint8_t>(Inventory::FIRST_BAG_EQUIP_SLOT + bagIndex);
auto packet = owner_.packetParsers_
? owner_.packetParsers_->buildUseItem(wowBag, static_cast<uint8_t>(slotIndex), itemGuid, useSpellId)
: UseItemPacket::build(wowBag, static_cast<uint8_t>(slotIndex), itemGuid, useSpellId);
@ -1142,8 +1145,8 @@ void InventoryHandler::openItemBySlot(int backpackIndex) {
if (backpackIndex < 0 || backpackIndex >= owner_.inventory.getBackpackSize()) return;
if (owner_.inventory.getBackpackSlot(backpackIndex).empty()) return;
if (owner_.state != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = OpenItemPacket::build(0xFF, static_cast<uint8_t>(23 + backpackIndex));
LOG_INFO("openItemBySlot: CMSG_OPEN_ITEM bag=0xFF slot=", (23 + backpackIndex));
auto packet = OpenItemPacket::build(0xFF, static_cast<uint8_t>(Inventory::NUM_EQUIP_SLOTS + backpackIndex));
LOG_INFO("openItemBySlot: CMSG_OPEN_ITEM bag=0xFF slot=", (Inventory::NUM_EQUIP_SLOTS + backpackIndex));
owner_.socket->send(packet);
}
@ -1152,7 +1155,7 @@ void InventoryHandler::openItemInBag(int bagIndex, int slotIndex) {
if (slotIndex < 0 || slotIndex >= owner_.inventory.getBagSize(bagIndex)) return;
if (owner_.inventory.getBagSlot(bagIndex, slotIndex).empty()) return;
if (owner_.state != WorldState::IN_WORLD || !owner_.socket) return;
uint8_t wowBag = static_cast<uint8_t>(19 + bagIndex);
uint8_t wowBag = static_cast<uint8_t>(Inventory::FIRST_BAG_EQUIP_SLOT + bagIndex);
auto packet = OpenItemPacket::build(wowBag, static_cast<uint8_t>(slotIndex));
LOG_INFO("openItemInBag: CMSG_OPEN_ITEM bag=", (int)wowBag, " slot=", slotIndex);
owner_.socket->send(packet);
@ -1178,7 +1181,7 @@ void InventoryHandler::splitItem(uint8_t srcBag, uint8_t srcSlot, uint8_t count)
int freeBp = owner_.inventory.findFreeBackpackSlot();
if (freeBp >= 0) {
uint8_t dstBag = 0xFF;
uint8_t dstSlot = static_cast<uint8_t>(23 + freeBp);
uint8_t dstSlot = static_cast<uint8_t>(Inventory::NUM_EQUIP_SLOTS + freeBp);
LOG_INFO("splitItem: src(bag=", (int)srcBag, " slot=", (int)srcSlot,
") count=", (int)count, " -> dst(bag=0xFF slot=", (int)dstSlot, ")");
auto packet = SplitItemPacket::build(srcBag, srcSlot, dstBag, dstSlot, count);
@ -1189,7 +1192,7 @@ void InventoryHandler::splitItem(uint8_t srcBag, uint8_t srcSlot, uint8_t count)
int bagSize = owner_.inventory.getBagSize(b);
for (int s = 0; s < bagSize; s++) {
if (owner_.inventory.getBagSlot(b, s).empty()) {
uint8_t dstBag = static_cast<uint8_t>(19 + b);
uint8_t dstBag = static_cast<uint8_t>(Inventory::FIRST_BAG_EQUIP_SLOT + b);
uint8_t dstSlot = static_cast<uint8_t>(s);
LOG_INFO("splitItem: src(bag=", (int)srcBag, " slot=", (int)srcSlot,
") count=", (int)count, " -> dst(bag=", (int)dstBag,
@ -1224,8 +1227,8 @@ void InventoryHandler::swapBagSlots(int srcBagIndex, int dstBagIndex) {
owner_.inventory.swapBagContents(srcBagIndex, dstBagIndex);
if (owner_.socket && owner_.socket->isConnected()) {
uint8_t srcSlot = static_cast<uint8_t>(19 + srcBagIndex);
uint8_t dstSlot = static_cast<uint8_t>(19 + dstBagIndex);
uint8_t srcSlot = static_cast<uint8_t>(Inventory::FIRST_BAG_EQUIP_SLOT + srcBagIndex);
uint8_t dstSlot = static_cast<uint8_t>(Inventory::FIRST_BAG_EQUIP_SLOT + dstBagIndex);
LOG_INFO("swapBagSlots: bag ", srcBagIndex, " (slot ", (int)srcSlot,
") <-> bag ", dstBagIndex, " (slot ", (int)dstSlot, ")");
auto packet = SwapItemPacket::build(255, dstSlot, 255, srcSlot);
@ -1245,7 +1248,7 @@ void InventoryHandler::unequipToBackpack(EquipSlot equipSlot) {
uint8_t srcBag = 0xFF;
uint8_t srcSlot = static_cast<uint8_t>(equipSlot);
uint8_t dstBag = 0xFF;
uint8_t dstSlot = static_cast<uint8_t>(23 + freeSlot);
uint8_t dstSlot = static_cast<uint8_t>(Inventory::NUM_EQUIP_SLOTS + freeSlot);
LOG_INFO("UnequipToBackpack: equipSlot=", (int)srcSlot,
" -> backpackIndex=", freeSlot, " (dstSlot=", (int)dstSlot, ")");
@ -1535,7 +1538,7 @@ bool InventoryHandler::attachItemFromBackpack(int backpackIndex) {
mailAttachments_[i].itemGuid = itemGuid;
mailAttachments_[i].item = slot.item;
mailAttachments_[i].srcBag = 0xFF;
mailAttachments_[i].srcSlot = static_cast<uint8_t>(23 + backpackIndex);
mailAttachments_[i].srcSlot = static_cast<uint8_t>(Inventory::NUM_EQUIP_SLOTS + backpackIndex);
return true;
}
}
@ -1547,7 +1550,7 @@ bool InventoryHandler::attachItemFromBag(int bagIndex, int slotIndex) {
if (slotIndex < 0 || slotIndex >= owner_.inventory.getBagSize(bagIndex)) return false;
const auto& slot = owner_.inventory.getBagSlot(bagIndex, slotIndex);
if (slot.empty()) return false;
uint64_t bagGuid = owner_.equipSlotGuids_[19 + bagIndex];
uint64_t bagGuid = owner_.equipSlotGuids_[Inventory::FIRST_BAG_EQUIP_SLOT + bagIndex];
if (bagGuid == 0) return false;
auto it = owner_.containerContents_.find(bagGuid);
if (it == owner_.containerContents_.end()) return false;
@ -1558,7 +1561,7 @@ bool InventoryHandler::attachItemFromBag(int bagIndex, int slotIndex) {
if (!mailAttachments_[i].occupied()) {
mailAttachments_[i].itemGuid = itemGuid;
mailAttachments_[i].item = slot.item;
mailAttachments_[i].srcBag = static_cast<uint8_t>(19 + bagIndex);
mailAttachments_[i].srcBag = static_cast<uint8_t>(Inventory::FIRST_BAG_EQUIP_SLOT + bagIndex);
mailAttachments_[i].srcSlot = static_cast<uint8_t>(slotIndex);
return true;
}
@ -1727,7 +1730,7 @@ void InventoryHandler::withdrawItem(uint8_t srcBag, uint8_t srcSlot) {
owner_.addSystemChatMessage("Inventory is full.");
return;
}
uint8_t dstSlot = static_cast<uint8_t>(23 + freeSlot);
uint8_t dstSlot = static_cast<uint8_t>(Inventory::NUM_EQUIP_SLOTS + freeSlot);
auto packet = SwapItemPacket::build(0xFF, dstSlot, srcBag, srcSlot);
owner_.socket->send(packet);
}
@ -2222,7 +2225,7 @@ void InventoryHandler::useEquipmentSet(uint32_t setId) {
for (int bp = 0; bp < 16 && !found; ++bp) {
if (owner_.getBackpackItemGuid(bp) == itemGuid) {
srcBag = 0xFF;
srcSlot = static_cast<uint8_t>(23 + bp);
srcSlot = static_cast<uint8_t>(Inventory::NUM_EQUIP_SLOTS + bp);
found = true;
}
}
@ -2230,7 +2233,7 @@ void InventoryHandler::useEquipmentSet(uint32_t setId) {
int bagSize = owner_.inventory.getBagSize(bag);
for (int s = 0; s < bagSize && !found; ++s) {
if (owner_.getBagItemGuid(bag, s) == itemGuid) {
srcBag = static_cast<uint8_t>(19 + bag);
srcBag = static_cast<uint8_t>(Inventory::FIRST_BAG_EQUIP_SLOT + bag);
srcSlot = static_cast<uint8_t>(s);
found = true;
}
@ -2352,6 +2355,8 @@ void InventoryHandler::handleItemQueryResponse(network::Packet& packet) {
// Without this, the entry stays in pendingItemQueries_ forever, blocking retries.
if (packet.getSize() >= 4) {
packet.setReadPos(0);
// High bit indicates a negative (invalid/missing) item entry response;
// mask it off so we can still clear the pending query by entry ID.
uint32_t rawEntry = packet.readUInt32() & ~0x80000000u;
owner_.pendingItemQueries_.erase(rawEntry);
}
@ -2377,7 +2382,7 @@ void InventoryHandler::handleItemQueryResponse(network::Packet& packet) {
std::string msg = "Received: " + link;
if (it->count > 1) msg += " x" + std::to_string(it->count);
owner_.addSystemChatMessage(msg);
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager()) sfx->playLootItem();
}
if (owner_.itemLootCallback_) owner_.itemLootCallback_(data.entry, it->count, data.quality, itemName);
@ -2707,7 +2712,7 @@ void InventoryHandler::rebuildOnlineInventory() {
// Bag contents (BAG1-BAG4 are equip slots 19-22)
for (int bagIdx = 0; bagIdx < 4; bagIdx++) {
uint64_t bagGuid = owner_.equipSlotGuids_[19 + bagIdx];
uint64_t bagGuid = owner_.equipSlotGuids_[Inventory::FIRST_BAG_EQUIP_SLOT + bagIdx];
if (bagGuid == 0) continue;
// Determine bag size from container fields or item template
@ -2731,11 +2736,11 @@ void InventoryHandler::rebuildOnlineInventory() {
owner_.inventory.setBagSize(bagIdx, numSlots);
// Also set bagSlots on the equipped bag item (for UI display)
auto& bagEquipSlot = owner_.inventory.getEquipSlot(static_cast<EquipSlot>(19 + bagIdx));
auto& bagEquipSlot = owner_.inventory.getEquipSlot(static_cast<EquipSlot>(Inventory::FIRST_BAG_EQUIP_SLOT + bagIdx));
if (!bagEquipSlot.empty()) {
ItemDef bagDef = bagEquipSlot.item;
bagDef.bagSlots = numSlots;
owner_.inventory.setEquipSlot(static_cast<EquipSlot>(19 + bagIdx), bagDef);
owner_.inventory.setEquipSlot(static_cast<EquipSlot>(Inventory::FIRST_BAG_EQUIP_SLOT + bagIdx), bagDef);
}
// Populate bag slot items
@ -3144,7 +3149,7 @@ void InventoryHandler::handleTrainerBuySucceeded(network::Packet& packet) {
owner_.addSystemChatMessage("You have learned " + name + ".");
else
owner_.addSystemChatMessage("Spell learned.");
if (auto* renderer = core::Application::getInstance().getRenderer())
if (auto* renderer = owner_.services().renderer)
if (auto* sfx = renderer->getUiSoundManager()) sfx->playQuestActivate();
owner_.fireAddonEvent("TRAINER_UPDATE", {});
owner_.fireAddonEvent("SPELLS_CHANGED", {});
@ -3166,7 +3171,7 @@ void InventoryHandler::handleTrainerBuyFailed(network::Packet& packet) {
else if (errorCode != 0) msg += " (error " + std::to_string(errorCode) + ")";
owner_.addUIError(msg);
owner_.addSystemChatMessage(msg);
if (auto* renderer = core::Application::getInstance().getRenderer())
if (auto* renderer = owner_.services().renderer)
if (auto* sfx = renderer->getUiSoundManager()) sfx->playError();
}

View file

@ -1816,7 +1816,7 @@ void MovementHandler::loadTaxiDbc() {
if (taxiDbcLoaded_) return;
taxiDbcLoaded_ = true;
auto* am = core::Application::getInstance().getAssetManager();
auto* am = owner_.services().assetManager;
if (!am || !am->isInitialized()) return;
auto nodesDbc = am->loadDBC("TaxiNodes.dbc");
@ -2005,9 +2005,8 @@ void MovementHandler::applyTaxiMountForCurrentNode() {
if (mountId == 541) mountId = 0;
}
if (mountId == 0) {
auto& app = core::Application::getInstance();
uint32_t gryphonId = app.getGryphonDisplayId();
uint32_t wyvernId = app.getWyvernDisplayId();
uint32_t gryphonId = owner_.services().gryphonDisplayId;
uint32_t wyvernId = owner_.services().wyvernDisplayId;
if (isAlliance && gryphonId != 0) mountId = gryphonId;
if (!isAlliance && wyvernId != 0) mountId = wyvernId;
if (mountId == 0) {
@ -2496,7 +2495,7 @@ void MovementHandler::loadAreaTriggerDbc() {
if (owner_.areaTriggerDbcLoaded_) return;
owner_.areaTriggerDbcLoaded_ = true;
auto* am = core::Application::getInstance().getAssetManager();
auto* am = owner_.services().assetManager;
if (!am || !am->isInitialized()) return;
auto dbc = am->loadDBC("AreaTrigger.dbc");

View file

@ -282,6 +282,7 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo
/*uint32_t splineId =*/ packet.readUInt32();
uint32_t pointCount = packet.readUInt32();
// Cap waypoints to prevent DoS from malformed packets allocating huge arrays
if (pointCount > 256) return false;
// points + endPoint (no splineMode in Classic)
@ -362,7 +363,9 @@ void ClassicPacketParsers::writeMovementPayload(network::Packet& packet, const M
// Transport data (Classic ONTRANSPORT = 0x02000000, no timestamp)
if (wireFlags & ClassicMoveFlags::ONTRANSPORT) {
// Packed transport GUID
// Packed GUID compression: only transmit non-zero bytes of the 8-byte GUID.
// The mask byte indicates which positions are present (bit N = byte N included).
// This is the standard WoW packed GUID wire format across all expansions.
uint8_t transMask = 0;
uint8_t transGuidBytes[8];
int transGuidByteCount = 0;

View file

@ -155,6 +155,7 @@ bool TbcPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock&
/*uint32_t splineId =*/ packet.readUInt32();
uint32_t pointCount = packet.readUInt32();
// Cap waypoints to prevent DoS from malformed packets allocating huge arrays
if (pointCount > 256) return false;
// points + endPoint (no splineMode in TBC)
@ -690,6 +691,8 @@ bool TbcPacketParsers::parseMonsterMove(network::Packet& packet, MonsterMoveData
if (pointCount == 0) return true;
if (pointCount > 16384) return false;
// Spline points are stored uncompressed when Catmull-Rom interpolation (0x80000)
// or linear movement (0x2000) flags are set; otherwise they use packed delta format
bool uncompressed = (data.splineFlags & (0x00080000 | 0x00002000)) != 0;
if (uncompressed) {
for (uint32_t i = 0; i < pointCount - 1; i++) {
@ -1359,6 +1362,8 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data)
return false;
}
// Cap hit targets to prevent oversized allocations from malformed spell packets.
// 128 is well above any real WoW AOE spell target count (max ~20 in practice).
const uint8_t rawHitCount = packet.readUInt8();
if (rawHitCount > 128) {
LOG_WARNING("[TBC] Spell go: hitCount capped (requested=", static_cast<int>(rawHitCount), ")");
@ -1819,6 +1824,8 @@ bool TbcPacketParsers::parseGuildRoster(network::Packet& packet, GuildRosterData
}
uint32_t numMembers = packet.readUInt32();
// Safety cap — guilds rarely exceed 500 members; 1000 prevents excessive
// memory allocation from malformed packets while covering all real cases
const uint32_t MAX_GUILD_MEMBERS = 1000;
if (numMembers > MAX_GUILD_MEMBERS) {
LOG_WARNING("TBC GuildRoster: numMembers capped (requested=", numMembers, ")");

View file

@ -469,7 +469,7 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
owner_.questCompleteCallback_(questId, it->title);
}
// Play quest-complete sound
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
sfx->playQuestComplete();
}
@ -533,7 +533,10 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
}
}
}
if (reqCount == 0) reqCount = count; // last-resort: avoid 0/0 display
// Some quests (e.g. escort/event quests) report kill credit updates without
// a corresponding objective count in SMSG_QUEST_QUERY_RESPONSE. Fall back to
// current count so the progress display shows "N/N" instead of "N/0".
if (reqCount == 0) reqCount = count;
quest.killCounts[entry] = {count, reqCount};
std::string creatureName = owner_.getCachedCreatureName(entry);
@ -1092,7 +1095,7 @@ void QuestHandler::acceptQuest() {
pendingQuestAcceptNpcGuids_[questId] = npcGuid;
// Play quest-accept sound
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
sfx->playQuestActivate();
}

View file

@ -20,9 +20,12 @@ namespace game {
// LFG join result codes from LFGJoinResult enum (WotLK 3.3.5a).
// Case 0 = success (no error message needed), returns nullptr so the caller
// knows not to display an error string.
static const char* lfgJoinResultString(uint8_t result) {
switch (result) {
case 0: return nullptr;
case 0: return nullptr; // LFG_JOIN_OK
case 1: return "Role check failed.";
case 2: return "No LFG slots available for your group.";
case 3: return "No LFG object found.";
@ -37,6 +40,7 @@ static const char* lfgJoinResultString(uint8_t result) {
case 12: return "A party member is marked as a deserter.";
case 13: return "You are on a random dungeon cooldown.";
case 14: return "A party member is on a random dungeon cooldown.";
case 15: return "Cannot join dungeon finder."; // LFG_JOIN_INTERNAL_ERROR
case 16: return "No spec/role available.";
default: return "Cannot join dungeon finder.";
}
@ -1049,7 +1053,7 @@ void SocialHandler::handleDuelRequested(network::Packet& packet) {
}
pendingDuelRequest_ = true;
owner_.addSystemChatMessage(duelChallengerName_ + " challenges you to a duel!");
if (auto* renderer = core::Application::getInstance().getRenderer())
if (auto* renderer = owner_.services().renderer)
if (auto* sfx = renderer->getUiSoundManager()) sfx->playTargetSelect();
if (owner_.addonEventCallback_) owner_.addonEventCallback_("DUEL_REQUESTED", {duelChallengerName_});
}
@ -1215,7 +1219,7 @@ void SocialHandler::handleGroupInvite(network::Packet& packet) {
pendingInviterName = data.inviterName;
if (!data.inviterName.empty())
owner_.addSystemChatMessage(data.inviterName + " has invited you to a group.");
if (auto* renderer = core::Application::getInstance().getRenderer())
if (auto* renderer = owner_.services().renderer)
if (auto* sfx = renderer->getUiSoundManager()) sfx->playTargetSelect();
if (owner_.addonEventCallback_)
owner_.addonEventCallback_("PARTY_INVITE_REQUEST", {data.inviterName});

View file

@ -446,6 +446,18 @@ void SpellHandler::confirmPetUnlearn() {
petUnlearnCost_ = 0;
}
uint32_t SpellHandler::findOnUseSpellId(uint32_t itemId) const {
if (auto* info = owner_.getItemInfo(itemId)) {
for (const auto& sp : info->spells) {
// spellTrigger 0 = "Use", 5 = "No Delay" — both are player-activated on-use effects
if (sp.spellId != 0 && (sp.spellTrigger == 0 || sp.spellTrigger == 5)) {
return sp.spellId;
}
}
}
return 0;
}
void SpellHandler::useItemBySlot(int backpackIndex) {
if (backpackIndex < 0 || backpackIndex >= owner_.inventory.getBackpackSize()) return;
const auto& slot = owner_.inventory.getBackpackSlot(backpackIndex);
@ -457,19 +469,10 @@ void SpellHandler::useItemBySlot(int backpackIndex) {
}
if (itemGuid != 0 && owner_.state == WorldState::IN_WORLD && owner_.socket) {
uint32_t useSpellId = 0;
if (auto* info = owner_.getItemInfo(slot.item.itemId)) {
for (const auto& sp : info->spells) {
if (sp.spellId != 0 && (sp.spellTrigger == 0 || sp.spellTrigger == 5)) {
useSpellId = sp.spellId;
break;
}
}
}
uint32_t useSpellId = findOnUseSpellId(slot.item.itemId);
auto packet = owner_.packetParsers_
? owner_.packetParsers_->buildUseItem(0xFF, static_cast<uint8_t>(23 + backpackIndex), itemGuid, useSpellId)
: UseItemPacket::build(0xFF, static_cast<uint8_t>(23 + backpackIndex), itemGuid, useSpellId);
? owner_.packetParsers_->buildUseItem(0xFF, static_cast<uint8_t>(Inventory::NUM_EQUIP_SLOTS + backpackIndex), itemGuid, useSpellId)
: UseItemPacket::build(0xFF, static_cast<uint8_t>(Inventory::NUM_EQUIP_SLOTS + backpackIndex), itemGuid, useSpellId);
owner_.socket->send(packet);
} else if (itemGuid == 0) {
owner_.addSystemChatMessage("Cannot use that item right now.");
@ -483,7 +486,7 @@ void SpellHandler::useItemInBag(int bagIndex, int slotIndex) {
if (slot.empty()) return;
uint64_t itemGuid = 0;
uint64_t bagGuid = owner_.equipSlotGuids_[19 + bagIndex];
uint64_t bagGuid = owner_.equipSlotGuids_[Inventory::FIRST_BAG_EQUIP_SLOT + bagIndex];
if (bagGuid != 0) {
auto it = owner_.containerContents_.find(bagGuid);
if (it != owner_.containerContents_.end() && slotIndex < static_cast<int>(it->second.numSlots)) {
@ -498,17 +501,8 @@ void SpellHandler::useItemInBag(int bagIndex, int slotIndex) {
" itemGuid=0x", std::hex, itemGuid, std::dec);
if (itemGuid != 0 && owner_.state == WorldState::IN_WORLD && owner_.socket) {
uint32_t useSpellId = 0;
if (auto* info = owner_.getItemInfo(slot.item.itemId)) {
for (const auto& sp : info->spells) {
if (sp.spellId != 0 && (sp.spellTrigger == 0 || sp.spellTrigger == 5)) {
useSpellId = sp.spellId;
break;
}
}
}
uint8_t wowBag = static_cast<uint8_t>(19 + bagIndex);
uint32_t useSpellId = findOnUseSpellId(slot.item.itemId);
uint8_t wowBag = static_cast<uint8_t>(Inventory::FIRST_BAG_EQUIP_SLOT + bagIndex);
auto packet = owner_.packetParsers_
? owner_.packetParsers_->buildUseItem(wowBag, static_cast<uint8_t>(slotIndex), itemGuid, useSpellId)
: UseItemPacket::build(wowBag, static_cast<uint8_t>(slotIndex), itemGuid, useSpellId);
@ -603,7 +597,7 @@ void SpellHandler::loadTalentDbc() {
if (talentDbcLoaded_) return;
talentDbcLoaded_ = true;
auto* am = core::Application::getInstance().getAssetManager();
auto* am = owner_.services().assetManager;
if (!am || !am->isInitialized()) return;
// Load Talent.dbc
@ -794,13 +788,14 @@ void SpellHandler::handleCastFailed(network::Packet& packet) {
currentCastSpellId_ = 0;
castTimeRemaining_ = 0.0f;
owner_.lastInteractedGoGuid_ = 0;
owner_.pendingGameObjectInteractGuid_ = 0;
craftQueueSpellId_ = 0;
craftQueueRemaining_ = 0;
queuedSpellId_ = 0;
queuedSpellTarget_ = 0;
// Stop precast sound
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* ssm = renderer->getSpellSoundManager()) {
ssm->stopPrecast();
}
@ -822,7 +817,7 @@ void SpellHandler::handleCastFailed(network::Packet& packet) {
msg.message = errMsg;
owner_.addLocalChatMessage(msg);
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
sfx->playError();
}
@ -874,7 +869,7 @@ void SpellHandler::handleSpellStart(network::Packet& packet) {
// Play precast sound — skip profession/tradeskill spells
if (!owner_.isProfessionSpell(data.spellId)) {
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* ssm = renderer->getSpellSoundManager()) {
owner_.loadSpellNameCache();
auto it = owner_.spellNameCache_.find(data.spellId);
@ -912,7 +907,7 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
if (data.casterUnit == owner_.playerGuid) {
// Play cast-complete sound
if (!owner_.isProfessionSpell(data.spellId)) {
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* ssm = renderer->getSpellSoundManager()) {
owner_.loadSpellNameCache();
auto it = owner_.spellNameCache_.find(data.spellId);
@ -936,7 +931,7 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
}
if (isMeleeAbility) {
if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_();
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* csm = renderer->getCombatSoundManager()) {
csm->playWeaponSwing(audio::CombatSoundManager::WeaponSize::MEDIUM, false);
csm->playImpact(audio::CombatSoundManager::WeaponSize::MEDIUM,
@ -947,16 +942,28 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
const bool wasInTimedCast = casting_ && (data.spellId == currentCastSpellId_);
LOG_WARNING("[GO-DIAG] SPELL_GO: spellId=", data.spellId,
" casting=", casting_, " currentCast=", currentCastSpellId_,
" wasInTimedCast=", wasInTimedCast,
" lastGoGuid=0x", std::hex, owner_.lastInteractedGoGuid_,
" pendingGoGuid=0x", owner_.pendingGameObjectInteractGuid_, std::dec);
casting_ = false;
castIsChannel_ = false;
currentCastSpellId_ = 0;
castTimeRemaining_ = 0.0f;
// Gather node looting
// Gather node looting: re-send CMSG_LOOT now that the cast completed.
if (wasInTimedCast && owner_.lastInteractedGoGuid_ != 0) {
LOG_WARNING("[GO-DIAG] Sending CMSG_LOOT for GO 0x", std::hex,
owner_.lastInteractedGoGuid_, std::dec);
owner_.lootTarget(owner_.lastInteractedGoGuid_);
owner_.lastInteractedGoGuid_ = 0;
}
// Clear the GO interaction guard so future cancelCast() calls work
// normally. Without this, pendingGameObjectInteractGuid_ stays stale
// and suppresses CMSG_CANCEL_CAST for ALL subsequent spell casts.
owner_.pendingGameObjectInteractGuid_ = 0;
if (owner_.spellCastAnimCallback_) {
owner_.spellCastAnimCallback_(owner_.playerGuid, false, false);
@ -983,7 +990,7 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
if (tgt == owner_.playerGuid) { targetsPlayer = true; break; }
}
if (targetsPlayer) {
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* ssm = renderer->getSpellSoundManager()) {
owner_.loadSpellNameCache();
auto it = owner_.spellNameCache_.find(data.spellId);
@ -1029,7 +1036,7 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
}
if (playerIsHit || playerHitEnemy) {
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* ssm = renderer->getSpellSoundManager()) {
owner_.loadSpellNameCache();
auto it = owner_.spellNameCache_.find(data.spellId);
@ -1389,7 +1396,7 @@ void SpellHandler::handleAchievementEarned(network::Packet& packet) {
owner_.earnedAchievements_.insert(achievementId);
owner_.achievementDates_[achievementId] = earnDate;
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
sfx->playAchievementAlert();
}
@ -1456,21 +1463,22 @@ void SpellHandler::handlePetSpells(network::Packet& packet) {
return;
}
if (!packet.hasRemaining(4)) goto done;
// Parse optional pet fields — bail on truncated packets but always log+fire below.
do {
if (!packet.hasRemaining(4)) break;
/*uint16_t dur =*/ packet.readUInt16();
/*uint16_t timer =*/ packet.readUInt16();
if (!packet.hasRemaining(2)) goto done;
if (!packet.hasRemaining(2)) break;
owner_.petReact_ = packet.readUInt8();
owner_.petCommand_ = packet.readUInt8();
if (!packet.hasRemaining(GameHandler::PET_ACTION_BAR_SLOTS * 4u)) goto done;
if (!packet.hasRemaining(GameHandler::PET_ACTION_BAR_SLOTS * 4u)) break;
for (int i = 0; i < GameHandler::PET_ACTION_BAR_SLOTS; ++i) {
owner_.petActionSlots_[i] = packet.readUInt32();
}
if (!packet.hasRemaining(1)) goto done;
{
if (!packet.hasRemaining(1)) break;
uint8_t spellCount = packet.readUInt8();
owner_.petSpellList_.clear();
owner_.petAutocastSpells_.clear();
@ -1483,9 +1491,8 @@ void SpellHandler::handlePetSpells(network::Packet& packet) {
owner_.petAutocastSpells_.insert(spellId);
}
}
}
} while (false);
done:
LOG_INFO("SMSG_PET_SPELLS: petGuid=0x", std::hex, owner_.petGuid_, std::dec,
" react=", static_cast<int>(owner_.petReact_), " command=", static_cast<int>(owner_.petCommand_),
" spells=", owner_.petSpellList_.size());
@ -1614,7 +1621,11 @@ void SpellHandler::resetCastState() {
queuedSpellId_ = 0;
queuedSpellTarget_ = 0;
owner_.pendingGameObjectInteractGuid_ = 0;
owner_.lastInteractedGoGuid_ = 0;
// lastInteractedGoGuid_ is intentionally NOT cleared here — it must survive
// until handleSpellGo sends CMSG_LOOT after the server-side cast completes.
// handleSpellGo clears it after use (line 958). Previously this was cleared
// here, which meant the client-side timer fallback destroyed the guid before
// SMSG_SPELL_GO arrived, preventing loot from opening on quest chests.
}
void SpellHandler::resetAllState() {
@ -1667,7 +1678,7 @@ void SpellHandler::loadSpellNameCache() const {
if (owner_.spellNameCacheLoaded_) return;
owner_.spellNameCacheLoaded_ = true;
auto* am = core::Application::getInstance().getAssetManager();
auto* am = owner_.services().assetManager;
if (!am || !am->isInitialized()) return;
auto dbc = am->loadDBC("Spell.dbc");
@ -1779,7 +1790,7 @@ void SpellHandler::loadSkillLineAbilityDbc() {
if (owner_.skillLineAbilityLoaded_) return;
owner_.skillLineAbilityLoaded_ = true;
auto* am = core::Application::getInstance().getAssetManager();
auto* am = owner_.services().assetManager;
if (!am || !am->isInitialized()) return;
auto slaDbc = am->loadDBC("SkillLineAbility.dbc");
@ -1880,7 +1891,7 @@ const std::string& SpellHandler::getSpellDescription(uint32_t spellId) const {
std::string SpellHandler::getEnchantName(uint32_t enchantId) const {
if (enchantId == 0) return {};
auto* am = core::Application::getInstance().getAssetManager();
auto* am = owner_.services().assetManager;
if (!am || !am->isInitialized()) return {};
auto dbc = am->loadDBC("SpellItemEnchantment.dbc");
if (!dbc || !dbc->isLoaded()) return {};
@ -1928,7 +1939,7 @@ void SpellHandler::loadSkillLineDbc() {
if (owner_.skillLineDbcLoaded_) return;
owner_.skillLineDbcLoaded_ = true;
auto* am = core::Application::getInstance().getAssetManager();
auto* am = owner_.services().assetManager;
if (!am || !am->isInitialized()) return;
auto dbc = am->loadDBC("SkillLine.dbc");
@ -2141,7 +2152,7 @@ void SpellHandler::handlePlaySpellVisual(network::Packet& packet) {
uint64_t casterGuid = packet.readUInt64();
uint32_t visualId = packet.readUInt32();
if (visualId == 0) return;
auto* renderer = core::Application::getInstance().getRenderer();
auto* renderer = owner_.services().renderer;
if (!renderer) return;
glm::vec3 spawnPos;
if (casterGuid == owner_.playerGuid) {
@ -2339,7 +2350,7 @@ void SpellHandler::handleSpellFailure(network::Packet& packet) {
craftQueueRemaining_ = 0;
queuedSpellId_ = 0;
queuedSpellTarget_ = 0;
if (auto* renderer = core::Application::getInstance().getRenderer()) {
if (auto* renderer = owner_.services().renderer) {
if (auto* ssm = renderer->getSpellSoundManager()) {
ssm->stopPrecast();
}

View file

@ -203,16 +203,17 @@ void TransportManager::loadPathFromNodes(uint32_t pathId, const std::vector<glm:
path.points.push_back({cumulativeMs, waypoints[i]});
}
// Add explicit wrap segment (last → first) for looping paths
// Add explicit wrap segment (last → first) for looping paths.
// By duplicating the first point at the end with cumulative time, the path
// becomes time-closed and evalTimedCatmullRom handles wrap via modular time
// instead of index wrapping — so looping is always false after construction.
if (looping) {
float wrapDist = glm::distance(waypoints.back(), waypoints.front());
uint32_t wrapMs = glm::max(1u, segMsFromDist(wrapDist));
cumulativeMs += wrapMs;
path.points.push_back({cumulativeMs, waypoints.front()}); // Duplicate first point
path.looping = false; // Time-closed path, no need for index wrapping
} else {
path.looping = false;
path.points.push_back({cumulativeMs, waypoints.front()});
}
path.looping = false;
path.durationMs = cumulativeMs;
paths_[pathId] = path;
@ -301,14 +302,16 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
// Guard against bad fallback Z curves on some remapped transport paths (notably icebreakers),
// where path offsets can sink far below sea level when we only have spawn-time data.
// Skip Z clamping for world-coordinate paths (TaxiPathNode) where values are absolute positions.
// Clamp fallback Z offsets for non-world-coordinate paths to prevent transport
// models from sinking below sea level on paths derived only from spawn-time data
// (notably icebreaker routes where the DBC path has steep vertical curves).
constexpr float kMinFallbackZOffset = -2.0f;
constexpr float kMaxFallbackZOffset = 8.0f;
if (!path.worldCoords) {
if (transport.useClientAnimation && transport.serverUpdateCount <= 1) {
constexpr float kMinFallbackZOffset = -2.0f;
pathOffset.z = glm::max(pathOffset.z, kMinFallbackZOffset);
}
if (!transport.useClientAnimation && !transport.hasServerClock) {
constexpr float kMinFallbackZOffset = -2.0f;
constexpr float kMaxFallbackZOffset = 8.0f;
pathOffset.z = glm::clamp(pathOffset.z, kMinFallbackZOffset, kMaxFallbackZOffset);
}
}

View file

@ -216,6 +216,13 @@ uint32_t WardenEmulator::hookAPI(const std::string& dllName,
return stubAddr;
}
uint32_t WardenEmulator::getAPIAddress(const std::string& dllName, const std::string& funcName) const {
auto libIt = apiAddresses_.find(dllName);
if (libIt == apiAddresses_.end()) return 0;
auto funcIt = libIt->second.find(funcName);
return (funcIt != libIt->second.end()) ? funcIt->second : 0;
}
void WardenEmulator::setupCommonAPIHooks() {
LOG_INFO("WardenEmulator: Setting up common Windows API hooks...");
@ -614,6 +621,7 @@ bool WardenEmulator::freeMemory(uint32_t) { return false; }
uint32_t WardenEmulator::getRegister(int) { return 0; }
void WardenEmulator::setRegister(int, uint32_t) {}
void WardenEmulator::setupCommonAPIHooks() {}
uint32_t WardenEmulator::getAPIAddress(const std::string&, const std::string&) const { return 0; }
uint32_t WardenEmulator::writeData(const void*, size_t) { return 0; }
std::vector<uint8_t> WardenEmulator::readData(uint32_t, size_t) { return {}; }
void WardenEmulator::hookCode(uc_engine*, uint64_t, uint32_t, void*) {}

View file

@ -116,6 +116,9 @@ bool hmacSha1Matches(const uint8_t seedBytes[4], const std::string& text, const
return outLen == SHA_DIGEST_LENGTH && std::memcmp(out, expected, SHA_DIGEST_LENGTH) == 0;
}
// Pre-computed HMAC-SHA1 hashes of known door M2 models that Warden checks
// to verify the client hasn't modified collision geometry (wall-hack detection).
// These hashes match the unmodified 3.3.5a client data files.
const std::unordered_map<std::string, std::array<uint8_t, 20>>& knownDoorHashes() {
static const std::unordered_map<std::string, std::array<uint8_t, 20>> k = {
{"world\\lordaeron\\stratholme\\activedoodads\\doors\\nox_door_plague.m2",
@ -437,8 +440,21 @@ void WardenHandler::handleWardenData(network::Packet& packet) {
}
}
// Load the module (decrypt, decompress, parse, relocate)
// Load the module (decrypt, decompress, parse, relocate, init)
wardenLoadedModule_ = std::make_shared<WardenModule>();
// Inject crypto and socket so module callbacks (sendPacket, generateRC4)
// can reach the network layer during initializeModule().
wardenLoadedModule_->setCallbackDependencies(
wardenCrypto_.get(),
[this](const uint8_t* data, size_t len) {
if (!wardenCrypto_ || !owner_.socket) return;
std::vector<uint8_t> plaintext(data, data + len);
auto encrypted = wardenCrypto_->encrypt(plaintext);
network::Packet pkt(wireOpcode(Opcode::CMSG_WARDEN_DATA));
for (uint8_t b : encrypted) pkt.writeUInt8(b);
owner_.socket->send(pkt);
LOG_DEBUG("Warden: Module sendPacket callback sent ", len, " bytes");
});
if (wardenLoadedModule_->load(wardenModuleData_, wardenModuleHash_, wardenModuleKey_)) { // codeql[cpp/weak-cryptographic-algorithm]
LOG_INFO("Warden: Module loaded successfully (image size=",
wardenLoadedModule_->getModuleSize(), " bytes)");
@ -781,7 +797,7 @@ void WardenHandler::handleWardenData(network::Packet& packet) {
std::replace(np.begin(), np.end(), '/', '\\');
auto knownIt = knownDoorHashes().find(np);
if (knownIt != knownDoorHashes().end()) { found = true; hash.assign(knownIt->second.begin(), knownIt->second.end()); }
auto* am = core::Application::getInstance().getAssetManager();
auto* am = owner_.services().assetManager;
if (am && am->isInitialized() && !found) {
std::vector<uint8_t> fd;
std::string rp = resolveCaseInsensitiveDataPath(am->getDataPath(), filePath);
@ -1194,7 +1210,7 @@ void WardenHandler::handleWardenData(network::Packet& packet) {
hash.assign(knownIt->second.begin(), knownIt->second.end());
}
auto* am = core::Application::getInstance().getAssetManager();
auto* am = owner_.services().assetManager;
if (am && am->isInitialized() && !found) {
std::vector<uint8_t> fileData;
std::string resolvedFsPath =

View file

@ -14,12 +14,16 @@
namespace wowee {
namespace game {
// Bounds-checked little-endian reads for PE parsing — malformed Warden modules
// must not cause out-of-bounds access.
static inline uint32_t readLE32(const std::vector<uint8_t>& data, size_t offset) {
if (offset + 4 > data.size()) return 0;
return data[offset] | (uint32_t(data[offset+1]) << 8)
| (uint32_t(data[offset+2]) << 16) | (uint32_t(data[offset+3]) << 24);
}
static inline uint16_t readLE16(const std::vector<uint8_t>& data, size_t offset) {
if (offset + 2 > data.size()) return 0;
return data[offset] | (uint16_t(data[offset+1]) << 8);
}
@ -95,12 +99,14 @@ bool WardenMemory::parsePE(const std::vector<uint8_t>& fileData) {
if (rawDataSize == 0 || rawDataOffset == 0) continue;
// Clamp copy size to file and image bounds
// Clamp copy size to file and image bounds.
// Guard against underflow: if offset exceeds buffer size, skip the section
// entirely rather than wrapping to a huge uint32_t in the subtraction.
if (rawDataOffset >= fileData.size() || virtualAddr >= imageSize_) continue;
uint32_t copySize = std::min(rawDataSize, virtualSize);
if (rawDataOffset + copySize > fileData.size())
copySize = static_cast<uint32_t>(fileData.size()) - rawDataOffset;
if (virtualAddr + copySize > imageSize_)
copySize = imageSize_ - virtualAddr;
uint32_t maxFromFile = static_cast<uint32_t>(fileData.size()) - rawDataOffset;
uint32_t maxFromImage = imageSize_ - virtualAddr;
copySize = std::min({copySize, maxFromFile, maxFromImage});
std::memcpy(image_.data() + virtualAddr, fileData.data() + rawDataOffset, copySize);

View file

@ -1,4 +1,5 @@
#include "game/warden_module.hpp"
#include "game/warden_crypto.hpp"
#include "auth/crypto.hpp"
#include "core/logger.hpp"
#include <cstring>
@ -30,14 +31,26 @@ namespace wowee {
namespace game {
// ============================================================================
// Thread-local pointer to the active WardenModule instance during initializeModule().
// C function pointer callbacks (sendPacket, validateModule, generateRC4) can't capture
// state, so they use this to reach the module's crypto and socket dependencies.
static thread_local WardenModule* tl_activeModule = nullptr;
// WardenModule Implementation
// ============================================================================
void WardenModule::setCallbackDependencies(WardenCrypto* crypto, SendPacketFunc sendFunc) {
callbackCrypto_ = crypto;
callbackSendPacket_ = std::move(sendFunc);
}
WardenModule::WardenModule()
: loaded_(false)
, moduleMemory_(nullptr)
, moduleSize_(0)
, moduleBase_(0x400000) // Default module base address
// 0x400000 is the default PE image base for 32-bit Windows executables.
// Warden modules are loaded as if they were PE DLLs at this base address.
, moduleBase_(0x400000)
{
}
@ -74,13 +87,14 @@ bool WardenModule::load(const std::vector<uint8_t>& moduleData,
// Step 3: Verify RSA signature
if (!verifyRSASignature(decryptedData_)) {
// Expected with placeholder modulus — verification is skipped gracefully
// Signature mismatch is non-fatal — private-server modules use a different key.
}
// Step 4: Strip RSA signature (last 256 bytes) then zlib decompress
// Step 4: Strip RSA-2048 signature (last 256 bytes = 2048 bits) then zlib decompress.
static constexpr size_t kRsaSignatureSize = 256;
std::vector<uint8_t> dataWithoutSig;
if (decryptedData_.size() > 256) {
dataWithoutSig.assign(decryptedData_.begin(), decryptedData_.end() - 256);
if (decryptedData_.size() > kRsaSignatureSize) {
dataWithoutSig.assign(decryptedData_.begin(), decryptedData_.end() - kRsaSignatureSize);
} else {
dataWithoutSig = decryptedData_;
}
@ -99,13 +113,10 @@ bool WardenModule::load(const std::vector<uint8_t>& moduleData,
LOG_ERROR("WardenModule: Address relocations failed; continuing with unrelocated image");
}
// Step 7: Bind APIs
if (!bindAPIs()) {
LOG_ERROR("WardenModule: API binding failed!");
// Note: Currently returns true (stub) on both Windows and Linux
}
// Step 8: Initialize module
// Step 7+8: Initialize module (creates emulator) then bind APIs (patches IAT).
// API binding must happen after emulator setup (needs stub addresses) but before
// the module entry point is called (needs resolved imports). Both are handled
// inside initializeModule().
if (!initializeModule()) {
LOG_ERROR("WardenModule: Module initialization failed; continuing with stub callbacks");
}
@ -332,30 +343,30 @@ bool WardenModule::verifyRSASignature(const std::vector<uint8_t>& data) {
// Extract data without signature
std::vector<uint8_t> dataWithoutSig(data.begin(), data.end() - 256);
// Hardcoded WoW 3.3.5a Warden RSA public key
// Hardcoded WoW Warden RSA public key (same across 1.12.1, 2.4.3, 3.3.5a)
// Exponent: 0x010001 (65537)
const uint32_t exponent = 0x010001;
// Modulus (256 bytes) - Extracted from WoW 3.3.5a (build 12340) client
// Extracted from Wow.exe at offset 0x005e3a03 (.rdata section)
// This is the actual RSA-2048 public key modulus used by Warden
// Modulus (256 bytes) — RSA-2048 public key used by the WoW client to verify
// Warden module signatures. Confirmed against namreeb/WardenSigning ClientKey.hpp
// and SkullSecurity wiki (Warden_Modules page).
const uint8_t modulus[256] = {
0x51, 0xAD, 0x57, 0x75, 0x16, 0x92, 0x0A, 0x0E, 0xEB, 0xFA, 0xF8, 0x1B, 0x37, 0x49, 0x7C, 0xDD,
0x47, 0xDA, 0x5E, 0x02, 0x8D, 0x96, 0x75, 0x21, 0x27, 0x59, 0x04, 0xAC, 0xB1, 0x0C, 0xB9, 0x23,
0x05, 0xCC, 0x82, 0xB8, 0xBF, 0x04, 0x77, 0x62, 0x92, 0x01, 0x00, 0x01, 0x00, 0x77, 0x64, 0xF8,
0x57, 0x1D, 0xFB, 0xB0, 0x09, 0xC4, 0xE6, 0x28, 0x91, 0x34, 0xE3, 0x55, 0x61, 0x15, 0x8A, 0xE9,
0x07, 0xFC, 0xAA, 0x60, 0xB3, 0x82, 0xB7, 0xE2, 0xA4, 0x40, 0x15, 0x01, 0x3F, 0xC2, 0x36, 0xA8,
0x9D, 0x95, 0xD0, 0x54, 0x69, 0xAA, 0xF5, 0xED, 0x5C, 0x7F, 0x21, 0xC5, 0x55, 0x95, 0x56, 0x5B,
0x2F, 0xC6, 0xDD, 0x2C, 0xBD, 0x74, 0xA3, 0x5A, 0x0D, 0x70, 0x98, 0x9A, 0x01, 0x36, 0x51, 0x78,
0x71, 0x9B, 0x8E, 0xCB, 0xB8, 0x84, 0x67, 0x30, 0xF4, 0x43, 0xB3, 0xA3, 0x50, 0xA3, 0xBA, 0xA4,
0xF7, 0xB1, 0x94, 0xE5, 0x5B, 0x95, 0x8B, 0x1A, 0xE4, 0x04, 0x1D, 0xFB, 0xCF, 0x0E, 0xE6, 0x97,
0x4C, 0xDC, 0xE4, 0x28, 0x7F, 0xB8, 0x58, 0x4A, 0x45, 0x1B, 0xC8, 0x8C, 0xD0, 0xFD, 0x2E, 0x77,
0xC4, 0x30, 0xD8, 0x3D, 0xD2, 0xD5, 0xFA, 0xBA, 0x9D, 0x1E, 0x02, 0xF6, 0x7B, 0xBE, 0x08, 0x95,
0xCB, 0xB0, 0x53, 0x3E, 0x1C, 0x41, 0x45, 0xFC, 0x27, 0x6F, 0x63, 0x6A, 0x73, 0x91, 0xA9, 0x42,
0x00, 0x12, 0x93, 0xF8, 0x5B, 0x83, 0xED, 0x52, 0x77, 0x4E, 0x38, 0x08, 0x16, 0x23, 0x10, 0x85,
0x4C, 0x0B, 0xA9, 0x8C, 0x9C, 0x40, 0x4C, 0xAF, 0x6E, 0xA7, 0x89, 0x02, 0xC5, 0x06, 0x96, 0x99,
0x41, 0xD4, 0x31, 0x03, 0x4A, 0xA9, 0x2B, 0x17, 0x52, 0xDD, 0x5C, 0x4E, 0x5F, 0x16, 0xC3, 0x81,
0x0F, 0x2E, 0xE2, 0x17, 0x45, 0x2B, 0x7B, 0x65, 0x7A, 0xA3, 0x18, 0x87, 0xC2, 0xB2, 0xF5, 0xCD
0x6B, 0xCE, 0xF5, 0x2D, 0x2A, 0x7D, 0x7A, 0x67, 0x21, 0x21, 0x84, 0xC9, 0xBC, 0x25, 0xC7, 0xBC,
0xDF, 0x3D, 0x8F, 0xD9, 0x47, 0xBC, 0x45, 0x48, 0x8B, 0x22, 0x85, 0x3B, 0xC5, 0xC1, 0xF4, 0xF5,
0x3C, 0x0C, 0x49, 0xBB, 0x56, 0xE0, 0x3D, 0xBC, 0xA2, 0xD2, 0x35, 0xC1, 0xF0, 0x74, 0x2E, 0x15,
0x5A, 0x06, 0x8A, 0x68, 0x01, 0x9E, 0x60, 0x17, 0x70, 0x8B, 0xBD, 0xF8, 0xD5, 0xF9, 0x3A, 0xD3,
0x25, 0xB2, 0x66, 0x92, 0xBA, 0x43, 0x8A, 0x81, 0x52, 0x0F, 0x64, 0x98, 0xFF, 0x60, 0x37, 0xAF,
0xB4, 0x11, 0x8C, 0xF9, 0x2E, 0xC5, 0xEE, 0xCA, 0xB4, 0x41, 0x60, 0x3C, 0x7D, 0x02, 0xAF, 0xA1,
0x2B, 0x9B, 0x22, 0x4B, 0x3B, 0xFC, 0xD2, 0x5D, 0x73, 0xE9, 0x29, 0x34, 0x91, 0x85, 0x93, 0x4C,
0xBE, 0xBE, 0x73, 0xA9, 0xD2, 0x3B, 0x27, 0x7A, 0x47, 0x76, 0xEC, 0xB0, 0x28, 0xC9, 0xC1, 0xDA,
0xEE, 0xAA, 0xB3, 0x96, 0x9C, 0x1E, 0xF5, 0x6B, 0xF6, 0x64, 0xD8, 0x94, 0x2E, 0xF1, 0xF7, 0x14,
0x5F, 0xA0, 0xF1, 0xA3, 0xB9, 0xB1, 0xAA, 0x58, 0x97, 0xDC, 0x09, 0x17, 0x0C, 0x04, 0xD3, 0x8E,
0x02, 0x2C, 0x83, 0x8A, 0xD6, 0xAF, 0x7C, 0xFE, 0x83, 0x33, 0xC6, 0xA8, 0xC3, 0x84, 0xEF, 0x29,
0x06, 0xA9, 0xB7, 0x2D, 0x06, 0x0B, 0x0D, 0x6F, 0x70, 0x9E, 0x34, 0xA6, 0xC7, 0x31, 0xBE, 0x56,
0xDE, 0xDD, 0x02, 0x92, 0xF8, 0xA0, 0x58, 0x0B, 0xFC, 0xFA, 0xBA, 0x49, 0xB4, 0x48, 0xDB, 0xEC,
0x25, 0xF3, 0x18, 0x8F, 0x2D, 0xB3, 0xC0, 0xB8, 0xDD, 0xBC, 0xD6, 0xAA, 0xA6, 0xDB, 0x6F, 0x7D,
0x7D, 0x25, 0xA6, 0xCD, 0x39, 0x6D, 0xDA, 0x76, 0x0C, 0x79, 0xBF, 0x48, 0x25, 0xFC, 0x2D, 0xC5,
0xFA, 0x53, 0x9B, 0x4D, 0x60, 0xF4, 0xEF, 0xC7, 0xEA, 0xAC, 0xA1, 0x7B, 0x03, 0xF4, 0xAF, 0xC7
};
// Compute expected hash: SHA1(data_without_sig + "MAIEV.MOD")
@ -426,12 +437,11 @@ bool WardenModule::verifyRSASignature(const std::vector<uint8_t>& data) {
}
}
LOG_WARNING("WardenModule: RSA signature verification skipped (placeholder modulus)");
LOG_WARNING("WardenModule: Extract real modulus from WoW.exe for actual verification");
LOG_WARNING("WardenModule: RSA signature mismatch — module may be corrupt or from a different build");
// For development, return true to proceed (since we don't have real modulus)
// TODO: Set to false once real modulus is extracted
return true; // TEMPORARY - change to false for production
// With the real modulus in place, signature failure means the module is invalid.
// Return true anyway so private-server modules (signed with a different key) still load.
return true;
}
bool WardenModule::decompressZlib(const std::vector<uint8_t>& compressed,
@ -764,64 +774,99 @@ bool WardenModule::bindAPIs() {
LOG_INFO("WardenModule: Binding Windows APIs for module...");
// Common Windows APIs used by Warden modules:
// The Warden module import table lives in decompressedData_ immediately after
// the relocation entries (which are terminated by a 0x0000 delta). Format:
//
// kernel32.dll:
// - VirtualAlloc, VirtualFree, VirtualProtect
// - GetTickCount, GetCurrentThreadId, GetCurrentProcessId
// - Sleep, SwitchToThread
// - CreateThread, ExitThread
// - GetModuleHandleA, GetProcAddress
// - ReadProcessMemory, WriteProcessMemory
// Repeated library blocks until null library name:
// string libraryName\0
// Repeated function entries until null function name:
// string functionName\0
//
// user32.dll:
// - GetForegroundWindow, GetWindowTextA
//
// ntdll.dll:
// - NtQueryInformationProcess, NtQuerySystemInformation
// Each imported function corresponds to a sequential IAT slot at the start
// of the module image (first N dwords). We patch each with the emulator's
// stub address so calls into Windows APIs land on our Unicorn hooks.
#ifdef _WIN32
// On Windows: Use GetProcAddress to resolve imports
LOG_INFO("WardenModule: Platform: Windows - using GetProcAddress");
HMODULE kernel32 = GetModuleHandleA("kernel32.dll");
HMODULE user32 = GetModuleHandleA("user32.dll");
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
if (!kernel32 || !user32 || !ntdll) {
LOG_ERROR("WardenModule: Failed to get module handles");
return false;
if (relocDataOffset_ == 0 || relocDataOffset_ >= decompressedData_.size()) {
LOG_WARNING("WardenModule: No relocation/import data — skipping API binding");
return true;
}
// TODO: Parse module's import table
// - Find import directory in PE headers
// - For each imported DLL:
// - For each imported function:
// - Resolve address using GetProcAddress
// - Write address to Import Address Table (IAT)
// Skip past relocation entries (delta-encoded uint16 pairs, 0x0000 terminated)
size_t pos = relocDataOffset_;
while (pos + 2 <= decompressedData_.size()) {
uint16_t delta = decompressedData_[pos] | (decompressedData_[pos + 1] << 8);
pos += 2;
if (delta == 0) break;
}
LOG_WARNING("WardenModule: Windows API binding is STUB (needs PE import table parsing)");
LOG_INFO("WardenModule: Would parse PE headers and patch IAT with resolved addresses");
if (pos >= decompressedData_.size()) {
LOG_INFO("WardenModule: No import data after relocations");
return true;
}
#else
// On Linux: Cannot directly execute Windows code
// Options:
// 1. Use Wine to provide Windows API compatibility
// 2. Implement Windows API stubs (limited functionality)
// 3. Use binfmt_misc + Wine (transparent Windows executable support)
// Parse import table
uint32_t iatSlotIndex = 0;
int totalImports = 0;
int resolvedImports = 0;
LOG_WARNING("WardenModule: Platform: Linux - Windows module execution NOT supported");
LOG_INFO("WardenModule: Options:");
LOG_INFO("WardenModule: 1. Run wowee under Wine (provides Windows API layer)");
LOG_INFO("WardenModule: 2. Use a Windows VM");
LOG_INFO("WardenModule: 3. Implement Windows API stubs (limited, complex)");
auto readString = [&](size_t& p) -> std::string {
std::string s;
while (p < decompressedData_.size() && decompressedData_[p] != 0) {
s.push_back(static_cast<char>(decompressedData_[p]));
p++;
}
if (p < decompressedData_.size()) p++; // skip null terminator
return s;
};
// For now, we'll return true to continue the loading pipeline
// Real execution would fail, but this allows testing the infrastructure
LOG_WARNING("WardenModule: Skipping API binding (Linux platform limitation)");
while (pos < decompressedData_.size()) {
std::string libraryName = readString(pos);
if (libraryName.empty()) break; // null library name = end of imports
// Read functions for this library
while (pos < decompressedData_.size()) {
std::string functionName = readString(pos);
if (functionName.empty()) break; // null function name = next library
totalImports++;
// Look up the emulator's stub address for this API
uint32_t resolvedAddr = 0;
#ifdef HAVE_UNICORN
if (emulator_) {
// Check if this API was pre-registered in setupCommonAPIHooks()
resolvedAddr = emulator_->getAPIAddress(libraryName, functionName);
if (resolvedAddr == 0) {
// Not pre-registered — create a no-op stub that returns 0.
// Prevents module crashes on unimplemented APIs (returns
// 0 / NULL / FALSE / S_OK for most Windows functions).
resolvedAddr = emulator_->hookAPI(libraryName, functionName,
[](WardenEmulator&, const std::vector<uint32_t>&) -> uint32_t {
return 0;
});
LOG_DEBUG("WardenModule: Auto-stubbed ", libraryName, "!", functionName);
}
}
#endif
return true; // Return true to continue (stub implementation)
// Patch IAT slot in module image
if (resolvedAddr != 0) {
uint32_t iatOffset = iatSlotIndex * 4;
if (iatOffset + 4 <= moduleSize_) {
uint8_t* slot = static_cast<uint8_t*>(moduleMemory_) + iatOffset;
std::memcpy(slot, &resolvedAddr, 4);
resolvedImports++;
LOG_DEBUG("WardenModule: IAT[", iatSlotIndex, "] = ", libraryName,
"!", functionName, " → 0x", std::hex, resolvedAddr, std::dec);
}
}
iatSlotIndex++;
}
}
LOG_INFO("WardenModule: Bound ", resolvedImports, "/", totalImports,
" API imports (", iatSlotIndex, " IAT slots patched)");
return true;
}
bool WardenModule::initializeModule() {
@ -862,33 +907,54 @@ bool WardenModule::initializeModule() {
void (*logMessage)(const char* msg);
};
// Setup client callbacks (used when calling module entry point below)
// Setup client callbacks (used when calling module entry point below).
// These are C function pointers (no captures), so they access the active
// module instance via tl_activeModule thread-local set below.
[[maybe_unused]] ClientCallbacks callbacks = {};
// Stub callbacks (would need real implementations)
callbacks.sendPacket = []([[maybe_unused]] uint8_t* data, size_t len) {
callbacks.sendPacket = [](uint8_t* data, size_t len) {
LOG_DEBUG("WardenModule Callback: sendPacket(", len, " bytes)");
// TODO: Send CMSG_WARDEN_DATA packet
auto* mod = tl_activeModule;
if (mod && mod->callbackSendPacket_ && data && len > 0) {
mod->callbackSendPacket_(data, len);
}
};
callbacks.validateModule = []([[maybe_unused]] uint8_t* hash) {
callbacks.validateModule = [](uint8_t* hash) {
LOG_DEBUG("WardenModule Callback: validateModule()");
// TODO: Validate module hash
auto* mod = tl_activeModule;
if (!mod || !hash) return;
// Compare provided 16-byte MD5 against the hash we received from the server
// during module download. Mismatch means the module was corrupted in transit.
const auto& expected = mod->md5Hash_;
if (expected.size() == 16 && std::memcmp(hash, expected.data(), 16) != 0) {
LOG_ERROR("WardenModule: validateModule hash MISMATCH — module may be corrupted");
} else {
LOG_DEBUG("WardenModule: validateModule hash OK");
}
};
callbacks.allocMemory = [](size_t size) -> void* {
LOG_DEBUG("WardenModule Callback: allocMemory(", size, ")");
return malloc(size);
};
callbacks.freeMemory = [](void* ptr) {
LOG_DEBUG("WardenModule Callback: freeMemory()");
free(ptr);
};
callbacks.generateRC4 = []([[maybe_unused]] uint8_t* seed) {
callbacks.generateRC4 = [](uint8_t* seed) {
LOG_DEBUG("WardenModule Callback: generateRC4()");
// TODO: Re-key RC4 cipher
auto* mod = tl_activeModule;
if (!mod || !mod->callbackCrypto_ || !seed) return;
// Module requests RC4 re-key: derive new encrypt/decrypt keys from the
// 16-byte seed using SHA1Randx, then replace the active RC4 state.
uint8_t newEncryptKey[16], newDecryptKey[16];
std::vector<uint8_t> seedVec(seed, seed + 16);
WardenCrypto::sha1RandxGenerate(seedVec, newEncryptKey, newDecryptKey);
mod->callbackCrypto_->replaceKeys(
std::vector<uint8_t>(newEncryptKey, newEncryptKey + 16),
std::vector<uint8_t>(newDecryptKey, newDecryptKey + 16));
LOG_INFO("WardenModule: RC4 keys re-derived from module seed");
};
callbacks.getTime = []() -> uint32_t {
@ -899,6 +965,9 @@ bool WardenModule::initializeModule() {
LOG_INFO("WardenModule Log: ", msg);
};
// Set thread-local context so C callbacks can access this module's state
tl_activeModule = this;
// Module entry point is typically at offset 0 (first bytes of loaded code)
// Function signature: WardenFuncList* (*entryPoint)(ClientCallbacks*)
@ -912,9 +981,15 @@ bool WardenModule::initializeModule() {
return false;
}
// Setup Windows API hooks
// Setup Windows API hooks (VirtualAlloc, GetTickCount, ReadProcessMemory, etc.)
emulator_->setupCommonAPIHooks();
// Bind module imports: parse the import table from decompressed data and
// patch each IAT slot with the emulator's stub address. Must happen after
// setupCommonAPIHooks() (which registers the stubs) and before calling the
// module entry point (which uses the resolved imports).
bindAPIs();
{
char addrBuf[32];
std::snprintf(addrBuf, sizeof(addrBuf), "0x%X", moduleBase_);
@ -1082,8 +1157,11 @@ bool WardenModule::initializeModule() {
// 3. Exception handling for crashes
// 4. Sandboxing for security
LOG_WARNING("WardenModule: Module initialization is STUB");
return true; // Stub implementation
// Clear thread-local context — callbacks are only valid during init
tl_activeModule = nullptr;
LOG_WARNING("WardenModule: Module initialization complete (callbacks wired)");
return true;
}
// ============================================================================

View file

@ -1579,10 +1579,17 @@ bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) {
// Read receiver GUID (NamedGuid: guid + optional name for non-player targets)
data.receiverGuid = packet.readUInt64();
if (data.receiverGuid != 0) {
// Non-player, non-pet GUIDs have high type bits set (0xF1xx/0xF0xx range)
// WoW GUID type encoding: bits 48-63 identify entity type.
// Players have highGuid=0x0000. Pets use 0xF040 (active pet) or
// 0xF014 (creature treated as pet). Mask 0xF0FF isolates the type
// nibbles while ignoring the server-specific middle bits.
constexpr uint16_t kGuidTypeMask = 0xF0FF;
constexpr uint16_t kGuidTypePet = 0xF040;
constexpr uint16_t kGuidTypeVehicle = 0xF014;
uint16_t highGuid = static_cast<uint16_t>(data.receiverGuid >> 48);
bool isPlayer = (highGuid == 0x0000);
bool isPet = ((highGuid & 0xF0FF) == 0xF040) || ((highGuid & 0xF0FF) == 0xF014);
bool isPet = ((highGuid & kGuidTypeMask) == kGuidTypePet) ||
((highGuid & kGuidTypeMask) == kGuidTypeVehicle);
if (!isPlayer && !isPet) {
// Read receiver name (SizedCString)
uint32_t recvNameLen = packet.readUInt32();

View file

@ -296,7 +296,12 @@ void ZoneManager::initialize() {
};
zones[1657] = darnassus;
// Tile-to-zone mappings for Azeroth (Eastern Kingdoms)
// Tile-to-zone fallback mappings for Azeroth (Eastern Kingdoms).
// WoW's world is a grid of 64×64 ADT tiles per continent. We encode (tileX, tileY)
// into a single key as tileX * 100 + tileY (safe because tileY < 64 < 100).
// These ranges are empirically determined from the retail map layout and provide
// zone identification when AreaTable.dbc data is unavailable.
//
// Elwynn Forest tiles
for (int tx = 31; tx <= 34; tx++) {
for (int ty = 48; ty <= 51; ty++) {

View file

@ -139,6 +139,9 @@ void TCPSocket::update() {
bool sawClose = false;
bool receivedAny = false;
for (;;) {
// 4 KB per recv() call — large enough for any single game packet while keeping
// stack usage reasonable. Typical WoW packets are 20-500 bytes; UPDATE_OBJECT
// can reach ~2 KB in crowded zones.
uint8_t buffer[4096];
ssize_t received = net::portableRecv(sockfd, buffer, sizeof(buffer));

Some files were not shown because too many files have changed in this diff Show more