Freezing was decided by matching model paths against a list of portal
names, so every game object with a looping idle sat in its bind pose
unless someone had thought to name it — fishing pools were the reported
case, but braziers, banners and waterwheels were frozen the same way.
Freeze only the types whose pose is server state (door, button, chest,
trap, goober, destructible building, trapdoor) and let everything else
play its idle, which is what retail does. A game object's model spawns
from its display id before its type is known, so an object that spawns
with the query still in flight is frozen conservatively and revisited
when GAMEOBJECT_QUERY_RESPONSE arrives, via a new info callback.
The open v2.0.29-preview section had been collecting entries as work
landed, but stopped at the terrain-seam fix; everything after it went
unrecorded. Adds the two missing sections: rendering (brightness multiply
over water, FSR3 16-bit storage, validation-layer env var), bank and guild
bank, crafting, quests, GM tools, and the two changes since.
Also narrows a comment in macos_platform.hpp -- W takes no diacritics.
SDL2 leaves text input enabled for the whole session, so AppKit routes key
events through NSTextInputContext even in normal gameplay. Holding a key
with diacritics -- W, A, S, E -- opened the accent chooser over the game
instead of repeating the key.
Register ApplePressAndHoldEnabled=NO before SDL_Init brings up
NSApplication. The registration domain is process-scoped, so the user's
saved preferences are untouched; if a global setting explicitly enables
press-and-hold, fall back to this application's own domain, which outranks
NSGlobalDomain.
Needs an Objective-C++ shim, wired into both wowee and wowee_editor since
both compile window.cpp.
Barber cuts updated the character data and inventory preview but never
rebuilt the in-world 3D model, so the new hair/facial hair only showed
after a restart or teleport. Add a player-model-rebuild callback fired on
PLAYER_BYTES / PLAYER_BYTES_2 changes; Application re-spawns the model in
place via the same path teleport uses. Guard on an actual appearance diff
(PLAYER_BYTES_2 also carries rest state), preserve the current position,
and reset equipment dirty tracking so armor and weapons re-composite onto
the fresh model.
Funnel every ship-facing path through TransportManager::transportModelBowOffset
(facing = direction of travel + fixed per-model bow offset), replacing the
scattered per-entry 180-degree hull corrections. berthRunsParallel() names the
side-on dock routes, and buildTaxiSegmentSpline is extracted static/testable so
the cyclic-wrap behavior is unit-covered without a DBC.
Route the four config call sites (login.cfg, settings.cfg, last_character.cfg,
characters/) through a shared core::getConfigRoot(). A portable.txt marker or an
existing config/ folder next to the executable redirects config to <exe_dir>/config;
otherwise the per-user location (%APPDATA%\wowee or ~/.wowee) is used as before.
On first portable launch, migratePortableConfigIfNeeded() seeds the portable
folder from the existing per-user config so saved server profiles, settings, and
characters carry over.
Parse per-animation M2 color alpha tracks across vanilla and WotLK layouts so hidden lumberjack bundle meshes remain culled until their authored death sequence.
Keep scripted worker emotes and display swaps coherent across movement and async model loads. Rework food and drink presentation around the seated aura lifecycle, repeat consumption audio, preserve real cast animations on auto-stand, and retain potion swigs.
Add server-driven intoxication movement, camera, and post-process effects plus focused regression coverage for M2 alpha tracks and consumable classification.
Wall collision only blocked triangles carrying the explicit collision
flag (0x08), so rendered geometry without it — the Deeprun Tram gates —
was walk-through, letting players escape onto the exterior shell.
Blocking is now the union of collision hulls (0x08, tunnels rely on
invisible ones) and rendered non-detail surfaces (0x20 without 0x04);
detail geometry still never blocks.
Transport registrations were erased on the first frame the GameObject
render instance didn't exist yet, silently racing the budgeted GO spawn
queue — tram cars never registered as transports. Retry until the
instance spawns and only drop (with a warning) after ~30s.
Weapon models share CharacterRenderer's models map with NPC composites
keyed by creature displayId, and the weapon id counter started at 1000
inside that range. Once enough weapon reloads advanced the counter onto
a loaded NPC's displayId, attachWeapon reused the NPC model as the
weapon mesh and stomped its textures. Weapon ids now start at
0x40000000, above every other id scheme in the map (displayIds,
player models at 60000+, projectiles capped below 999000).
The back-sheath offsets and angles are now runtime-tunable and the
/sheathtune chat command updates them and reloads equipped weapons
immediately, so weapon placement can be iterated in game instead of
rebuilding per guess. Current defaults unchanged; settled values get
baked into SheathTuning.
Mounting stows drawn weapons like the original client: the mount
callback sets the sheathed state and reloads equipped weapons onto
their sheath attachment points.
syncCreatureStealthVisuals() ran every frame and paid a mutex-locked
getEntity() plus a dynamic_pointer_cast for every spawned creature,
adding ~100k lock acquisitions and RTTI casts per second in crowded
zones. Scan every 15th frame instead, read the unlocked main-thread
entity map, and replace the dynamic cast with a type check and
static_cast. Stealth flags flip rarely, so the ~150ms worst-case
latency on the translucency fade is imperceptible.
Helm and shoulder M2s were re-read, re-parsed and re-uploaded once per NPC spawn,
with a fresh model id each time — so a crowd wearing the same gear paid for the
same mesh repeatedly. Cache the parsed geometry by path (with negative caching so
a missing file is not re-probed) and the renderer model id by (path, texture).
The id is keyed by texture as well as mesh because attachWeapon() binds the
texture to the model id: sharing one id across recolours of the same mesh would
let the last NPC spawned retexture every other NPC wearing it.
spawnOnlineCreature carried a fallback that read and parsed the creature M2 from
disk inline. The async loader already publishes every creature model before a
spawn is attempted, so the fallback was unreachable — but it left file I/O one
missed invariant away from running mid-frame. The spawn now treats a cache miss
as 'not ready' and re-queues through the async path, and the sync loader is gone.
Entering the world blocks the main loop for ~1.6s while the world loader presents
its own frames, so the loop never beat the watchdog and it force-released mouse
capture mid-load. The loader now beats the watchdog at each progress step.
Also time the render phases, mirroring the existing IN_GAME update stages — the
stall was invisible because render() had no instrumentation.
Show the actual Mining Pick model while the local character performs a
mining cast instead of leaving the equipped main-hand weapon visible.
Mining already used the correct gather animation, but the attachment path
kept rendering the normal equipped weapon. Add a scoped mining-pick override
that attaches ItemDisplayInfo 6568 to the right hand for the cast duration
and restores the equipped weapons when the cast ends.
Keep the temporary tool authoritative across equipment refreshes while the
cast is active, and track the character instance that owns the override so
interrupted casts, respawns, or logout transitions cannot leave a later
character stuck with the temporary attachment.
Wire the appearance composer into animation callbacks so mining start and
finish events can drive the held-tool override directly.
Build passes. All 33 tests pass.
The third merge-blocking issue Kelsidavis flagged in review of PR #96:
unconditionally preferring any WMO/M2 floor over terrain fixed
underground landings (terrain has no notion of being underground - a
WMO tunnel beneath a mountain, like Ironforge's flight point, reports
the outdoor mountain surface far above the correct tunnel floor), but
could just as easily snap an outdoor landing down onto an unrelated
structure sitting underneath it.
Now captures the character's render-position Z the moment the landing
clamp arms (WorldEntryCallbackHandler::taxiLandingReferenceZ_) -
wherever the taxi flight simulation itself left the player, before
terrain/WMO streaming has a chance to move things around - and picks
whichever candidate (terrain/WMO/M2) is numerically closest to that
reference, rather than preferring non-terrain sources outright. Still
correctly picks the WMO tunnel floor for Ironforge (the flight lands
right at it, ~0 distance, vs. ~267 units to the terrain surface above)
while no longer able to prefer a distant WMO/M2 floor under an outdoor
landing over the much-closer terrain surface.
Generate core/version.hpp from `git describe --tags --abbrev=0` so the client
always reports the last tagged release, refreshed on every build rather than only
when cmake happens to reconfigure. The date carries no clock time, so the header
changes at most once a day instead of forcing a recompile every build.
Also un-ignore cmake/*.cmake: the repo's *.cmake rule is aimed at build output,
and would have silently dropped the new module from the tree.
A sharpened blade should glint. Resolve the enchant's visual through
SpellItemEnchantment -> ItemVisuals -> ItemVisualEffects and attach the effect M2
to the weapon model's item-visual attachment points, riding the weapon transform.
Such a model is nothing but the additive FX batches that attached weapons
otherwise drop, so effect instances are exempted from that cull, keep their
animation advanced, and are forced additive and unlit — their materials declare
Mod/alpha blending, which would otherwise composite the glow card's black
background as an opaque quad.
Applying an enchant leaves the displayInfoId untouched, so the equipment-dirty
check now also tracks per-slot enchant ids; the visual appears without re-equipping.
Every frame the render loop spawned and destroyed OS threads via
std::async: the M2 doodad animation update, three secondary command
buffer recording tasks, up to N bone-matrix chunks in the M2 and
character renderers, and two collision floor queries while moving —
hundreds of thread create/destroy cycles per second. Route all of them
through a shared persistent core::ThreadPool sized to the machine.
Bone-matrix chunk dispatch now runs its last chunk on the calling
thread so the nested case (M2 update running on a pool worker while
submitting sub-work) always makes progress and cannot deadlock.
The DBC loader skipped binary DBCs in Data/db/ on Linux because
filenames are lowercase but code requested mixed-case names. This
caused the corrupted CSV fallback to be used, where nearly all
integer fields (including GeosetGroup) were misclassified as strings
and exported as string-block lookups instead of numeric values.
Add case-insensitive filename matching for the Data/db/ directory
scan and prioritize expansion overlay DBCs. Fix kGeosetBareShins
from 503 to 501, add per-race lowestInGroup fallback (Gnome, Tauren),
and use DBC layout JSON for field indices in game_screen_hud. Exclude
field 0 (record ID) from string detection in both dbc_to_csv and the
asset extractor to prevent future CSV corruption.
The previous fix pinned the ride offset completely fixed to stop
riding from being a no-op identity, which correctly carries the
player along but also means WASD input while riding did nothing -
"once I was attached I couldn't move at all."
Compare this frame's actual render position (after the movement
system processes WASD, which runs earlier in the frame) against
where the ride-lock code placed the player last frame
(lastM2RideLockedCanonical_). That difference is exactly how far the
player tried to walk this frame, independent of how far the
transport itself moved underneath them; add it onto the persisted
offset. Standing still holds a fixed deck position (delta is zero),
active input still moves the player around on the deck, and the
delta baseline resets cleanly (hasM2RideLock_) whenever a fresh
boarding sets a new starting offset.
Break the monolithic 1360-line world_map.cpp into 16 focused modules
under src/rendering/world_map/:
Architecture:
- world_map_facade: public API composing all components (PIMPL)
- world_map_types: Vulkan-free domain types (Zone, ViewLevel, etc.)
- data_repository: DBC zone loading, ZMP pixel map, POI/overlay storage
- coordinate_projection: UV projection, zone/continent lookups
- composite_renderer: Vulkan tile pipeline + off-screen compositing
- exploration_state: server mask + local exploration tracking
- view_state_machine: COSMIC→WORLD→CONTINENT→ZONE navigation
- input_handler: keyboard/mouse input → InputAction mapping
- overlay_renderer: layer-based ImGui overlay system (OCP)
- map_resolver: cross-map navigation (Outland, Northrend, etc.)
- zone_metadata: level ranges and faction data
Overlay layers (each an IOverlayLayer):
- player_marker, party_dot, taxi_node, poi_marker, quest_poi,
corpse_marker, zone_highlight, coordinate_display, subzone_tooltip
Fixes:
- Player marker no longer bleeds across continents (only shown when
player is in a zone belonging to the displayed continent)
- Zone hover uses DBC-projected AABB rectangles (restored from
original working behavior)
- Exploration overlay rendering for zone view subzones
Tests:
- 6 new test files covering coordinate projection, exploration state,
map resolver, view state machine, zone metadata, and integration
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
Extract CatmullRomSpline (include/math/spline.hpp, src/math/spline.cpp) as a
standalone, immutable, thread-safe spline module with O(log n) binary segment
search and fused position+tangent evaluation — replacing the duplicated O(n)
evalTimedCatmullRom/orientationFromTangent pair in TransportManager.
Consolidate 7 copies of spline packet parsing into shared functions in
game/spline_packet.{hpp,cpp}: parseMonsterMoveSplineBody (WotLK/TBC),
parseMonsterMoveSplineBodyVanilla, parseClassicMoveUpdateSpline,
parseWotlkMoveUpdateSpline, and decodePackedDelta. Named SplineFlag constants
replace magic hex literals throughout.
Extract TransportPathRepository (game/transport_path_repository.{hpp,cpp}) from
TransportManager — owns path data, DBC loading, and path inference. Paths stored
as PathEntry wrapping CatmullRomSpline + metadata (zOnly, fromDBC, worldCoords).
TransportManager reduced from ~1200 to ~500 lines, focused on transport lifecycle
and server sync.
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
buildDefaultPlayerGeosets() was inserting all submeshIds 0-99 into
activeGeosets, showing every hair variation simultaneously. Now uses
the hairGeosetMap_ (from CharHairGeosets.dbc) to select only the
correct hair scalp geoset for the player's race/sex/style, matching
the existing NPC geoset filtering logic in EntitySpawner.
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`
pendingPlayerSpawns_, deferredEquipmentQueue_, and
pendingGameObjectSpawns_ are consumed from the front via erase(begin()),
which is O(n) on vector (shifts all elements). With many spawns queued
(entering a city), this made the processing loop O(n²). deque supports
O(1) front erasure. pendingCreatureSpawns_ already used deque.
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.