Commit graph

3003 commits

Author SHA1 Message Date
Paul
85f8d05061 feat: add multi-platform Docker build system for Linux, macOS, and Windows
Replace the single Ubuntu-based container build with a dedicated
Dockerfile, build script, and launcher for each target platform.

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

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

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

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

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

Other:
- Update vcpkg.json with ffmpeg feature flags
- Update resources/wowee.rc version string
2026-03-30 20:17:41 +03:00
Kelsi
c1c28d4216 fix: quest objective GOs never granted credit — REPORT_USE skipped for chests
CMSG_GAMEOBJ_REPORT_USE was only sent for non-chest GOs. Chest-type
(type=3) and name-matched chest-like GOs (Bundle of Wood, etc.) went
through a separate path that sent CMSG_GAMEOBJ_USE + CMSG_LOOT but
skipped REPORT_USE. On AzerothCore, REPORT_USE triggers the server-side
HandleGameobjectReportUse which calls GossipHello on the GO script —
this is where many quest objective scripts grant credit.

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

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

No std::rand() or srand() calls remain in the codebase.
2026-03-29 21:01:51 -07:00
Kelsi
a55eacfe70 fix: weather particles and cycle durations deterministic due to unseeded rand()
8 rand() calls in weather.cpp used C rand() which defaults to seed 1.
Weather intensity rolls, cycle durations, and particle Y positions were
identical on every launch. Replaced with a file-local mt19937 seeded
from random_device, matching the RNG already present in getRandomPosition.
2026-03-29 20:53:38 -07:00
Kelsi
294c91d84a fix: migrate 197 unsafe packet bounds checks to hasRemaining/getRemainingSize
All domain handler files used 'packet.getSize() - packet.getReadPos()'
which underflows to ~2^64 when readPos exceeds size (documented in
commit ed63b029). The game_handler.cpp and packet_parsers were migrated
to hasRemaining(N) in an earlier cleanup, but the domain handlers were
created after that migration by the PR #23 split, copying the old
unsafe patterns back in. Now uses hasRemaining(N) for comparisons and
getRemainingSize() for assignments across all 7 handler files.
2026-03-29 20:53:26 -07:00
Kelsi
849542d01d fix: doodad/mount animations synchronized due to unseeded rand()
All 8 rand() calls for animation time offsets and variation timers in
m2_renderer.cpp used C rand() which defaults to seed 1 without srand(),
producing identical sequences every launch. Trees, torches, and grass
all swayed in sync. Replaced with std::mt19937 seeded from
random_device. Same fix for 4 mount idle fidget/sound timer sites in
renderer.cpp which mixed rand() with the mt19937 already present.
2026-03-29 20:42:10 -07:00
Kelsi
16aaf58198 fix: M2 readString uint32 overflow in bounds check
offset + length was computed in uint32_t before comparing to size_t.
A crafted M2 with offset=0xFFFFFFFF, length=2 wraps to 1 in uint32,
passing the check and reading out of bounds. Now uses size_t arithmetic,
matching the readArray fix from an earlier round.
2026-03-29 20:41:56 -07:00
Kelsi
fa1643dc90 fix: WMO readArray integer overflow in bounds check
count * sizeof(T) was computed in uint32_t — a large count value from a
crafted WMO file could wrap to a small number, pass the bounds check,
then attempt a multi-GB allocation causing OOM/crash. Now uses 64-bit
arithmetic with a 64MB sanity cap, matching the M2 loader pattern.
2026-03-29 20:32:47 -07:00
Kelsi
b007a525a6 fix: Lua UnitIsAFK/UnitIsDND/UnitIsGhost/UnitIsPVPFreeForAll read wrong field
All four functions read UNIT_FIELD_FLAGS instead of PLAYER_FLAGS.
- AFK (0x01) hit UNIT_FLAG_SERVER_CONTROLLED — vendors flagged as AFK
- DND (0x02) hit UNIT_FLAG_NON_ATTACKABLE — guards flagged as DND
- Ghost (0x100) hit UNIT_FLAG_IMMUNE_TO_PC — immune NPCs flagged as ghost
- FFA PvP (0x80000) hit UNIT_FLAG_PACIFIED — pacified mobs flagged FFA

All now correctly read PLAYER_FLAGS with the right bit masks (0x01,
0x02, 0x10, 0x80 respectively), matching entity_controller.cpp which
already uses the correct field.
2026-03-29 20:32:39 -07:00
Kelsi
f02be1ffac fix: tolower/toupper UB on signed char at 10 remaining call sites
Final sweep across mpq_manager, application, auth_screen, wmo_renderer,
character_renderer, and terrain_manager. All now use the unsigned char
cast pattern. No remaining bare ::tolower/::toupper or std::tolower(c)
calls on signed char in the codebase.
2026-03-29 20:27:16 -07:00
Kelsi
34e384e1b2 fix: tavern music always played first track — index never incremented
tavernTrackIndex was initialized to 0 but never modified, so the player
always heard TavernAlliance01.mp3. Added post-increment to rotate
through the 3 available tracks on each tavern entry.
2026-03-29 20:27:08 -07:00
Kelsi
a1575ec678 fix: WDT MWMO parser used unbounded strlen on chunk data
std::strlen on raw MWMO chunk data has no upper bound if the chunk
lacks a null terminator (truncated/corrupt WDT file). Replaced with
strnlen bounded by chunkSize, matching the ADT parser fix in d776226f.
2026-03-29 20:26:58 -07:00
Kelsi
7f5cad63cd fix: WMO group debug log throttle was per-process, not per-model
static int logCount/batchLogCount inside the per-group parse loop
accumulated globally, so after the first WMO with many sub-chunks
loaded, no subsequent WMO group would ever log. Changed to function-
local / loop-index-based throttle so each group gets its own window.
2026-03-29 20:14:53 -07:00
Kelsi
fa15a3de1f fix: transport elapsed time lost millisecond precision after ~4.5 hours
elapsedTime_ was float (32-bit, ~7 significant digits). At 16384
seconds the float can only represent integers, so elapsedTime_*1000
jumps in 1-second steps — ships and elevators visibly jerk. Changed to
double (53-bit mantissa) which maintains sub-millisecond precision for
~285 million years. Also changed lastServerUpdate to double to match.
2026-03-29 20:14:45 -07:00
Kelsi
ef25785404 fix: terrain chunk UBO allocation failure crashed GPU via null descriptor
vmaCreateBuffer return value was silently discarded in both loadTerrain
and loadTerrainIncremental. If allocation failed (OOM/fragmentation),
the chunk proceeded with a VK_NULL_HANDLE UBO, causing the GPU to read
from an invalid descriptor on the next draw call. Now checks the return
value and skips the chunk on failure.
2026-03-29 20:14:35 -07:00
Kelsi
c1f6364814 cleanup: remove misleading (void)flags — variable IS used for crit check
The cast falsely suggests flags is unused but it's read on the next
line for isCrit = (flags & 0x02). Also inlines the periodicLog discard.
2026-03-29 20:05:45 -07:00
Kelsi
568a14852d fix: WMO MODS parser raw memcpy without bounds check
The doodad set name read used raw memcpy(20 bytes) bypassing the safe
read<T> template that returns {} on OOB. A truncated WMO file would
read past the vector's storage. Added bounds check before the memcpy.
2026-03-29 20:05:37 -07:00
Kelsi
b5fba65277 fix: BLP loader OOB read on ARGB8888 and signed overflow on dimensions
ARGB8888 decompression read pixelCount*4 bytes from mipData without
checking that mipSize was large enough — a truncated BLP caused heap
OOB reads. Also, 'int pixelCount = width * height' overflowed for
large dimensions (signed int UB). Now validates dimensions <= 4096,
uses uint32_t arithmetic, and checks mipSize >= required for ARGB8888.
2026-03-29 20:05:29 -07:00
Kelsi
59bbeaca62 fix: ::toupper/::tolower UB on signed char at 5 remaining call sites
std::toupper(int) and std::tolower(int) have undefined behavior when
passed a negative value. These sites passed raw signed char without
casting to unsigned char first, unlike the rest of the codebase which
already uses the correct pattern. Affects auth (account names), world
packets, and mount sound path matching.
2026-03-29 19:58:36 -07:00
Kelsi
d776226fd1 fix: ADT parser OOB reads on sub-chunk headers and unterminated strings
1. MCNK sub-chunk bounds checks didn't account for the 8-byte header
   skip, so parseMCVT/parseMCNR could read up to 8 bytes past the
   validated buffer when sub-chunk headers are present (the common case).

2. parseMTEX/parseMMDX/parseMWMO used unbounded strlen on raw chunk
   data. A truncated file without a null terminator would read past the
   chunk boundary. Replaced with strnlen bounded by remaining size.

Also removes dead debug code: empty magic buffer copy, cathedral WMO
search, and Stormwind placement dump (which also had ::toupper UB).
2026-03-29 19:58:28 -07:00
Kelsi
f2237c5531 perf: switch 3 spawn queues from vector to deque
pendingPlayerSpawns_, deferredEquipmentQueue_, and
pendingGameObjectSpawns_ are consumed from the front via erase(begin()),
which is O(n) on vector (shifts all elements). With many spawns queued
(entering a city), this made the processing loop O(n²). deque supports
O(1) front erasure. pendingCreatureSpawns_ already used deque.
2026-03-29 19:51:26 -07:00
Kelsi
3b7ac068d2 perf: hoist key array read out of per-sequence loop in parseAnimTrackVanilla
readArray was called inside the loop on every iteration, re-parsing the
entire flat key array via memcpy. For a model with 200 sequences and
10k keys this produced ~24MB of redundant copying. Now reads once before
the loop (matching how allTimestamps was already handled).
2026-03-29 19:51:17 -07:00
Kelsi
c4d2b1709e fix: SMSG_RANDOM_ROLL parsed fields in wrong order — garbled /roll output
WotLK format is min(4)+max(4)+result(4)+guid(8)=20 bytes. The parser
read guid(8) first (treating min|max as a uint64), then targetGuid(8)
(non-existent field), then the actual values at wrong offsets. Every
/roll message showed garbled numbers and a bogus roller identity.

Also adds a hasRemaining guard for the 64 bytes of damage/armor/resist
fields in the item query parser — previously read past end with silent
zero-fill on truncated packets.
2026-03-29 19:51:07 -07:00
Kelsi
0ee57f4257 fix: FBlock comment said 'CImVector 4 bytes RGBA' but code reads C3Vector
The comment would lead a maintainer to "fix" the working code to read
4-byte RGBA instead of 3-float C3Vector. Updated to match the actual
M2 particle FBlock color format (3 floats, values 0-255, per WoWDev).
2026-03-29 19:43:57 -07:00
Kelsi
d731e0112e fix: std::tolower(char) UB on signed char at 3 call sites
std::tolower(int) has undefined behavior when passed a negative value,
which signed char produces for bytes > 127. The rest of the codebase
correctly casts to unsigned char first; these 3 sites were missed.
2026-03-29 19:43:46 -07:00
Kelsi
27b2322444 fix: chat mention highlight only covered first line of wrapped messages
The golden tint rect was drawn before rendering with a hardcoded single-
line height. Multi-line wrapped messages only had the first line
highlighted. Now drawn after EndGroup() using GetItemRectMin/Max so the
rect covers all wrapped lines.

Also fixes std::tolower(char) UB at two call sites — negative char
values (extended ASCII) are undefined behavior without unsigned cast.
2026-03-29 19:43:38 -07:00
Kelsi
ed63b029cd fix: getRemainingSize() underflowed when readPos exceeded data size
Both operands are size_t (unsigned), so if readPos > data.size() the
subtraction wrapped to ~0 instead of returning 0. This could happen
via setReadPos() which has no bounds check. Downstream hasRemaining()
was already safe but getRemainingSize() callers (e.g. hasFullPackedGuid)
would see billions of bytes available.
2026-03-29 19:36:41 -07:00
Kelsi
9da97e5e88 fix: partial send on non-blocking socket silently dropped data
A single send() that returned fewer bytes than requested was logged but
not retried, leaving the server with a truncated packet. This causes an
irreversible TCP framing desync (next header lands mid-payload) that
manifests as a disconnect under network pressure. Added a retry loop
that handles EWOULDBLOCK with a brief yield.

Also rejects payloads > 64KB instead of silently truncating the 16-bit
CMSG size field, which would have written a wrong header while still
appending all bytes.
2026-03-29 19:36:32 -07:00
Kelsi
e5b4e86600 fix: misleading indentation on BAG_UPDATE/UNIT_INVENTORY_CHANGED emits
The two emit calls were indented 12 spaces (suggesting a nested block)
instead of 8 (matching the enclosing if). Same class of maintenance
trap as the PLAYER_ALIVE/PLAYER_UNGHOST fix in b3abf04d.
2026-03-29 19:31:29 -07:00
Kelsi
061a21da8f fix: guild event string never appended; /leave left stale party state
1. Contradictory condition (!numStrings && numStrings >= 1) was always
   false, so unknown guild event messages never included the server's
   context string. Fixed to just numStrings >= 1.

2. leaveParty() only sent the packet without clearing partyData or
   firing addon events, so /leave left party frames visible until the
   server pushed an update. Now delegates to leaveGroup() which handles
   both the packet and local state cleanup.
2026-03-29 19:31:21 -07:00
Kelsi
3da3638790 cleanup: remove misleading (void)reasonType — variable IS used below
The cast falsely suggests reasonType is unused, but it's read on lines
3699-3702 for AFK/vote-kick differentiation. Same class of issue as
the (void)isPlayerTarget fix in commit 6731e584.
2026-03-29 19:23:05 -07:00
Kelsi
84c0ced228 fix: friend cache inserted empty key; ignore erase before server confirm
handleFriendStatus inserted into friendsCache with an empty playerName
when the name query hadn't resolved yet, creating a phantom "" entry.
Now guards with !playerName.empty().

removeIgnore erased from ignoreCache immediately without waiting for
server confirmation, desyncing the cache if the server rejected. Now
only clears the GUID set and lets the next SMSG_IGNORE_LIST rebuild
the cache, consistent with how removeFriend works.
2026-03-29 19:22:55 -07:00
Kelsi
731d9a88fb fix: SMSG_AUCTION_BIDDER_NOTIFICATION read itemEntry at wrong offset
The handler treated the second uint32 (auctionId) as itemEntry. The
real itemEntry is at byte 24 after auctionHouseId(4)+auctionId(4)+
bidderGuid(8)+bidAmount(4)+outbidAmount(4). Outbid chat messages always
referenced the wrong item.
2026-03-29 19:22:44 -07:00
Kelsi
5b9b8b59ba refactor: extract buildItemDef from 4 copy-pasted blocks in rebuildOnlineInventory
The same 25-line block copying ~20 fields from itemInfoCache_ into
ItemDef was duplicated for equipment, backpack, keyring, and bag slots.
Extracted into buildItemDef() so new fields only need adding once.
Net -100 lines.
2026-03-29 19:16:36 -07:00
Kelsi
e72cb4d380 fix: async creature upload budget blocked cache hits and failures
The per-tick GPU upload budget check ran before consuming async futures,
so after 1 upload ALL remaining ready results were deferred — including
permanent failures and cache hits that need zero GPU work. Moved the
budget gate after failure/cache-hit processing so only actual uploads
count. Re-queues over-budget results as pending spawns for next frame.
2026-03-29 19:16:27 -07:00
Kelsi
05cfcaacf6 fix: clearTarget fired PLAYER_TARGET_CHANGED before zeroing targetGuid
Callbacks and addons querying the current target during this event saw
the old (stale) target instead of null. setTarget correctly updates the
GUID before firing — clearTarget now does the same.
2026-03-29 19:16:18 -07:00
Kelsi
bf63d8e385 fix: static lastSpellCount shared across SpellHandler instances
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
The spellbook tab dirty check used a function-local static, meaning
switching to a character with the same spell count would skip the
rebuild and return the previous character's tabs. Changed to an
instance member so each SpellHandler tracks its own count.
2026-03-29 19:08:58 -07:00
Kelsi
e629898bfb fix: nameplate health bar division by zero when maxHealth is 0
Freshly spawned entities have maxHealth=0 before fields populate.
0/0 produces NaN which propagates through all geometry calculations
for that nameplate frame. Guard with a maxHealth>0 check.
2026-03-29 19:08:51 -07:00
Kelsi
7462fdd41f refactor: extract buildForceAck from 5 duplicated force-ACK blocks
All five force-ACK handlers (speed, root, flag, collision-height,
knockback) repeated the same ~25-line GUID+counter+movementInfo+coord-
conversion+send sequence. Extracted into buildForceAck() which returns
a ready-to-send packet with the movement payload already written.

This also fixes a transport coordinate conversion bug: the collision-
height handler was the only one that omitted the ONTRANSPORT check,
causing position desync when riding boats/zeppelins. buildForceAck
handles transport coords uniformly for all callers.

Net -80 lines.
2026-03-29 19:08:42 -07:00
Kelsi
5fcb30be1a cleanup: remove dead debug loop in buildCreatureDisplayLookups
Loop iterated 20 hair geoset lookups for Human Male but the if-body
was empty — the LOG statement that was presumably there was removed
but the loop skeleton was left behind.
2026-03-29 19:01:41 -07:00