The warmup loop waited up to 20 seconds for getHeightAt() to return a
terrain height within 15 units of spawn Z before accepting the ground
as ready. In practice, the terrain was loaded and the character was
visibly standing on it, but the height sample didn't match closely
enough (terrain LOD, chunk boundary, or server Z vs client height
mismatch).
Reduce the tile-count fallback timeout from 20s to 5s: if at least 4
tiles are loaded after 5 seconds, accept the ground as ready. The
exact height check still runs in the first 5 seconds for fast-path
cases where it does match.
Remove all OpenGL/GLEW code and dependencies. The Vulkan renderer has
been the sole active backend for months; these files were dead code.
Deleted (8 files, 641 lines):
- rendering/mesh.cpp+hpp: OpenGL VAO/VBO/EBO wrapper (never instantiated)
- rendering/shader.cpp+hpp: OpenGL GLSL compiler (replaced by VkShaderModule)
- rendering/scene.cpp+hpp: Scene graph holding Mesh objects (created but
never populated — all rendering uses Vulkan sub-renderers directly)
- rendering/video_player.cpp+hpp: FFmpeg+GL texture uploader (never
included by any other file — login video feature can be re-implemented
with VkTexture when needed)
Cleaned up:
- renderer.hpp: remove Scene forward-decl, getScene() accessor, scene member
- renderer.cpp: remove scene.hpp/shader.hpp includes, Scene create/destroy
- application.cpp: remove stale "GL/glew.h removed" comment
- CMakeLists.txt: remove find_package(OpenGL/GLEW), source/header entries,
and target_link_libraries for OpenGL::GL and GLEW::GLEW
- PKGBUILD: remove glew dependency
- BUILD_INSTRUCTIONS.md: remove glew from all platform install commands
- Dockerfile: fix LLVM apt repo codename (jammy → noble) for ubuntu:24.04
- build-linux.sh: add missing mkdir -p /wowee-build-src before tar extraction
- Dockerfile: remove dead ENV OSXCROSS_VERSION=1.5 and its unset
- CMakeLists: scope -undefined dynamic_lookup to wowee target only
- GameServices: remove redundant game:: qualifier inside namespace game
- application.cpp: zero out gameServices_ after gameHandler reset in shutdown
Introduce `GameServices` struct — an explicit dependency bundle that
`Application` populates and passes to `GameHandler` at construction time.
Eliminates all 47 hidden `Application::getInstance()` calls in
`src/game/*.cpp`, completing SOLID-D (dependency-inversion) cleanup.
Changes:
- New `include/game/game_services.hpp` — `struct GameServices` carrying
pointers to `Renderer`, `AssetManager`, `ExpansionRegistry`, and two
taxi-mount display IDs
- `GameHandler(GameServices&)` replaces default constructor; exposes
`services() const` accessor for domain handlers
- `Application` holds `game::GameServices gameServices_`; populates it
after all subsystems are created, then constructs `GameHandler`
(fixes latent init-order bug: `GameHandler` was previously created
before `AssetManager` / `ExpansionRegistry`)
- `game_handler.cpp`: duplicate `isActiveExpansion` / `isClassicLikeExpansion` /
`isPreWotlk` anonymous-namespace helpers removed; `game_utils.hpp`
included instead
- All domain handlers (`InventoryHandler`, `SpellHandler`, `MovementHandler`,
`CombatHandler`, `QuestHandler`, `SocialHandler`, `WardenHandler`) replace
`Application::getInstance().getXxx()` with `owner_.services().xxx`
SDL2 requires video/window functions to be called from the main thread
(the one that called SDL_Init). The watchdog thread was calling
SDL_SetRelativeMouseMode, SDL_ShowCursor, and SDL_SetWindowGrab directly
on stall detection — undefined behavior on macOS (Cocoa requires main-
thread UI calls) and unsafe on other platforms.
Now the watchdog sets an atomic flag, and the main loop checks it at the
top of each iteration, executing the SDL calls on the correct thread.
Final sweep across mpq_manager, application, auth_screen, wmo_renderer,
character_renderer, and terrain_manager. All now use the unsigned char
cast pattern. No remaining bare ::tolower/::toupper or std::tolower(c)
calls on signed char in the codebase.
std::tolower(int) has undefined behavior when passed a negative value,
which signed char produces for bytes > 127. The rest of the codebase
correctly casts to unsigned char first; these 3 sites were missed.
The per-tick GPU upload budget check ran before consuming async futures,
so after 1 upload ALL remaining ready results were deferred — including
permanent failures and cache hits that need zero GPU work. Moved the
budget gate after failure/cache-hit processing so only actual uploads
count. Re-queues over-budget results as pending spawns for next frame.
Loop iterated 20 hair geoset lookups for Human Male but the if-body
was empty — the LOG statement that was presumably there was removed
but the loop skeleton was left behind.
Stormwind WMO collision takes 25+ seconds to fully load. The warmup
ground check couldn't detect the WMO floor because collision data
wasn't finalized yet. Player spawned and immediately fell through
the unloaded WMO floor into the terrain below (Dun Morogh).
New approach: suspendGravityFor(10s) after world entry. Gravity is
disabled (Z position frozen) until either:
1. A floor is detected by the collision system (gravity resumes instantly)
2. The 10-second timer expires (gravity resumes as fallback)
This handles the case where WMO collision loads during the first few
seconds of gameplay — the player hovers at spawn Z until the floor
appears, then lands normally.
Also fixes faction language for chat (ORCISH for Horde, COMMON for
Alliance) and adds SMSG_MESSAGECHAT diagnostic logging.
loadOnlineWorldTerrain() was called directly from the worldEntryCallback
inside the packet handler, running the 20s warmup loop synchronously.
This blocked ALL packet processing and froze the game for 20-41 seconds.
Now defers the world reload to pendingWorldEntry_ which is processed on
the next frame, outside the packet handler. Position and camera snap
immediately so the player doesn't drift at the old location.
The /y respawn report was actually a server-initiated teleport (possibly
anti-spam or area trigger) that hit this 41-second blocking path.
Equipment: the first emitOtherPlayerEquipment call fired before any item
queries returned, sending all-zero displayIds that stripped players naked.
Now skips the callback when resolved=0 (waiting for queries). Equipment
only applies once at least one item resolves, preventing the naked flash.
BG announcer: broadened filter to match ALL chat types (not just SYSTEM),
and added more patterns: "BGAnnouncer", "[H: N, A: N]" with spaces.
Also added diagnostic logging in setOnlinePlayerEquipment to trace
displayId counts reaching the renderer.
Action bar hearthstone: the slot was type SPELL (spell 8690) not ITEM.
castSpell sends CMSG_CAST_SPELL which the server rejects for item-use
spells. Now detects item-use spells via getItemIdForSpell() and routes
through useItemById() instead, sending CMSG_USE_ITEM correctly.
Far same-map teleport: hearthstone on the same continent (e.g., Westfall
→ Stormwind on Azeroth) skipped the loading screen, so the player fell
through unloaded terrain. Now triggers a full world reload with loading
screen for teleports > 500 units, with the warmup ground check ensuring
WMO floors are loaded before spawning.
Stormwind players stand on WMO floors ~95m above terrain. The previous
check only tested if terrain existed at the spawn XY (it did — far below).
Now checks WMO floor first, then terrain, requiring the ground to be within
15 units of spawn Z. Falls back to tile count after 10s.
Also adds diagnostic logging for useItemBySlot (hearthstone debug).
Stormwind players stand on WMO floors, not terrain. The terrain-only
check passed immediately (terrain exists below the city) but the WMO
floor hadn't loaded yet, so the player fell through.
Now checks three ground sources in order:
1. Terrain height at spawn point
2. WMO floor height at spawn point (for cities/buildings)
3. After 8s, accepts if 4+ terrain tiles are loaded (fallback)
Won't exit warmup until at least one ground source returns valid height,
or the 25s hard cap is reached.
The terrain readiness check was using getCharacterPosition() which is
(0,0,0) during warmup — always returned a valid height and exited
immediately, causing the player to spawn before terrain loaded.
Now uses the server-provided spawn coordinates (x,y,z from world entry)
converted to render coords for the terrain query. Also logs when terrain
isn't ready after 5 seconds to show warmup progress.
Player spawn callbacks and equipment re-emit chain confirmed working.
Added terrain readiness check to the warmup exit condition: the loading
screen won't drop until getHeightAt(playerPos) returns a valid height,
ensuring the ground exists under the player's feet before spawning.
Also increased warmup hard cap from 15s to 25s to give terrain more time
to load in cities like Stormwind with dense WMO/M2 assets.
Equipment re-emit chain confirmed working: items resolve 3-4 seconds
after spawn and equipment is re-applied with valid displayIds.
NPC and other-player melee swing callback was hardcoded to animation 16
(unarmed attack). Now tries 17 (1H weapon), 18 (2H weapon) first with
hasAnimation() check, falling back to 16 if neither exists on the model.
Inspect: CMSG_INSPECT was writing full uint64 GUID instead of packed GUID.
Server silently rejected the malformed packet. Fixed both InspectPacket and
QueryInspectAchievementsPacket to use writePackedGuid().
Follow: was a no-op (only stored GUID). Added client-side auto-follow system:
camera controller walks toward followed entity, faces target, cancels on
WASD/mouse input, stops within 3 units, cancels at 40+ units distance.
Party commands:
- /lootmethod (ffa/roundrobin/master/group/nbg) sends CMSG_LOOT_METHOD
- /lootthreshold (0-5 or quality name) sets minimum loot quality
- /raidconvert converts party to raid (leader only)
Equipment diagnostic logging still active for debugging naked players.
Shoulder pieces are M2 model attachments (like helmets), not body geosets.
Load left shoulder at attachment point 5, right shoulder at point 6.
Models resolved from ItemDisplayInfo.dbc LeftModel/RightModel fields,
with race/gender suffix variants tried first. Applied to both online
player and NPC equipment paths.
Other players previously appeared partially naked — only chest, legs, feet,
hands, cape, and tabard rendered. Now renders full equipment:
- Helmet M2 model: loads from ItemDisplayInfo.dbc with race/gender suffix,
attaches at head bone (point 0/11), hides hair geoset under helm
- Weapons: mainhand (attachment 1) and offhand (attachment 2) M2 models
loaded from ItemDisplayInfo, with Weapon/Shield path fallback
- Wrist/bracer geoset (group 8): applies when no chest sleeve overrides
- Belt/waist geoset (group 18): reads GeosetGroup1 from ItemDisplayInfo
- Shoulder M2 attachments deferred (separate bone attachment system)
Also applied same wrist/waist geosets to NPC and character preview paths.
Minimap: batch 9 individual vkUpdateDescriptorSets into single call.
Mail: change money/COD fields from uint32 to uint64 in CMSG_SEND_MAIL and
SMSG_MAIL_LIST_RESULT for WotLK 3.3.5a. Classic keeps uint32 on the wire.
Fixes money truncation and packet misalignment causing mail failures.
Other-player capes: add cape texture loading to setOnlinePlayerEquipment().
The cape geoset was enabled but no texture was loaded, leaving capes blank.
Now mirrors the local-player path: looks up ItemDisplayInfo.dbc, finds cape
texture candidates, applies via setGroupTextureOverride/setTextureSlotOverride.
Zone toasts: suppress duplicate zone toast when the zone text overlay is
already showing the same zone name. Fixes double "Entering: Stormwind City".
Network: enable TCP_NODELAY on both auth and world sockets after connect(),
disabling Nagle's algorithm to eliminate up to 200ms buffering delay on
small packets (movement, spell casts, chat).
Rendering: track material and bone descriptor sets in M2 renderer to skip
redundant vkCmdBindDescriptorSets calls between batches sharing same textures.
- Replace count()+operator[] double lookups with find() or try_emplace()
in gameObjectInstances_, playerTextureSlotsByModelId_, onlinePlayerAppearance_
- Add Entity::isUnit() helper; replace 5 dynamic_cast<Unit*> in per-frame
UI rendering (nameplates, combat text, pet frame) with isUnit()+static_cast
- Add constexpr kInv255 reciprocal for per-pixel normal map generation loops
in character_renderer and wmo_renderer
Squared distance optimizations across 30 files:
- Convert glm::length() comparisons to glm::dot() (no sqrt)
- Use glm::inversesqrt() for check-then-normalize patterns (1 rsqrt vs 2 sqrt)
- Defer sqrt to after early-out checks in collision/movement code
- Hottest paths: camera_controller (21), weather particles, WMO collision,
transport movement, creature interpolation, nameplate culling
Container and algorithm improvements:
- std::map<string> → std::unordered_map for asset/DBC/MPQ/warden caches
- std::mutex → std::shared_mutex for asset_manager and mpq_manager caches
- std::sort → std::partial_sort in lighting_manager (top-2 of N volumes)
- Double-lookup find()+operator[] → insert_or_assign in game_handler
- Add reserve() for per-frame vectors: weather, swim_effects, WMO/M2 collision
Threading and synchronization:
- Replace 1ms busy-wait polling with condition_variable in character_renderer
- Move timestamp capture before mutex in logger
- Use memory_order_acquire/release for normal map completion signaling
API additions:
- DBC getStringView()/getStringViewByOffset() for zero-copy string access
- Parse creature display IDs from SMSG_CREATURE_QUERY_SINGLE_RESPONSE
Wrap string-to-number conversions in try-catch where input comes from
external sources (realm address port, last_world.cfg, keybinding config,
ADT tile filenames) to prevent crashes on malformed data.
Extract shared M2+skin loading logic into Application::loadWeaponM2(),
replacing duplicate 15-line blocks in loadEquippedWeapons() and
tryAttachCreatureVirtualWeapons(). Future weapon loading changes only
need to update one place.
Add vkDeviceWaitIdle after world loading completes to ensure all async
texture uploads and resource creation are fully flushed before the
first render frame. Mitigates intermittent NVIDIA driver crashes at
vkCmdBeginRenderPass during initial world entry.
Add mapDisplayName() with friendly names for continents: "Eastern
Kingdoms", "Kalimdor", "Outland", "Northrend". The loading screen
previously showed WDT directory names like "Expansion01" when
Map.dbc's localized name field was empty or matched the internal name.
Entity positions are in canonical WoW coords (X=north, Y=west) but the
audio listener uses render coords (X=west, Y=north) from the camera.
Without conversion, distance attenuation was computed on swapped axes,
making NPC ambient sounds (peasant voices, etc.) play at wrong volumes
regardless of actual distance.
Log GPU name and vendor ID during VkContext initialization for easier
debugging of GPU-specific issues (FSR3, driver compat, etc.). Add
isAmdGpu()/isNvidiaGpu() accessors.
Temporarily log SMSG_PLAY_SOUND and SMSG_PLAY_OBJECT_SOUND at WARN
level (sound ID, name, file path) to diagnose unidentified ambient
NPC sounds reported by the user.
CharSections.dbc has different field layouts between stock WotLK (textures
at field 4-6) and Classic/TBC/Turtle/HD-textured WotLK (VariationIndex at
field 4). Add detectCharSectionsFields() that probes field-4 values at
runtime to determine the correct layout, so both stock and modded clients
work without JSON changes.
Also add BLOODELF_MALE/FEMALE and DRAENEI_MALE/FEMALE voice types to the
NPC voice system — previously all Blood Elf and Draenei NPCs fell through
to GENERIC (random dwarf/gnome/night elf/orc mix).
DISPLAY_SIZE_CHANGED fires when the window is resized via
SDL_WINDOWEVENT_RESIZED, allowing UI addons to adapt their layout
to the new screen dimensions (5 FrameXML registrations).
UNIT_QUEST_LOG_CHANGED("player") fires alongside QUEST_LOG_UPDATE
at all 6 quest log modification points, for addons that register
for this variant instead (4 FrameXML registrations).
PR #19 (572bb4ef) swapped CharSections.dbc field indices, placing
Texture1-3 at fields 4-6 and VariationIndex/ColorIndex at 8-9. Binary
analysis of the actual DBC files (Classic, TBC, Turtle — all identical
layout, no WotLK-specific override) confirms the correct order is:
Field 4 = VariationIndex
Field 5 = ColorIndex
Field 6 = Texture1 (string)
Field 7 = Texture2 (string)
Field 8 = Texture3 (string)
Field 9 = Flags
With the wrong indices, VariationIndex/ColorIndex reads returned string
offsets (garbage values that never matched), so all CharSections lookups
failed silently — producing white untextured character models at the
login screen and in-world.
Fixes all 4 expansion JSON layouts, hardcoded fallbacks in
character_preview.cpp, application.cpp, and character_create_screen.cpp.
Also handles the single-layer edge case (body skin only, no face/underwear)
by loading the texture directly instead of skipping compositing.
Change the bare shin (no boots) default from geoset 502 to 503 across
all four code paths (character creation, character preview, equipment
update, NPC rendering).
Geoset 503 has Y width ~0.44 which better matches the thigh mesh
width (~0.42) than 502's width (~0.39), reducing the visible gap at
the knee joint where lower and upper leg meshes meet.
Previously Lua addon errors only logged to the log file. Now they
display as red UI error text to the player (same as spell errors and
game warnings), helping addon developers debug issues in real-time.
Add LuaErrorCallback to LuaEngine, fire it from event handler and
frame OnEvent pcall error paths. Wire the callback to GameHandler's
addUIError in application.cpp.
Fire ACHIEVEMENT_EARNED event when a player earns an achievement,
enabling achievement tracking addons.
Add 15 previously unmapped chat type → addon event mappings:
- CHAT_MSG_ACHIEVEMENT, CHAT_MSG_GUILD_ACHIEVEMENT
- CHAT_MSG_WHISPER_INFORM (echo of sent whispers)
- CHAT_MSG_RAID_LEADER, CHAT_MSG_BATTLEGROUND_LEADER
- CHAT_MSG_MONSTER_SAY/YELL/EMOTE/WHISPER
- CHAT_MSG_RAID_BOSS_EMOTE/WHISPER
- CHAT_MSG_BG_SYSTEM_NEUTRAL/ALLIANCE/HORDE
These events are needed by boss mod addons (DBM, BigWigs) to detect
boss emotes, by achievement trackers, and by chat filter addons that
process all message types.