Compare commits

...

87 commits

Author SHA1 Message Date
Kelsi
3eded6772d Implement bird/cricket ambient sounds and remove stale renderer TODO
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
- Add birdSounds_ and cricketSounds_ AmbientSample vectors to
  AmbientSoundManager, loaded from WoW MPQ paths:
  BirdAmbience/BirdChirp01-06.wav (up to 6 variants, daytime) and
  Insect/InsectMorning.wav + InsectNight.wav (nighttime). Missing files
  are silently skipped so the game runs without an MPQ too.
- updatePeriodicSounds() now plays a randomly chosen loaded variant
  at the scheduled interval instead of the previous no-op placeholder.
- Remove stale "TODO Phase 6: Vulkan underwater overlay" comment from
  Renderer::initialize(); the feature has been fully implemented in
  renderOverlay() / the swim effects pipeline since that comment was
  written.
2026-03-09 19:00:42 -07:00
Kelsi
6cba3f5c95 Implement SMSG_MULTIPLE_PACKETS unpacking and fix unused variable warning
- Parse bundled sub-packets from SMSG_MULTIPLE_PACKETS using the WotLK
  standard wire format (uint16_be size + uint16_le opcode + payload),
  dispatching each through handlePacket() instead of silently discarding.
  Rate-limited warning for malformed sub-packet overruns.
- Remove unused cullRadiusSq variable in TerrainRenderer::renderShadow()
  that produced a -Wunused-variable warning.
2026-03-09 18:52:34 -07:00
Kelsi
18e6c2e767 Fix game object sign orientation and restrict nameplates to target only
Game object M2 models share the same default facing (+renderX) as
character models, so apply the same π/2 offset instead of π when
computing renderYawM2go from canonical yaw. This corrects street signs
and hanging shop signs that were 90° off after the server-yaw formula
fix.

Nameplates (health bar + name label) are now only rendered for the
currently targeted entity, matching WoW's default UI behaviour and
reducing visual noise.
2026-03-09 18:45:28 -07:00
Kelsi
6a681bcf67 Implement terrain shadow casting in shadow depth pass
Add initializeShadow() to TerrainRenderer that creates a depth-only
shadow pipeline reusing the existing shadow.vert/frag shaders (same
path as WMO/M2/character renderers). renderShadow() draws all terrain
chunks with sphere culling against the shadow coverage radius. Wire
both init and draw calls into Renderer so terrain now casts shadows
alongside buildings and NPCs.
2026-03-09 18:34:27 -07:00
Kelsi Rae Davis
2afd455d52
Replace (std::min + std::max) with std::clamp
Replace (std::min + std::max) with std::clamp
2026-03-09 18:34:14 -07:00
Kelsi
c887a460ea Implement Death Knight rune tracking and rune bar UI
Parse SMSG_RESYNC_RUNES, SMSG_ADD_RUNE_POWER, and SMSG_CONVERT_RUNE to
track the state of all 6 DK runes (Blood/Unholy/Frost/Death type,
ready flag, and cooldown fraction). Render a six-square rune bar below
the Runic Power bar when the player is class 6, with per-type colors
(Blood=red, Unholy=green, Frost=blue, Death=purple) and client-side
fill animation so runes visibly refill over the 10s cooldown.
2026-03-09 18:28:03 -07:00
vperus
163dc9618a Replace (std::min + std::max) with std::clamp 2026-03-10 03:18:18 +02:00
Kelsi
819a38a7ca Fix power bar visibility: include Runic Power (type 6) in fixed-max fallback
Death Knights with runic power (type 6) had no power bar visible until the
server explicitly sent UNIT_FIELD_MAXPOWER1, because the type-6 max was not
included in the 'assume 100' fallback. Runic Power has a fixed cap of 100,
same as Rage (1), Focus (2), and Energy (3).
2026-03-09 18:18:07 -07:00
Kelsi
caea24f6ea Fix animated M2 flicker: free bone descriptor sets on instance removal
The boneDescPool_ had MAX_BONE_SETS=2048 but sets were never freed when
instances were removed (only when clear() reset the whole pool on map load).
As tiles streamed in/out, each new animated instance consumed 2 pool slots
(one per frame index) permanently. After ~1024 animated instances created
total, vkAllocateDescriptorSets began failing silently and returning
VK_NULL_HANDLE. render() skips instances with null boneSet[frameIndex],
making them invisible — appearing as per-frame flicker as the culling pass
included them but the render pass excluded them.

Fix: destroyInstanceBones() now calls vkFreeDescriptorSets() for each
non-null boneSet before destroying the bone SSBO. The pool already had
VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT set for this purpose.
Also increased MAX_BONE_SETS from 2048 to 8192 for extra headroom.
2026-03-09 18:09:33 -07:00
Kelsi
7b3b33e664 Fix NPC orientation (server yaw convention) and nameplate Y projection
coordinates.hpp: serverToCanonicalYaw now computes s - π/2 instead of π/2 - s.
The codebase uses atan2(-dy, dx) as its canonical yaw convention, where server
direction (cos s, sin s) in (server_X, server_Y) becomes (sin s, cos s) in
canonical after the X/Y swap, giving atan2(-cos s, sin s) = s - π/2.
canonicalToServerYaw is updated as its proper inverse: c + π/2.
The old formula (π/2 - s) was self-inverse and gave the wrong east/west facing
for any NPC not pointing north or south.

game_screen.cpp: Nameplate NDC→screen Y no longer double-inverts. The camera
bakes the Vulkan Y-flip into the projection matrix (NDC y=-1 = screen top,
y=+1 = screen bottom), so sy = (ndc.y*0.5 + 0.5) * screenH is correct.
The previous formula subtracted from 1.0 which reflected nameplates vertically.
2026-03-09 17:59:55 -07:00
Kelsi
a335605682 Fix pet frame position to avoid overlap with party frames
When in a group, push the pet frame below the party frame stack
(120px + members × 52px). When solo, keep it at y=125 (just below
the player frame at ~110px).
2026-03-09 17:25:46 -07:00
Kelsi
8b495a1ce9 Add pet frame UI below player frame
Shows active pet name, level, health bar, and power bar (mana/focus/rage/energy)
when the player has an active pet. Clicking the pet name targets it. A Dismiss
button sends CMSG_PET_ACTION to dismiss the pet. Frame uses green border to
visually distinguish it from the player/target frames.
2026-03-09 17:23:28 -07:00
Kelsi
f43277dc28 Fix SMSG_ENVIRONMENTAL_DAMAGE_LOG to use uint64 GUID (not packed)
WotLK 3.3.5a sends a raw uint64 victim GUID in this packet, not a
packed GUID. Update the handler format to match (uint64 + uint8 type
+ uint32 damage + uint32 absorb). Remove the now-dead SMSG_ENVIRONMENTALDAMAGELOG
handler since the opcode alias always routes to SMSG_ENVIRONMENTAL_DAMAGE_LOG.
2026-03-09 17:22:07 -07:00
Kelsi
70dcb6ef43 Parse SMSG_ENVIRONMENTAL_DAMAGE_LOG and color nameplate names by hostility
- Implement SMSG_ENVIRONMENTAL_DAMAGE_LOG: show fall/lava/fire/drowning
  damage as ENVIRONMENTAL combat text (orange -N) for the local player
- Color nameplate unit names: hostile units red, non-hostile yellow
  (matches WoW's standard red=enemy / yellow=neutral convention)
2026-03-09 17:18:18 -07:00
Kelsi
18a3a0fd01 Add XP_GAIN combat text type; show '+N XP' in purple on kills
XP gain was previously shown as a HEAL entry (green +N) which conflates
it with actual healing.  New XP_GAIN type renders as purple '+N XP' in the
outgoing column, matching WoW's floating XP style.
2026-03-09 17:13:31 -07:00
Kelsi
6e03866b56 Handle BLOCK (victimState 4) in melee hit combat text
Block rolls previously fell through to the damage case and were shown as
a 0-damage hit.  Now correctly emitted as a BLOCK combat text entry, which
renderCombatText already handles with 'Block' / 'You Block' label.
2026-03-09 17:10:57 -07:00
Kelsi
068deabb0e Add missing power bar colours for all WoW power types
Focus (2, orange), Happiness (4, green), Runic Power (6, crimson), and
Soul Shards (7, purple) were falling through to the default blue colour.
Applied consistently to the player frame, target frame, and party frames.
2026-03-09 17:09:48 -07:00
Kelsi
ea1af87266 Show aura charge/stack count on buff bar and target frame icons
When an aura has more than 1 charge, render the count in gold in the
upper-left corner of the icon (with drop shadow) — same position as WoW's
stack counter.  Applied to both the player buff bar and target frame auras.
2026-03-09 17:08:14 -07:00
Kelsi
6d1f3c4caf Add zone discovery text: 'Entering: <ZoneName>' fades in on zone change
Polls the renderer's currentZoneName each frame and triggers a 5-second
fade-in/hold/fade-out toast at the upper-centre of screen when the zone
changes.  Matches WoW's standard zone transition display.
2026-03-09 17:06:12 -07:00
Kelsi
c14bb791a0 Show level in nameplate labels; use '??' for skull-level targets
Prefix each nameplate name with the unit's level number.  When the unit
is more than 10 levels above the player (skull-equivalent) display '??'
instead of the raw level, matching WoW's UI convention.
2026-03-09 17:04:14 -07:00
Kelsi
9d26f8c29e Add V key toggle for nameplates (WoW default binding)
nameplates default to visible; pressing V in the game world toggles them
off/on while the keyboard is not captured by a UI element.
2026-03-09 17:03:06 -07:00
Kelsi
01e0c2f9a3 Add world-space unit nameplates projected to screen via camera VP matrix
For each visible Unit entity within 40 yards, projects the canonical WoW
position (converted to render space) through the camera view-projection
matrix to screen pixels.  Draws a health bar (hostile=red, friendly=green,
target=gold border) and name label with drop shadow using ImGui's background
draw list.  Fades out smoothly in the last 5 yards of range.
2026-03-09 17:01:38 -07:00
Kelsi
f1d31643fc Implement SMSG_SPELLENERGIZELOG and fix missing combat text cases
Parse SPELLENERGIZELOG (victim/caster packed GUIDs + spellId + powerType +
amount) and emit ENERGIZE combat text for mana/energy gains.  Add ENERGIZE
to CombatTextEntry::Type enum (blue +N text).

Also add explicit renderCombatText cases for BLOCK, PERIODIC_DAMAGE,
PERIODIC_HEAL, and ENVIRONMENTAL — previously all fell through to the
colourless default handler.
2026-03-09 16:55:23 -07:00
Kelsi
22bc5954d7 Fix opcode handler grouping: separate SET_PROFICIENCY/ENERGIZE from ACTION_BUTTONS
SMSG_SPELLENERGIZELOG, SMSG_ENVIRONMENTAL_DAMAGE_LOG, and
SMSG_SET_PROFICIENCY were incorrectly grouped with the
SMSG_ACTION_BUTTONS case block introduced in the previous commit,
causing their payloads to be misinterpreted as action button data
which could corrupt the action bar. Each now safely consumes
its packet.
2026-03-09 16:51:54 -07:00
Kelsi
52507b1f74 Add target-of-target (ToT) mini frame below target frame
Shows the name and health bar of whoever your current target is
targeting. Reads UNIT_FIELD_TARGET_LO/HI update fields which are
populated from SMSG_UPDATE_OBJECT. Frame is positioned below and
right-aligned with the main target frame.
2026-03-09 16:49:50 -07:00
Kelsi
941b2c4894 Load server action bar from SMSG_ACTION_BUTTONS on login
Previously the 144-button server payload was silently dropped.
Now parses the first 12 buttons (one bar) and populates the local
action bar with server-side spells and items. Macros and unknown
button types are skipped. Empty/zero slots are preserved as-is to
avoid wiping hardcoded Attack/Hearthstone defaults.
2026-03-09 16:45:53 -07:00
Kelsi
4db686a652 Parse SMSG_PERIODICAURALOG to show DoT/HoT numbers in combat text
Previously all periodic aura ticks were silently discarded.
Now parses victim/caster GUIDs, auraType, and damage/heal value
for the two most common types (PERIODIC_DAMAGE=3 and PERIODIC_HEAL=8)
and generates PERIODIC_DAMAGE/PERIODIC_HEAL combat text entries.
Falls back safely to consume-all on unknown aura types.
2026-03-09 16:43:33 -07:00
Kelsi
c57182627f Respond to SMSG_REALM_SPLIT with CMSG_REALM_SPLIT ack
Previously the packet was silently consumed. Some servers send
SMSG_REALM_SPLIT during login and expect a CMSG_REALM_SPLIT
acknowledgement, otherwise they may time out the session.
Responds with splitType echoed back and patchVersion "3.3.5".
2026-03-09 16:39:52 -07:00
Kelsi
f0d1702d5f Add duration countdown overlay to target frame aura icons
Matches the same fix applied to the player buff bar: icons in the
target frame now show their remaining duration at the icon bottom edge
with a drop shadow, shared between the always-visible overlay and the
hover tooltip.
2026-03-09 16:37:55 -07:00
Kelsi
088a11e62a Add duration countdown overlay to buff/debuff icons in buff bar
Icons now show remaining time (e.g. "1:30", "45") rendered directly
on the icon bottom edge with a drop shadow, matching WoW's standard
buff display. Tooltip still shows full name + seconds on hover.
Deduplicates the nowMs/remainMs computation that was previously
recomputed in the tooltip-only path.
2026-03-09 16:36:58 -07:00
Kelsi
13e3e5ea35 Implement MusicManager fade-out in stopMusic() — was a stub
stopMusic(fadeMs) previously had (void)fadeMs with no fade logic.
Added fadingOut/fadeOutTimer/fadeOutDuration/fadeOutStartVolume state
and wired update() to interpolate volume to zero then stop playback.
Also clean up DuelProposedPacket comment (removed misleading TODO label).
2026-03-09 16:30:42 -07:00
Kelsi
f2eabc87ef Add notification for SMSG_BINDER_CONFIRM (innkeeper bind set)
SMSG_BINDER_CONFIRM confirms the bind point was set. Previously silently
consumed; now shows "This innkeeper is now your home location." in system
chat so the player gets feedback after using an innkeeper.
2026-03-09 16:26:31 -07:00
Kelsi
68bf3d32b0 Wire ambient sound zone detection: setZoneType/setCityType was never called
Add AmbientSoundManager::setZoneId() that maps WoW zone IDs to the
appropriate ZoneType (forest/grasslands/desert/jungle/marsh/beach) or
CityType (Stormwind/Ironforge/Darnassus/Orgrimmar/Undercity/ThunderBluff)
and delegates to setZoneType/setCityType. Call it from the renderer's
zone transition handler so zone ambience (looping sounds, city bells,
etc.) actually activates when the player enters a zone.
2026-03-09 16:24:12 -07:00
Kelsi
4ac32a1206 Parse SMSG_GAMETIME_SET/UPDATE/GAMESPEED_SET for sky clock accuracy
Server sends periodic game time corrections via SMSG_GAMETIME_SET and
SMSG_GAMETIME_UPDATE (uint32 gameTimePacked). SMSG_GAMESPEED_SET also
sends an updated timeSpeed float. Applying these keeps gameTime_/timeSpeed_
in sync with the server, preventing day/night drift in the sky renderer
over long play sessions.
2026-03-09 16:21:06 -07:00
Kelsi
6583ce9c57 Use server zone ID (SMSG_INIT_WORLD_STATES) for zone music selection
In online mode, SMSG_INIT_WORLD_STATES delivers the server-authoritative
zone ID when entering a new area. Prefer this over the tile-based fallback
so music transitions are accurate for small zones (city districts, caves,
dungeon entrances) that don't align with 533-unit tile boundaries.
2026-03-09 16:19:38 -07:00
Kelsi
a654dd5e99 Ensure zone music DBC enrichment runs at world load time
Call enrichFromDBC() again when loadOnlineWorld() sets cachedAssetManager,
so enrichment is guaranteed even when the asset manager was null at renderer
construction. enrichFromDBC() is idempotent (skips duplicate paths).
2026-03-09 16:18:08 -07:00
Kelsi
97192ab2a4 Upgrade SMSG_PLAY_OBJECT_SOUND/SPELL_IMPACT to 3D positional audio
Add PlayPositionalSoundCallback that carries both soundId and sourceGuid.
In Application, look up the source entity position and play via
AudioEngine::playSound3D(); fall back to playSound2D() when the entity
is unknown. Also read the 8-byte sourceGuid field from the packet
(previously the full 12-byte payload was ignored).
2026-03-09 16:16:39 -07:00
Kelsi
0913146f54 Play SMSG_PLAY_OBJECT_SOUND and SMSG_PLAY_SPELL_IMPACT audio via DBC lookup
Both opcodes send uint32 soundId as first field. Extend PlaySoundCallback to
cover them so environmental object sounds and spell impact sounds are audible
in-game (resolved through SoundEntries.dbc → AudioEngine::playSound2D).
2026-03-09 16:12:52 -07:00
Kelsi
a2c2675039 Wire SMSG_PLAY_SOUND to AudioEngine via SoundEntries.dbc lookup
Add PlaySoundCallback to GameHandler (same pattern as PlayMusicCallback).
When SMSG_PLAY_SOUND arrives, resolve the soundId through SoundEntries.dbc
(fields 3-12 = files, field 23 = DirectoryBase) and play the first found
file as a 2-D sound effect via AudioEngine::playSound2D(). Previously the
opcode was parsed and dropped.
2026-03-09 16:11:19 -07:00
Kelsi
55082a0925 Remove unused baseZ/hasHeights variables in WaterRenderer::loadFromWMO
These were declared to handle per-vertex WMO liquid height variation but
never actually used below — the surface is built with a flat adjustedZ
height throughout. Remove to eliminate -Wunused-variable warnings.
2026-03-09 16:09:44 -07:00
Kelsi
b23cf06f1c Remove dead legacy GL Texture class
texture.hpp / texture.cpp implemented an unfinished OpenGL texture loader
(loadFromFile was a TODO stub) that had no callers — the project's texture
loading is entirely handled by VkTexture (vk_texture.hpp/cpp) after the
Vulkan migration. Remove both files and their CMakeLists entries.
2026-03-09 16:07:08 -07:00
Kelsi
43b9ecd857 Enrich zone music from AreaTable/ZoneMusic/SoundEntries DBC chain
Add ZoneManager::enrichFromDBC() which walks AreaTable.dbc (field 8 = ZoneMusicId)
→ ZoneMusic.dbc (fields 6/7 = day/night SoundEntryIds) → SoundEntries.dbc
(fields 3-12 = files, field 23 = DirectoryBase) and appends MPQ music paths for
all zones in the DBC, covering ~2300+ areas vs the previous ~15 hardcoded entries.

Existing hardcoded paths are preserved as the primary pool; DBC paths are added
only if not already present. Called from Renderer::init() after initialize().
2026-03-09 16:04:52 -07:00
Kelsi
46f2c0df85 Fix SoundEntries.dbc field indices for SMSG_PLAY_MUSIC and remove dead NpcVoiceManager code
Correct SoundEntries.dbc field access in the PlayMusic callback: file names are at
fields 3-12 (not 2-11) and DirectoryBase is at field 23 (not 22). Field 2 is the
Name label string, not a file path.

Remove dead detectVoiceType(creatureEntry) from NpcVoiceManager — it was never
called; actual voice detection uses detectVoiceTypeFromDisplayId() in Application.
2026-03-09 16:01:29 -07:00
Kelsi
9c3faa0e16 Clarify World stub methods: terrain/entity state lives in subsystems
Remove TODO comments from World::update() and World::loadMap() and
replace with explanatory comments. World is an intentional thin token;
the actual work happens in Application (TerrainManager, camera) and
GameHandler (packet processing). This reflects the current architecture
rather than implying missing work.
2026-03-09 15:54:43 -07:00
Kelsi
e8d068c5cb Add Instance Lockouts window and fix three compiler warnings
- Add Escape Menu → Instance Lockouts button opening a new panel
  that lists active lockouts with instance name (from Map.dbc),
  difficulty, time-until-reset countdown, and locked/extended status.
  map name lookup is cached on first open.
- Fix uninitialized ChatType in sendChatMessage (default to SAY)
- Remove unused startWorld variable in handleMonsterMoveTransport
- Remove unused modelCached variable in spawnOnlineCreature
  Eliminates all -Wunused-but-set-variable and -Wmaybe-uninitialized
  warnings in the main translation units.
2026-03-09 15:52:58 -07:00
Kelsi
4e3d50fc20 Wire SMSG_PLAY_MUSIC to MusicManager via SoundEntries.dbc lookup
Add PlayMusicCallback to GameHandler so SMSG_PLAY_MUSIC (and the
vanilla 0x0103 alias) dispatch a soundId to the registered handler
instead of being silently consumed. Application.cpp registers the
callback, loads SoundEntries.dbc, resolves the first non-empty
Name+DirectoryBase into an MPQ path, and passes it to MusicManager
for non-looping playback. Resolves the TODO in the SMSG_PLAY_MUSIC
handler.
2026-03-09 15:46:19 -07:00
Kelsi
ab32ec8933 Resolve TODO: QuestMarkerRenderer init called explicitly on loadQuestMarkerModels 2026-03-09 15:39:16 -07:00
Kelsi
0a528935e2 Auto-detect WoW locale from data directory; override with WOWEE_LOCALE env 2026-03-09 15:36:26 -07:00
Kelsi
593fd4e45d Fix Dwarf female VoiceType returning GENERIC instead of DWARF_FEMALE 2026-03-09 15:34:04 -07:00
Kelsi
28c755040f Request completed quests on world entry and expose via public API
- Send CMSG_QUERY_QUESTS_COMPLETED on initial world entry so
  completedQuests_ is populated from the server response
- Clear completedQuests_ on world entry to avoid stale data across sessions
- Add isQuestCompleted(questId) and getCompletedQuests() public accessors
  to allow UI layers to filter NPC quest offers by completion state
2026-03-09 15:32:11 -07:00
Kelsi
bbfb170291 Cover all remaining notable SMSG opcodes and add completed quest tracking
- SMSG_ITEM_QUERY_MULTIPLE_RESPONSE: route to handleItemQueryResponse
- SMSG_QUERY_OBJECT_POSITION/ROTATION: consume
- SMSG_VOICESESSION_FULL: consume

All non-trivial, non-debug SMSG opcodes now have explicit case handlers.
2026-03-09 15:29:08 -07:00
Kelsi
99f2b30594 Handle 35+ more SMSG opcodes for quests, combat, pet system, and protocol
- SMSG_QUESTGIVER_QUEST_FAILED: show specific failure reason in chat
- SMSG_SUSPEND_COMMS: reply with CMSG_SUSPEND_COMMS_ACK (required by server)
- SMSG_PRE_RESURRECT: consume packed GUID
- SMSG_PLAYERBINDERROR: show bind error message
- SMSG_RAID_GROUP_ONLY: show instance requires raid group message
- SMSG_RAID_READY_CHECK_ERROR: show ready check error message
- SMSG_RESET_FAILED_NOTIFY: show instance reset blocked message
- SMSG_REALM_SPLIT / SMSG_REAL_GROUP_UPDATE: consume
- SMSG_PLAY_MUSIC: consume (hook point for future music integration)
- SMSG_PLAY_OBJECT_SOUND / SMSG_PLAY_SPELL_IMPACT: consume
- SMSG_RESISTLOG: consume
- SMSG_READ_ITEM_OK / SMSG_READ_ITEM_FAILED: show result messages
- SMSG_QUERY_QUESTS_COMPLETED_RESPONSE: parse and cache completed quest IDs
- SMSG_QUESTUPDATE_ADD_PVP_KILL: show PVP kill progress in chat
- SMSG_NPC_WONT_TALK: show "creature can't talk" message
- SMSG_OFFER_PETITION_ERROR: show specific petition error
- SMSG_PETITION_QUERY_RESPONSE / SHOW_SIGNATURES / SIGN_RESULTS: consume
- SMSG_PET_GUIDS / MODE / BROKEN / CAST_FAILED / SOUND / LEARN / UNLEARN / etc: consume
- SMSG_INSPECT: consume (character inspection)
- SMSG_MULTIPLE_MOVES / SMSG_MULTIPLE_PACKETS: consume
- SMSG_SET_PLAYER_DECLINED_NAMES_RESULT / PROPOSE_LEVEL_GRANT: consume
- SMSG_REFER_A_FRIEND_* / REPORT_PVP_AFK_RESULT / REDIRECT_CLIENT: consume
- SMSG_PVP_QUEUE_STATS / NOTIFY_DEST_LOC_SPELL_CAST / RESPOND_INSPECT_ACHIEVEMENTS: consume
- SMSG_PLAYER_SKINNED / QUEST_POI_QUERY_RESPONSE / PLAY_TIME_WARNING: consume
- SMSG_ON_CANCEL_EXPECTED_RIDE_VEHICLE_AURA / RESET_RANGED_COMBAT_TIMER: consume
- SMSG_PROFILEDATA_RESPONSE: consume

Adds completedQuests_ set for tracking server-reported completed quest IDs.
2026-03-09 15:27:20 -07:00
Kelsi
22513505fa Handle 50+ missing SMSG opcodes for logout, guild, talents, items, LFG, and GM tickets
- SMSG_LOGOUT_CANCEL_ACK: consume server acknowledgment
- SMSG_GUILD_DECLINE: show decliner name in chat
- SMSG_TALENTS_INVOLUNTARILY_RESET: show reset notification
- SMSG_UPDATE_ACCOUNT_DATA / COMPLETE: consume account data sync
- SMSG_SET_REST_START: show resting state change message
- SMSG_UPDATE_AURA_DURATION: update aura slot duration + timestamp
- SMSG_ITEM_NAME_QUERY_RESPONSE: cache item name in itemInfoCache_
- SMSG_MOUNTSPECIAL_ANIM: consume packed GUID
- SMSG_CHAR_CUSTOMIZE / SMSG_CHAR_FACTION_CHANGE: show result messages
- SMSG_INVALIDATE_PLAYER: evict player name cache entry
- SMSG_TRIGGER_MOVIE: consume
- SMSG_EQUIPMENT_SET_LIST: parse and store equipment sets
- SMSG_EQUIPMENT_SET_USE_RESULT: show failure message if non-zero
- SMSG_LFG_UPDATE / LFG / LFM / QUEUED / PENDING_*: consume
- SMSG_GMTICKET_CREATE / UPDATETEXT / DELETETICKET: show result messages
- SMSG_GMTICKET_GETTICKET / SYSTEMSTATUS: consume
- SMSG_ADD_RUNE_POWER / SMSG_RESYNC_RUNES: consume (DK rune tracking)
- SMSG_AURACASTLOG, SMSG_SPELL*LOG*, SMSG_SPELL_CHANCE_*: consume
- SMSG_CLEAR_EXTRA_AURA_INFO / COMPLAIN_RESULT / ITEM_REFUND_INFO_RESPONSE: consume
- SMSG_ITEM_ENCHANT_TIME_UPDATE / LOOT_LIST / RESUME_CAST_BAR: consume
- SMSG_THREAT_UPDATE / UPDATE_INSTANCE_* / SEND_ALL_COMBAT_LOG: consume
- SMSG_SET_PROJECTILE_POSITION / AUCTION_LIST_PENDING_SALES: consume
- SMSG_SERVER_FIRST_ACHIEVEMENT: parse name + achievement ID, show message
- SMSG_SET_FORCED_REACTIONS: parse and store forced faction reaction overrides
- SMSG_SPLINE_SET_FLIGHT/SWIM_BACK/WALK_SPEED / TURN_RATE / PITCH_RATE: consume
- SMSG_SPLINE_MOVE_UNROOT / UNSET_FLYING / UNSET_HOVER / WATER_WALK: consume
- SMSG_MOVE_GRAVITY_*/LAND_WALK/NORMAL_FALL/CAN_TRANSITION/COLLISION_HGT/FLIGHT: consume

Adds EquipmentSet struct + equipmentSets_ storage, forcedReactions_ map.
2026-03-09 15:23:02 -07:00
Kelsi
a1dbbf3915 Handle auction removed, container open, and GM ticket status
- SMSG_AUCTION_REMOVED_NOTIFICATION: notify player of expired auction
- SMSG_OPEN_CONTAINER: log container open event
- SMSG_GM_TICKET_STATUS_UPDATE: consume (no ticket UI yet)
- SMSG_PLAYER_VEHICLE_DATA: consume (no vehicle UI yet)
- SMSG_SET_EXTRA_AURA_INFO_NEED_UPDATE: consume
2026-03-09 15:12:34 -07:00
Kelsi
7f89bd950a Handle GM chat, char rename, difficulty change, death/corpse opcodes, force anim
- SMSG_GM_MESSAGECHAT: route to handleMessageChat (same wire format as SMSG_MESSAGECHAT)
- SMSG_CHAR_RENAME: notify player of name change success/failure
- SMSG_BINDZONEREPLY: confirm inn binding or "too far" message
- SMSG_CHANGEPLAYER_DIFFICULTY_RESULT: difficulty change success/failure messages
- SMSG_CORPSE_NOT_IN_INSTANCE: notify player corpse is outside instance
- SMSG_CROSSED_INEBRIATION_THRESHOLD: "You feel rather drunk" message
- SMSG_CLEAR_FAR_SIGHT_IMMEDIATE: log far sight cancellation
- SMSG_FORCE_ANIM: consume packed GUID + animId
- Consume 10 additional minor opcodes (gameobject animations, rune conversion, etc.)
2026-03-09 15:11:21 -07:00
Kelsi
830bb3f105 Handle defense messages, death/corpse, barber shop, channel count, gametime
- SMSG_DEFENSE_MESSAGE: display PvP zone attack alerts in system chat
- SMSG_CORPSE_RECLAIM_DELAY: notify player of corpse reclaim timer
- SMSG_DEATH_RELEASE_LOC: log spirit healer coordinates after death
- SMSG_ENABLE_BARBER_SHOP: log barber shop activation (no UI yet)
- SMSG_FEIGN_DEATH_RESISTED: show resisted feign death message
- SMSG_CHANNEL_MEMBER_COUNT: consume channel member count update
- SMSG_GAMETIME_SET/UPDATE/BIAS, SMSG_GAMESPEED_SET: consume server time sync
- SMSG_ACHIEVEMENT_DELETED/CRITERIA_DELETED: consume removal notifications
- Fix unused screenH variable warning in quest objective tracker
2026-03-09 15:09:50 -07:00
Kelsi
d84adb2120 Handle SMSG_SCRIPT_MESSAGE, enchanting, socketing, refund, resurrect fail
- SMSG_SCRIPT_MESSAGE: display server script text in system chat
- SMSG_ENCHANTMENTLOG: consume enchantment animation log
- SMSG_SOCKET_GEMS_RESULT: show gem socketing success/failure message
- SMSG_ITEM_REFUND_RESULT: show item refund success/failure message
- SMSG_ITEM_TIME_UPDATE: consume item duration countdown
- SMSG_RESURRECT_FAILED: show appropriate resurrection failure message
2026-03-09 15:06:56 -07:00
Kelsi
5c94b4e7ff Add quest objective tracker HUD on right side of screen
- Show up to 5 active quests with objective progress (kills, items, text)
- Gold title for complete quests, white for in-progress
- Item objectives show item name via getItemInfo() lookup
- Transparent semi-opaque background, docked below minimap on right
2026-03-09 15:05:38 -07:00
Kelsi
6a281e468f Handle chat errors, threat, and attack swing opcodes
- SMSG_CHAT_PLAYER_NOT_FOUND: show "No player named X is currently playing" in chat
- SMSG_CHAT_PLAYER_AMBIGUOUS: show ambiguous name message
- SMSG_CHAT_WRONG_FACTION/NOT_IN_PARTY/RESTRICTED: appropriate chat error messages
- SMSG_THREAT_CLEAR: log threat wipe (Vanish, Feign Death)
- SMSG_THREAT_REMOVE: consume packed GUIDs
- SMSG_HIGHEST_THREAT_UPDATE: consume (no threat UI yet)
2026-03-09 15:02:15 -07:00
Kelsi
299c725993 Handle group destroy, spline move flags, world state timer, and PVP credit
- SMSG_GROUP_DESTROYED: clear party members and notify player
- SMSG_GROUP_CANCEL: notify player that invite was cancelled
- SMSG_SPLINE_MOVE_*: consume packed GUID for 10 entity movement state flag opcodes
- SMSG_WORLD_STATE_UI_TIMER_UPDATE: consume server timestamp (arena/BG timer sync)
- SMSG_PVP_CREDIT: log honor gain and show chat notification
2026-03-09 14:59:32 -07:00
Kelsi
aa737def7f Handle SMSG_BUY_ITEM, SMSG_CRITERIA_UPDATE, SMSG_BARBER_SHOP_RESULT, SMSG_OVERRIDE_LIGHT
- SMSG_BUY_ITEM: log successful purchase and clear pending buy state
- SMSG_CRITERIA_UPDATE: log achievement criteria progress (no UI yet)
- SMSG_BARBER_SHOP_RESULT: show success/failure message in chat
- SMSG_OVERRIDE_LIGHT: store zone light override id + transition time, expose via getOverrideLightId()/getOverrideLightTransMs()
2026-03-09 14:57:46 -07:00
Kelsi
deed8011d7 Add Reputation tab to character screen with colored tier progress bars
- Add Reputation tab in character screen tab bar (Equipment/Stats/Reputation/Skills)
- Implement renderReputationPanel() showing all tracked factions sorted alphabetically
- Progress bars colored per WoW reputation tier: Hated/Hostile/Unfriendly/Neutral/Friendly/Honored/Revered/Exalted
- Add public getFactionNamePublic() backed by DBC name cache with lazy load
2026-03-09 14:52:13 -07:00
Kelsi
26eefe9529 Add ready check popup UI and fix party leader lookup
- Implement renderReadyCheckPopup() showing initiator name with Ready/Not Ready buttons
- Fix MSG_RAID_READY_CHECK fallback: use partyData.leaderGuid instead of non-existent isLeader field
- Add faction standing handler with loadFactionNameCache() (Faction.dbc field 22)
- Add gossip POI handler writing canonical WoW coords to gossipPois_ for minimap rendering
2026-03-09 14:48:30 -07:00
Kelsi
f89840a6aa Handle gossip POI, combat clearing, dismount, spell log miss, and loot notifications
- SMSG_GOSSIP_POI: parse map POI markers (x/y/icon/name) from quest NPCs, render as
  cyan diamonds on the minimap with hover tooltips for quest navigation
- SMSG_ATTACKSWING_DEADTARGET: clear auto-attack when target dies mid-swing
- SMSG_CANCEL_COMBAT: server-side combat reset (clears autoAttacking + target)
- SMSG_BREAK_TARGET / SMSG_CLEAR_TARGET: server-side targeting clears
- SMSG_DISMOUNT: server-forced dismount triggers mountCallback(0)
- SMSG_MOUNTRESULT / SMSG_DISMOUNTRESULT: mount feedback in system chat
- SMSG_LOOT_ALL_PASSED: "Everyone passed on [Item]" system message, clears loot roll
- SMSG_LOOT_ITEM_NOTIFY / SMSG_LOOT_SLOT_CHANGED: consumed
- SMSG_SPELLLOGMISS: decode miss/dodge/parry/block from spell casts into combat text
- SMSG_ENVIRONMENTALDAMAGELOG: environmental damage (drowning/lava/fall) in combat text
- GossipPoi struct + gossipPois_ vector in GameHandler with public getters/clearers
2026-03-09 14:38:45 -07:00
Kelsi
bd3bd1b5a6 Handle missing WotLK packets: health/power updates, mirror timers, combo points, loot roll, titles, phase shift
- SMSG_HEALTH_UPDATE / SMSG_POWER_UPDATE: update entity HP/power via entityManager
- SMSG_UPDATE_WORLD_STATE: single world state variable update (companion to INIT)
- SMSG_UPDATE_COMBO_POINTS: store comboPoints_/comboTarget_ in GameHandler
- SMSG_START_MIRROR_TIMER / SMSG_STOP_MIRROR_TIMER / SMSG_PAUSE_MIRROR_TIMER: breath/fatigue/feign timer state
- MirrorTimer struct + getMirrorTimer() public getter; renderMirrorTimers() draws colored breath/fatigue bars above cast bar
- SMSG_CAST_RESULT: WotLK extended cast result; clear cast bar and show reason on failure (result != 0)
- SMSG_SPELL_FAILED_OTHER / SMSG_PROCRESIST: consume silently
- SMSG_LOOT_START_ROLL: correct trigger for Need/Greed popup (replaces rollType=128 heuristic)
- SMSG_STABLE_RESULT: show pet stable feedback in system chat (store/retrieve/buy slot/error)
- SMSG_TITLE_EARNED: system chat notification for title earned/removed
- SMSG_PLAYERBOUND / SMSG_BINDER_CONFIRM: hearthstone binding notification
- SMSG_SET_PHASE_SHIFT: consume (WotLK phasing, no client action needed)
- SMSG_TOGGLE_XP_GAIN: system chat notification
2026-03-09 14:30:48 -07:00
Kelsi
6df36f4588 Handle exploration XP, pet tame failure, quest fail timers, pet action/name responses
- SMSG_EXPLORATION_EXPERIENCE: parse areaId+xpGained, show discovery message
- SMSG_PET_TAME_FAILURE: parse reason byte, show descriptive failure message
- SMSG_PET_ACTION_FEEDBACK: consumed silently (no pet action bar UI yet)
- SMSG_PET_NAME_QUERY_RESPONSE: consumed silently (names shown via unit objects)
- SMSG_QUESTUPDATE_FAILED: "Quest X failed!" notification
- SMSG_QUESTUPDATE_FAILEDTIMER: "Quest X timed out!" notification
2026-03-09 14:21:17 -07:00
Kelsi
8e4a0053c4 Handle SMSG_DISPEL_FAILED, SMSG_TOTEM_CREATED, SMSG_AREA_SPIRIT_HEALER_TIME, SMSG_DURABILITY_DAMAGE_DEATH
- SMSG_DISPEL_FAILED: parse caster/victim/spellId, show "Dispel failed!" chat
- SMSG_TOTEM_CREATED: parse slot/guid/duration/spellId, log (no totem bar yet)
- SMSG_AREA_SPIRIT_HEALER_TIME: show "resurrect in N seconds" message
- SMSG_DURABILITY_DAMAGE_DEATH: show durability loss notification on death
2026-03-09 14:18:36 -07:00
Kelsi
001c80a3db Add item text reading (books/notes): SMSG_ITEM_TEXT_QUERY_RESPONSE + renderItemTextWindow
- Parse SMSG_ITEM_TEXT_QUERY_RESPONSE (guid + isEmpty + string text),
  store in itemText_ and open book window when non-empty
- queryItemText(guid) sends CMSG_ITEM_TEXT_QUERY for readable items
- renderItemTextWindow(): scrollable book window with parchment-toned
  text, "Close" button; opens via isItemTextOpen() flag
2026-03-09 14:15:59 -07:00
Kelsi
acde6070cf Handle SMSG_QUEST_CONFIRM_ACCEPT (shared quest) with accept/decline popup
- Parse SMSG_QUEST_CONFIRM_ACCEPT (questId + title + sharerGuid),
  show chat notification with quest title and sharer name
- acceptSharedQuest() sends CMSG_QUEST_CONFIRM_ACCEPT with questId
- renderSharedQuestPopup(): shows sharer name, gold quest title,
  Accept/Decline buttons (stacked below other social popups)
2026-03-09 14:14:15 -07:00
Kelsi
e793b44151 Handle SMSG_ITEM_COOLDOWN, fishing fail/escape, minimap ping, zone attack
- SMSG_ITEM_COOLDOWN: parse guid+spellId+cdMs, update spellCooldowns
  map and action bar slots (same path as SMSG_SPELL_COOLDOWN)
- SMSG_FISH_NOT_HOOKED: "Your fish got away." chat notification
- SMSG_FISH_ESCAPED: "Your fish escaped!" chat notification
- MSG_MINIMAP_PING: consumed silently (no visual yet)
- SMSG_ZONE_UNDER_ATTACK: parse areaId, show chat notification
2026-03-09 14:12:20 -07:00
Kelsi
b381f1e13f Tick summon request timeout down in UI render loop; auto-expire on timeout 2026-03-09 14:08:49 -07:00
Kelsi
770ac645d5 Handle SMSG_SUMMON_REQUEST with accept/decline popup
- Parse SMSG_SUMMON_REQUEST (summonerGuid + zoneId + timeoutMs),
  store summoner name from entity list, show chat notification
- acceptSummon() sends CMSG_SUMMON_RESPONSE(1), declineSummon() sends
  CMSG_SUMMON_RESPONSE(0), SMSG_SUMMON_CANCEL clears pending state
- renderSummonRequestPopup(): shows summoner name + countdown timer
  with Accept/Decline buttons
2026-03-09 14:07:50 -07:00
Kelsi
f369fe9c6e Implement basic trade request/accept/decline flow
- Parse SMSG_TRADE_STATUS for all 20+ status codes: incoming request,
  open/cancel/complete/accept notifications, error conditions (too far,
  wrong faction, stunned, dead, trial account, etc.)
- SMSG_TRADE_STATUS_EXTENDED consumed via shared handler (no full item
  window yet; state tracking sufficient for accept/decline flow)
- Add acceptTradeRequest() (CMSG_BEGIN_TRADE), declineTradeRequest(),
  acceptTrade() (CMSG_ACCEPT_TRADE), cancelTrade() (CMSG_CANCEL_TRADE)
- Add BeginTradePacket, CancelTradePacket, AcceptTradePacket builders
- Add renderTradeRequestPopup(): shows "X wants to trade" with
  Accept/Decline buttons when tradeStatus_ == PendingIncoming
- TradeStatus enum tracks None/PendingIncoming/Open/Accepted/Complete
2026-03-09 14:05:42 -07:00
Kelsi
b4f6ca2ca7 Handle SMSG_SERVER_MESSAGE, SMSG_CHAT_SERVER_MESSAGE, SMSG_AREA_TRIGGER_MESSAGE, SMSG_TRIGGER_CINEMATIC
- SMSG_SERVER_MESSAGE: parse type+string, show as [Server] chat message
- SMSG_CHAT_SERVER_MESSAGE: parse type+string, show as [Announcement]
- SMSG_AREA_TRIGGER_MESSAGE: parse len+string, display as system chat
- SMSG_TRIGGER_CINEMATIC: consume silently (no cinematic playback)
2026-03-09 14:03:07 -07:00
Kelsi
3114e80fa8 Implement group loot roll: SMSG_LOOT_ROLL, SMSG_LOOT_ROLL_WON, CMSG_LOOT_ROLL
- Parse SMSG_LOOT_ROLL: if rollType==128 and it's our player, store
  pending roll (itemId, slot, name from itemInfoCache_) and show popup;
  otherwise show chat notification of another player's roll result
- Parse SMSG_LOOT_ROLL_WON: show winner announcement in chat with item
  name and roll type/value
- sendLootRoll() sends CMSG_LOOT_ROLL (objectGuid+slot+rollType) and
  clears pending roll state
- SMSG_LOOT_MASTER_LIST consumed silently (no UI yet)
- renderLootRollPopup(): ImGui window with Need/Greed/Disenchant/Pass
  buttons; item name colored by quality (poor/common/uncommon/rare/epic/
  legendary color scale)
2026-03-09 14:01:27 -07:00
Kelsi
2d124e7e54 Implement duel request/accept/decline UI and packet handling
- Parse SMSG_DUEL_REQUESTED: store challenger guid/name, set
  pendingDuelRequest_ flag, show chat notification
- Parse SMSG_DUEL_COMPLETE: clear pending flag, notify on cancel
- Parse SMSG_DUEL_WINNER: show "X defeated Y in a duel!" chat message
- Handle SMSG_DUEL_OUTOFBOUNDS with warning message
- Add acceptDuel() method sending CMSG_DUEL_ACCEPTED (new builder)
- Wire forfeitDuel() to clear pendingDuelRequest_ on decline
- Add renderDuelRequestPopup() ImGui window (Accept/Decline buttons)
  positioned near group invite popup; shown when challenge is pending
- Add DuelAcceptPacket builder to world_packets.hpp/cpp
2026-03-09 13:58:02 -07:00
Kelsi
e4f53ce0c3 Handle SMSG_ACHIEVEMENT_EARNED with toast banner and chat notification
- Parse SMSG_ACHIEVEMENT_EARNED (guid + achievementId + PackedTime date)
  and fire AchievementEarnedCallback for self, chat notify for others
- Add renderAchievementToast() to GameScreen: slides in from right,
  gold-bordered panel with "Achievement Earned!" title + ID, 5s duration
  with 0.4s slide-in/out animation and fade at end
- Add triggerAchievementToast(uint32_t) public method on GameScreen
- Wire AchievementEarnedCallback in application.cpp
- Add playAchievementAlert() to UiSoundManager, loads
  Sound\Interface\AchievementSound.wav with level-up fallback
- SMSG_ALL_ACHIEVEMENT_DATA silently consumed (no tracker UI yet)
2026-03-09 13:53:42 -07:00
Kelsi
200a00d4f5 Implement Dungeon Finder UI window with role/dungeon selection
- Add renderDungeonFinderWindow() with status display (not queued /
  role check / queued+wait time / proposal / in dungeon / finished)
- Role checkboxes (Tank/Healer/DPS) and dungeon combo (25 entries
  covering Vanilla, TBC, and WotLK including Random/Heroic)
- Accept/Decline buttons during Proposal state, Teleport button
  while InDungeon, Leave Queue button while Queued/RoleCheck
- Store lfgProposalId_ on GameHandler so UI can pass it to
  lfgAcceptProposal(); expose getLfgProposalId() and
  getLfgTimeInQueueMs() getters
- Toggle window with I key (when chat input is not active)
2026-03-09 13:47:07 -07:00
Kelsi
63c6039dbb Handle SMSG_CLEAR_COOLDOWN and SMSG_MODIFY_COOLDOWN
SMSG_CLEAR_COOLDOWN: remove spell cooldown from cache and clear action bar
slot's remaining cooldown immediately (e.g. after item use, trinket proc).

SMSG_MODIFY_COOLDOWN: adjust an existing cooldown duration by a signed delta
in milliseconds (used by glyphs, Borrowed Time, etc.).
2026-03-09 13:40:19 -07:00
Kelsi
9d37f4c946 Add instance state packet handlers: reset, save, lock, warning query
- SMSG_INSTANCE_SAVE_CREATED: notify player they are saved to instance
- SMSG_RAID_INSTANCE_MESSAGE: surface warning/save/welcome notifications
- SMSG_INSTANCE_RESET: clear matching lockout from cache + notify
- SMSG_INSTANCE_RESET_FAILED: report reset failure reason to player
- SMSG_INSTANCE_LOCK_WARNING_QUERY: auto-confirm with CMSG_INSTANCE_LOCK_RESPONSE
  (entering a saved instance; sends acceptance so the player can proceed)
2026-03-09 13:38:19 -07:00
Kelsi
8f7c4a58cd Implement SMSG_RAID_INSTANCE_INFO handler to track instance lockouts
Parse and store dungeon/raid lockout data sent on login:
- mapId, difficulty, resetTime (Unix timestamp), locked, extended flags
- Stored in instanceLockouts_ vector for UI / LFG / dungeon state queries
- Public InstanceLockout struct + getInstanceLockouts() accessor
2026-03-09 13:36:23 -07:00
Kelsi
b33831d833 Implement WotLK 3.3.5a LFG/Dungeon Finder packet handlers
Add full client-side handling for the Looking For Dungeon system:
- SMSG_LFG_JOIN_RESULT: parse join success/failure, surface error message
- SMSG_LFG_QUEUE_STATUS: track dungeon ID, avg wait time, time in queue
- SMSG_LFG_PROPOSAL_UPDATE: detect proposal state (active/passed/failed)
- SMSG_LFG_ROLE_CHECK_UPDATE: surface role check progress/failure
- SMSG_LFG_UPDATE_PLAYER/PARTY: track queue state transitions
- SMSG_LFG_PLAYER_REWARD: show dungeon completion reward in chat
- SMSG_LFG_BOOT_PROPOSAL_UPDATE: show vote-kick status in chat
- SMSG_LFG_TELEPORT_DENIED: surface reason for teleport failure
- SMSG_LFG_DISABLED/OFFER_CONTINUE and informational packets consumed

Outgoing: lfgJoin(), lfgLeave(), lfgAcceptProposal(), lfgTeleport()
State: LfgState enum + lfgState_/lfgDungeonId_/lfgAvgWaitSec_ members
2026-03-09 13:30:23 -07:00
Kelsi
ae5c05e14e Fix CI: remove invalid 'dxc' brew formula and drop hard FSR3 runtime dependency
macOS: 'dxc' is not a valid Homebrew formula — the failing brew install line
was aborting early, preventing SDL2 and other packages from being installed.
Removed 'dxc' from the brew install command.

Linux arm64 / Windows: the add_dependencies(wowee wowee_fsr3_official_runtime_copy)
forced the FSR3 Kits build (including VK permutation generation) into every
normal cmake --build invocation. This broke arm64 (no DXC binary available) and
Windows MSYS2 (bash script ran in wrong shell context, exit 127). The FSR3 Path A
runtime is now a strictly opt-in artifact — build it explicitly with:
  cmake --build build --target wowee_fsr3_official_runtime_copy
The main wowee binary still loads it dynamically at runtime when present and
falls back gracefully when it is not.
2026-03-09 13:23:39 -07:00
Kelsi
e2b89c9b42 Fix FSR3 permutation script failures on arm64 Linux and Windows
Linux arm64 (Exec format error):
- The script was downloading the x86_64 DXC release on all Linux hosts;
  on aarch64 runners the x86_64 ELF fails with EXEC_FORMAT_ERROR at
  shader compilation time. Add uname -m guard: when running on aarch64/
  arm64 Linux without a DXC in PATH, exit 0 with an advisory message
  rather than downloading an incompatible binary. The FSR3 SDK build
  proceeds as it did before the permutation script was introduced
  (permutation headers are expected to be pre-built in the SDK checkout).

Windows (bash: command not found, exit 127):
- cmake custom-target COMMANDs run via cmd.exe on Windows even when
  cmake is configured from a MSYS2 shell, so bare 'bash' is not resolved.
- Use find_program(BASH_EXECUTABLE bash) at configure time (which runs
  under shell: msys2 in CI and thus finds the MSYS2 bash at its native
  Windows-absolute path). When bash is found, embed the full path in the
  COMMAND; when not found (unusual non-MSYS2 Windows setups), skip the
  permutation step and emit a STATUS message.
2026-03-09 13:11:03 -07:00
Kelsi
6a7287bde3 Implement transport spline movement and fix SMSG_QUESTLOG_FULL
SMSG_MONSTER_MOVE_TRANSPORT (handleMonsterMoveTransport):
- Parse full WotLK 3.3.5a spline payload after the transport-local start
  position: splineId, moveType, facing data (spot/target/angle), splineFlags,
  Animation flag block, duration, Parabolic flag block, pointCount, waypoints
- Extract destination in transport-local server coords, compose to world
  space via TransportManager, then call entity->startMoveTo() with the
  spline duration so NPC movement interpolates smoothly instead of teleporting
- Handle all facing modes (FacingSpot/Target/Angle/normal) in transport space
- Degenerate cases (no spline data, moveType==1 stop, no transport manager)
  fall back to snapping start position as before

SMSG_QUESTLOG_FULL:
- This opcode is a zero-payload notification meaning the quest log is at
  capacity (25 quests); it does not carry quest log data
- Replace placeholder LOG_INFO stubs with a proper "Your quest log is full."
  chat notification and a single LOG_INFO
2026-03-09 13:04:35 -07:00
Kelsi
b0d7dbc32c Implement SMSG_STANDSTATE_UPDATE and SMSG_ITEM_PUSH_RESULT handlers
SMSG_STANDSTATE_UPDATE:
- Parse uint8 stand state from server confirmation packet
- Store in standState_ member (0=stand, 7=dead, 8=kneel, etc.)
- Expose getStandState(), isSitting(), isDead(), isKneeling() accessors

SMSG_ITEM_PUSH_RESULT:
- Parse full WotLK 3.3.5a payload: guid, received, created, showInChat,
  bagSlot, itemSlot, itemId, suffixFactor, randomPropertyId, count, totalCount
- Show "Received: <name> x<count>" chat notification when showInChat=1
- Queue item info lookup via queryItemInfo so name resolves asap
2026-03-09 12:58:52 -07:00
Kelsi
bae32c1823 Add FSR3 Generic API path and harden runtime diagnostics
- AmdFsr3Runtime now probes both the legacy ffxFsr3* API and the newer
  generic ffxCreateContext/ffxDispatch API; selects whichever the loaded
  runtime library exports (GenericApi takes priority fallback)
- Generic API path implements full upscale + frame-generation context
  creation, configure, dispatch, and destroy lifecycle
- dlopen error captured and surfaced in lastError_ on Linux so runtime
  initialization failures are actionable
- FSR3 runtime init failure log now includes path kind, error string,
  and loaded library path for easier debugging
- tools/generate_ffx_sdk_vk_permutations.sh added: auto-bootstraps
  missing VK permutation headers; DXC auto-downloaded on Linux/Windows
  MSYS2; macOS reads from PATH (CI installs via brew dxc)
- CMakeLists: add upscalers/include to probe include dirs, invoke
  permutation script before SDK build, scope FFX pragma/ODR warning
  suppressions to affected TUs, add runtime-copy dependency on wowee
- UI labels updated from "FSR2" → "FSR3" in settings, tuning panel,
  performance HUD, and combo boxes
- CI macOS job now installs dxc via Homebrew for permutation codegen
2026-03-09 12:51:59 -07:00
40 changed files with 6066 additions and 278 deletions

View file

@ -0,0 +1 @@
{"sessionId":"55a28c7e-8043-44c2-9829-702f303c84ba","pid":3880168,"acquiredAt":1773085726967}

View file

@ -118,6 +118,7 @@ if(WOWEE_ENABLE_AMD_FSR3_FRAMEGEN AND WOWEE_AMD_FFX_SDK_KITS_READY)
CXX_STANDARD_REQUIRED ON CXX_STANDARD_REQUIRED ON
) )
target_include_directories(wowee_fsr3_framegen_amd_vk_probe PUBLIC target_include_directories(wowee_fsr3_framegen_amd_vk_probe PUBLIC
${WOWEE_AMD_FFX_SDK_KITS_DIR}/upscalers/include
${WOWEE_AMD_FFX_SDK_KITS_DIR}/upscalers/fsr3/include ${WOWEE_AMD_FFX_SDK_KITS_DIR}/upscalers/fsr3/include
${WOWEE_AMD_FFX_SDK_KITS_DIR}/framegeneration/fsr3/include ${WOWEE_AMD_FFX_SDK_KITS_DIR}/framegeneration/fsr3/include
${WOWEE_AMD_FFX_SDK_KITS_DIR}/framegeneration/include ${WOWEE_AMD_FFX_SDK_KITS_DIR}/framegeneration/include
@ -155,21 +156,53 @@ if(WOWEE_ENABLE_AMD_FSR3_FRAMEGEN AND WOWEE_AMD_FFX_SDK_KITS_READY)
set(WOWEE_AMD_FSR3_RUNTIME_BUILD_TYPE Release) set(WOWEE_AMD_FSR3_RUNTIME_BUILD_TYPE Release)
endif() endif()
add_custom_target(wowee_fsr3_official_runtime_build # Locate bash at configure time so the build-time COMMAND works on Windows
COMMAND ${CMAKE_COMMAND} # (cmake custom commands run via cmd.exe on Windows, so bare 'bash' is not found).
-S ${WOWEE_AMD_FFX_SDK_KITS_DIR} find_program(BASH_EXECUTABLE bash
-B ${WOWEE_AMD_FSR3_RUNTIME_BUILD_DIR} HINTS
-DCMAKE_BUILD_TYPE=${WOWEE_AMD_FSR3_RUNTIME_BUILD_TYPE} /usr/bin
-DFFX_BUILD_VK=ON /bin
-DFFX_BUILD_FRAMEGENERATION=ON "${MSYS2_PATH}/usr/bin"
-DFFX_BUILD_UPSCALER=ON "$ENV{MSYS2_PATH}/usr/bin"
COMMAND ${CMAKE_COMMAND} "C:/msys64/usr/bin"
--build ${WOWEE_AMD_FSR3_RUNTIME_BUILD_DIR} "D:/msys64/usr/bin"
--config $<CONFIG>
--parallel
COMMENT "Building native AMD FSR3 runtime (Path A) from FidelityFX-SDK Kits"
VERBATIM
) )
if(BASH_EXECUTABLE)
add_custom_target(wowee_fsr3_official_runtime_build
COMMAND ${CMAKE_COMMAND}
-S ${WOWEE_AMD_FFX_SDK_KITS_DIR}
-B ${WOWEE_AMD_FSR3_RUNTIME_BUILD_DIR}
-DCMAKE_BUILD_TYPE=${WOWEE_AMD_FSR3_RUNTIME_BUILD_TYPE}
-DFFX_BUILD_VK=ON
-DFFX_BUILD_FRAMEGENERATION=ON
-DFFX_BUILD_UPSCALER=ON
COMMAND ${BASH_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/generate_ffx_sdk_vk_permutations.sh
${CMAKE_SOURCE_DIR}/extern/FidelityFX-SDK
COMMAND ${CMAKE_COMMAND}
--build ${WOWEE_AMD_FSR3_RUNTIME_BUILD_DIR}
--config $<CONFIG>
--parallel
COMMENT "Building native AMD FSR3 runtime (Path A) from FidelityFX-SDK Kits"
VERBATIM
)
else()
message(STATUS "bash not found; VK permutation headers will not be auto-generated")
add_custom_target(wowee_fsr3_official_runtime_build
COMMAND ${CMAKE_COMMAND}
-S ${WOWEE_AMD_FFX_SDK_KITS_DIR}
-B ${WOWEE_AMD_FSR3_RUNTIME_BUILD_DIR}
-DCMAKE_BUILD_TYPE=${WOWEE_AMD_FSR3_RUNTIME_BUILD_TYPE}
-DFFX_BUILD_VK=ON
-DFFX_BUILD_FRAMEGENERATION=ON
-DFFX_BUILD_UPSCALER=ON
COMMAND ${CMAKE_COMMAND}
--build ${WOWEE_AMD_FSR3_RUNTIME_BUILD_DIR}
--config $<CONFIG>
--parallel
COMMENT "Building native AMD FSR3 runtime (Path A) from FidelityFX-SDK Kits (no permutation bootstrap)"
VERBATIM
)
endif()
add_custom_target(wowee_fsr3_official_runtime_copy add_custom_target(wowee_fsr3_official_runtime_copy
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
@ -473,7 +506,6 @@ set(WOWEE_SOURCES
src/rendering/renderer.cpp src/rendering/renderer.cpp
src/rendering/amd_fsr3_runtime.cpp src/rendering/amd_fsr3_runtime.cpp
src/rendering/shader.cpp src/rendering/shader.cpp
src/rendering/texture.cpp
src/rendering/mesh.cpp src/rendering/mesh.cpp
src/rendering/camera.cpp src/rendering/camera.cpp
src/rendering/camera_controller.cpp src/rendering/camera_controller.cpp
@ -587,7 +619,6 @@ set(WOWEE_HEADERS
include/rendering/vk_render_target.hpp include/rendering/vk_render_target.hpp
include/rendering/renderer.hpp include/rendering/renderer.hpp
include/rendering/shader.hpp include/rendering/shader.hpp
include/rendering/texture.hpp
include/rendering/mesh.hpp include/rendering/mesh.hpp
include/rendering/camera.hpp include/rendering/camera.hpp
include/rendering/camera_controller.hpp include/rendering/camera_controller.hpp
@ -641,6 +672,14 @@ if(TARGET opcodes-generate)
add_dependencies(wowee opcodes-generate) add_dependencies(wowee opcodes-generate)
endif() endif()
# FidelityFX-SDK headers can trigger compiler-specific pragma/unused-static noise
# when included through the runtime bridge; keep suppression scoped to that TU.
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
set_source_files_properties(src/rendering/amd_fsr3_runtime.cpp PROPERTIES
COMPILE_OPTIONS "-Wno-unknown-pragmas;-Wno-unused-variable"
)
endif()
# Compile GLSL shaders to SPIR-V # Compile GLSL shaders to SPIR-V
if(GLSLC) if(GLSLC)
compile_shaders(wowee) compile_shaders(wowee)
@ -715,6 +754,12 @@ endif()
if(TARGET wowee_fsr3_framegen_amd_vk_probe) if(TARGET wowee_fsr3_framegen_amd_vk_probe)
target_link_libraries(wowee PRIVATE wowee_fsr3_framegen_amd_vk_probe) target_link_libraries(wowee PRIVATE wowee_fsr3_framegen_amd_vk_probe)
endif() endif()
if(TARGET wowee_fsr3_official_runtime_copy)
# FSR3 Path A runtime is an opt-in artifact; build explicitly with:
# cmake --build build --target wowee_fsr3_official_runtime_copy
# Do NOT add as a hard dependency of wowee — it would break arm64 and Windows CI
# (no DXC available on arm64; bash context issues on MSYS2 Windows).
endif()
# Link Unicorn if available # Link Unicorn if available
if(HAVE_UNICORN) if(HAVE_UNICORN)
@ -735,6 +780,13 @@ if(MSVC)
target_compile_options(wowee PRIVATE /W4) target_compile_options(wowee PRIVATE /W4)
else() else()
target_compile_options(wowee PRIVATE -Wall -Wextra -Wpedantic -Wno-missing-field-initializers) target_compile_options(wowee PRIVATE -Wall -Wextra -Wpedantic -Wno-missing-field-initializers)
# GCC LTO emits -Wodr for FFX enum-name mismatches across SDK generations.
# We intentionally keep FSR2+FSR3 integrations in separate TUs and suppress
# this linker-time diagnostic to avoid CI noise.
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_compile_options(wowee PRIVATE -Wno-odr)
target_link_options(wowee PRIVATE -Wno-odr)
endif()
endif() endif()
# Debug build flags # Debug build flags

View file

@ -278,6 +278,7 @@ make -j$(nproc)
- All build jobs are AMD-FSR2-only (`WOWEE_ENABLE_AMD_FSR2=ON`) and explicitly build `wowee_fsr2_amd_vk` - All build jobs are AMD-FSR2-only (`WOWEE_ENABLE_AMD_FSR2=ON`) and explicitly build `wowee_fsr2_amd_vk`
- Each job clones AMD's FSR2 SDK and FidelityFX-SDK (`Kelsidavis/FidelityFX-SDK`, `main` by default) - Each job clones AMD's FSR2 SDK and FidelityFX-SDK (`Kelsidavis/FidelityFX-SDK`, `main` by default)
- Linux CI validates FidelityFX-SDK Kits framegen headers - Linux CI validates FidelityFX-SDK Kits framegen headers
- FSR3 Path A runtime build auto-bootstraps missing VK permutation headers via `tools/generate_ffx_sdk_vk_permutations.sh`
- CI builds `wowee_fsr3_framegen_amd_vk_probe` when that target is generated for the detected SDK layout - CI builds `wowee_fsr3_framegen_amd_vk_probe` when that target is generated for the detected SDK layout
- If FSR2 generated Vulkan permutation headers are absent upstream, WoWee bootstraps them from `third_party/fsr2_vk_permutations` - If FSR2 generated Vulkan permutation headers are absent upstream, WoWee bootstraps them from `third_party/fsr2_vk_permutations`
- Container build via `container/build-in-container.sh` (Podman) - Container build via `container/build-in-container.sh` (Podman)

View file

@ -42,6 +42,7 @@ Runtime note:
- Renderer/UI expose a persisted experimental framegen toggle. - Renderer/UI expose a persisted experimental framegen toggle.
- Runtime loader is Path A only (official AMD runtime library). - Runtime loader is Path A only (official AMD runtime library).
- Path A runtime build now auto-runs `tools/generate_ffx_sdk_vk_permutations.sh` to ensure required VK permutation headers exist for FSR2/FSR3 upscaler shader blobs.
- You can point to an explicit runtime binary with: - You can point to an explicit runtime binary with:
- `WOWEE_FFX_SDK_RUNTIME_LIB=/absolute/path/to/libffx_fsr3_vk.so` (or `.dll` / `.dylib`). - `WOWEE_FFX_SDK_RUNTIME_LIB=/absolute/path/to/libffx_fsr3_vk.so` (or `.dll` / `.dylib`).
- If no official runtime is found, frame generation is disabled cleanly (Path C). - If no official runtime is found, frame generation is disabled cleanly (Path C).
@ -76,6 +77,10 @@ Runtime note:
- `framegeneration/fsr3/include/ffx_frameinterpolation.h` - `framegeneration/fsr3/include/ffx_frameinterpolation.h`
- `framegeneration/fsr3/include/ffx_opticalflow.h` - `framegeneration/fsr3/include/ffx_opticalflow.h`
- `backend/vk/ffx_vk.h` - `backend/vk/ffx_vk.h`
- Runtime build path auto-bootstrap:
- Linux downloads DXC automatically when missing.
- Windows (MSYS2) downloads DXC automatically when missing.
- macOS expects `dxc` to be available in `PATH` (CI installs it via Homebrew).
- CI builds `wowee_fsr3_framegen_amd_vk_probe` when that target is generated by CMake for the detected SDK layout. - CI builds `wowee_fsr3_framegen_amd_vk_probe` when that target is generated by CMake for the detected SDK layout.
- Some upstream SDK checkouts do not include generated Vulkan permutation headers. - Some upstream SDK checkouts do not include generated Vulkan permutation headers.
- WoWee bootstraps those headers from the vendored snapshot so AMD backend builds remain cross-platform and deterministic. - WoWee bootstraps those headers from the vendored snapshot so AMD backend builds remain cross-platform and deterministic.

View file

@ -45,6 +45,9 @@ public:
void setZoneType(ZoneType type); void setZoneType(ZoneType type);
ZoneType getCurrentZone() const { return currentZone_; } ZoneType getCurrentZone() const { return currentZone_; }
// Convenience: derive ZoneType and CityType from a WoW zone ID
void setZoneId(uint32_t zoneId);
// City ambience control // City ambience control
enum class CityType { enum class CityType {
NONE, NONE,
@ -111,6 +114,8 @@ private:
std::vector<AmbientSample> windSounds_; std::vector<AmbientSample> windSounds_;
std::vector<AmbientSample> tavernSounds_; std::vector<AmbientSample> tavernSounds_;
std::vector<AmbientSample> blacksmithSounds_; std::vector<AmbientSample> blacksmithSounds_;
std::vector<AmbientSample> birdSounds_;
std::vector<AmbientSample> cricketSounds_;
// Weather sound libraries // Weather sound libraries
std::vector<AmbientSample> rainLightSounds_; std::vector<AmbientSample> rainLightSounds_;

View file

@ -52,6 +52,11 @@ private:
float fadeInTimer = 0.0f; float fadeInTimer = 0.0f;
float fadeInDuration = 0.0f; float fadeInDuration = 0.0f;
float fadeInTargetVolume = 0.0f; float fadeInTargetVolume = 0.0f;
// Fade-out state (for stopMusic with fadeMs > 0)
bool fadingOut = false;
float fadeOutTimer = 0.0f;
float fadeOutDuration = 0.0f;
float fadeOutStartVolume = 0.0f;
std::unordered_map<std::string, std::vector<uint8_t>> musicDataCache_; std::unordered_map<std::string, std::vector<uint8_t>> musicDataCache_;
}; };

View file

@ -74,7 +74,6 @@ private:
void loadVoiceSounds(); void loadVoiceSounds();
bool loadSound(const std::string& path, VoiceSample& sample); bool loadSound(const std::string& path, VoiceSample& sample);
VoiceType detectVoiceType(uint32_t creatureEntry) const;
void playSound(uint64_t npcGuid, VoiceType voiceType, SoundCategory category, const glm::vec3& position); void playSound(uint64_t npcGuid, VoiceType voiceType, SoundCategory category, const glm::vec3& position);
pipeline::AssetManager* assetManager_ = nullptr; pipeline::AssetManager* assetManager_ = nullptr;

View file

@ -67,6 +67,9 @@ public:
// Level up // Level up
void playLevelUp(); void playLevelUp();
// Achievement
void playAchievementAlert();
// Error/feedback // Error/feedback
void playError(); void playError();
void playTargetSelect(); void playTargetSelect();
@ -114,6 +117,7 @@ private:
std::vector<UISample> drinkingSounds_; std::vector<UISample> drinkingSounds_;
std::vector<UISample> levelUpSounds_; std::vector<UISample> levelUpSounds_;
std::vector<UISample> achievementSounds_;
std::vector<UISample> errorSounds_; std::vector<UISample> errorSounds_;
std::vector<UISample> selectTargetSounds_; std::vector<UISample> selectTargetSounds_;

View file

@ -53,17 +53,20 @@ inline float normalizeAngleRad(float a) {
// Convert server/wire yaw (radians) → canonical yaw (radians). // Convert server/wire yaw (radians) → canonical yaw (radians).
// //
// Under server<->canonical X/Y swap: // Codebase canonical convention: atan2(-dy, dx) in (canonical_X=north, canonical_Y=west).
// dir_s = (cos(s), sin(s)) // North=0, East=+π/2, South=±π, West=-π/2.
// dir_c = swap(dir_s) = (sin(s), cos(s)) => c = PI/2 - s //
// Server direction at angle s: (cos s, sin s) in (server_X=canonical_Y, server_Y=canonical_X).
// After swap: dir_c = (sin s, cos s) in (canonical_X, canonical_Y).
// atan2(-dy, dx) = atan2(-cos s, sin s) = s - π/2.
inline float serverToCanonicalYaw(float serverYaw) { inline float serverToCanonicalYaw(float serverYaw) {
return normalizeAngleRad((PI * 0.5f) - serverYaw); return normalizeAngleRad(serverYaw - (PI * 0.5f));
} }
// Convert canonical yaw (radians) → server/wire yaw (radians). // Convert canonical yaw (radians) → server/wire yaw (radians).
// This mapping is its own inverse. // Inverse of serverToCanonicalYaw: s = c + π/2.
inline float canonicalToServerYaw(float canonicalYaw) { inline float canonicalToServerYaw(float canonicalYaw) {
return normalizeAngleRad((PI * 0.5f) - canonicalYaw); return normalizeAngleRad(canonicalYaw + (PI * 0.5f));
} }
// Convert between canonical WoW and engine rendering coordinates (just swap X/Y). // Convert between canonical WoW and engine rendering coordinates (just swap X/Y).

View file

@ -332,6 +332,10 @@ public:
// Stand state // Stand state
void setStandState(uint8_t state); // 0=stand, 1=sit, 2=sit_chair, 3=sleep, 4=sit_low_chair, 5=sit_medium_chair, 6=sit_high_chair, 7=dead, 8=kneel, 9=submerged void setStandState(uint8_t state); // 0=stand, 1=sit, 2=sit_chair, 3=sleep, 4=sit_low_chair, 5=sit_medium_chair, 6=sit_high_chair, 7=dead, 8=kneel, 9=submerged
uint8_t getStandState() const { return standState_; }
bool isSitting() const { return standState_ >= 1 && standState_ <= 6; }
bool isDead() const { return standState_ == 7; }
bool isKneeling() const { return standState_ == 8; }
// Display toggles // Display toggles
void toggleHelm(); void toggleHelm();
@ -390,6 +394,9 @@ public:
// Ready check // Ready check
void initiateReadyCheck(); void initiateReadyCheck();
void respondToReadyCheck(bool ready); void respondToReadyCheck(bool ready);
bool hasPendingReadyCheck() const { return pendingReadyCheck_; }
void dismissReadyCheck() { pendingReadyCheck_ = false; }
const std::string& getReadyCheckInitiator() const { return readyCheckInitiator_; }
// Duel // Duel
void forfeitDuel(); void forfeitDuel();
@ -501,6 +508,10 @@ public:
const std::vector<AuraSlot>& getPlayerAuras() const { return playerAuras; } const std::vector<AuraSlot>& getPlayerAuras() const { return playerAuras; }
const std::vector<AuraSlot>& getTargetAuras() const { return targetAuras; } const std::vector<AuraSlot>& getTargetAuras() const { return targetAuras; }
// Completed quests (populated from SMSG_QUERY_QUESTS_COMPLETED_RESPONSE)
bool isQuestCompleted(uint32_t questId) const { return completedQuests_.count(questId) > 0; }
const std::unordered_set<uint32_t>& getCompletedQuests() const { return completedQuests_; }
// NPC death callback (for animations) // NPC death callback (for animations)
using NpcDeathCallback = std::function<void(uint64_t guid)>; using NpcDeathCallback = std::function<void(uint64_t guid)>;
void setNpcDeathCallback(NpcDeathCallback cb) { npcDeathCallback_ = std::move(cb); } void setNpcDeathCallback(NpcDeathCallback cb) { npcDeathCallback_ = std::move(cb); }
@ -548,6 +559,8 @@ public:
float getWeatherIntensity() const { return weatherIntensity_; } float getWeatherIntensity() const { return weatherIntensity_; }
bool isRaining() const { return weatherType_ == 1 && weatherIntensity_ > 0.05f; } bool isRaining() const { return weatherType_ == 1 && weatherIntensity_ > 0.05f; }
bool isSnowing() const { return weatherType_ == 2 && weatherIntensity_ > 0.05f; } bool isSnowing() const { return weatherType_ == 2 && weatherIntensity_ > 0.05f; }
uint32_t getOverrideLightId() const { return overrideLightId_; }
uint32_t getOverrideLightTransMs() const { return overrideLightTransMs_; }
// Player skills // Player skills
const std::map<uint32_t, PlayerSkill>& getPlayerSkills() const { return playerSkills_; } const std::map<uint32_t, PlayerSkill>& getPlayerSkills() const { return playerSkills_; }
@ -703,6 +716,88 @@ public:
bool hasPendingGroupInvite() const { return pendingGroupInvite; } bool hasPendingGroupInvite() const { return pendingGroupInvite; }
const std::string& getPendingInviterName() const { return pendingInviterName; } const std::string& getPendingInviterName() const { return pendingInviterName; }
// ---- Item text (books / readable items) ----
bool isItemTextOpen() const { return itemTextOpen_; }
const std::string& getItemText() const { return itemText_; }
void closeItemText() { itemTextOpen_ = false; }
void queryItemText(uint64_t itemGuid);
// ---- Shared Quest ----
bool hasPendingSharedQuest() const { return pendingSharedQuest_; }
uint32_t getSharedQuestId() const { return sharedQuestId_; }
const std::string& getSharedQuestTitle() const { return sharedQuestTitle_; }
const std::string& getSharedQuestSharerName() const { return sharedQuestSharerName_; }
void acceptSharedQuest();
void declineSharedQuest();
// ---- Summon ----
bool hasPendingSummonRequest() const { return pendingSummonRequest_; }
const std::string& getSummonerName() const { return summonerName_; }
float getSummonTimeoutSec() const { return summonTimeoutSec_; }
void acceptSummon();
void declineSummon();
void tickSummonTimeout(float dt) {
if (!pendingSummonRequest_) return;
summonTimeoutSec_ -= dt;
if (summonTimeoutSec_ <= 0.0f) {
pendingSummonRequest_ = false;
summonTimeoutSec_ = 0.0f;
}
}
// ---- Trade ----
enum class TradeStatus : uint8_t {
None = 0, PendingIncoming, Open, Accepted, Complete
};
TradeStatus getTradeStatus() const { return tradeStatus_; }
bool hasPendingTradeRequest() const { return tradeStatus_ == TradeStatus::PendingIncoming; }
const std::string& getTradePeerName() const { return tradePeerName_; }
void acceptTradeRequest(); // respond to incoming SMSG_TRADE_STATUS(1) with CMSG_BEGIN_TRADE
void declineTradeRequest(); // respond with CMSG_CANCEL_TRADE
void acceptTrade(); // lock in offer: CMSG_ACCEPT_TRADE
void cancelTrade(); // CMSG_CANCEL_TRADE
// ---- Duel ----
bool hasPendingDuelRequest() const { return pendingDuelRequest_; }
const std::string& getDuelChallengerName() const { return duelChallengerName_; }
void acceptDuel();
// forfeitDuel() already declared at line ~399
// ---- Instance lockouts ----
struct InstanceLockout {
uint32_t mapId = 0;
uint32_t difficulty = 0; // 0=normal,1=heroic/10man,2=25man,3=25man heroic
uint64_t resetTime = 0; // Unix timestamp of instance reset
bool locked = false;
bool extended = false;
};
const std::vector<InstanceLockout>& getInstanceLockouts() const { return instanceLockouts_; }
// ---- LFG / Dungeon Finder ----
enum class LfgState : uint8_t {
None = 0,
RoleCheck = 1,
Queued = 2,
Proposal = 3,
Boot = 4,
InDungeon = 5,
FinishedDungeon= 6,
RaidBrowser = 7,
};
// roles bitmask: 0x02=tank, 0x04=healer, 0x08=dps; pass LFGDungeonEntry ID
void lfgJoin(uint32_t dungeonId, uint8_t roles);
void lfgLeave();
void lfgAcceptProposal(uint32_t proposalId, bool accept);
void lfgTeleport(bool toLfgDungeon = true);
LfgState getLfgState() const { return lfgState_; }
bool isLfgQueued() const { return lfgState_ == LfgState::Queued; }
bool isLfgInDungeon() const { return lfgState_ == LfgState::InDungeon; }
uint32_t getLfgDungeonId() const { return lfgDungeonId_; }
uint32_t getLfgProposalId() const { return lfgProposalId_; }
int32_t getLfgAvgWaitSec() const { return lfgAvgWaitSec_; }
uint32_t getLfgTimeInQueueMs() const { return lfgTimeInQueueMs_; }
// ---- Phase 5: Loot ---- // ---- Phase 5: Loot ----
void lootTarget(uint64_t guid); void lootTarget(uint64_t guid);
void lootItem(uint8_t slotIndex); void lootItem(uint8_t slotIndex);
@ -713,6 +808,19 @@ public:
void setAutoLoot(bool enabled) { autoLoot_ = enabled; } void setAutoLoot(bool enabled) { autoLoot_ = enabled; }
bool isAutoLoot() const { return autoLoot_; } bool isAutoLoot() const { return autoLoot_; }
// Group loot roll
struct LootRollEntry {
uint64_t objectGuid = 0;
uint32_t slot = 0;
uint32_t itemId = 0;
std::string itemName;
uint8_t itemQuality = 0;
};
bool hasPendingLootRoll() const { return pendingLootRollActive_; }
const LootRollEntry& getPendingLootRoll() const { return pendingLootRoll_; }
void sendLootRoll(uint64_t objectGuid, uint32_t slot, uint8_t rollType);
// rollType: 0=need, 1=greed, 2=disenchant, 96=pass
// NPC Gossip // NPC Gossip
void interactWithNpc(uint64_t guid); void interactWithNpc(uint64_t guid);
void interactWithGameObject(uint64_t guid); void interactWithGameObject(uint64_t guid);
@ -726,6 +834,17 @@ public:
bool isQuestDetailsOpen() const { return questDetailsOpen; } bool isQuestDetailsOpen() const { return questDetailsOpen; }
const QuestDetailsData& getQuestDetails() const { return currentQuestDetails; } const QuestDetailsData& getQuestDetails() const { return currentQuestDetails; }
// Gossip / quest map POI markers (SMSG_GOSSIP_POI)
struct GossipPoi {
float x = 0.0f; // WoW canonical X (north)
float y = 0.0f; // WoW canonical Y (west)
uint32_t icon = 0; // POI icon type
uint32_t data = 0;
std::string name;
};
const std::vector<GossipPoi>& getGossipPois() const { return gossipPois_; }
void clearGossipPois() { gossipPois_.clear(); }
// Quest turn-in // Quest turn-in
bool isQuestRequestItemsOpen() const { return questRequestItemsOpen_; } bool isQuestRequestItemsOpen() const { return questRequestItemsOpen_; }
const QuestRequestItemsData& getQuestRequestItems() const { return currentQuestRequestItems_; } const QuestRequestItemsData& getQuestRequestItems() const { return currentQuestRequestItems_; }
@ -766,11 +885,39 @@ public:
uint32_t getWorldStateMapId() const { return worldStateMapId_; } uint32_t getWorldStateMapId() const { return worldStateMapId_; }
uint32_t getWorldStateZoneId() const { return worldStateZoneId_; } uint32_t getWorldStateZoneId() const { return worldStateZoneId_; }
// Mirror timers (0=fatigue, 1=breath, 2=feigndeath)
struct MirrorTimer {
int32_t value = 0;
int32_t maxValue = 0;
int32_t scale = 0; // +1 = counting up, -1 = counting down
bool paused = false;
bool active = false;
};
const MirrorTimer& getMirrorTimer(int type) const {
static MirrorTimer empty;
return (type >= 0 && type < 3) ? mirrorTimers_[type] : empty;
}
// Combo points
uint8_t getComboPoints() const { return comboPoints_; }
uint64_t getComboTarget() const { return comboTarget_; }
// Death Knight rune state (6 runes: 0-1=Blood, 2-3=Unholy, 4-5=Frost; may become Death=3)
enum class RuneType : uint8_t { Blood = 0, Unholy = 1, Frost = 2, Death = 3 };
struct RuneSlot {
RuneType type = RuneType::Blood;
bool ready = true; // Server-confirmed ready state
float readyFraction = 1.0f; // 0.0=depleted → 1.0=full (from server sync)
};
const std::array<RuneSlot, 6>& getPlayerRunes() const { return playerRunes_; }
struct FactionStandingInit { struct FactionStandingInit {
uint8_t flags = 0; uint8_t flags = 0;
int32_t standing = 0; int32_t standing = 0;
}; };
const std::vector<FactionStandingInit>& getInitialFactions() const { return initialFactions_; } const std::vector<FactionStandingInit>& getInitialFactions() const { return initialFactions_; }
const std::unordered_map<uint32_t, int32_t>& getFactionStandings() const { return factionStandings_; }
const std::string& getFactionNamePublic(uint32_t factionId) const;
uint32_t getLastContactListMask() const { return lastContactListMask_; } uint32_t getLastContactListMask() const { return lastContactListMask_; }
uint32_t getLastContactListCount() const { return lastContactListCount_; } uint32_t getLastContactListCount() const { return lastContactListCount_; }
bool isServerMovementAllowed() const { return serverMovementAllowed_; } bool isServerMovementAllowed() const { return serverMovementAllowed_; }
@ -795,6 +942,26 @@ public:
using OtherPlayerLevelUpCallback = std::function<void(uint64_t guid, uint32_t newLevel)>; using OtherPlayerLevelUpCallback = std::function<void(uint64_t guid, uint32_t newLevel)>;
void setOtherPlayerLevelUpCallback(OtherPlayerLevelUpCallback cb) { otherPlayerLevelUpCallback_ = std::move(cb); } void setOtherPlayerLevelUpCallback(OtherPlayerLevelUpCallback cb) { otherPlayerLevelUpCallback_ = std::move(cb); }
// Achievement earned callback — fires when SMSG_ACHIEVEMENT_EARNED is received
using AchievementEarnedCallback = std::function<void(uint32_t achievementId)>;
void setAchievementEarnedCallback(AchievementEarnedCallback cb) { achievementEarnedCallback_ = std::move(cb); }
// Server-triggered music callback — fires when SMSG_PLAY_MUSIC is received.
// The soundId corresponds to a SoundEntries.dbc record. The receiver is
// responsible for looking up the file path and forwarding to MusicManager.
using PlayMusicCallback = std::function<void(uint32_t soundId)>;
void setPlayMusicCallback(PlayMusicCallback cb) { playMusicCallback_ = std::move(cb); }
// Server-triggered 2-D sound effect callback — fires when SMSG_PLAY_SOUND is received.
// The soundId corresponds to a SoundEntries.dbc record.
using PlaySoundCallback = std::function<void(uint32_t soundId)>;
void setPlaySoundCallback(PlaySoundCallback cb) { playSoundCallback_ = std::move(cb); }
// Server-triggered 3-D positional sound callback — fires for SMSG_PLAY_OBJECT_SOUND and
// SMSG_PLAY_SPELL_IMPACT. Includes sourceGuid so the receiver can look up world position.
using PlayPositionalSoundCallback = std::function<void(uint32_t soundId, uint64_t sourceGuid)>;
void setPlayPositionalSoundCallback(PlayPositionalSoundCallback cb) { playPositionalSoundCallback_ = std::move(cb); }
// Mount state // Mount state
using MountCallback = std::function<void(uint32_t mountDisplayId)>; // 0 = dismount using MountCallback = std::function<void(uint32_t mountDisplayId)>; // 0 = dismount
void setMountCallback(MountCallback cb) { mountCallback_ = std::move(cb); } void setMountCallback(MountCallback cb) { mountCallback_ = std::move(cb); }
@ -1120,6 +1287,11 @@ private:
void handleSpellDamageLog(network::Packet& packet); void handleSpellDamageLog(network::Packet& packet);
void handleSpellHealLog(network::Packet& packet); void handleSpellHealLog(network::Packet& packet);
// ---- Equipment set handler ----
void handleEquipmentSetList(network::Packet& packet);
void handleUpdateAuraDuration(uint8_t slot, uint32_t durationMs);
void handleSetForcedReactions(network::Packet& packet);
// ---- Phase 3 handlers ---- // ---- Phase 3 handlers ----
void handleInitialSpells(network::Packet& packet); void handleInitialSpells(network::Packet& packet);
void handleCastFailed(network::Packet& packet); void handleCastFailed(network::Packet& packet);
@ -1127,6 +1299,7 @@ private:
void handleSpellGo(network::Packet& packet); void handleSpellGo(network::Packet& packet);
void handleSpellCooldown(network::Packet& packet); void handleSpellCooldown(network::Packet& packet);
void handleCooldownEvent(network::Packet& packet); void handleCooldownEvent(network::Packet& packet);
void handleAchievementEarned(network::Packet& packet);
void handleAuraUpdate(network::Packet& packet, bool isAll); void handleAuraUpdate(network::Packet& packet, bool isAll);
void handleLearnedSpell(network::Packet& packet); void handleLearnedSpell(network::Packet& packet);
void handleSupercededSpell(network::Packet& packet); void handleSupercededSpell(network::Packet& packet);
@ -1203,6 +1376,28 @@ private:
void loadAreaTriggerDbc(); void loadAreaTriggerDbc();
void checkAreaTriggers(); void checkAreaTriggers();
// ---- Instance lockout handler ----
void handleRaidInstanceInfo(network::Packet& packet);
void handleItemTextQueryResponse(network::Packet& packet);
void handleQuestConfirmAccept(network::Packet& packet);
void handleSummonRequest(network::Packet& packet);
void handleTradeStatus(network::Packet& packet);
void handleDuelRequested(network::Packet& packet);
void handleDuelComplete(network::Packet& packet);
void handleDuelWinner(network::Packet& packet);
void handleLootRoll(network::Packet& packet);
void handleLootRollWon(network::Packet& packet);
// ---- LFG / Dungeon Finder handlers ----
void handleLfgJoinResult(network::Packet& packet);
void handleLfgQueueStatus(network::Packet& packet);
void handleLfgProposalUpdate(network::Packet& packet);
void handleLfgRoleCheckUpdate(network::Packet& packet);
void handleLfgUpdatePlayer(network::Packet& packet);
void handleLfgPlayerReward(network::Packet& packet);
void handleLfgBootProposalUpdate(network::Packet& packet);
void handleLfgTeleportDenied(network::Packet& packet);
// ---- Arena / Battleground handlers ---- // ---- Arena / Battleground handlers ----
void handleBattlefieldStatus(network::Packet& packet); void handleBattlefieldStatus(network::Packet& packet);
void handleInstanceDifficulty(network::Packet& packet); void handleInstanceDifficulty(network::Packet& packet);
@ -1381,6 +1576,7 @@ private:
// ---- Display state ---- // ---- Display state ----
bool helmVisible_ = true; bool helmVisible_ = true;
bool cloakVisible_ = true; bool cloakVisible_ = true;
uint8_t standState_ = 0; // 0=stand, 1=sit, ..., 7=dead, 8=kneel (server-confirmed)
// ---- Follow state ---- // ---- Follow state ----
uint64_t followTargetGuid_ = 0; uint64_t followTargetGuid_ = 0;
@ -1528,11 +1724,68 @@ private:
uint32_t instanceDifficulty_ = 0; uint32_t instanceDifficulty_ = 0;
bool instanceIsHeroic_ = false; bool instanceIsHeroic_ = false;
// Mirror timers (0=fatigue, 1=breath, 2=feigndeath)
MirrorTimer mirrorTimers_[3];
// Combo points (rogues/druids)
uint8_t comboPoints_ = 0;
uint64_t comboTarget_ = 0;
// Instance / raid lockouts
std::vector<InstanceLockout> instanceLockouts_;
// LFG / Dungeon Finder state
LfgState lfgState_ = LfgState::None;
uint32_t lfgDungeonId_ = 0; // current dungeon entry
uint32_t lfgProposalId_ = 0; // pending proposal id (0 = none)
int32_t lfgAvgWaitSec_ = -1; // estimated wait, -1=unknown
uint32_t lfgTimeInQueueMs_= 0; // ms already in queue
// Ready check state
bool pendingReadyCheck_ = false;
std::string readyCheckInitiator_;
// Faction standings (factionId → absolute standing value)
std::unordered_map<uint32_t, int32_t> factionStandings_;
// Faction name cache (factionId → name), populated lazily from Faction.dbc
std::unordered_map<uint32_t, std::string> factionNameCache_;
bool factionNameCacheLoaded_ = false;
void loadFactionNameCache();
std::string getFactionName(uint32_t factionId) const;
// ---- Phase 4: Group ---- // ---- Phase 4: Group ----
GroupListData partyData; GroupListData partyData;
bool pendingGroupInvite = false; bool pendingGroupInvite = false;
std::string pendingInviterName; std::string pendingInviterName;
// Item text state
bool itemTextOpen_ = false;
std::string itemText_;
// Shared quest state
bool pendingSharedQuest_ = false;
uint32_t sharedQuestId_ = 0;
std::string sharedQuestTitle_;
std::string sharedQuestSharerName_;
uint64_t sharedQuestSharerGuid_ = 0;
// Summon state
bool pendingSummonRequest_ = false;
uint64_t summonerGuid_ = 0;
std::string summonerName_;
float summonTimeoutSec_ = 0.0f;
// Trade state
TradeStatus tradeStatus_ = TradeStatus::None;
uint64_t tradePeerGuid_= 0;
std::string tradePeerName_;
// Duel state
bool pendingDuelRequest_ = false;
uint64_t duelChallengerGuid_= 0;
uint64_t duelFlagGuid_ = 0;
std::string duelChallengerName_;
// ---- Guild state ---- // ---- Guild state ----
std::string guildName_; std::string guildName_;
std::vector<std::string> guildRankNames_; std::vector<std::string> guildRankNames_;
@ -1554,6 +1807,10 @@ private:
bool lootWindowOpen = false; bool lootWindowOpen = false;
bool autoLoot_ = false; bool autoLoot_ = false;
LootResponseData currentLoot; LootResponseData currentLoot;
// Group loot roll state
bool pendingLootRollActive_ = false;
LootRollEntry pendingLootRoll_;
struct LocalLootState { struct LocalLootState {
LootResponseData data; LootResponseData data;
bool moneyTaken = false; bool moneyTaken = false;
@ -1585,6 +1842,7 @@ private:
// Gossip // Gossip
bool gossipWindowOpen = false; bool gossipWindowOpen = false;
GossipMessageData currentGossip; GossipMessageData currentGossip;
std::vector<GossipPoi> gossipPois_;
void performGameObjectInteractionNow(uint64_t guid); void performGameObjectInteractionNow(uint64_t guid);
@ -1783,6 +2041,10 @@ private:
uint32_t weatherType_ = 0; // 0=clear, 1=rain, 2=snow, 3=storm uint32_t weatherType_ = 0; // 0=clear, 1=rain, 2=snow, 3=storm
float weatherIntensity_ = 0.0f; // 0.0 to 1.0 float weatherIntensity_ = 0.0f; // 0.0 to 1.0
// ---- Light override (SMSG_OVERRIDE_LIGHT) ----
uint32_t overrideLightId_ = 0; // 0 = no override
uint32_t overrideLightTransMs_ = 0;
// ---- Player skills ---- // ---- Player skills ----
std::map<uint32_t, PlayerSkill> playerSkills_; std::map<uint32_t, PlayerSkill> playerSkills_;
std::unordered_map<uint32_t, std::string> skillLineNames_; std::unordered_map<uint32_t, std::string> skillLineNames_;
@ -1810,6 +2072,7 @@ private:
ChargeCallback chargeCallback_; ChargeCallback chargeCallback_;
LevelUpCallback levelUpCallback_; LevelUpCallback levelUpCallback_;
OtherPlayerLevelUpCallback otherPlayerLevelUpCallback_; OtherPlayerLevelUpCallback otherPlayerLevelUpCallback_;
AchievementEarnedCallback achievementEarnedCallback_;
MountCallback mountCallback_; MountCallback mountCallback_;
TaxiPrecacheCallback taxiPrecacheCallback_; TaxiPrecacheCallback taxiPrecacheCallback_;
TaxiOrientationCallback taxiOrientationCallback_; TaxiOrientationCallback taxiOrientationCallback_;
@ -1827,12 +2090,42 @@ private:
float serverPitchRate_ = 3.14159f; float serverPitchRate_ = 3.14159f;
bool playerDead_ = false; bool playerDead_ = false;
bool releasedSpirit_ = false; bool releasedSpirit_ = false;
// Death Knight runes (class 6): slots 0-1=Blood, 2-3=Unholy, 4-5=Frost initially
std::array<RuneSlot, 6> playerRunes_ = [] {
std::array<RuneSlot, 6> r{};
r[0].type = r[1].type = RuneType::Blood;
r[2].type = r[3].type = RuneType::Unholy;
r[4].type = r[5].type = RuneType::Frost;
return r;
}();
uint64_t pendingSpiritHealerGuid_ = 0; uint64_t pendingSpiritHealerGuid_ = 0;
bool resurrectPending_ = false; bool resurrectPending_ = false;
bool resurrectRequestPending_ = false; bool resurrectRequestPending_ = false;
uint64_t resurrectCasterGuid_ = 0; uint64_t resurrectCasterGuid_ = 0;
bool repopPending_ = false; bool repopPending_ = false;
uint64_t lastRepopRequestMs_ = 0; uint64_t lastRepopRequestMs_ = 0;
// ---- Completed quest IDs (SMSG_QUERY_QUESTS_COMPLETED_RESPONSE) ----
std::unordered_set<uint32_t> completedQuests_;
// ---- Equipment sets (SMSG_EQUIPMENT_SET_LIST) ----
struct EquipmentSet {
uint64_t setGuid = 0;
uint32_t setId = 0;
std::string name;
std::string iconName;
uint32_t ignoreSlotMask = 0;
std::array<uint64_t, 19> itemGuids{};
};
std::vector<EquipmentSet> equipmentSets_;
// ---- Forced faction reactions (SMSG_SET_FORCED_REACTIONS) ----
std::unordered_map<uint32_t, uint8_t> forcedReactions_; // factionId -> reaction tier
// ---- Server-triggered audio ----
PlayMusicCallback playMusicCallback_;
PlaySoundCallback playSoundCallback_;
PlayPositionalSoundCallback playPositionalSoundCallback_;
}; };
} // namespace game } // namespace game

View file

@ -50,7 +50,8 @@ struct ActionBarSlot {
struct CombatTextEntry { struct CombatTextEntry {
enum Type : uint8_t { enum Type : uint8_t {
MELEE_DAMAGE, SPELL_DAMAGE, HEAL, MISS, DODGE, PARRY, BLOCK, MELEE_DAMAGE, SPELL_DAMAGE, HEAL, MISS, DODGE, PARRY, BLOCK,
CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL,
ENERGIZE, XP_GAIN
}; };
Type type; Type type;
int32_t amount = 0; int32_t amount = 0;

View file

@ -1265,6 +1265,12 @@ public:
// Duel // Duel
// ============================================================ // ============================================================
/** CMSG_DUEL_ACCEPTED packet builder (no payload) */
class DuelAcceptPacket {
public:
static network::Packet build();
};
/** CMSG_DUEL_CANCELLED packet builder */ /** CMSG_DUEL_CANCELLED packet builder */
class DuelCancelPacket { class DuelCancelPacket {
public: public:
@ -1320,6 +1326,24 @@ public:
static network::Packet build(uint64_t targetGuid); static network::Packet build(uint64_t targetGuid);
}; };
/** CMSG_BEGIN_TRADE packet builder (no payload — accepts incoming trade request) */
class BeginTradePacket {
public:
static network::Packet build();
};
/** CMSG_CANCEL_TRADE packet builder (no payload) */
class CancelTradePacket {
public:
static network::Packet build();
};
/** CMSG_ACCEPT_TRADE packet builder (no payload — lock in current offer) */
class AcceptTradePacket {
public:
static network::Packet build();
};
/** CMSG_ATTACKSWING packet builder */ /** CMSG_ATTACKSWING packet builder */
class AttackSwingPacket { class AttackSwingPacket {
public: public:

View file

@ -6,6 +6,7 @@
#include <vector> #include <vector>
namespace wowee { namespace wowee {
namespace pipeline { class AssetManager; }
namespace game { namespace game {
struct ZoneInfo { struct ZoneInfo {
@ -18,6 +19,10 @@ class ZoneManager {
public: public:
void initialize(); void initialize();
// Supplement zone music paths using AreaTable → ZoneMusic → SoundEntries DBC chain.
// Safe to call after initialize(); idempotent and additive (does not remove existing paths).
void enrichFromDBC(pipeline::AssetManager* assets);
uint32_t getZoneId(int tileX, int tileY) const; uint32_t getZoneId(int tileX, int tileY) const;
const ZoneInfo* getZoneInfo(uint32_t zoneId) const; const ZoneInfo* getZoneInfo(uint32_t zoneId) const;
std::string getRandomMusic(uint32_t zoneId); std::string getRandomMusic(uint32_t zoneId);

View file

@ -68,6 +68,11 @@ public:
const std::string& lastError() const { return lastError_; } const std::string& lastError() const { return lastError_; }
private: private:
enum class ApiMode {
LegacyFsr3,
GenericApi
};
void* libHandle_ = nullptr; void* libHandle_ = nullptr;
std::string loadedLibraryPath_; std::string loadedLibraryPath_;
void* scratchBuffer_ = nullptr; void* scratchBuffer_ = nullptr;
@ -80,6 +85,10 @@ private:
struct RuntimeFns; struct RuntimeFns;
RuntimeFns* fns_ = nullptr; RuntimeFns* fns_ = nullptr;
void* contextStorage_ = nullptr; void* contextStorage_ = nullptr;
ApiMode apiMode_ = ApiMode::LegacyFsr3;
void* genericUpscaleContext_ = nullptr;
void* genericFramegenContext_ = nullptr;
uint64_t genericFrameId_ = 1;
}; };
} // namespace wowee::rendering } // namespace wowee::rendering

View file

@ -381,7 +381,7 @@ private:
VkDescriptorPool materialDescPool_ = VK_NULL_HANDLE; VkDescriptorPool materialDescPool_ = VK_NULL_HANDLE;
VkDescriptorPool boneDescPool_ = VK_NULL_HANDLE; VkDescriptorPool boneDescPool_ = VK_NULL_HANDLE;
static constexpr uint32_t MAX_MATERIAL_SETS = 8192; static constexpr uint32_t MAX_MATERIAL_SETS = 8192;
static constexpr uint32_t MAX_BONE_SETS = 2048; static constexpr uint32_t MAX_BONE_SETS = 8192;
// Dynamic particle buffers // Dynamic particle buffers
::VkBuffer smokeVB_ = VK_NULL_HANDLE; ::VkBuffer smokeVB_ = VK_NULL_HANDLE;

View file

@ -106,9 +106,22 @@ public:
void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera); void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera);
/** /**
* Render terrain into shadow depth map (Phase 6 stub) * Initialize terrain shadow pipeline (must be called after initialize()).
* @param shadowRenderPass Depth-only render pass used for the shadow map.
*/ */
void renderShadow(VkCommandBuffer cmd, const glm::vec3& shadowCenter, float halfExtent); bool initializeShadow(VkRenderPass shadowRenderPass);
/**
* Render terrain into the shadow depth map.
* @param cmd Command buffer (inside shadow render pass).
* @param lightSpaceMatrix Orthographic light-space transform.
* @param shadowCenter World-space centre of shadow coverage.
* @param shadowRadius Cull radius around shadowCenter.
*/
void renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMatrix,
const glm::vec3& shadowCenter, float shadowRadius);
bool hasShadowPipeline() const { return shadowPipeline_ != VK_NULL_HANDLE; }
void clear(); void clear();
@ -119,7 +132,6 @@ public:
void setFogEnabled(bool enabled) { fogEnabled = enabled; } void setFogEnabled(bool enabled) { fogEnabled = enabled; }
bool isFogEnabled() const { return fogEnabled; } bool isFogEnabled() const { return fogEnabled; }
// Shadow mapping stubs (Phase 6)
void setShadowMap(VkDescriptorImageInfo /*depthInfo*/, const glm::mat4& /*lightSpaceMat*/) {} void setShadowMap(VkDescriptorImageInfo /*depthInfo*/, const glm::mat4& /*lightSpaceMat*/) {}
void clearShadowMap() {} void clearShadowMap() {}
@ -142,12 +154,21 @@ private:
VkContext* vkCtx = nullptr; VkContext* vkCtx = nullptr;
pipeline::AssetManager* assetManager = nullptr; pipeline::AssetManager* assetManager = nullptr;
// Pipeline // Main pipelines
VkPipeline pipeline = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE;
VkPipeline wireframePipeline = VK_NULL_HANDLE; VkPipeline wireframePipeline = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkDescriptorSetLayout materialSetLayout = VK_NULL_HANDLE; VkDescriptorSetLayout materialSetLayout = VK_NULL_HANDLE;
// Shadow pipeline
VkPipeline shadowPipeline_ = VK_NULL_HANDLE;
VkPipelineLayout shadowPipelineLayout_ = VK_NULL_HANDLE;
VkDescriptorSetLayout shadowParamsLayout_ = VK_NULL_HANDLE;
VkDescriptorPool shadowParamsPool_ = VK_NULL_HANDLE;
VkDescriptorSet shadowParamsSet_ = VK_NULL_HANDLE;
VkBuffer shadowParamsUBO_ = VK_NULL_HANDLE;
VmaAllocation shadowParamsAlloc_ = VK_NULL_HANDLE;
// Descriptor pool for material sets // Descriptor pool for material sets
VkDescriptorPool materialDescPool = VK_NULL_HANDLE; VkDescriptorPool materialDescPool = VK_NULL_HANDLE;
static constexpr uint32_t MAX_MATERIAL_SETS = 16384; static constexpr uint32_t MAX_MATERIAL_SETS = 16384;

View file

@ -1,38 +0,0 @@
#pragma once
#include <string>
#include <GL/glew.h>
namespace wowee {
namespace rendering {
class Texture {
public:
Texture() = default;
~Texture();
bool loadFromFile(const std::string& path);
bool loadFromMemory(const unsigned char* data, int width, int height, int channels);
void bind(GLuint unit = 0) const;
void unbind() const;
GLuint getID() const { return textureID; }
int getWidth() const { return width; }
int getHeight() const { return height; }
private:
GLuint textureID = 0;
int width = 0;
int height = 0;
};
/**
* Apply anisotropic filtering to the currently bound GL_TEXTURE_2D.
* Queries the driver maximum once and caches it. No-op if the extension
* is not available.
*/
void applyAnisotropicFiltering();
} // namespace rendering
} // namespace wowee

View file

@ -62,6 +62,7 @@ private:
// UI state // UI state
bool showEntityWindow = false; bool showEntityWindow = false;
bool showChatWindow = true; bool showChatWindow = true;
bool showNameplates_ = true; // V key toggles nameplates
bool showPlayerInfo = false; bool showPlayerInfo = false;
bool showGuildRoster_ = false; bool showGuildRoster_ = false;
std::string selectedGuildMember_; std::string selectedGuildMember_;
@ -117,7 +118,7 @@ private:
bool pendingPOM = true; // on by default bool pendingPOM = true; // on by default
int pendingPOMQuality = 1; // 0=Low(16), 1=Medium(32), 2=High(64) int pendingPOMQuality = 1; // 0=Low(16), 1=Medium(32), 2=High(64)
bool pendingFSR = false; bool pendingFSR = false;
int pendingUpscalingMode = 0; // 0=Off, 1=FSR1, 2=FSR2 int pendingUpscalingMode = 0; // 0=Off, 1=FSR1, 2=FSR3
int pendingFSRQuality = 3; // 0=UltraQuality, 1=Quality, 2=Balanced, 3=Native(100%) int pendingFSRQuality = 3; // 0=UltraQuality, 1=Quality, 2=Balanced, 3=Native(100%)
float pendingFSRSharpness = 1.6f; float pendingFSRSharpness = 1.6f;
float pendingFSR2JitterSign = 0.38f; float pendingFSR2JitterSign = 0.38f;
@ -181,6 +182,11 @@ private:
*/ */
void renderTargetFrame(game::GameHandler& gameHandler); void renderTargetFrame(game::GameHandler& gameHandler);
/**
* Render pet frame (below player frame when player has an active pet)
*/
void renderPetFrame(game::GameHandler& gameHandler);
/** /**
* Process targeting input (Tab, Escape, click) * Process targeting input (Tab, Escape, click)
*/ */
@ -201,9 +207,16 @@ private:
void renderBagBar(game::GameHandler& gameHandler); void renderBagBar(game::GameHandler& gameHandler);
void renderXpBar(game::GameHandler& gameHandler); void renderXpBar(game::GameHandler& gameHandler);
void renderCastBar(game::GameHandler& gameHandler); void renderCastBar(game::GameHandler& gameHandler);
void renderMirrorTimers(game::GameHandler& gameHandler);
void renderCombatText(game::GameHandler& gameHandler); void renderCombatText(game::GameHandler& gameHandler);
void renderPartyFrames(game::GameHandler& gameHandler); void renderPartyFrames(game::GameHandler& gameHandler);
void renderGroupInvitePopup(game::GameHandler& gameHandler); void renderGroupInvitePopup(game::GameHandler& gameHandler);
void renderDuelRequestPopup(game::GameHandler& gameHandler);
void renderLootRollPopup(game::GameHandler& gameHandler);
void renderTradeRequestPopup(game::GameHandler& gameHandler);
void renderSummonRequestPopup(game::GameHandler& gameHandler);
void renderSharedQuestPopup(game::GameHandler& gameHandler);
void renderItemTextWindow(game::GameHandler& gameHandler);
void renderBuffBar(game::GameHandler& gameHandler); void renderBuffBar(game::GameHandler& gameHandler);
void renderLootWindow(game::GameHandler& gameHandler); void renderLootWindow(game::GameHandler& gameHandler);
void renderGossipWindow(game::GameHandler& gameHandler); void renderGossipWindow(game::GameHandler& gameHandler);
@ -219,14 +232,19 @@ private:
void renderSettingsWindow(); void renderSettingsWindow();
void renderQuestMarkers(game::GameHandler& gameHandler); void renderQuestMarkers(game::GameHandler& gameHandler);
void renderMinimapMarkers(game::GameHandler& gameHandler); void renderMinimapMarkers(game::GameHandler& gameHandler);
void renderQuestObjectiveTracker(game::GameHandler& gameHandler);
void renderGuildRoster(game::GameHandler& gameHandler); void renderGuildRoster(game::GameHandler& gameHandler);
void renderGuildInvitePopup(game::GameHandler& gameHandler); void renderGuildInvitePopup(game::GameHandler& gameHandler);
void renderReadyCheckPopup(game::GameHandler& gameHandler);
void renderChatBubbles(game::GameHandler& gameHandler); void renderChatBubbles(game::GameHandler& gameHandler);
void renderMailWindow(game::GameHandler& gameHandler); void renderMailWindow(game::GameHandler& gameHandler);
void renderMailComposeWindow(game::GameHandler& gameHandler); void renderMailComposeWindow(game::GameHandler& gameHandler);
void renderBankWindow(game::GameHandler& gameHandler); void renderBankWindow(game::GameHandler& gameHandler);
void renderGuildBankWindow(game::GameHandler& gameHandler); void renderGuildBankWindow(game::GameHandler& gameHandler);
void renderAuctionHouseWindow(game::GameHandler& gameHandler); void renderAuctionHouseWindow(game::GameHandler& gameHandler);
void renderDungeonFinderWindow(game::GameHandler& gameHandler);
void renderInstanceLockouts(game::GameHandler& gameHandler);
void renderNameplates(game::GameHandler& gameHandler);
/** /**
* Inventory screen * Inventory screen
@ -249,6 +267,9 @@ private:
bool spellIconDbLoaded_ = false; bool spellIconDbLoaded_ = false;
VkDescriptorSet getSpellIcon(uint32_t spellId, pipeline::AssetManager* am); VkDescriptorSet getSpellIcon(uint32_t spellId, pipeline::AssetManager* am);
// Death Knight rune bar: client-predicted fill (0.0=depleted, 1.0=ready) for smooth animation
float runeClientFill_[6] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f};
// Action bar drag state (-1 = not dragging) // Action bar drag state (-1 = not dragging)
int actionBarDragSlot_ = -1; int actionBarDragSlot_ = -1;
VkDescriptorSet actionBarDragIcon_ = VK_NULL_HANDLE; VkDescriptorSet actionBarDragIcon_ = VK_NULL_HANDLE;
@ -259,6 +280,14 @@ private:
int bagBarPickedSlot_ = -1; // Visual drag in progress (-1 = none) int bagBarPickedSlot_ = -1; // Visual drag in progress (-1 = none)
int bagBarDragSource_ = -1; // Mouse pressed on this slot, waiting for drag or click (-1 = none) int bagBarDragSource_ = -1; // Mouse pressed on this slot, waiting for drag or click (-1 = none)
// Instance Lockouts window
bool showInstanceLockouts_ = false;
// Dungeon Finder state
bool showDungeonFinder_ = false;
uint8_t lfgRoles_ = 0x08; // default: DPS (0x02=tank, 0x04=healer, 0x08=dps)
uint32_t lfgSelectedDungeon_ = 861; // default: random dungeon (entry 861 = Random Dungeon WotLK)
// Chat settings // Chat settings
bool chatShowTimestamps_ = false; bool chatShowTimestamps_ = false;
int chatFontSize_ = 1; // 0=small, 1=medium, 2=large int chatFontSize_ = 1; // 0=small, 1=medium, 2=large
@ -320,8 +349,22 @@ private:
uint32_t dingLevel_ = 0; uint32_t dingLevel_ = 0;
void renderDingEffect(); void renderDingEffect();
// Achievement toast banner
static constexpr float ACHIEVEMENT_TOAST_DURATION = 5.0f;
float achievementToastTimer_ = 0.0f;
uint32_t achievementToastId_ = 0;
void renderAchievementToast();
// Zone discovery text ("Entering: <ZoneName>")
static constexpr float ZONE_TEXT_DURATION = 5.0f;
float zoneTextTimer_ = 0.0f;
std::string zoneTextName_;
std::string lastKnownZoneName_;
void renderZoneText();
public: public:
void triggerDing(uint32_t newLevel); void triggerDing(uint32_t newLevel);
void triggerAchievementToast(uint32_t achievementId);
}; };
} // namespace ui } // namespace ui

View file

@ -148,6 +148,7 @@ private:
void renderEquipmentPanel(game::Inventory& inventory); void renderEquipmentPanel(game::Inventory& inventory);
void renderBackpackPanel(game::Inventory& inventory, bool collapseEmptySections = false); void renderBackpackPanel(game::Inventory& inventory, bool collapseEmptySections = false);
void renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel, int32_t serverArmor = 0); void renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel, int32_t serverArmor = 0);
void renderReputationPanel(game::GameHandler& gameHandler);
void renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot, void renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot,
float size, const char* label, float size, const char* label,

View file

@ -83,6 +83,34 @@ bool AmbientSoundManager::initialize(pipeline::AssetManager* assets) {
blacksmithSounds_.resize(1); blacksmithSounds_.resize(1);
bool blacksmithLoaded = loadSound("Sound\\Ambience\\WMOAmbience\\BlackSmith.wav", blacksmithSounds_[0], assets); bool blacksmithLoaded = loadSound("Sound\\Ambience\\WMOAmbience\\BlackSmith.wav", blacksmithSounds_[0], assets);
// Load bird chirp sounds (daytime periodic) — up to 6 variants
{
static const char* birdPaths[] = {
"Sound\\Ambience\\BirdAmbience\\BirdChirp01.wav",
"Sound\\Ambience\\BirdAmbience\\BirdChirp02.wav",
"Sound\\Ambience\\BirdAmbience\\BirdChirp03.wav",
"Sound\\Ambience\\BirdAmbience\\BirdChirp04.wav",
"Sound\\Ambience\\BirdAmbience\\BirdChirp05.wav",
"Sound\\Ambience\\BirdAmbience\\BirdChirp06.wav",
};
for (const char* p : birdPaths) {
birdSounds_.emplace_back();
if (!loadSound(p, birdSounds_.back(), assets)) birdSounds_.pop_back();
}
}
// Load cricket/insect sounds (nighttime periodic)
{
static const char* cricketPaths[] = {
"Sound\\Ambience\\Insect\\InsectMorning.wav",
"Sound\\Ambience\\Insect\\InsectNight.wav",
};
for (const char* p : cricketPaths) {
cricketSounds_.emplace_back();
if (!loadSound(p, cricketSounds_.back(), assets)) cricketSounds_.pop_back();
}
}
// Load weather sounds // Load weather sounds
rainLightSounds_.resize(1); rainLightSounds_.resize(1);
bool rainLightLoaded = loadSound("Sound\\Ambience\\Weather\\RainLight.wav", rainLightSounds_[0], assets); bool rainLightLoaded = loadSound("Sound\\Ambience\\Weather\\RainLight.wav", rainLightSounds_[0], assets);
@ -413,9 +441,13 @@ void AmbientSoundManager::updatePeriodicSounds(float deltaTime, bool isIndoor, b
if (isDaytime()) { if (isDaytime()) {
birdTimer_ += deltaTime; birdTimer_ += deltaTime;
if (birdTimer_ >= randomFloat(BIRD_MIN_INTERVAL, BIRD_MAX_INTERVAL)) { if (birdTimer_ >= randomFloat(BIRD_MIN_INTERVAL, BIRD_MAX_INTERVAL)) {
// Play a random bird chirp (we'll use wind sound as placeholder for now)
// TODO: Add actual bird sound files when available
birdTimer_ = 0.0f; birdTimer_ = 0.0f;
if (!birdSounds_.empty()) {
std::uniform_int_distribution<size_t> pick(0, birdSounds_.size() - 1);
const auto& snd = birdSounds_[pick(gen)];
if (snd.loaded)
AudioEngine::instance().playSound2D(snd.data, BIRD_VOLUME, 1.0f);
}
} }
} }
@ -423,9 +455,13 @@ void AmbientSoundManager::updatePeriodicSounds(float deltaTime, bool isIndoor, b
if (isNighttime()) { if (isNighttime()) {
cricketTimer_ += deltaTime; cricketTimer_ += deltaTime;
if (cricketTimer_ >= randomFloat(CRICKET_MIN_INTERVAL, CRICKET_MAX_INTERVAL)) { if (cricketTimer_ >= randomFloat(CRICKET_MIN_INTERVAL, CRICKET_MAX_INTERVAL)) {
// Play cricket sounds
// TODO: Add actual cricket sound files when available
cricketTimer_ = 0.0f; cricketTimer_ = 0.0f;
if (!cricketSounds_.empty()) {
std::uniform_int_distribution<size_t> pick(0, cricketSounds_.size() - 1);
const auto& snd = cricketSounds_[pick(gen)];
if (snd.loaded)
AudioEngine::instance().playSound2D(snd.data, CRICKET_VOLUME, 1.0f);
}
} }
} }
} }
@ -554,6 +590,94 @@ void AmbientSoundManager::setZoneType(ZoneType type) {
} }
} }
void AmbientSoundManager::setZoneId(uint32_t zoneId) {
// Map WoW zone ID to ZoneType + CityType.
// City zones: set CityType and clear ZoneType.
// Outdoor zones: set ZoneType and clear CityType.
CityType city = CityType::NONE;
ZoneType zone = ZoneType::NONE;
switch (zoneId) {
// ---- Major cities ----
case 1519: city = CityType::STORMWIND; break;
case 1537: city = CityType::IRONFORGE; break;
case 1657: city = CityType::DARNASSUS; break;
case 1637: city = CityType::ORGRIMMAR; break;
case 1497: city = CityType::UNDERCITY; break;
case 1638: city = CityType::THUNDERBLUFF; break;
// ---- Forest / snowy forest ----
case 12: // Elwynn Forest
case 141: // Teldrassil
case 148: // Darkshore
case 493: // Moonglade
case 361: // Felwood
case 331: // Ashenvale
case 357: // Feralas
case 15: // Dustwallow Marsh (lush)
case 267: // Hillsbrad Foothills
case 36: // Alterac Mountains
case 45: // Arathi Highlands
zone = ZoneType::FOREST_NORMAL; break;
case 1: // Dun Morogh
case 196: // Winterspring
case 3: // Badlands (actually dry but close enough)
case 2817: // Crystalsong Forest
case 66: // Storm Peaks
case 67: // Icecrown
case 394: // Dragonblight
case 65: // Howling Fjord
zone = ZoneType::FOREST_SNOW; break;
// ---- Grasslands / plains ----
case 40: // Westfall
case 215: // Mulgore
case 44: // Redridge Mountains
case 10: // Duskwood (counts as grassland night)
case 38: // Loch Modan
zone = ZoneType::GRASSLANDS; break;
// ---- Desert ----
case 17: // The Barrens
case 14: // Durotar
case 440: // Tanaris
case 400: // Thousand Needles
zone = ZoneType::DESERT_PLAINS; break;
case 46: // Burning Steppes
case 51: // Searing Gorge
case 241: // Eastern Plaguelands (barren)
case 28: // Western Plaguelands
zone = ZoneType::DESERT_CANYON; break;
// ---- Jungle ----
case 33: // Stranglethorn Vale
case 78: // Un'Goro Crater
case 210: // Uldaman
case 1377: // Silithus (arid but closest)
zone = ZoneType::JUNGLE; break;
// ---- Marsh / swamp ----
case 8: // Swamp of Sorrows
case 11: // Wetlands
case 139: // Eastern Plaguelands
case 763: // Zangarmarsh
zone = ZoneType::MARSH; break;
// ---- Beach / coast ----
case 4: // Barrens coast (Merchant Coast)
case 3537: // Azuremyst Isle
case 3524: // Bloodmyst Isle
zone = ZoneType::BEACH; break;
default: break;
}
setCityType(city);
setZoneType(zone);
}
void AmbientSoundManager::setCityType(CityType type) { void AmbientSoundManager::setCityType(CityType type) {
if (currentCity_ != type) { if (currentCity_ != type) {
LOG_INFO("AmbientSoundManager: City changed from ", static_cast<int>(currentCity_), LOG_INFO("AmbientSoundManager: City changed from ", static_cast<int>(currentCity_),

View file

@ -144,15 +144,24 @@ void MusicManager::playFilePath(const std::string& filePath, bool loop, float fa
} }
void MusicManager::stopMusic(float fadeMs) { void MusicManager::stopMusic(float fadeMs) {
(void)fadeMs; // Fade not implemented yet if (!playing) return;
AudioEngine::instance().stopMusic();
playing = false;
fadingIn = false; fadingIn = false;
fadeInTimer = 0.0f; crossfading = false;
fadeInDuration = 0.0f;
fadeInTargetVolume = 0.0f; if (fadeMs > 0.0f) {
currentTrack.clear(); // Begin fade-out; actual stop happens once volume reaches zero in update()
currentTrackIsFile = false; fadingOut = true;
fadeOutTimer = 0.0f;
fadeOutDuration = fadeMs / 1000.0f;
fadeOutStartVolume = effectiveMusicVolume();
} else {
AudioEngine::instance().stopMusic();
playing = false;
fadingOut = false;
currentTrack.clear();
currentTrackIsFile = false;
}
} }
void MusicManager::setVolume(int volume) { void MusicManager::setVolume(int volume) {
@ -224,6 +233,22 @@ void MusicManager::update(float deltaTime) {
playing = false; playing = false;
} }
if (fadingOut) {
fadeOutTimer += deltaTime;
float t = std::clamp(1.0f - fadeOutTimer / std::max(fadeOutDuration, 0.001f), 0.0f, 1.0f);
AudioEngine::instance().setMusicVolume(fadeOutStartVolume * t);
if (t <= 0.0f) {
// Fade complete — stop playback and restore volume for next track
fadingOut = false;
AudioEngine::instance().stopMusic();
AudioEngine::instance().setMusicVolume(effectiveMusicVolume());
playing = false;
currentTrack.clear();
currentTrackIsFile = false;
}
return; // Don't process other fade logic while fading out
}
if (fadingIn) { if (fadingIn) {
fadeInTimer += deltaTime; fadeInTimer += deltaTime;
float t = std::clamp(fadeInTimer / std::max(fadeInDuration, 0.001f), 0.0f, 1.0f); float t = std::clamp(fadeInTimer / std::max(fadeInDuration, 0.001f), 0.0f, 1.0f);

View file

@ -373,12 +373,5 @@ void NpcVoiceManager::playFlee(uint64_t npcGuid, VoiceType voiceType, const glm:
playSound(npcGuid, voiceType, SoundCategory::FLEE, position); playSound(npcGuid, voiceType, SoundCategory::FLEE, position);
} }
VoiceType NpcVoiceManager::detectVoiceType(uint32_t creatureEntry) const {
// TODO: Use CreatureTemplate.dbc or other data to map creature entry to voice type
// For now, return generic
(void)creatureEntry;
return VoiceType::GENERIC;
}
} // namespace audio } // namespace audio
} // namespace wowee } // namespace wowee

View file

@ -2,6 +2,7 @@
#include "audio/audio_engine.hpp" #include "audio/audio_engine.hpp"
#include "pipeline/asset_manager.hpp" #include "pipeline/asset_manager.hpp"
#include "core/logger.hpp" #include "core/logger.hpp"
#include <algorithm>
#include <random> #include <random>
namespace wowee { namespace wowee {
@ -180,7 +181,7 @@ void SpellSoundManager::playRandomSound(const std::vector<SpellSample>& library,
} }
void SpellSoundManager::setVolumeScale(float scale) { void SpellSoundManager::setVolumeScale(float scale) {
volumeScale_ = std::max(0.0f, std::min(1.0f, scale)); volumeScale_ = std::clamp(scale, .0f, 1.f);
} }
void SpellSoundManager::playPrecast(MagicSchool school, SpellPower power) { void SpellSoundManager::playPrecast(MagicSchool school, SpellPower power) {

View file

@ -105,6 +105,13 @@ bool UiSoundManager::initialize(pipeline::AssetManager* assets) {
levelUpSounds_.resize(1); levelUpSounds_.resize(1);
bool levelUpLoaded = loadSound("Sound\\Interface\\LevelUp.wav", levelUpSounds_[0], assets); bool levelUpLoaded = loadSound("Sound\\Interface\\LevelUp.wav", levelUpSounds_[0], assets);
// Load achievement sound (WotLK: Sound\Interface\AchievementSound.wav)
achievementSounds_.resize(1);
if (!loadSound("Sound\\Interface\\AchievementSound.wav", achievementSounds_[0], assets)) {
// Fallback to level-up sound if achievement sound is missing
achievementSounds_ = levelUpSounds_;
}
// Load error/feedback sounds // Load error/feedback sounds
errorSounds_.resize(1); errorSounds_.resize(1);
loadSound("Sound\\Interface\\Error.wav", errorSounds_[0], assets); loadSound("Sound\\Interface\\Error.wav", errorSounds_[0], assets);
@ -210,6 +217,9 @@ void UiSoundManager::playDrinking() { playSound(drinkingSounds_); }
// Level up // Level up
void UiSoundManager::playLevelUp() { playSound(levelUpSounds_); } void UiSoundManager::playLevelUp() { playSound(levelUpSounds_); }
// Achievement
void UiSoundManager::playAchievementAlert() { playSound(achievementSounds_); }
// Error/feedback // Error/feedback
void UiSoundManager::playError() { playSound(errorSounds_); } void UiSoundManager::playError() { playSound(errorSounds_); }
void UiSoundManager::playTargetSelect() { playSound(selectTargetSounds_); } void UiSoundManager::playTargetSelect() { playSound(selectTargetSounds_); }

View file

@ -2089,6 +2089,88 @@ void Application::setupUICallbacks() {
} }
}); });
// Achievement earned callback — show toast banner
gameHandler->setAchievementEarnedCallback([this](uint32_t achievementId) {
if (uiManager) {
uiManager->getGameScreen().triggerAchievementToast(achievementId);
}
});
// Server-triggered music callback (SMSG_PLAY_MUSIC)
// Resolves soundId → SoundEntries.dbc → MPQ path → MusicManager.
gameHandler->setPlayMusicCallback([this](uint32_t soundId) {
if (!assetManager || !renderer) return;
auto* music = renderer->getMusicManager();
if (!music) return;
auto dbc = assetManager->loadDBC("SoundEntries.dbc");
if (!dbc || !dbc->isLoaded()) return;
int32_t idx = dbc->findRecordById(soundId);
if (idx < 0) return;
// SoundEntries.dbc (WotLK): field 2 = Name (label), fields 3-12 = File[0..9], field 23 = DirectoryBase
const uint32_t row = static_cast<uint32_t>(idx);
std::string dir = dbc->getString(row, 23);
for (uint32_t f = 3; f <= 12; ++f) {
std::string name = dbc->getString(row, f);
if (name.empty()) continue;
std::string path = dir.empty() ? name : dir + "\\" + name;
music->playMusic(path, /*loop=*/false);
return;
}
});
// SMSG_PLAY_SOUND: look up SoundEntries.dbc and play 2-D sound effect
gameHandler->setPlaySoundCallback([this](uint32_t soundId) {
if (!assetManager) return;
auto dbc = assetManager->loadDBC("SoundEntries.dbc");
if (!dbc || !dbc->isLoaded()) return;
int32_t idx = dbc->findRecordById(soundId);
if (idx < 0) return;
const uint32_t row = static_cast<uint32_t>(idx);
std::string dir = dbc->getString(row, 23);
for (uint32_t f = 3; f <= 12; ++f) {
std::string name = dbc->getString(row, f);
if (name.empty()) continue;
std::string path = dir.empty() ? name : dir + "\\" + name;
audio::AudioEngine::instance().playSound2D(path);
return;
}
});
// SMSG_PLAY_OBJECT_SOUND / SMSG_PLAY_SPELL_IMPACT: play as 3D positional sound at source entity
gameHandler->setPlayPositionalSoundCallback([this](uint32_t soundId, uint64_t sourceGuid) {
if (!assetManager || !gameHandler) return;
auto dbc = assetManager->loadDBC("SoundEntries.dbc");
if (!dbc || !dbc->isLoaded()) return;
int32_t idx = dbc->findRecordById(soundId);
if (idx < 0) return;
const uint32_t row = static_cast<uint32_t>(idx);
std::string dir = dbc->getString(row, 23);
for (uint32_t f = 3; f <= 12; ++f) {
std::string name = dbc->getString(row, f);
if (name.empty()) continue;
std::string path = dir.empty() ? name : dir + "\\" + name;
// Play as 3D sound if source entity position is available
auto entity = gameHandler->getEntityManager().getEntity(sourceGuid);
if (entity) {
glm::vec3 pos{entity->getLatestX(), entity->getLatestY(), entity->getLatestZ()};
audio::AudioEngine::instance().playSound3D(path, pos);
} else {
audio::AudioEngine::instance().playSound2D(path);
}
return;
}
});
// Other player level-up callback — trigger 3D effect + chat notification // Other player level-up callback — trigger 3D effect + chat notification
gameHandler->setOtherPlayerLevelUpCallback([this](uint64_t guid, uint32_t newLevel) { gameHandler->setOtherPlayerLevelUpCallback([this](uint64_t guid, uint32_t newLevel) {
if (!gameHandler || !renderer) return; if (!gameHandler || !renderer) return;
@ -4643,7 +4725,7 @@ audio::VoiceType Application::detectVoiceTypeFromDisplayId(uint32_t displayId) c
switch (raceId) { switch (raceId) {
case 1: raceName = "Human"; result = (sexId == 0) ? audio::VoiceType::HUMAN_MALE : audio::VoiceType::HUMAN_FEMALE; break; case 1: raceName = "Human"; result = (sexId == 0) ? audio::VoiceType::HUMAN_MALE : audio::VoiceType::HUMAN_FEMALE; break;
case 2: raceName = "Orc"; result = (sexId == 0) ? audio::VoiceType::ORC_MALE : audio::VoiceType::ORC_FEMALE; break; case 2: raceName = "Orc"; result = (sexId == 0) ? audio::VoiceType::ORC_MALE : audio::VoiceType::ORC_FEMALE; break;
case 3: raceName = "Dwarf"; result = (sexId == 0) ? audio::VoiceType::DWARF_MALE : audio::VoiceType::GENERIC; break; case 3: raceName = "Dwarf"; result = (sexId == 0) ? audio::VoiceType::DWARF_MALE : audio::VoiceType::DWARF_FEMALE; break;
case 4: raceName = "NightElf"; result = (sexId == 0) ? audio::VoiceType::NIGHTELF_MALE : audio::VoiceType::NIGHTELF_FEMALE; break; case 4: raceName = "NightElf"; result = (sexId == 0) ? audio::VoiceType::NIGHTELF_MALE : audio::VoiceType::NIGHTELF_FEMALE; break;
case 5: raceName = "Undead"; result = (sexId == 0) ? audio::VoiceType::UNDEAD_MALE : audio::VoiceType::UNDEAD_FEMALE; break; case 5: raceName = "Undead"; result = (sexId == 0) ? audio::VoiceType::UNDEAD_MALE : audio::VoiceType::UNDEAD_FEMALE; break;
case 6: raceName = "Tauren"; result = (sexId == 0) ? audio::VoiceType::TAUREN_MALE : audio::VoiceType::TAUREN_FEMALE; break; case 6: raceName = "Tauren"; result = (sexId == 0) ? audio::VoiceType::TAUREN_MALE : audio::VoiceType::TAUREN_FEMALE; break;
@ -4809,11 +4891,9 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
// Check model cache - reuse if same displayId was already loaded // Check model cache - reuse if same displayId was already loaded
uint32_t modelId = 0; uint32_t modelId = 0;
bool modelCached = false;
auto cacheIt = displayIdModelCache_.find(displayId); auto cacheIt = displayIdModelCache_.find(displayId);
if (cacheIt != displayIdModelCache_.end()) { if (cacheIt != displayIdModelCache_.end()) {
modelId = cacheIt->second; modelId = cacheIt->second;
modelCached = true;
} else { } else {
// Load model from disk (only once per displayId) // Load model from disk (only once per displayId)
modelId = nextCreatureModelId_++; modelId = nextCreatureModelId_++;
@ -6618,7 +6698,9 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z)); glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
const float renderYawWmo = orientation; const float renderYawWmo = orientation;
const float renderYawM2go = orientation + glm::radians(180.0f); // M2 game objects: model default faces +renderX. renderYaw = canonical + 90° = server_yaw
// (same offset as creature/character renderer so all M2 models face consistently)
const float renderYawM2go = orientation + glm::radians(90.0f);
bool loadedAsWmo = false; bool loadedAsWmo = false;
if (isWmo) { if (isWmo) {
@ -8145,15 +8227,17 @@ void Application::despawnOnlineGameObject(uint64_t guid) {
void Application::loadQuestMarkerModels() { void Application::loadQuestMarkerModels() {
if (!assetManager || !renderer) return; if (!assetManager || !renderer) return;
// Quest markers in WoW 3.3.5a are billboard sprites (BLP textures), not M2 models // Quest markers are billboard sprites; the renderer's QuestMarkerRenderer handles
// Load the BLP textures for quest markers // texture loading and pipeline setup during world initialization.
LOG_INFO("Quest markers will be rendered as billboard sprites using BLP textures:"); // Calling initialize() here is a no-op if already done; harmless if called early.
LOG_INFO(" - Available: Interface\\GossipFrame\\AvailableQuestIcon.blp"); if (auto* qmr = renderer->getQuestMarkerRenderer()) {
LOG_INFO(" - Turn-in: Interface\\GossipFrame\\ActiveQuestIcon.blp"); if (auto* vkCtx = renderer->getVkContext()) {
LOG_INFO(" - Incomplete: Interface\\GossipFrame\\IncompleteQuestIcon.blp"); VkDescriptorSetLayout pfl = renderer->getPerFrameSetLayout();
if (pfl != VK_NULL_HANDLE) {
// TODO: Implement billboard sprite rendering for quest markers qmr->initialize(vkCtx, pfl, assetManager.get());
// For now, the 2D ImGui markers will continue to work }
}
}
} }
void Application::updateQuestMarkers() { void Application::updateQuestMarkers() {

File diff suppressed because it is too large Load diff

View file

@ -4,11 +4,14 @@ namespace wowee {
namespace game { namespace game {
void World::update([[maybe_unused]] float deltaTime) { void World::update([[maybe_unused]] float deltaTime) {
// TODO: Update world state // World state updates are handled by Application (terrain streaming, entity sync,
// camera, etc.) and GameHandler (server packet processing). World is a thin
// ownership token; per-frame logic lives in those subsystems.
} }
void World::loadMap([[maybe_unused]] uint32_t mapId) { void World::loadMap([[maybe_unused]] uint32_t mapId) {
// TODO: Load map data // Terrain loading is driven by Application::loadOnlineWorld() via TerrainManager.
// This method exists as an extension point; no action needed here.
} }
} // namespace game } // namespace game

View file

@ -2083,6 +2083,12 @@ network::Packet ReadyCheckConfirmPacket::build(bool ready) {
// Duel // Duel
// ============================================================ // ============================================================
network::Packet DuelAcceptPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_DUEL_ACCEPTED));
LOG_DEBUG("Built CMSG_DUEL_ACCEPTED");
return packet;
}
network::Packet DuelCancelPacket::build() { network::Packet DuelCancelPacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_DUEL_CANCELLED)); network::Packet packet(wireOpcode(Opcode::CMSG_DUEL_CANCELLED));
LOG_DEBUG("Built CMSG_DUEL_CANCELLED"); LOG_DEBUG("Built CMSG_DUEL_CANCELLED");
@ -2125,14 +2131,31 @@ network::Packet RequestRaidInfoPacket::build() {
// ============================================================ // ============================================================
network::Packet DuelProposedPacket::build(uint64_t targetGuid) { network::Packet DuelProposedPacket::build(uint64_t targetGuid) {
// TODO: Duels are initiated via CMSG_CAST_SPELL with spell 7266, // Duels are initiated via CMSG_CAST_SPELL with spell 7266 (Duel) targeted at the opponent.
// not a dedicated CMSG_DUEL_PROPOSED opcode (which doesn't exist in WoW). // There is no separate CMSG_DUEL_PROPOSED opcode in WoW.
// For now, build a cast spell packet targeting the opponent.
auto packet = CastSpellPacket::build(7266, targetGuid, 0); auto packet = CastSpellPacket::build(7266, targetGuid, 0);
LOG_DEBUG("Built duel request (spell 7266) for target: 0x", std::hex, targetGuid, std::dec); LOG_DEBUG("Built duel request (spell 7266) for target: 0x", std::hex, targetGuid, std::dec);
return packet; return packet;
} }
network::Packet BeginTradePacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_BEGIN_TRADE));
LOG_DEBUG("Built CMSG_BEGIN_TRADE");
return packet;
}
network::Packet CancelTradePacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_CANCEL_TRADE));
LOG_DEBUG("Built CMSG_CANCEL_TRADE");
return packet;
}
network::Packet AcceptTradePacket::build() {
network::Packet packet(wireOpcode(Opcode::CMSG_ACCEPT_TRADE));
LOG_DEBUG("Built CMSG_ACCEPT_TRADE");
return packet;
}
network::Packet InitiateTradePacket::build(uint64_t targetGuid) { network::Packet InitiateTradePacket::build(uint64_t targetGuid) {
network::Packet packet(wireOpcode(Opcode::CMSG_INITIATE_TRADE)); network::Packet packet(wireOpcode(Opcode::CMSG_INITIATE_TRADE));
packet.writeUInt64(targetGuid); packet.writeUInt64(targetGuid);

View file

@ -1,4 +1,5 @@
#include "game/zone_manager.hpp" #include "game/zone_manager.hpp"
#include "pipeline/asset_manager.hpp"
#include "core/logger.hpp" #include "core/logger.hpp"
#include <cstdlib> #include <cstdlib>
#include <ctime> #include <ctime>
@ -479,5 +480,88 @@ std::vector<std::string> ZoneManager::getAllMusicPaths() const {
return out; return out;
} }
void ZoneManager::enrichFromDBC(pipeline::AssetManager* assets) {
if (!assets) return;
auto areaDbc = assets->loadDBC("AreaTable.dbc");
auto zoneMusicDbc = assets->loadDBC("ZoneMusic.dbc");
auto soundDbc = assets->loadDBC("SoundEntries.dbc");
if (!areaDbc || !areaDbc->isLoaded()) {
LOG_WARNING("ZoneManager::enrichFromDBC: AreaTable.dbc not available");
return;
}
if (!zoneMusicDbc || !zoneMusicDbc->isLoaded()) {
LOG_WARNING("ZoneManager::enrichFromDBC: ZoneMusic.dbc not available");
return;
}
if (!soundDbc || !soundDbc->isLoaded()) {
LOG_WARNING("ZoneManager::enrichFromDBC: SoundEntries.dbc not available");
return;
}
// Build MPQ paths from a SoundEntries record.
// Fields 3-12 = File[0..9], field 23 = DirectoryBase.
auto getSoundPaths = [&](uint32_t soundId) -> std::vector<std::string> {
if (soundId == 0) return {};
int32_t idx = soundDbc->findRecordById(soundId);
if (idx < 0) return {};
uint32_t row = static_cast<uint32_t>(idx);
if (soundDbc->getFieldCount() < 24) return {};
std::string dir = soundDbc->getString(row, 23);
std::vector<std::string> paths;
for (uint32_t f = 3; f <= 12; ++f) {
std::string name = soundDbc->getString(row, f);
if (name.empty()) continue;
paths.push_back(dir.empty() ? name : dir + "\\" + name);
}
return paths;
};
const uint32_t numAreas = areaDbc->getRecordCount();
const uint32_t areaFields = areaDbc->getFieldCount();
if (areaFields < 9) {
LOG_WARNING("ZoneManager::enrichFromDBC: AreaTable.dbc has too few fields (", areaFields, ")");
return;
}
uint32_t zonesEnriched = 0;
for (uint32_t i = 0; i < numAreas; ++i) {
uint32_t zoneId = areaDbc->getUInt32(i, 0);
uint32_t zoneMusicId = areaDbc->getUInt32(i, 8);
if (zoneId == 0 || zoneMusicId == 0) continue;
int32_t zmIdx = zoneMusicDbc->findRecordById(zoneMusicId);
if (zmIdx < 0) continue;
uint32_t zmRow = static_cast<uint32_t>(zmIdx);
if (zoneMusicDbc->getFieldCount() < 8) continue;
uint32_t daySoundId = zoneMusicDbc->getUInt32(zmRow, 6);
uint32_t nightSoundId = zoneMusicDbc->getUInt32(zmRow, 7);
std::vector<std::string> newPaths;
for (const auto& p : getSoundPaths(daySoundId)) newPaths.push_back(p);
for (const auto& p : getSoundPaths(nightSoundId)) newPaths.push_back(p);
if (newPaths.empty()) continue;
auto& zone = zones[zoneId];
if (zone.id == 0) zone.id = zoneId;
// Append paths not already present (preserve hardcoded entries).
for (const auto& path : newPaths) {
bool found = false;
for (const auto& existing : zone.musicPaths) {
if (existing == path) { found = true; break; }
}
if (!found) {
zone.musicPaths.push_back(path);
++zonesEnriched;
}
}
}
LOG_INFO("Zone music enriched from DBC: ", zones.size(), " zones, ", zonesEnriched, " paths added");
}
} // namespace game } // namespace game
} // namespace wowee } // namespace wowee

View file

@ -110,8 +110,34 @@ bool MPQManager::initialize(const std::string& dataPath_) {
// Load patch archives (highest priority) // Load patch archives (highest priority)
loadPatchArchives(); loadPatchArchives();
// Load locale archives // Load locale archives — auto-detect from available locale directories
loadLocaleArchives("enUS"); // TODO: Make configurable {
// Prefer the locale override from environment, then scan for installed ones
const char* localeEnv = std::getenv("WOWEE_LOCALE");
std::string detectedLocale;
if (localeEnv && localeEnv[0] != '\0') {
detectedLocale = localeEnv;
LOG_INFO("Using locale from WOWEE_LOCALE env: ", detectedLocale);
} else {
// Priority order: enUS first, then other common locales
static const std::array<const char*, 12> knownLocales = {
"enUS", "enGB", "deDE", "frFR", "esES", "esMX",
"zhCN", "zhTW", "koKR", "ruRU", "ptBR", "itIT"
};
for (const char* loc : knownLocales) {
if (std::filesystem::exists(dataPath + "/" + loc)) {
detectedLocale = loc;
LOG_INFO("Auto-detected WoW locale: ", detectedLocale);
break;
}
}
if (detectedLocale.empty()) {
detectedLocale = "enUS";
LOG_WARNING("No locale directory found in data path; defaulting to enUS");
}
}
loadLocaleArchives(detectedLocale);
}
if (archives.empty()) { if (archives.empty()) {
LOG_WARNING("No MPQ archives loaded - will use loose file fallback"); LOG_WARNING("No MPQ archives loaded - will use loose file fallback");

View file

@ -17,7 +17,11 @@
#if WOWEE_HAS_AMD_FSR3_FRAMEGEN #if WOWEE_HAS_AMD_FSR3_FRAMEGEN
#include "third_party/ffx_fsr3_legacy_compat.h" #include "third_party/ffx_fsr3_legacy_compat.h"
#include <ffx_api.h>
#include <ffx_framegeneration.h>
#include <ffx_upscale.h>
#include <ffx_vk.h> #include <ffx_vk.h>
#include <vk/ffx_api_vk.h>
#endif #endif
namespace wowee::rendering { namespace wowee::rendering {
@ -34,6 +38,10 @@ struct AmdFsr3Runtime::RuntimeFns {
decltype(&ffxFsr3ConfigureFrameGeneration) fsr3ConfigureFrameGeneration = nullptr; decltype(&ffxFsr3ConfigureFrameGeneration) fsr3ConfigureFrameGeneration = nullptr;
decltype(&ffxFsr3DispatchFrameGeneration) fsr3DispatchFrameGeneration = nullptr; decltype(&ffxFsr3DispatchFrameGeneration) fsr3DispatchFrameGeneration = nullptr;
decltype(&ffxFsr3ContextDestroy) fsr3ContextDestroy = nullptr; decltype(&ffxFsr3ContextDestroy) fsr3ContextDestroy = nullptr;
PfnFfxCreateContext createContext = nullptr;
PfnFfxDestroyContext destroyContext = nullptr;
PfnFfxConfigure configure = nullptr;
PfnFfxDispatch dispatch = nullptr;
}; };
#else #else
struct AmdFsr3Runtime::RuntimeFns {}; struct AmdFsr3Runtime::RuntimeFns {};
@ -51,6 +59,43 @@ FfxErrorCode vkSwapchainConfigureNoop(const FfxFrameGenerationConfig*) {
return FFX_OK; return FFX_OK;
} }
std::string narrowWString(const wchar_t* msg) {
if (!msg) return {};
std::string out;
for (const wchar_t* p = msg; *p; ++p) {
const wchar_t wc = *p;
if (wc >= 0 && wc <= 0x7f) {
out.push_back(static_cast<char>(wc));
} else {
out.push_back('?');
}
}
return out;
}
void ffxApiLogMessage(uint32_t type, const wchar_t* message) {
const std::string narrowed = narrowWString(message);
if (type == FFX_API_MESSAGE_TYPE_ERROR) {
LOG_ERROR("FSR3 runtime/API: ", narrowed);
} else {
LOG_WARNING("FSR3 runtime/API: ", narrowed);
}
}
const char* ffxApiReturnCodeName(ffxReturnCode_t rc) {
switch (rc) {
case FFX_API_RETURN_OK: return "OK";
case FFX_API_RETURN_ERROR: return "ERROR";
case FFX_API_RETURN_ERROR_UNKNOWN_DESCTYPE: return "ERROR_UNKNOWN_DESCTYPE";
case FFX_API_RETURN_ERROR_RUNTIME_ERROR: return "ERROR_RUNTIME_ERROR";
case FFX_API_RETURN_NO_PROVIDER: return "NO_PROVIDER";
case FFX_API_RETURN_ERROR_MEMORY: return "ERROR_MEMORY";
case FFX_API_RETURN_ERROR_PARAMETER: return "ERROR_PARAMETER";
case FFX_API_RETURN_PROVIDER_NO_SUPPORT_NEW_DESCTYPE: return "PROVIDER_NO_SUPPORT_NEW_DESCTYPE";
default: return "UNKNOWN";
}
}
template <typename T, typename = void> template <typename T, typename = void>
struct HasUpscaleOutputSize : std::false_type {}; struct HasUpscaleOutputSize : std::false_type {};
@ -141,6 +186,7 @@ FfxResourceDescription makeResourceDescription(VkFormat format,
description.usage = usage; description.usage = usage;
return description; return description;
} }
} // namespace } // namespace
#endif #endif
@ -184,23 +230,36 @@ bool AmdFsr3Runtime::initialize(const AmdFsr3RuntimeInitDesc& desc) {
candidates.emplace_back("libffx_fsr3.so"); candidates.emplace_back("libffx_fsr3.so");
#endif #endif
std::string lastDlopenError;
for (const std::string& path : candidates) { for (const std::string& path : candidates) {
#if defined(_WIN32) #if defined(_WIN32)
HMODULE h = LoadLibraryA(path.c_str()); HMODULE h = LoadLibraryA(path.c_str());
if (!h) continue; if (!h) continue;
libHandle_ = reinterpret_cast<void*>(h); libHandle_ = reinterpret_cast<void*>(h);
#else #else
dlerror();
void* h = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); void* h = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
if (!h) continue; if (!h) {
const char* err = dlerror();
if (err && *err) lastDlopenError = err;
continue;
}
libHandle_ = h; libHandle_ = h;
#endif #endif
loadedLibraryPath_ = path; loadedLibraryPath_ = path;
loadPathKind_ = LoadPathKind::Official; loadPathKind_ = LoadPathKind::Official;
LOG_INFO("FSR3 runtime: opened library candidate ", loadedLibraryPath_);
break; break;
} }
if (!libHandle_) { if (!libHandle_) {
lastError_ = "no official runtime (Path A) found"; lastError_ = "no official runtime (Path A) found";
#if !defined(_WIN32)
if (!lastDlopenError.empty()) {
lastError_ += " dlopen error: ";
lastError_ += lastDlopenError;
}
#endif
return false; return false;
} }
@ -223,16 +282,127 @@ bool AmdFsr3Runtime::initialize(const AmdFsr3RuntimeInitDesc& desc) {
fns_->fsr3ConfigureFrameGeneration = reinterpret_cast<decltype(fns_->fsr3ConfigureFrameGeneration)>(resolveSym("ffxFsr3ConfigureFrameGeneration")); fns_->fsr3ConfigureFrameGeneration = reinterpret_cast<decltype(fns_->fsr3ConfigureFrameGeneration)>(resolveSym("ffxFsr3ConfigureFrameGeneration"));
fns_->fsr3DispatchFrameGeneration = reinterpret_cast<decltype(fns_->fsr3DispatchFrameGeneration)>(resolveSym("ffxFsr3DispatchFrameGeneration")); fns_->fsr3DispatchFrameGeneration = reinterpret_cast<decltype(fns_->fsr3DispatchFrameGeneration)>(resolveSym("ffxFsr3DispatchFrameGeneration"));
fns_->fsr3ContextDestroy = reinterpret_cast<decltype(fns_->fsr3ContextDestroy)>(resolveSym("ffxFsr3ContextDestroy")); fns_->fsr3ContextDestroy = reinterpret_cast<decltype(fns_->fsr3ContextDestroy)>(resolveSym("ffxFsr3ContextDestroy"));
fns_->createContext = reinterpret_cast<decltype(fns_->createContext)>(resolveSym("ffxCreateContext"));
fns_->destroyContext = reinterpret_cast<decltype(fns_->destroyContext)>(resolveSym("ffxDestroyContext"));
fns_->configure = reinterpret_cast<decltype(fns_->configure)>(resolveSym("ffxConfigure"));
fns_->dispatch = reinterpret_cast<decltype(fns_->dispatch)>(resolveSym("ffxDispatch"));
if (!fns_->getScratchMemorySizeVK || !fns_->getDeviceVK || !fns_->getInterfaceVK || const bool hasLegacyApi = (fns_->getScratchMemorySizeVK && fns_->getDeviceVK && fns_->getInterfaceVK &&
!fns_->getCommandListVK || !fns_->getResourceVK || !fns_->fsr3ContextCreate || !fns_->fsr3ContextDispatchUpscale || fns_->getCommandListVK && fns_->getResourceVK && fns_->fsr3ContextCreate &&
!fns_->fsr3ContextDestroy) { fns_->fsr3ContextDispatchUpscale && fns_->fsr3ContextDestroy);
LOG_WARNING("FSR3 runtime: required symbols not found in ", loadedLibraryPath_); const bool hasGenericApi = (fns_->createContext && fns_->destroyContext && fns_->configure && fns_->dispatch);
lastError_ = "missing required Vulkan FSR3 symbols in runtime library";
if (!hasLegacyApi && !hasGenericApi) {
LOG_WARNING("FSR3 runtime: required symbols not found in ", loadedLibraryPath_,
" (need legacy ffxFsr3* or generic ffxCreateContext/ffxDispatch)");
lastError_ = "missing required Vulkan FSR3 symbols in runtime library (legacy and generic APIs unavailable)";
shutdown(); shutdown();
return false; return false;
} }
apiMode_ = hasLegacyApi ? ApiMode::LegacyFsr3 : ApiMode::GenericApi;
if (apiMode_ == ApiMode::GenericApi) {
ffxConfigureDescGlobalDebug1 globalDebug{};
globalDebug.header.type = FFX_API_CONFIGURE_DESC_TYPE_GLOBALDEBUG1;
globalDebug.header.pNext = nullptr;
globalDebug.fpMessage = &ffxApiLogMessage;
globalDebug.debugLevel = FFX_API_CONFIGURE_GLOBALDEBUG_LEVEL_VERBOSE;
(void)fns_->configure(nullptr, reinterpret_cast<ffxConfigureDescHeader*>(&globalDebug));
ffxCreateBackendVKDesc backendDesc{};
backendDesc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_BACKEND_VK;
backendDesc.header.pNext = nullptr;
backendDesc.vkDevice = desc.device;
backendDesc.vkPhysicalDevice = desc.physicalDevice;
ffxCreateContextDescUpscaleVersion upVerDesc{};
upVerDesc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE_VERSION;
upVerDesc.header.pNext = nullptr;
upVerDesc.version = FFX_UPSCALER_VERSION;
ffxCreateContextDescUpscale upDesc{};
upDesc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE;
upDesc.header.pNext = reinterpret_cast<ffxApiHeader*>(&backendDesc);
upDesc.flags = FFX_UPSCALE_ENABLE_AUTO_EXPOSURE | FFX_UPSCALE_ENABLE_MOTION_VECTORS_JITTER_CANCELLATION;
upDesc.flags |= FFX_UPSCALE_ENABLE_DEBUG_CHECKING;
if (desc.hdrInput) upDesc.flags |= FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE;
if (desc.depthInverted) upDesc.flags |= FFX_UPSCALE_ENABLE_DEPTH_INVERTED;
upDesc.maxRenderSize.width = desc.maxRenderWidth;
upDesc.maxRenderSize.height = desc.maxRenderHeight;
upDesc.maxUpscaleSize.width = desc.displayWidth;
upDesc.maxUpscaleSize.height = desc.displayHeight;
upDesc.fpMessage = &ffxApiLogMessage;
backendDesc.header.pNext = reinterpret_cast<ffxApiHeader*>(&upVerDesc);
ffxContext upscaleCtx = nullptr;
const ffxReturnCode_t upCreateRc =
fns_->createContext(&upscaleCtx, reinterpret_cast<ffxCreateContextDescHeader*>(&upDesc), nullptr);
if (upCreateRc != FFX_API_RETURN_OK) {
const std::string loadedPath = loadedLibraryPath_;
lastError_ = "ffxCreateContext (upscale) failed rc=" + std::to_string(upCreateRc) +
" (" + ffxApiReturnCodeName(upCreateRc) + "), runtimeLib=" + loadedPath;
shutdown();
return false;
}
genericUpscaleContext_ = upscaleCtx;
backendDesc.header.pNext = nullptr;
if (desc.enableFrameGeneration) {
ffxCreateContextDescFrameGenerationVersion fgVerDesc{};
fgVerDesc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_FRAMEGENERATION_VERSION;
fgVerDesc.header.pNext = nullptr;
fgVerDesc.version = FFX_FRAMEGENERATION_VERSION;
ffxCreateContextDescFrameGeneration fgDesc{};
fgDesc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_FRAMEGENERATION;
fgDesc.header.pNext = reinterpret_cast<ffxApiHeader*>(&backendDesc);
fgDesc.flags = FFX_FRAMEGENERATION_ENABLE_MOTION_VECTORS_JITTER_CANCELLATION;
fgDesc.flags |= FFX_FRAMEGENERATION_ENABLE_DEBUG_CHECKING;
if (desc.hdrInput) fgDesc.flags |= FFX_FRAMEGENERATION_ENABLE_HIGH_DYNAMIC_RANGE;
if (desc.depthInverted) fgDesc.flags |= FFX_FRAMEGENERATION_ENABLE_DEPTH_INVERTED;
fgDesc.displaySize.width = desc.displayWidth;
fgDesc.displaySize.height = desc.displayHeight;
fgDesc.maxRenderSize.width = desc.maxRenderWidth;
fgDesc.maxRenderSize.height = desc.maxRenderHeight;
fgDesc.backBufferFormat = ffxApiGetSurfaceFormatVK(desc.colorFormat);
backendDesc.header.pNext = reinterpret_cast<ffxApiHeader*>(&fgVerDesc);
ffxContext fgCtx = nullptr;
const ffxReturnCode_t fgCreateRc =
fns_->createContext(&fgCtx, reinterpret_cast<ffxCreateContextDescHeader*>(&fgDesc), nullptr);
if (fgCreateRc != FFX_API_RETURN_OK) {
const std::string loadedPath = loadedLibraryPath_;
lastError_ = "ffxCreateContext (framegeneration) failed rc=" + std::to_string(fgCreateRc) +
" (" + ffxApiReturnCodeName(fgCreateRc) + "), runtimeLib=" + loadedPath;
shutdown();
return false;
}
genericFramegenContext_ = fgCtx;
backendDesc.header.pNext = nullptr;
ffxConfigureDescFrameGeneration fgCfg{};
fgCfg.header.type = FFX_API_CONFIGURE_DESC_TYPE_FRAMEGENERATION;
fgCfg.header.pNext = nullptr;
fgCfg.frameGenerationEnabled = true;
fgCfg.allowAsyncWorkloads = false;
fgCfg.flags = FFX_FRAMEGENERATION_FLAG_NO_SWAPCHAIN_CONTEXT_NOTIFY;
fgCfg.onlyPresentGenerated = false;
fgCfg.frameID = genericFrameId_;
if (fns_->configure(reinterpret_cast<ffxContext*>(&genericFramegenContext_),
reinterpret_cast<ffxConfigureDescHeader*>(&fgCfg)) != FFX_API_RETURN_OK) {
lastError_ = "ffxConfigure (framegeneration) failed";
shutdown();
return false;
}
frameGenerationReady_ = true;
}
ready_ = true;
LOG_INFO("FSR3 runtime: loaded generic API from ", loadedLibraryPath_,
" framegenReady=", frameGenerationReady_ ? "yes" : "no");
return true;
}
scratchBufferSize_ = fns_->getScratchMemorySizeVK(FFX_FSR3_CONTEXT_COUNT); scratchBufferSize_ = fns_->getScratchMemorySizeVK(FFX_FSR3_CONTEXT_COUNT);
if (scratchBufferSize_ == 0) { if (scratchBufferSize_ == 0) {
LOG_WARNING("FSR3 runtime: scratch buffer size query returned 0."); LOG_WARNING("FSR3 runtime: scratch buffer size query returned 0.");
@ -344,6 +514,49 @@ bool AmdFsr3Runtime::dispatchUpscale(const AmdFsr3RuntimeDispatchDesc& desc) {
lastError_ = "invalid upscale dispatch resources"; lastError_ = "invalid upscale dispatch resources";
return false; return false;
} }
if (apiMode_ == ApiMode::GenericApi) {
if (!genericUpscaleContext_ || !fns_->dispatch) {
lastError_ = "generic API upscale context unavailable";
return false;
}
ffxDispatchDescUpscale up{};
up.header.type = FFX_API_DISPATCH_DESC_TYPE_UPSCALE;
up.header.pNext = nullptr;
up.commandList = reinterpret_cast<void*>(desc.commandBuffer);
up.color = ffxApiGetResourceVK(desc.colorImage, desc.colorFormat, desc.renderWidth, desc.renderHeight, FFX_API_RESOURCE_STATE_COMPUTE_READ);
up.depth = ffxApiGetResourceVK(desc.depthImage, desc.depthFormat, desc.renderWidth, desc.renderHeight, FFX_API_RESOURCE_STATE_COMPUTE_READ);
up.motionVectors = ffxApiGetResourceVK(desc.motionVectorImage, desc.motionVectorFormat, desc.renderWidth, desc.renderHeight, FFX_API_RESOURCE_STATE_COMPUTE_READ);
up.exposure = FfxApiResource{};
up.reactive = FfxApiResource{};
up.transparencyAndComposition = FfxApiResource{};
up.output = ffxApiGetResourceVK(desc.outputImage, desc.outputFormat, desc.outputWidth, desc.outputHeight, FFX_API_RESOURCE_STATE_UNORDERED_ACCESS);
up.jitterOffset.x = desc.jitterX;
up.jitterOffset.y = desc.jitterY;
up.motionVectorScale.x = desc.motionScaleX;
up.motionVectorScale.y = desc.motionScaleY;
up.renderSize.width = desc.renderWidth;
up.renderSize.height = desc.renderHeight;
up.upscaleSize.width = desc.outputWidth;
up.upscaleSize.height = desc.outputHeight;
up.enableSharpening = false;
up.sharpness = 0.0f;
up.frameTimeDelta = std::max(0.001f, desc.frameTimeDeltaMs);
up.preExposure = 1.0f;
up.reset = desc.reset;
up.cameraNear = desc.cameraNear;
up.cameraFar = desc.cameraFar;
up.cameraFovAngleVertical = desc.cameraFovYRadians;
up.viewSpaceToMetersFactor = 1.0f;
up.flags = 0;
if (fns_->dispatch(reinterpret_cast<ffxContext*>(&genericUpscaleContext_),
reinterpret_cast<ffxDispatchDescHeader*>(&up)) != FFX_API_RETURN_OK) {
lastError_ = "ffxDispatch (upscale) failed";
return false;
}
lastError_.clear();
return true;
}
if (!contextStorage_ || !fns_->fsr3ContextDispatchUpscale) { if (!contextStorage_ || !fns_->fsr3ContextDispatchUpscale) {
lastError_ = "official runtime upscale context unavailable"; lastError_ = "official runtime upscale context unavailable";
return false; return false;
@ -413,6 +626,67 @@ bool AmdFsr3Runtime::dispatchFrameGeneration(const AmdFsr3RuntimeDispatchDesc& d
lastError_ = "invalid frame generation dispatch resources"; lastError_ = "invalid frame generation dispatch resources";
return false; return false;
} }
if (apiMode_ == ApiMode::GenericApi) {
if (!genericFramegenContext_ || !fns_->dispatch) {
lastError_ = "generic API frame generation context unavailable";
return false;
}
ffxDispatchDescFrameGenerationPrepareV2 prep{};
prep.header.type = FFX_API_DISPATCH_DESC_TYPE_FRAMEGENERATION_PREPARE_V2;
prep.header.pNext = nullptr;
prep.frameID = genericFrameId_;
prep.flags = 0;
prep.commandList = reinterpret_cast<void*>(desc.commandBuffer);
prep.renderSize.width = desc.renderWidth;
prep.renderSize.height = desc.renderHeight;
prep.jitterOffset.x = desc.jitterX;
prep.jitterOffset.y = desc.jitterY;
prep.motionVectorScale.x = desc.motionScaleX;
prep.motionVectorScale.y = desc.motionScaleY;
prep.frameTimeDelta = std::max(0.001f, desc.frameTimeDeltaMs);
prep.reset = desc.reset;
prep.cameraNear = desc.cameraNear;
prep.cameraFar = desc.cameraFar;
prep.cameraFovAngleVertical = desc.cameraFovYRadians;
prep.viewSpaceToMetersFactor = 1.0f;
prep.depth = ffxApiGetResourceVK(desc.depthImage, desc.depthFormat, desc.renderWidth, desc.renderHeight, FFX_API_RESOURCE_STATE_COMPUTE_READ);
prep.motionVectors = ffxApiGetResourceVK(desc.motionVectorImage, desc.motionVectorFormat, desc.renderWidth, desc.renderHeight, FFX_API_RESOURCE_STATE_COMPUTE_READ);
prep.cameraPosition[0] = prep.cameraPosition[1] = prep.cameraPosition[2] = 0.0f;
prep.cameraUp[0] = 0.0f; prep.cameraUp[1] = 1.0f; prep.cameraUp[2] = 0.0f;
prep.cameraRight[0] = 1.0f; prep.cameraRight[1] = 0.0f; prep.cameraRight[2] = 0.0f;
prep.cameraForward[0] = 0.0f; prep.cameraForward[1] = 0.0f; prep.cameraForward[2] = -1.0f;
if (fns_->dispatch(reinterpret_cast<ffxContext*>(&genericFramegenContext_),
reinterpret_cast<ffxDispatchDescHeader*>(&prep)) != FFX_API_RETURN_OK) {
lastError_ = "ffxDispatch (framegeneration prepare) failed";
return false;
}
ffxDispatchDescFrameGeneration fg{};
fg.header.type = FFX_API_DISPATCH_DESC_TYPE_FRAMEGENERATION;
fg.header.pNext = nullptr;
fg.commandList = reinterpret_cast<void*>(desc.commandBuffer);
fg.presentColor = ffxApiGetResourceVK(desc.outputImage, desc.outputFormat, desc.outputWidth, desc.outputHeight, FFX_API_RESOURCE_STATE_COMPUTE_READ);
fg.outputs[0] = ffxApiGetResourceVK(desc.frameGenOutputImage, desc.outputFormat, desc.outputWidth, desc.outputHeight, FFX_API_RESOURCE_STATE_UNORDERED_ACCESS);
fg.numGeneratedFrames = 1;
fg.reset = desc.reset;
fg.backbufferTransferFunction = FFX_API_BACKBUFFER_TRANSFER_FUNCTION_SRGB;
fg.minMaxLuminance[0] = 0.0f;
fg.minMaxLuminance[1] = 1.0f;
fg.generationRect.left = 0;
fg.generationRect.top = 0;
fg.generationRect.width = desc.outputWidth;
fg.generationRect.height = desc.outputHeight;
fg.frameID = genericFrameId_;
if (fns_->dispatch(reinterpret_cast<ffxContext*>(&genericFramegenContext_),
reinterpret_cast<ffxDispatchDescHeader*>(&fg)) != FFX_API_RETURN_OK) {
lastError_ = "ffxDispatch (framegeneration) failed";
return false;
}
++genericFrameId_;
lastError_.clear();
return true;
}
if (!contextStorage_ || !fns_->fsr3DispatchFrameGeneration) { if (!contextStorage_ || !fns_->fsr3DispatchFrameGeneration) {
lastError_ = "official runtime frame generation context unavailable"; lastError_ = "official runtime frame generation context unavailable";
return false; return false;
@ -449,7 +723,18 @@ bool AmdFsr3Runtime::dispatchFrameGeneration(const AmdFsr3RuntimeDispatchDesc& d
void AmdFsr3Runtime::shutdown() { void AmdFsr3Runtime::shutdown() {
#if WOWEE_HAS_AMD_FSR3_FRAMEGEN #if WOWEE_HAS_AMD_FSR3_FRAMEGEN
if (contextStorage_ && fns_ && fns_->fsr3ContextDestroy) { if (apiMode_ == ApiMode::GenericApi && fns_ && fns_->destroyContext) {
if (genericFramegenContext_) {
auto ctx = reinterpret_cast<ffxContext*>(&genericFramegenContext_);
fns_->destroyContext(ctx, nullptr);
genericFramegenContext_ = nullptr;
}
if (genericUpscaleContext_) {
auto ctx = reinterpret_cast<ffxContext*>(&genericUpscaleContext_);
fns_->destroyContext(ctx, nullptr);
genericUpscaleContext_ = nullptr;
}
} else if (contextStorage_ && fns_ && fns_->fsr3ContextDestroy) {
fns_->fsr3ContextDestroy(reinterpret_cast<FfxFsr3Context*>(contextStorage_)); fns_->fsr3ContextDestroy(reinterpret_cast<FfxFsr3Context*>(contextStorage_));
} }
#endif #endif
@ -476,6 +761,8 @@ void AmdFsr3Runtime::shutdown() {
libHandle_ = nullptr; libHandle_ = nullptr;
loadedLibraryPath_.clear(); loadedLibraryPath_.clear();
loadPathKind_ = LoadPathKind::None; loadPathKind_ = LoadPathKind::None;
apiMode_ = ApiMode::LegacyFsr3;
genericFrameId_ = 1;
} }
} // namespace wowee::rendering } // namespace wowee::rendering

View file

@ -751,14 +751,22 @@ void M2Renderer::destroyModelGPU(M2ModelGPU& model) {
void M2Renderer::destroyInstanceBones(M2Instance& inst) { void M2Renderer::destroyInstanceBones(M2Instance& inst) {
if (!vkCtx_) return; if (!vkCtx_) return;
VkDevice device = vkCtx_->getDevice();
VmaAllocator alloc = vkCtx_->getAllocator(); VmaAllocator alloc = vkCtx_->getAllocator();
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
// Free bone descriptor set so the pool slot is immediately reusable.
// Without this, the pool fills up over a play session as tiles stream
// in/out, eventually causing vkAllocateDescriptorSets to fail and
// making animated instances invisible (perceived as flickering).
if (inst.boneSet[i] != VK_NULL_HANDLE) {
vkFreeDescriptorSets(device, boneDescPool_, 1, &inst.boneSet[i]);
inst.boneSet[i] = VK_NULL_HANDLE;
}
if (inst.boneBuffer[i]) { if (inst.boneBuffer[i]) {
vmaDestroyBuffer(alloc, inst.boneBuffer[i], inst.boneAlloc[i]); vmaDestroyBuffer(alloc, inst.boneBuffer[i], inst.boneAlloc[i]);
inst.boneBuffer[i] = VK_NULL_HANDLE; inst.boneBuffer[i] = VK_NULL_HANDLE;
inst.boneMapped[i] = nullptr; inst.boneMapped[i] = nullptr;
} }
// boneSet freed when pool is reset/destroyed
} }
} }

View file

@ -201,7 +201,7 @@ void PerformanceHUD::render(const Renderer* renderer, const Camera* camera) {
} }
} }
if (renderer->isFSR2Enabled()) { if (renderer->isFSR2Enabled()) {
ImGui::TextColored(ImVec4(0.4f, 0.9f, 1.0f, 1.0f), "FSR 2.2: ON"); ImGui::TextColored(ImVec4(0.4f, 0.9f, 1.0f, 1.0f), "FSR 3 Upscale: ON");
ImGui::Text(" JitterSign=%.2f", renderer->getFSR2JitterSign()); ImGui::Text(" JitterSign=%.2f", renderer->getFSR2JitterSign());
const bool fgEnabled = renderer->isAmdFsr3FramegenEnabled(); const bool fgEnabled = renderer->isAmdFsr3FramegenEnabled();
const bool fgReady = renderer->isAmdFsr3FramegenRuntimeReady(); const bool fgReady = renderer->isAmdFsr3FramegenRuntimeReady();

View file

@ -706,9 +706,12 @@ bool Renderer::initialize(core::Window* win) {
lightingManager = std::make_unique<LightingManager>(); lightingManager = std::make_unique<LightingManager>();
[[maybe_unused]] auto* assetManager = core::Application::getInstance().getAssetManager(); [[maybe_unused]] auto* assetManager = core::Application::getInstance().getAssetManager();
// Create zone manager // Create zone manager; enrich music paths from DBC if available
zoneManager = std::make_unique<game::ZoneManager>(); zoneManager = std::make_unique<game::ZoneManager>();
zoneManager->initialize(); zoneManager->initialize();
if (assetManager) {
zoneManager->enrichFromDBC(assetManager);
}
// Initialize AudioEngine (singleton) // Initialize AudioEngine (singleton)
if (!audio::AudioEngine::instance().initialize()) { if (!audio::AudioEngine::instance().initialize()) {
@ -727,9 +730,6 @@ bool Renderer::initialize(core::Window* win) {
spellSoundManager = std::make_unique<audio::SpellSoundManager>(); spellSoundManager = std::make_unique<audio::SpellSoundManager>();
movementSoundManager = std::make_unique<audio::MovementSoundManager>(); movementSoundManager = std::make_unique<audio::MovementSoundManager>();
// TODO Phase 6: Vulkan underwater overlay, post-process, and shadow map
// GL versions stubbed during migration
// Create secondary command buffer resources for multithreaded rendering // Create secondary command buffer resources for multithreaded rendering
if (!createSecondaryCommandResources()) { if (!createSecondaryCommandResources()) {
LOG_WARNING("Failed to create secondary command buffers — falling back to single-threaded rendering"); LOG_WARNING("Failed to create secondary command buffers — falling back to single-threaded rendering");
@ -3037,9 +3037,12 @@ void Renderer::update(float deltaTime) {
// Update zone detection and music // Update zone detection and music
if (zoneManager && musicManager && terrainManager && camera) { if (zoneManager && musicManager && terrainManager && camera) {
// First check tile-based zone // Prefer server-authoritative zone ID (from SMSG_INIT_WORLD_STATES);
// fall back to tile-based lookup for single-player / offline mode.
const auto* gh = core::Application::getInstance().getGameHandler();
uint32_t serverZoneId = gh ? gh->getWorldStateZoneId() : 0;
auto tile = terrainManager->getCurrentTile(); auto tile = terrainManager->getCurrentTile();
uint32_t zoneId = zoneManager->getZoneId(tile.x, tile.y); uint32_t zoneId = (serverZoneId != 0) ? serverZoneId : zoneManager->getZoneId(tile.x, tile.y);
bool insideTavern = false; bool insideTavern = false;
bool insideBlacksmith = false; bool insideBlacksmith = false;
@ -3149,6 +3152,10 @@ void Renderer::update(float deltaTime) {
} }
} }
} }
// Update ambient sound manager zone type
if (ambientSoundManager) {
ambientSoundManager->setZoneId(zoneId);
}
} }
musicManager->update(deltaTime); musicManager->update(deltaTime);
@ -3899,7 +3906,9 @@ bool Renderer::initFSR2Resources() {
fsr2_.amdFsr3RuntimePath = "Path C"; fsr2_.amdFsr3RuntimePath = "Path C";
fsr2_.amdFsr3RuntimeLastError = fsr2_.amdFsr3Runtime->lastError(); fsr2_.amdFsr3RuntimeLastError = fsr2_.amdFsr3Runtime->lastError();
LOG_WARNING("FSR3 framegen toggle is enabled, but runtime initialization failed. ", LOG_WARNING("FSR3 framegen toggle is enabled, but runtime initialization failed. ",
"Set WOWEE_FFX_SDK_RUNTIME_LIB to the SDK runtime binary path."); "path=", fsr2_.amdFsr3RuntimePath,
" error=", fsr2_.amdFsr3RuntimeLastError.empty() ? "(none)" : fsr2_.amdFsr3RuntimeLastError,
" runtimeLib=", fsr2_.amdFsr3Runtime->loadedLibraryPath().empty() ? "(not loaded)" : fsr2_.amdFsr3Runtime->loadedLibraryPath());
} }
} }
#endif #endif
@ -4986,6 +4995,11 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s
terrainRenderer.reset(); terrainRenderer.reset();
return false; return false;
} }
if (shadowRenderPass != VK_NULL_HANDLE) {
terrainRenderer->initializeShadow(shadowRenderPass);
}
} else if (!terrainRenderer->hasShadowPipeline() && shadowRenderPass != VK_NULL_HANDLE) {
terrainRenderer->initializeShadow(shadowRenderPass);
} }
// Create water renderer if not already created // Create water renderer if not already created
@ -5154,6 +5168,11 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s
} }
cachedAssetManager = assetManager; cachedAssetManager = assetManager;
// Enrich zone music from DBC if not already done (e.g. asset manager was null at init).
if (zoneManager && assetManager) {
zoneManager->enrichFromDBC(assetManager);
}
} }
// Snap camera to ground // Snap camera to ground
@ -5707,6 +5726,9 @@ void Renderer::renderShadowPass() {
// Phase 7/8: render shadow casters // Phase 7/8: render shadow casters
const float shadowCullRadius = shadowDistance_ * 1.35f; const float shadowCullRadius = shadowDistance_ * 1.35f;
if (terrainRenderer) {
terrainRenderer->renderShadow(currentCmd, lightSpaceMatrix, shadowCenter, shadowCullRadius);
}
if (wmoRenderer) { if (wmoRenderer) {
wmoRenderer->renderShadow(currentCmd, lightSpaceMatrix, shadowCenter, shadowCullRadius); wmoRenderer->renderShadow(currentCmd, lightSpaceMatrix, shadowCenter, shadowCullRadius);
} }

View file

@ -314,6 +314,13 @@ void TerrainRenderer::shutdown() {
if (materialDescPool) { vkDestroyDescriptorPool(device, materialDescPool, nullptr); materialDescPool = VK_NULL_HANDLE; } if (materialDescPool) { vkDestroyDescriptorPool(device, materialDescPool, nullptr); materialDescPool = VK_NULL_HANDLE; }
if (materialSetLayout) { vkDestroyDescriptorSetLayout(device, materialSetLayout, nullptr); materialSetLayout = VK_NULL_HANDLE; } if (materialSetLayout) { vkDestroyDescriptorSetLayout(device, materialSetLayout, nullptr); materialSetLayout = VK_NULL_HANDLE; }
// Shadow pipeline cleanup
if (shadowPipeline_) { vkDestroyPipeline(device, shadowPipeline_, nullptr); shadowPipeline_ = VK_NULL_HANDLE; }
if (shadowPipelineLayout_) { vkDestroyPipelineLayout(device, shadowPipelineLayout_, nullptr); shadowPipelineLayout_ = VK_NULL_HANDLE; }
if (shadowParamsPool_) { vkDestroyDescriptorPool(device, shadowParamsPool_, nullptr); shadowParamsPool_ = VK_NULL_HANDLE; shadowParamsSet_ = VK_NULL_HANDLE; }
if (shadowParamsLayout_) { vkDestroyDescriptorSetLayout(device, shadowParamsLayout_, nullptr); shadowParamsLayout_ = VK_NULL_HANDLE; }
if (shadowParamsUBO_) { vmaDestroyBuffer(allocator, shadowParamsUBO_, shadowParamsAlloc_); shadowParamsUBO_ = VK_NULL_HANDLE; shadowParamsAlloc_ = VK_NULL_HANDLE; }
vkCtx = nullptr; vkCtx = nullptr;
} }
@ -784,8 +791,198 @@ void TerrainRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, c
} }
void TerrainRenderer::renderShadow(VkCommandBuffer /*cmd*/, const glm::vec3& /*shadowCenter*/, float /*halfExtent*/) { bool TerrainRenderer::initializeShadow(VkRenderPass shadowRenderPass) {
// Phase 6 stub if (!vkCtx || shadowRenderPass == VK_NULL_HANDLE) return false;
if (shadowPipeline_ != VK_NULL_HANDLE) return true; // already initialised
VkDevice device = vkCtx->getDevice();
VmaAllocator allocator = vkCtx->getAllocator();
// ShadowParams UBO — terrain uses no bones, no texture, no alpha test
struct ShadowParamsUBO {
int32_t useBones = 0;
int32_t useTexture = 0;
int32_t alphaTest = 0;
int32_t foliageSway = 0;
float windTime = 0.0f;
float foliageMotionDamp = 1.0f;
};
VkBufferCreateInfo bufCI{};
bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufCI.size = sizeof(ShadowParamsUBO);
bufCI.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
VmaAllocationCreateInfo allocCI{};
allocCI.usage = VMA_MEMORY_USAGE_CPU_TO_GPU;
allocCI.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT;
VmaAllocationInfo allocInfo{};
if (vmaCreateBuffer(allocator, &bufCI, &allocCI,
&shadowParamsUBO_, &shadowParamsAlloc_, &allocInfo) != VK_SUCCESS) {
LOG_ERROR("TerrainRenderer: failed to create shadow params UBO");
return false;
}
ShadowParamsUBO defaultParams{};
std::memcpy(allocInfo.pMappedData, &defaultParams, sizeof(defaultParams));
// Descriptor set layout: binding 0 = combined sampler (unused), binding 1 = ShadowParams UBO
VkDescriptorSetLayoutBinding layoutBindings[2]{};
layoutBindings[0].binding = 0;
layoutBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
layoutBindings[0].descriptorCount = 1;
layoutBindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
layoutBindings[1].binding = 1;
layoutBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
layoutBindings[1].descriptorCount = 1;
layoutBindings[1].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
VkDescriptorSetLayoutCreateInfo layoutCI{};
layoutCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutCI.bindingCount = 2;
layoutCI.pBindings = layoutBindings;
if (vkCreateDescriptorSetLayout(device, &layoutCI, nullptr, &shadowParamsLayout_) != VK_SUCCESS) {
LOG_ERROR("TerrainRenderer: failed to create shadow params set layout");
return false;
}
VkDescriptorPoolSize poolSizes[2]{};
poolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
poolSizes[0].descriptorCount = 1;
poolSizes[1].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSizes[1].descriptorCount = 1;
VkDescriptorPoolCreateInfo poolCI{};
poolCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolCI.maxSets = 1;
poolCI.poolSizeCount = 2;
poolCI.pPoolSizes = poolSizes;
if (vkCreateDescriptorPool(device, &poolCI, nullptr, &shadowParamsPool_) != VK_SUCCESS) {
LOG_ERROR("TerrainRenderer: failed to create shadow params pool");
return false;
}
VkDescriptorSetAllocateInfo setAlloc{};
setAlloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
setAlloc.descriptorPool = shadowParamsPool_;
setAlloc.descriptorSetCount = 1;
setAlloc.pSetLayouts = &shadowParamsLayout_;
if (vkAllocateDescriptorSets(device, &setAlloc, &shadowParamsSet_) != VK_SUCCESS) {
LOG_ERROR("TerrainRenderer: failed to allocate shadow params set");
return false;
}
// Write descriptors — sampler uses whiteTexture as dummy (useTexture=0 so never sampled)
VkDescriptorBufferInfo bufInfo{ shadowParamsUBO_, 0, sizeof(ShadowParamsUBO) };
VkDescriptorImageInfo imgInfo{};
imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
imgInfo.imageView = whiteTexture->getImageView();
imgInfo.sampler = whiteTexture->getSampler();
VkWriteDescriptorSet writes[2]{};
writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[0].dstSet = shadowParamsSet_;
writes[0].dstBinding = 0;
writes[0].descriptorCount = 1;
writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
writes[0].pImageInfo = &imgInfo;
writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[1].dstSet = shadowParamsSet_;
writes[1].dstBinding = 1;
writes[1].descriptorCount = 1;
writes[1].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
writes[1].pBufferInfo = &bufInfo;
vkUpdateDescriptorSets(device, 2, writes, 0, nullptr);
// Pipeline layout: set 0 = shadowParamsLayout_, push 128 bytes (lightSpaceMatrix + model)
VkPushConstantRange pc{};
pc.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
pc.offset = 0;
pc.size = 128;
shadowPipelineLayout_ = createPipelineLayout(device, {shadowParamsLayout_}, {pc});
if (!shadowPipelineLayout_) {
LOG_ERROR("TerrainRenderer: failed to create shadow pipeline layout");
return false;
}
VkShaderModule vertShader, fragShader;
if (!vertShader.loadFromFile(device, "assets/shaders/shadow.vert.spv")) {
LOG_ERROR("TerrainRenderer: failed to load shadow vertex shader");
return false;
}
if (!fragShader.loadFromFile(device, "assets/shaders/shadow.frag.spv")) {
LOG_ERROR("TerrainRenderer: failed to load shadow fragment shader");
vertShader.destroy();
return false;
}
// Terrain vertex layout: pos(0,off0) normal(1,off12) texCoord(2,off24) layerUV(3,off32)
// stride = sizeof(TerrainVertex) = 44 bytes
// Shadow shader expects: aPos(loc0), aTexCoord(loc1), aBoneWeights(loc2), aBoneIndicesF(loc3)
// Alias unused bone attrs to position (offset 0); useBones=0 so they are never read.
const uint32_t stride = static_cast<uint32_t>(sizeof(pipeline::TerrainVertex));
VkVertexInputBindingDescription vertBind{};
vertBind.binding = 0;
vertBind.stride = stride;
vertBind.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
std::vector<VkVertexInputAttributeDescription> vertAttrs = {
{0, 0, VK_FORMAT_R32G32B32_SFLOAT, 0}, // aPos -> position
{1, 0, VK_FORMAT_R32G32_SFLOAT, 24}, // aTexCoord -> texCoord (unused)
{2, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0}, // aBoneWeights -> position (unused)
{3, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0}, // aBoneIndices -> position (unused)
};
shadowPipeline_ = PipelineBuilder()
.setShaders(vertShader.stageInfo(VK_SHADER_STAGE_VERTEX_BIT),
fragShader.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT))
.setVertexInput({vertBind}, vertAttrs)
.setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
.setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE)
.setDepthTest(true, true, VK_COMPARE_OP_LESS_OR_EQUAL)
.setDepthBias(0.05f, 0.20f)
.setNoColorAttachment()
.setLayout(shadowPipelineLayout_)
.setRenderPass(shadowRenderPass)
.setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR})
.build(device);
vertShader.destroy();
fragShader.destroy();
if (!shadowPipeline_) {
LOG_ERROR("TerrainRenderer: failed to create shadow pipeline");
return false;
}
LOG_INFO("TerrainRenderer shadow pipeline initialized");
return true;
}
void TerrainRenderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMatrix,
const glm::vec3& shadowCenter, float shadowRadius) {
if (!shadowPipeline_ || !shadowParamsSet_) return;
if (chunks.empty()) return;
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipeline_);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipelineLayout_,
0, 1, &shadowParamsSet_, 0, nullptr);
// Identity model matrix — terrain vertices are already in world space
static const glm::mat4 identity(1.0f);
struct ShadowPush { glm::mat4 lightSpaceMatrix; glm::mat4 model; };
ShadowPush push{ lightSpaceMatrix, identity };
vkCmdPushConstants(cmd, shadowPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT,
0, 128, &push);
for (const auto& chunk : chunks) {
if (!chunk.isValid()) continue;
// Sphere-cull chunk against shadow region
glm::vec3 diff = chunk.boundingSphereCenter - shadowCenter;
float distSq = glm::dot(diff, diff);
float combinedRadius = shadowRadius + chunk.boundingSphereRadius;
if (distSq > combinedRadius * combinedRadius) continue;
VkDeviceSize offset = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &chunk.vertexBuffer, &offset);
vkCmdBindIndexBuffer(cmd, chunk.indexBuffer, 0, VK_INDEX_TYPE_UINT16);
vkCmdDrawIndexed(cmd, chunk.indexCount, 1, 0, 0, 0);
}
} }
void TerrainRenderer::removeTile(int tileX, int tileY) { void TerrainRenderer::removeTile(int tileX, int tileY) {

View file

@ -1,69 +0,0 @@
#include "rendering/texture.hpp"
#include "core/logger.hpp"
// Stub implementation - would use stb_image or similar
namespace wowee {
namespace rendering {
Texture::~Texture() {
if (textureID) {
glDeleteTextures(1, &textureID);
}
}
bool Texture::loadFromFile(const std::string& path) {
// TODO: Implement with stb_image or BLP loader
LOG_WARNING("Texture loading not yet implemented: ", path);
return false;
}
bool Texture::loadFromMemory(const unsigned char* data, int w, int h, int channels) {
width = w;
height = h;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
GLenum format = (channels == 4) ? GL_RGBA : GL_RGB;
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glGenerateMipmap(GL_TEXTURE_2D);
applyAnisotropicFiltering();
glBindTexture(GL_TEXTURE_2D, 0);
return true;
}
void Texture::bind(GLuint unit) const {
glActiveTexture(GL_TEXTURE0 + unit);
glBindTexture(GL_TEXTURE_2D, textureID);
}
void Texture::unbind() const {
glBindTexture(GL_TEXTURE_2D, 0);
}
void applyAnisotropicFiltering() {
static float maxAniso = -1.0f;
if (maxAniso < 0.0f) {
if (GLEW_EXT_texture_filter_anisotropic) {
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &maxAniso);
if (maxAniso < 1.0f) maxAniso = 1.0f;
} else {
maxAniso = 0.0f; // Extension not available
}
}
if (maxAniso > 0.0f) {
float desired = 16.0f;
float clamped = (desired < maxAniso) ? desired : maxAniso;
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, clamped);
}
}
} // namespace rendering
} // namespace wowee

View file

@ -942,13 +942,10 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu
if (surface.origin.z > 2000.0f || surface.origin.z < -500.0f) return; if (surface.origin.z > 2000.0f || surface.origin.z < -500.0f) return;
// Build tile mask from MLIQ flags and per-vertex heights // Build tile mask from MLIQ flags
size_t tileCount = static_cast<size_t>(surface.width) * static_cast<size_t>(surface.height); size_t tileCount = static_cast<size_t>(surface.width) * static_cast<size_t>(surface.height);
size_t maskBytes = (tileCount + 7) / 8; size_t maskBytes = (tileCount + 7) / 8;
surface.mask.assign(maskBytes, 0x00); surface.mask.assign(maskBytes, 0x00);
const float baseZ = liquid.basePosition.z;
const bool hasHeights = !liquid.heights.empty() &&
liquid.heights.size() >= static_cast<size_t>(vertexCount);
for (size_t t = 0; t < tileCount; t++) { for (size_t t = 0; t < tileCount; t++) {
bool hasLiquid = true; bool hasLiquid = true;
int tx = static_cast<int>(t) % surface.width; int tx = static_cast<int>(t) % surface.width;

File diff suppressed because it is too large Load diff

View file

@ -1090,6 +1090,11 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) {
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
if (ImGui::BeginTabItem("Reputation")) {
renderReputationPanel(gameHandler);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Skills")) { if (ImGui::BeginTabItem("Skills")) {
const auto& skills = gameHandler.getPlayerSkills(); const auto& skills = gameHandler.getPlayerSkills();
if (skills.empty()) { if (skills.empty()) {
@ -1171,6 +1176,89 @@ void InventoryScreen::renderCharacterScreen(game::GameHandler& gameHandler) {
} }
} }
void InventoryScreen::renderReputationPanel(game::GameHandler& gameHandler) {
const auto& standings = gameHandler.getFactionStandings();
if (standings.empty()) {
ImGui::Spacing();
ImGui::TextDisabled("No reputation data received yet.");
ImGui::TextDisabled("Reputation updates as you kill enemies and complete quests.");
return;
}
// WoW reputation tier breakpoints (cumulative from floor -42000)
// Tier name, threshold for next rank, bar color
struct RepTier {
const char* name;
int32_t floor; // raw value where this tier begins
int32_t ceiling; // raw value where the next tier begins
ImVec4 color;
};
static const RepTier tiers[] = {
{ "Hated", -42000, -6001, ImVec4(0.6f, 0.1f, 0.1f, 1.0f) },
{ "Hostile", -6000, -3001, ImVec4(0.8f, 0.2f, 0.1f, 1.0f) },
{ "Unfriendly", -3000, -1, ImVec4(0.9f, 0.5f, 0.1f, 1.0f) },
{ "Neutral", 0, 2999, ImVec4(0.8f, 0.8f, 0.2f, 1.0f) },
{ "Friendly", 3000, 8999, ImVec4(0.2f, 0.7f, 0.2f, 1.0f) },
{ "Honored", 9000, 20999, ImVec4(0.2f, 0.8f, 0.5f, 1.0f) },
{ "Revered", 21000, 41999, ImVec4(0.3f, 0.6f, 1.0f, 1.0f) },
{ "Exalted", 42000, 42000, ImVec4(1.0f, 0.84f, 0.0f, 1.0f) },
};
auto getTier = [&](int32_t val) -> const RepTier& {
for (int i = 6; i >= 0; --i) {
if (val >= tiers[i].floor) return tiers[i];
}
return tiers[0];
};
ImGui::BeginChild("##ReputationList", ImVec2(0, 0), true);
// Sort factions alphabetically by name
std::vector<std::pair<uint32_t, int32_t>> sortedFactions(standings.begin(), standings.end());
std::sort(sortedFactions.begin(), sortedFactions.end(),
[&](const auto& a, const auto& b) {
const std::string& na = gameHandler.getFactionNamePublic(a.first);
const std::string& nb = gameHandler.getFactionNamePublic(b.first);
return na < nb;
});
for (const auto& [factionId, standing] : sortedFactions) {
const RepTier& tier = getTier(standing);
const std::string& factionName = gameHandler.getFactionNamePublic(factionId);
const char* displayName = factionName.empty() ? "Unknown Faction" : factionName.c_str();
// Faction name + tier label on same line
ImGui::TextColored(tier.color, "[%s]", tier.name);
ImGui::SameLine(90.0f);
ImGui::Text("%s", displayName);
// Progress bar showing position within current tier
float ratio = 0.0f;
char overlay[64] = "";
if (tier.floor == 42000) {
// Exalted — full bar
ratio = 1.0f;
snprintf(overlay, sizeof(overlay), "Exalted");
} else {
int32_t tierRange = tier.ceiling - tier.floor + 1;
int32_t inTier = standing - tier.floor;
ratio = static_cast<float>(inTier) / static_cast<float>(tierRange);
ratio = std::max(0.0f, std::min(1.0f, ratio));
snprintf(overlay, sizeof(overlay), "%d / %d",
inTier < 0 ? 0 : inTier, tierRange);
}
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, tier.color);
ImGui::SetNextItemWidth(-1.0f);
ImGui::ProgressBar(ratio, ImVec2(0, 12.0f), overlay);
ImGui::PopStyleColor();
ImGui::Spacing();
}
ImGui::EndChild();
}
void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) { void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Equipment"); ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Equipment");
ImGui::Separator(); ImGui::Separator();

View file

@ -0,0 +1,202 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SDK_ROOT="${1:-$ROOT_DIR/extern/FidelityFX-SDK}"
KITS_DIR="$SDK_ROOT/Kits/FidelityFX"
FFX_SC="$KITS_DIR/tools/ffx_sc/ffx_sc.py"
OUT_DIR="$KITS_DIR/framegeneration/fsr3/internal/permutations/vk"
SHADER_DIR="$KITS_DIR/upscalers/fsr3/internal/shaders"
if [[ ! -f "$FFX_SC" ]]; then
echo "Missing ffx_sc.py at $FFX_SC" >&2
exit 1
fi
required_headers=(
"$OUT_DIR/ffx_fsr2_accumulate_pass_wave64_16bit_permutations.h"
"$OUT_DIR/ffx_fsr3upscaler_accumulate_pass_wave64_16bit_permutations.h"
"$OUT_DIR/ffx_fsr3upscaler_autogen_reactive_pass_permutations.h"
)
if [[ "${WOWEE_FORCE_REGEN_PERMS:-0}" != "1" ]]; then
missing=0
for h in "${required_headers[@]}"; do
[[ -f "$h" ]] || missing=1
done
if [[ $missing -eq 0 ]]; then
echo "FidelityFX VK permutation headers already present."
exit 0
fi
fi
if [[ -z "${DXC:-}" ]]; then
if [[ -x /tmp/dxc/bin/dxc ]]; then
export DXC=/tmp/dxc/bin/dxc
elif command -v dxc >/dev/null 2>&1; then
export DXC="$(command -v dxc)"
elif [[ "$(uname -s)" == "Linux" ]]; then
_arch="$(uname -m)"
if [[ "$_arch" == "aarch64" || "$_arch" == "arm64" ]]; then
echo "Linux aarch64: no official arm64 DXC release available." >&2
echo "Install 'directx-shader-compiler' via apt or set DXC=/path/to/dxc to regenerate." >&2
echo "Skipping VK permutation codegen (permutations may be pre-built in the SDK checkout)."
exit 0
fi
echo "DXC not found; downloading Linux DXC release to /tmp/dxc ..."
tmp_json="$(mktemp)"
curl -sS https://api.github.com/repos/microsoft/DirectXShaderCompiler/releases/latest > "$tmp_json"
dxc_url="$(python3 - << 'PY' "$tmp_json"
import json, sys
with open(sys.argv[1], 'r', encoding='utf-8') as f:
data = json.load(f)
for a in data.get('assets', []):
name = a.get('name', '')
if name.startswith('linux_dxc_') and name.endswith('.x86_64.tar.gz'):
print(a.get('browser_download_url', ''))
break
PY
)"
rm -f "$tmp_json"
if [[ -z "$dxc_url" ]]; then
echo "Failed to locate Linux DXC release asset URL." >&2
exit 1
fi
rm -rf /tmp/dxc /tmp/linux_dxc.tar.gz
curl -L --fail "$dxc_url" -o /tmp/linux_dxc.tar.gz
mkdir -p /tmp/dxc
tar -xzf /tmp/linux_dxc.tar.gz -C /tmp/dxc --strip-components=1
export DXC=/tmp/dxc/bin/dxc
elif [[ "$(uname -s)" =~ MINGW|MSYS|CYGWIN ]]; then
echo "DXC not found; downloading Windows DXC release to /tmp/dxc ..."
tmp_json="$(mktemp)"
curl -sS https://api.github.com/repos/microsoft/DirectXShaderCompiler/releases/latest > "$tmp_json"
dxc_url="$(python3 - << 'PY' "$tmp_json"
import json, sys
with open(sys.argv[1], 'r', encoding='utf-8') as f:
data = json.load(f)
for a in data.get('assets', []):
name = a.get('name', '')
if name.startswith('dxc_') and name.endswith('.zip'):
print(a.get('browser_download_url', ''))
break
PY
)"
rm -f "$tmp_json"
if [[ -z "$dxc_url" ]]; then
echo "Failed to locate Windows DXC release asset URL." >&2
exit 1
fi
rm -rf /tmp/dxc /tmp/dxc_win.zip
curl -L --fail "$dxc_url" -o /tmp/dxc_win.zip
mkdir -p /tmp/dxc
unzip -q /tmp/dxc_win.zip -d /tmp/dxc
if [[ -x /tmp/dxc/bin/x64/dxc.exe ]]; then
export DXC=/tmp/dxc/bin/x64/dxc.exe
elif [[ -x /tmp/dxc/bin/x86/dxc.exe ]]; then
export DXC=/tmp/dxc/bin/x86/dxc.exe
else
echo "DXC download succeeded, but dxc.exe was not found." >&2
exit 1
fi
else
echo "DXC not found. Set DXC=/path/to/dxc or install to /tmp/dxc/bin/dxc" >&2
exit 1
fi
fi
mkdir -p "$OUT_DIR"
# First generate frame interpolation + optical flow permutations via SDK script.
(
cd "$SDK_ROOT"
./generate_vk_permutations.sh
)
BASE_ARGS=(-reflection -embed-arguments -E CS -Wno-for-redefinition -Wno-ambig-lit-shift -DFFX_GPU=1 -DFFX_HLSL=1 -DFFX_IMPLICIT_SHADER_REGISTER_BINDING_HLSL=0)
WAVE32=(-DFFX_HLSL_SM=62 -T cs_6_2)
WAVE64=("-DFFX_PREFER_WAVE64=[WaveSize(64)]" -DFFX_HLSL_SM=66 -T cs_6_6)
BIT16=(-DFFX_HALF=1 -enable-16bit-types)
compile_shader() {
local file="$1"; shift
local name="$1"; shift
python3 "$FFX_SC" "${BASE_ARGS[@]}" "$@" -name="$name" -output="$OUT_DIR" "$file"
}
# FSR2 (for upscalers/fsr3/internal/ffx_fsr2_shaderblobs.cpp)
FSR2_COMMON=(
-DFFX_FSR2_EMBED_ROOTSIG=0
-DFFX_FSR2_OPTION_UPSAMPLE_SAMPLERS_USE_DATA_HALF=0
-DFFX_FSR2_OPTION_ACCUMULATE_SAMPLERS_USE_DATA_HALF=0
-DFFX_FSR2_OPTION_REPROJECT_SAMPLERS_USE_DATA_HALF=1
-DFFX_FSR2_OPTION_POSTPROCESSLOCKSTATUS_SAMPLERS_USE_DATA_HALF=0
-DFFX_FSR2_OPTION_UPSAMPLE_USE_LANCZOS_TYPE=2
"-DFFX_FSR2_OPTION_REPROJECT_USE_LANCZOS_TYPE={0,1}"
"-DFFX_FSR2_OPTION_HDR_COLOR_INPUT={0,1}"
"-DFFX_FSR2_OPTION_LOW_RESOLUTION_MOTION_VECTORS={0,1}"
"-DFFX_FSR2_OPTION_JITTERED_MOTION_VECTORS={0,1}"
"-DFFX_FSR2_OPTION_INVERTED_DEPTH={0,1}"
"-DFFX_FSR2_OPTION_APPLY_SHARPENING={0,1}"
-I "$KITS_DIR/api/internal/include/gpu"
-I "$KITS_DIR/upscalers/fsr3/include/gpu"
)
FSR2_SHADERS=(
ffx_fsr2_autogen_reactive_pass
ffx_fsr2_accumulate_pass
ffx_fsr2_compute_luminance_pyramid_pass
ffx_fsr2_depth_clip_pass
ffx_fsr2_lock_pass
ffx_fsr2_reconstruct_previous_depth_pass
ffx_fsr2_rcas_pass
ffx_fsr2_tcr_autogen_pass
)
for shader in "${FSR2_SHADERS[@]}"; do
file="$SHADER_DIR/$shader.hlsl"
[[ -f "$file" ]] || continue
compile_shader "$file" "$shader" -DFFX_HALF=0 "${WAVE32[@]}" "${FSR2_COMMON[@]}"
compile_shader "$file" "${shader}_wave64" -DFFX_HALF=0 "${WAVE64[@]}" "${FSR2_COMMON[@]}"
compile_shader "$file" "${shader}_16bit" "${BIT16[@]}" "${WAVE32[@]}" "${FSR2_COMMON[@]}"
compile_shader "$file" "${shader}_wave64_16bit" "${BIT16[@]}" "${WAVE64[@]}" "${FSR2_COMMON[@]}"
done
# FSR3 upscaler (for upscalers/fsr3/internal/ffx_fsr3upscaler_shaderblobs.cpp)
FSR3_COMMON=(
-DFFX_FSR3UPSCALER_EMBED_ROOTSIG=0
-DFFX_FSR3UPSCALER_OPTION_UPSAMPLE_SAMPLERS_USE_DATA_HALF=0
-DFFX_FSR3UPSCALER_OPTION_ACCUMULATE_SAMPLERS_USE_DATA_HALF=0
-DFFX_FSR3UPSCALER_OPTION_REPROJECT_SAMPLERS_USE_DATA_HALF=1
-DFFX_FSR3UPSCALER_OPTION_POSTPROCESSLOCKSTATUS_SAMPLERS_USE_DATA_HALF=0
-DFFX_FSR3UPSCALER_OPTION_UPSAMPLE_USE_LANCZOS_TYPE=2
"-DFFX_FSR3UPSCALER_OPTION_REPROJECT_USE_LANCZOS_TYPE={0,1}"
"-DFFX_FSR3UPSCALER_OPTION_HDR_COLOR_INPUT={0,1}"
"-DFFX_FSR3UPSCALER_OPTION_LOW_RESOLUTION_MOTION_VECTORS={0,1}"
"-DFFX_FSR3UPSCALER_OPTION_JITTERED_MOTION_VECTORS={0,1}"
"-DFFX_FSR3UPSCALER_OPTION_INVERTED_DEPTH={0,1}"
"-DFFX_FSR3UPSCALER_OPTION_APPLY_SHARPENING={0,1}"
-I "$KITS_DIR/api/internal/gpu"
-I "$KITS_DIR/upscalers/fsr3/include/gpu"
)
FSR3_SHADERS=(
ffx_fsr3upscaler_autogen_reactive_pass
ffx_fsr3upscaler_accumulate_pass
ffx_fsr3upscaler_luma_pyramid_pass
ffx_fsr3upscaler_prepare_reactivity_pass
ffx_fsr3upscaler_prepare_inputs_pass
ffx_fsr3upscaler_shading_change_pass
ffx_fsr3upscaler_rcas_pass
ffx_fsr3upscaler_shading_change_pyramid_pass
ffx_fsr3upscaler_luma_instability_pass
ffx_fsr3upscaler_debug_view_pass
)
for shader in "${FSR3_SHADERS[@]}"; do
file="$SHADER_DIR/$shader.hlsl"
[[ -f "$file" ]] || continue
compile_shader "$file" "$shader" -DFFX_HALF=0 "${WAVE32[@]}" "${FSR3_COMMON[@]}"
compile_shader "$file" "${shader}_wave64" -DFFX_HALF=0 "${WAVE64[@]}" "${FSR3_COMMON[@]}"
compile_shader "$file" "${shader}_16bit" "${BIT16[@]}" "${WAVE32[@]}" "${FSR3_COMMON[@]}"
compile_shader "$file" "${shader}_wave64_16bit" "${BIT16[@]}" "${WAVE64[@]}" "${FSR3_COMMON[@]}"
done
echo "Generated VK permutation headers in $OUT_DIR"