mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Compare commits
36 commits
2f0809b570
...
4be7910fdf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4be7910fdf | ||
|
|
b5a2175269 | ||
|
|
b3d8651db9 | ||
|
|
cda703b0f4 | ||
|
|
3202c1392d | ||
|
|
bc6cd6e5f2 | ||
|
|
9578e123cc | ||
|
|
71597c9a03 | ||
|
|
589ec3c263 | ||
|
|
0d002c9070 | ||
|
|
176b8bdc3d | ||
|
|
1808d98978 | ||
|
|
1aa404d670 | ||
|
|
f3415c2aff | ||
|
|
332c2f6d3f | ||
|
|
7220737d48 | ||
|
|
46365f4738 | ||
|
|
82d00c94c0 | ||
|
|
9809106a84 | ||
|
|
a8fd977a53 | ||
|
|
a3e0d36a72 | ||
|
|
3092d406fa | ||
|
|
0d9404c704 | ||
|
|
f7a79b436e | ||
|
|
e6741f815a | ||
|
|
79c8d93c45 | ||
|
|
593f06bdf7 | ||
|
|
dd67c88175 | ||
|
|
ed48a3c425 | ||
|
|
9d0da6242d | ||
|
|
d3241dce9e | ||
|
|
fed03f970c | ||
|
|
8493729a10 | ||
|
|
750b270502 | ||
|
|
dd7d74cb93 | ||
|
|
d6e398d814 |
23 changed files with 885 additions and 458 deletions
|
|
@ -550,6 +550,7 @@ set(WOWEE_SOURCES
|
||||||
src/ui/quest_log_screen.cpp
|
src/ui/quest_log_screen.cpp
|
||||||
src/ui/spellbook_screen.cpp
|
src/ui/spellbook_screen.cpp
|
||||||
src/ui/talent_screen.cpp
|
src/ui/talent_screen.cpp
|
||||||
|
src/ui/keybinding_manager.cpp
|
||||||
|
|
||||||
# Main
|
# Main
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
|
|
@ -653,6 +654,7 @@ set(WOWEE_HEADERS
|
||||||
include/ui/inventory_screen.hpp
|
include/ui/inventory_screen.hpp
|
||||||
include/ui/spellbook_screen.hpp
|
include/ui/spellbook_screen.hpp
|
||||||
include/ui/talent_screen.hpp
|
include/ui/talent_screen.hpp
|
||||||
|
include/ui/keybinding_manager.hpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set(WOWEE_PLATFORM_SOURCES)
|
set(WOWEE_PLATFORM_SOURCES)
|
||||||
|
|
|
||||||
|
|
@ -389,6 +389,7 @@ public:
|
||||||
network::Packet buildCastSpell(uint32_t spellId, uint64_t targetGuid, uint8_t castCount) override;
|
network::Packet buildCastSpell(uint32_t spellId, uint64_t targetGuid, uint8_t castCount) override;
|
||||||
network::Packet buildUseItem(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid, uint32_t spellId = 0) override;
|
network::Packet buildUseItem(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid, uint32_t spellId = 0) override;
|
||||||
bool parseCastFailed(network::Packet& packet, CastFailedData& data) override;
|
bool parseCastFailed(network::Packet& packet, CastFailedData& data) override;
|
||||||
|
bool parseCastResult(network::Packet& packet, uint32_t& spellId, uint8_t& result) override;
|
||||||
bool parseMessageChat(network::Packet& packet, MessageChatData& data) override;
|
bool parseMessageChat(network::Packet& packet, MessageChatData& data) override;
|
||||||
bool parseGameObjectQueryResponse(network::Packet& packet, GameObjectQueryResponseData& data) override;
|
bool parseGameObjectQueryResponse(network::Packet& packet, GameObjectQueryResponseData& data) override;
|
||||||
// Classic 1.12 SMSG_CREATURE_QUERY_RESPONSE lacks the iconName string that TBC/WotLK include
|
// Classic 1.12 SMSG_CREATURE_QUERY_RESPONSE lacks the iconName string that TBC/WotLK include
|
||||||
|
|
@ -418,6 +419,10 @@ public:
|
||||||
bool parseMonsterMove(network::Packet& packet, MonsterMoveData& data) override {
|
bool parseMonsterMove(network::Packet& packet, MonsterMoveData& data) override {
|
||||||
return MonsterMoveParser::parseVanilla(packet, data);
|
return MonsterMoveParser::parseVanilla(packet, data);
|
||||||
}
|
}
|
||||||
|
// Classic 1.12 SMSG_INITIAL_SPELLS: uint16 spellId + uint16 slot per entry (not uint32 + uint16)
|
||||||
|
bool parseInitialSpells(network::Packet& packet, InitialSpellsData& data) override {
|
||||||
|
return InitialSpellsParser::parse(packet, data, /*vanillaFormat=*/true);
|
||||||
|
}
|
||||||
// Classic 1.12 uses PackedGuid (not full uint64) and uint16 castFlags (not uint32)
|
// Classic 1.12 uses PackedGuid (not full uint64) and uint16 castFlags (not uint32)
|
||||||
bool parseSpellStart(network::Packet& packet, SpellStartData& data) override;
|
bool parseSpellStart(network::Packet& packet, SpellStartData& data) override;
|
||||||
bool parseSpellGo(network::Packet& packet, SpellGoData& data) override;
|
bool parseSpellGo(network::Packet& packet, SpellGoData& data) override;
|
||||||
|
|
|
||||||
|
|
@ -1758,7 +1758,10 @@ struct InitialSpellsData {
|
||||||
|
|
||||||
class InitialSpellsParser {
|
class InitialSpellsParser {
|
||||||
public:
|
public:
|
||||||
static bool parse(network::Packet& packet, InitialSpellsData& data);
|
// vanillaFormat=true: Classic 1.12 uint16 spellId + uint16 slot (4 bytes/spell)
|
||||||
|
// vanillaFormat=false: TBC/WotLK uint32 spellId + uint16 unk (6 bytes/spell)
|
||||||
|
static bool parse(network::Packet& packet, InitialSpellsData& data,
|
||||||
|
bool vanillaFormat = false);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** CMSG_CAST_SPELL packet builder */
|
/** CMSG_CAST_SPELL packet builder */
|
||||||
|
|
@ -2015,7 +2018,9 @@ public:
|
||||||
/** SMSG_LOOT_RESPONSE parser */
|
/** SMSG_LOOT_RESPONSE parser */
|
||||||
class LootResponseParser {
|
class LootResponseParser {
|
||||||
public:
|
public:
|
||||||
static bool parse(network::Packet& packet, LootResponseData& data);
|
// isWotlkFormat: true for WotLK 3.3.5a (22 bytes/item with randomSuffix+randomProp),
|
||||||
|
// false for Classic 1.12 and TBC 2.4.3 (14 bytes/item).
|
||||||
|
static bool parse(network::Packet& packet, LootResponseData& data, bool isWotlkFormat = true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -217,6 +217,10 @@ private:
|
||||||
static glm::quat interpolateQuat(const pipeline::M2AnimationTrack& track,
|
static glm::quat interpolateQuat(const pipeline::M2AnimationTrack& track,
|
||||||
int seqIdx, float time);
|
int seqIdx, float time);
|
||||||
|
|
||||||
|
// Attachment point lookup helper — shared by attachWeapon() and getAttachmentTransform()
|
||||||
|
bool findAttachmentBone(uint32_t modelId, uint32_t attachmentId,
|
||||||
|
uint16_t& outBoneIndex, glm::vec3& outOffset) const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* Build a composited character skin texture by alpha-blending overlay
|
* Build a composited character skin texture by alpha-blending overlay
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include <vulkan/vulkan.h>
|
#include <vulkan/vulkan.h>
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace rendering {
|
namespace rendering {
|
||||||
|
|
@ -25,5 +26,38 @@ struct GPUPushConstants {
|
||||||
glm::mat4 model;
|
glm::mat4 model;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Push constants for shadow rendering passes
|
||||||
|
struct ShadowPush {
|
||||||
|
glm::mat4 lightSpaceMatrix;
|
||||||
|
glm::mat4 model;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Uniform buffer for shadow rendering parameters (matches shader std140 layout)
|
||||||
|
struct ShadowParamsUBO {
|
||||||
|
int32_t useBones;
|
||||||
|
int32_t useTexture;
|
||||||
|
int32_t alphaTest;
|
||||||
|
int32_t foliageSway;
|
||||||
|
float windTime;
|
||||||
|
float foliageMotionDamp;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Timer utility for performance profiling queries
|
||||||
|
struct QueryTimer {
|
||||||
|
double* totalMs = nullptr;
|
||||||
|
uint32_t* callCount = nullptr;
|
||||||
|
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
|
||||||
|
QueryTimer(double* total, uint32_t* calls) : totalMs(total), callCount(calls) {}
|
||||||
|
~QueryTimer() {
|
||||||
|
if (callCount) {
|
||||||
|
(*callCount)++;
|
||||||
|
}
|
||||||
|
if (totalMs) {
|
||||||
|
auto end = std::chrono::steady_clock::now();
|
||||||
|
*totalMs += std::chrono::duration<double, std::milli>(end - start).count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace rendering
|
} // namespace rendering
|
||||||
} // namespace wowee
|
} // namespace wowee
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
#include <vulkan/vulkan.h>
|
#include <vulkan/vulkan.h>
|
||||||
#include <vk_mem_alloc.h>
|
#include <vk_mem_alloc.h>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <limits>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace rendering {
|
namespace rendering {
|
||||||
|
|
@ -56,5 +58,25 @@ inline bool vkCheck(VkResult result, [[maybe_unused]] const char* msg) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Environment variable utility functions
|
||||||
|
inline size_t envSizeMBOrDefault(const char* name, size_t defMb) {
|
||||||
|
const char* v = std::getenv(name);
|
||||||
|
if (!v || !*v) return defMb;
|
||||||
|
char* end = nullptr;
|
||||||
|
unsigned long long mb = std::strtoull(v, &end, 10);
|
||||||
|
if (end == v || mb == 0) return defMb;
|
||||||
|
if (mb > (std::numeric_limits<size_t>::max() / (1024ull * 1024ull))) return defMb;
|
||||||
|
return static_cast<size_t>(mb);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline size_t envSizeOrDefault(const char* name, size_t defValue) {
|
||||||
|
const char* v = std::getenv(name);
|
||||||
|
if (!v || !*v) return defValue;
|
||||||
|
char* end = nullptr;
|
||||||
|
unsigned long long n = std::strtoull(v, &end, 10);
|
||||||
|
if (end == v || n == 0) return defValue;
|
||||||
|
return static_cast<size_t>(n);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace rendering
|
} // namespace rendering
|
||||||
} // namespace wowee
|
} // namespace wowee
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
#include "ui/quest_log_screen.hpp"
|
#include "ui/quest_log_screen.hpp"
|
||||||
#include "ui/spellbook_screen.hpp"
|
#include "ui/spellbook_screen.hpp"
|
||||||
#include "ui/talent_screen.hpp"
|
#include "ui/talent_screen.hpp"
|
||||||
|
#include "ui/keybinding_manager.hpp"
|
||||||
#include <vulkan/vulkan.h>
|
#include <vulkan/vulkan.h>
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
@ -62,10 +63,13 @@ private:
|
||||||
// UI state
|
// UI state
|
||||||
bool showEntityWindow = false;
|
bool showEntityWindow = false;
|
||||||
bool showChatWindow = true;
|
bool showChatWindow = true;
|
||||||
|
bool showMinimap_ = true; // M key toggles minimap
|
||||||
bool showNameplates_ = true; // V key toggles nameplates
|
bool showNameplates_ = true; // V key toggles nameplates
|
||||||
bool showPlayerInfo = false;
|
bool showPlayerInfo = false;
|
||||||
bool showSocialFrame_ = false; // O key toggles social/friends list
|
bool showSocialFrame_ = false; // O key toggles social/friends list
|
||||||
bool showGuildRoster_ = false;
|
bool showGuildRoster_ = false;
|
||||||
|
bool showRaidFrames_ = true; // F key toggles raid/party frames
|
||||||
|
bool showWorldMap_ = false; // W key toggles world map
|
||||||
std::string selectedGuildMember_;
|
std::string selectedGuildMember_;
|
||||||
bool showGuildNoteEdit_ = false;
|
bool showGuildNoteEdit_ = false;
|
||||||
bool editingOfficerNote_ = false;
|
bool editingOfficerNote_ = false;
|
||||||
|
|
@ -111,6 +115,10 @@ private:
|
||||||
bool pendingMinimapNpcDots = false;
|
bool pendingMinimapNpcDots = false;
|
||||||
bool pendingSeparateBags = true;
|
bool pendingSeparateBags = true;
|
||||||
bool pendingAutoLoot = false;
|
bool pendingAutoLoot = false;
|
||||||
|
|
||||||
|
// Keybinding customization
|
||||||
|
int pendingRebindAction = -1; // -1 = not rebinding, otherwise action index
|
||||||
|
bool awaitingKeyPress = false;
|
||||||
bool pendingUseOriginalSoundtrack = true;
|
bool pendingUseOriginalSoundtrack = true;
|
||||||
bool pendingShowActionBar2 = true; // Show second action bar above main bar
|
bool pendingShowActionBar2 = true; // Show second action bar above main bar
|
||||||
float pendingActionBar2OffsetX = 0.0f; // Horizontal offset from default center position
|
float pendingActionBar2OffsetX = 0.0f; // Horizontal offset from default center position
|
||||||
|
|
|
||||||
89
include/ui/keybinding_manager.hpp
Normal file
89
include/ui/keybinding_manager.hpp
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
#ifndef WOWEE_KEYBINDING_MANAGER_HPP
|
||||||
|
#define WOWEE_KEYBINDING_MANAGER_HPP
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace wowee::ui {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages keybinding configuration for in-game actions.
|
||||||
|
* Supports loading/saving from config files and runtime rebinding.
|
||||||
|
*/
|
||||||
|
class KeybindingManager {
|
||||||
|
public:
|
||||||
|
enum class Action {
|
||||||
|
TOGGLE_CHARACTER_SCREEN,
|
||||||
|
TOGGLE_INVENTORY,
|
||||||
|
TOGGLE_BAGS,
|
||||||
|
TOGGLE_SPELLBOOK,
|
||||||
|
TOGGLE_TALENTS,
|
||||||
|
TOGGLE_QUESTS,
|
||||||
|
TOGGLE_MINIMAP,
|
||||||
|
TOGGLE_SETTINGS,
|
||||||
|
TOGGLE_CHAT,
|
||||||
|
TOGGLE_GUILD_ROSTER,
|
||||||
|
TOGGLE_DUNGEON_FINDER,
|
||||||
|
TOGGLE_WORLD_MAP,
|
||||||
|
TOGGLE_NAMEPLATES,
|
||||||
|
TOGGLE_RAID_FRAMES,
|
||||||
|
TOGGLE_QUEST_LOG,
|
||||||
|
ACTION_COUNT
|
||||||
|
};
|
||||||
|
|
||||||
|
static KeybindingManager& getInstance();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an action's keybinding was just pressed.
|
||||||
|
* Uses ImGui::IsKeyPressed() internally with the bound key.
|
||||||
|
*/
|
||||||
|
bool isActionPressed(Action action, bool repeat = false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the currently bound key for an action.
|
||||||
|
*/
|
||||||
|
ImGuiKey getKeyForAction(Action action) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebind an action to a different key.
|
||||||
|
*/
|
||||||
|
void setKeyForAction(Action action, ImGuiKey key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset all keybindings to defaults.
|
||||||
|
*/
|
||||||
|
void resetToDefaults();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load keybindings from config file.
|
||||||
|
*/
|
||||||
|
void loadFromConfigFile(const std::string& filePath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save keybindings to config file.
|
||||||
|
*/
|
||||||
|
void saveToConfigFile(const std::string& filePath) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get human-readable name for an action.
|
||||||
|
*/
|
||||||
|
static const char* getActionName(Action action);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all actions for iteration.
|
||||||
|
*/
|
||||||
|
static constexpr int getActionCount() { return static_cast<int>(Action::ACTION_COUNT); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
KeybindingManager();
|
||||||
|
|
||||||
|
std::unordered_map<int, ImGuiKey> bindings_; // action -> key
|
||||||
|
|
||||||
|
void initializeDefaults();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace wowee::ui
|
||||||
|
|
||||||
|
#endif // WOWEE_KEYBINDING_MANAGER_HPP
|
||||||
|
|
@ -5973,7 +5973,7 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
uint16_t geosetSleeves = pickGeoset(801, 8); // Bare wrists (group 8, controlled by chest)
|
uint16_t geosetSleeves = pickGeoset(801, 8); // Bare wrists (group 8, controlled by chest)
|
||||||
uint16_t geosetPants = pickGeoset(1301, 13); // Bare legs (group 13)
|
uint16_t geosetPants = pickGeoset(1301, 13); // Bare legs (group 13)
|
||||||
uint16_t geosetCape = 0; // Group 15 disabled unless cape is equipped
|
uint16_t geosetCape = 0; // Group 15 disabled unless cape is equipped
|
||||||
uint16_t geosetTabard = 0; // TODO: NPC tabard geosets currently flicker/apron; keep hidden for now
|
uint16_t geosetTabard = pickGeoset(1201, 12); // Group 12 (tabard), default variant 1201
|
||||||
rendering::VkTexture* npcCapeTextureId = nullptr;
|
rendering::VkTexture* npcCapeTextureId = nullptr;
|
||||||
|
|
||||||
// Load equipment geosets from ItemDisplayInfo.dbc
|
// Load equipment geosets from ItemDisplayInfo.dbc
|
||||||
|
|
@ -6022,7 +6022,11 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
if (gg > 0) geosetGloves = pickGeoset(static_cast<uint16_t>(301 + gg), 3);
|
if (gg > 0) geosetGloves = pickGeoset(static_cast<uint16_t>(301 + gg), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tabard (slot 9) intentionally disabled for now (see geosetTabard TODO above).
|
// Tabard (slot 9) → group 12 (tabard/robe mesh)
|
||||||
|
{
|
||||||
|
uint32_t gg = readGeosetGroup(9, "tabard");
|
||||||
|
if (gg > 0) geosetTabard = pickGeoset(static_cast<uint16_t>(1200 + gg), 12);
|
||||||
|
}
|
||||||
|
|
||||||
// Cape (slot 10) → group 15
|
// Cape (slot 10) → group 15
|
||||||
if (extra.equipDisplayId[10] != 0) {
|
if (extra.equipDisplayId[10] != 0) {
|
||||||
|
|
@ -6138,9 +6142,10 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
" sleeves=", geosetSleeves, " pants=", geosetPants,
|
" sleeves=", geosetSleeves, " pants=", geosetPants,
|
||||||
" boots=", geosetBoots, " gloves=", geosetGloves);
|
" boots=", geosetBoots, " gloves=", geosetGloves);
|
||||||
|
|
||||||
// TODO(#helmet-attach): NPC helmet attachment anchors are currently unreliable
|
// NOTE: NPC helmet attachment with fallback logic to use bone 0 if attachment
|
||||||
// on some humanoid models (floating/incorrect bone bind). Keep hidden for now.
|
// point 11 is missing. This improves compatibility with models that don't have
|
||||||
static constexpr bool kEnableNpcHelmetAttachmentsMainPath = false;
|
// attachment 11 explicitly defined.
|
||||||
|
static constexpr bool kEnableNpcHelmetAttachmentsMainPath = true;
|
||||||
// Load and attach helmet model if equipped
|
// Load and attach helmet model if equipped
|
||||||
if (kEnableNpcHelmetAttachmentsMainPath && extra.equipDisplayId[0] != 0 && itemDisplayDbc) {
|
if (kEnableNpcHelmetAttachmentsMainPath && extra.equipDisplayId[0] != 0 && itemDisplayDbc) {
|
||||||
int32_t helmIdx = itemDisplayDbc->findRecordById(extra.equipDisplayId[0]);
|
int32_t helmIdx = itemDisplayDbc->findRecordById(extra.equipDisplayId[0]);
|
||||||
|
|
@ -6482,84 +6487,6 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional NPC helmet attachments (kept disabled for stability: this path
|
|
||||||
// can increase spawn-time pressure and regress NPC visibility in crowded areas).
|
|
||||||
static constexpr bool kEnableNpcHelmetAttachments = false;
|
|
||||||
if (kEnableNpcHelmetAttachments &&
|
|
||||||
itDisplayData != displayDataMap_.end() &&
|
|
||||||
itDisplayData->second.extraDisplayId != 0) {
|
|
||||||
auto itExtra = humanoidExtraMap_.find(itDisplayData->second.extraDisplayId);
|
|
||||||
if (itExtra != humanoidExtraMap_.end()) {
|
|
||||||
const auto& extra = itExtra->second;
|
|
||||||
if (extra.equipDisplayId[0] != 0) { // Helm slot
|
|
||||||
auto itemDisplayDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
|
|
||||||
const auto* idiL2 = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
|
|
||||||
if (itemDisplayDbc) {
|
|
||||||
int32_t helmIdx = itemDisplayDbc->findRecordById(extra.equipDisplayId[0]);
|
|
||||||
if (helmIdx >= 0) {
|
|
||||||
std::string helmModelName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), idiL2 ? (*idiL2)["LeftModel"] : 1);
|
|
||||||
if (!helmModelName.empty()) {
|
|
||||||
size_t dotPos = helmModelName.rfind('.');
|
|
||||||
if (dotPos != std::string::npos) {
|
|
||||||
helmModelName = helmModelName.substr(0, dotPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
static const std::unordered_map<uint8_t, std::string> racePrefix = {
|
|
||||||
{1, "Hu"}, {2, "Or"}, {3, "Dw"}, {4, "Ni"}, {5, "Sc"},
|
|
||||||
{6, "Ta"}, {7, "Gn"}, {8, "Tr"}, {10, "Be"}, {11, "Dr"}
|
|
||||||
};
|
|
||||||
std::string genderSuffix = (extra.sexId == 0) ? "M" : "F";
|
|
||||||
std::string raceSuffix;
|
|
||||||
auto itRace = racePrefix.find(extra.raceId);
|
|
||||||
if (itRace != racePrefix.end()) {
|
|
||||||
raceSuffix = "_" + itRace->second + genderSuffix;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string helmPath;
|
|
||||||
std::vector<uint8_t> helmData;
|
|
||||||
if (!raceSuffix.empty()) {
|
|
||||||
helmPath = "Item\\ObjectComponents\\Head\\" + helmModelName + raceSuffix + ".m2";
|
|
||||||
helmData = assetManager->readFile(helmPath);
|
|
||||||
}
|
|
||||||
if (helmData.empty()) {
|
|
||||||
helmPath = "Item\\ObjectComponents\\Head\\" + helmModelName + ".m2";
|
|
||||||
helmData = assetManager->readFile(helmPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!helmData.empty()) {
|
|
||||||
auto helmModel = pipeline::M2Loader::load(helmData);
|
|
||||||
std::string skinPath = helmPath.substr(0, helmPath.size() - 3) + "00.skin";
|
|
||||||
auto skinData = assetManager->readFile(skinPath);
|
|
||||||
if (!skinData.empty() && helmModel.version >= 264) {
|
|
||||||
pipeline::M2Loader::loadSkin(skinData, helmModel);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (helmModel.isValid()) {
|
|
||||||
uint32_t helmModelId = nextCreatureModelId_++;
|
|
||||||
std::string helmTexName = itemDisplayDbc->getString(static_cast<uint32_t>(helmIdx), idiL2 ? (*idiL2)["LeftModelTexture"] : 3);
|
|
||||||
std::string helmTexPath;
|
|
||||||
if (!helmTexName.empty()) {
|
|
||||||
if (!raceSuffix.empty()) {
|
|
||||||
std::string suffixedTex = "Item\\ObjectComponents\\Head\\" + helmTexName + raceSuffix + ".blp";
|
|
||||||
if (assetManager->fileExists(suffixedTex)) {
|
|
||||||
helmTexPath = suffixedTex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (helmTexPath.empty()) {
|
|
||||||
helmTexPath = "Item\\ObjectComponents\\Head\\" + helmTexName + ".blp";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Attachment point 11 = Head
|
|
||||||
charRenderer->attachWeapon(instanceId, 11, helmModel, helmModelId, helmTexPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try attaching NPC held weapons; if update fields are not ready yet,
|
// Try attaching NPC held weapons; if update fields are not ready yet,
|
||||||
// IN_GAME retry loop will attempt again shortly.
|
// IN_GAME retry loop will attempt again shortly.
|
||||||
bool weaponsAttachedNow = tryAttachCreatureVirtualWeapons(guid, instanceId);
|
bool weaponsAttachedNow = tryAttachCreatureVirtualWeapons(guid, instanceId);
|
||||||
|
|
|
||||||
|
|
@ -1963,15 +1963,21 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
|
|
||||||
// ---- Loot start roll (Need/Greed popup trigger) ----
|
// ---- Loot start roll (Need/Greed popup trigger) ----
|
||||||
case Opcode::SMSG_LOOT_START_ROLL: {
|
case Opcode::SMSG_LOOT_START_ROLL: {
|
||||||
// uint64 objectGuid + uint32 mapId + uint32 lootSlot + uint32 itemId
|
// WotLK 3.3.5a: uint64 objectGuid + uint32 mapId + uint32 lootSlot + uint32 itemId
|
||||||
// + uint32 randomSuffix + uint32 randomPropId + uint32 countdown + uint8 voteMask
|
// + uint32 randomSuffix + uint32 randomPropId + uint32 countdown + uint8 voteMask (33 bytes)
|
||||||
if (packet.getSize() - packet.getReadPos() < 33) break;
|
// Classic/TBC: uint64 objectGuid + uint32 mapId + uint32 lootSlot + uint32 itemId
|
||||||
|
// + uint32 countdown + uint8 voteMask (25 bytes)
|
||||||
|
const bool isWotLK = isActiveExpansion("wotlk");
|
||||||
|
const size_t minSize = isWotLK ? 33u : 25u;
|
||||||
|
if (packet.getSize() - packet.getReadPos() < minSize) break;
|
||||||
uint64_t objectGuid = packet.readUInt64();
|
uint64_t objectGuid = packet.readUInt64();
|
||||||
/*uint32_t mapId =*/ packet.readUInt32();
|
/*uint32_t mapId =*/ packet.readUInt32();
|
||||||
uint32_t slot = packet.readUInt32();
|
uint32_t slot = packet.readUInt32();
|
||||||
uint32_t itemId = packet.readUInt32();
|
uint32_t itemId = packet.readUInt32();
|
||||||
/*uint32_t randSuffix =*/ packet.readUInt32();
|
if (isWotLK) {
|
||||||
/*uint32_t randProp =*/ packet.readUInt32();
|
/*uint32_t randSuffix =*/ packet.readUInt32();
|
||||||
|
/*uint32_t randProp =*/ packet.readUInt32();
|
||||||
|
}
|
||||||
/*uint32_t countdown =*/ packet.readUInt32();
|
/*uint32_t countdown =*/ packet.readUInt32();
|
||||||
/*uint8_t voteMask =*/ packet.readUInt8();
|
/*uint8_t voteMask =*/ packet.readUInt8();
|
||||||
// Trigger the roll popup for local player
|
// Trigger the roll popup for local player
|
||||||
|
|
@ -2344,11 +2350,18 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
|
|
||||||
// ---- Loot notifications ----
|
// ---- Loot notifications ----
|
||||||
case Opcode::SMSG_LOOT_ALL_PASSED: {
|
case Opcode::SMSG_LOOT_ALL_PASSED: {
|
||||||
// uint64 objectGuid + uint32 slot + uint32 itemId + uint32 randSuffix + uint32 randPropId
|
// WotLK 3.3.5a: uint64 objectGuid + uint32 slot + uint32 itemId + uint32 randSuffix + uint32 randPropId (24 bytes)
|
||||||
if (packet.getSize() - packet.getReadPos() < 24) break;
|
// Classic/TBC: uint64 objectGuid + uint32 slot + uint32 itemId (16 bytes)
|
||||||
|
const bool isWotLK = isActiveExpansion("wotlk");
|
||||||
|
const size_t minSize = isWotLK ? 24u : 16u;
|
||||||
|
if (packet.getSize() - packet.getReadPos() < minSize) break;
|
||||||
/*uint64_t objGuid =*/ packet.readUInt64();
|
/*uint64_t objGuid =*/ packet.readUInt64();
|
||||||
/*uint32_t slot =*/ packet.readUInt32();
|
/*uint32_t slot =*/ packet.readUInt32();
|
||||||
uint32_t itemId = packet.readUInt32();
|
uint32_t itemId = packet.readUInt32();
|
||||||
|
if (isWotLK) {
|
||||||
|
/*uint32_t randSuffix =*/ packet.readUInt32();
|
||||||
|
/*uint32_t randProp =*/ packet.readUInt32();
|
||||||
|
}
|
||||||
auto* info = getItemInfo(itemId);
|
auto* info = getItemInfo(itemId);
|
||||||
char buf[256];
|
char buf[256];
|
||||||
std::snprintf(buf, sizeof(buf), "Everyone passed on [%s].",
|
std::snprintf(buf, sizeof(buf), "Everyone passed on [%s].",
|
||||||
|
|
@ -2747,16 +2760,21 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
break;
|
break;
|
||||||
case Opcode::SMSG_SPELL_FAILURE: {
|
case Opcode::SMSG_SPELL_FAILURE: {
|
||||||
// WotLK: packed_guid + uint8 castCount + uint32 spellId + uint8 failReason
|
// WotLK: packed_guid + uint8 castCount + uint32 spellId + uint8 failReason
|
||||||
// TBC/Classic: full uint64 + uint8 castCount + uint32 spellId + uint8 failReason
|
// TBC: full uint64 + uint8 castCount + uint32 spellId + uint8 failReason
|
||||||
const bool tbcOrClassic = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
// Classic: full uint64 + uint32 spellId + uint8 failReason (NO castCount)
|
||||||
uint64_t failGuid = tbcOrClassic
|
const bool isClassic = isClassicLikeExpansion();
|
||||||
|
const bool isTbc = isActiveExpansion("tbc");
|
||||||
|
uint64_t failGuid = (isClassic || isTbc)
|
||||||
? (packet.getSize() - packet.getReadPos() >= 8 ? packet.readUInt64() : 0)
|
? (packet.getSize() - packet.getReadPos() >= 8 ? packet.readUInt64() : 0)
|
||||||
: UpdateObjectParser::readPackedGuid(packet);
|
: UpdateObjectParser::readPackedGuid(packet);
|
||||||
// Read castCount + spellId + failReason
|
// Classic omits the castCount byte; TBC and WotLK include it
|
||||||
if (packet.getSize() - packet.getReadPos() >= 6) {
|
const size_t remainingFields = isClassic ? 5u : 6u; // spellId(4)+reason(1) [+castCount(1)]
|
||||||
/*uint8_t castCount =*/ packet.readUInt8();
|
if (packet.getSize() - packet.getReadPos() >= remainingFields) {
|
||||||
|
if (!isClassic) /*uint8_t castCount =*/ packet.readUInt8();
|
||||||
/*uint32_t spellId =*/ packet.readUInt32();
|
/*uint32_t spellId =*/ packet.readUInt32();
|
||||||
uint8_t failReason = packet.readUInt8();
|
uint8_t rawFailReason = packet.readUInt8();
|
||||||
|
// Classic result enum starts at 0=AFFECTING_COMBAT; shift +1 for WotLK table
|
||||||
|
uint8_t failReason = isClassic ? static_cast<uint8_t>(rawFailReason + 1) : rawFailReason;
|
||||||
if (failGuid == playerGuid && failReason != 0) {
|
if (failGuid == playerGuid && failReason != 0) {
|
||||||
// Show interruption/failure reason in chat for player
|
// Show interruption/failure reason in chat for player
|
||||||
int pt = -1;
|
int pt = -1;
|
||||||
|
|
@ -4073,11 +4091,13 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Opcode::SMSG_WEATHER: {
|
case Opcode::SMSG_WEATHER: {
|
||||||
// Format: uint32 weatherType, float intensity, uint8 isAbrupt
|
// Classic 1.12: uint32 weatherType + float intensity (8 bytes, no isAbrupt)
|
||||||
if (packet.getSize() - packet.getReadPos() >= 9) {
|
// TBC 2.4.3 / WotLK 3.3.5a: uint32 weatherType + float intensity + uint8 isAbrupt (9 bytes)
|
||||||
|
if (packet.getSize() - packet.getReadPos() >= 8) {
|
||||||
uint32_t wType = packet.readUInt32();
|
uint32_t wType = packet.readUInt32();
|
||||||
float wIntensity = packet.readFloat();
|
float wIntensity = packet.readFloat();
|
||||||
/*uint8_t isAbrupt =*/ packet.readUInt8();
|
if (packet.getSize() - packet.getReadPos() >= 1)
|
||||||
|
/*uint8_t isAbrupt =*/ packet.readUInt8();
|
||||||
weatherType_ = wType;
|
weatherType_ = wType;
|
||||||
weatherIntensity_ = wIntensity;
|
weatherIntensity_ = wIntensity;
|
||||||
const char* typeName = (wType == 1) ? "Rain" : (wType == 2) ? "Snow" : (wType == 3) ? "Storm" : "Clear";
|
const char* typeName = (wType == 1) ? "Rain" : (wType == 2) ? "Snow" : (wType == 3) ? "Storm" : "Clear";
|
||||||
|
|
@ -12237,20 +12257,40 @@ void GameHandler::handleMoveKnockBack(network::Packet& packet) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void GameHandler::handleBattlefieldStatus(network::Packet& packet) {
|
void GameHandler::handleBattlefieldStatus(network::Packet& packet) {
|
||||||
|
// SMSG_BATTLEFIELD_STATUS wire format differs by expansion:
|
||||||
|
//
|
||||||
|
// Classic 1.12 (vmangos/cmangos):
|
||||||
|
// queueSlot(4) bgTypeId(4) unk(2) instanceId(4) isRegistered(1) statusId(4) [status fields...]
|
||||||
|
// STATUS_NONE sends only: queueSlot(4) bgTypeId(4)
|
||||||
|
//
|
||||||
|
// TBC 2.4.3 / WotLK 3.3.5a:
|
||||||
|
// queueSlot(4) arenaType(1) unk(1) bgTypeId(4) unk2(2) instanceId(4) isRated(1) statusId(4) [status fields...]
|
||||||
|
// STATUS_NONE sends only: queueSlot(4) arenaType(1)
|
||||||
|
|
||||||
if (packet.getSize() - packet.getReadPos() < 4) return;
|
if (packet.getSize() - packet.getReadPos() < 4) return;
|
||||||
uint32_t queueSlot = packet.readUInt32();
|
uint32_t queueSlot = packet.readUInt32();
|
||||||
|
|
||||||
// Minimal packet = just queueSlot + arenaType(1) when status is NONE
|
const bool classicFormat = isClassicLikeExpansion();
|
||||||
if (packet.getSize() - packet.getReadPos() < 1) {
|
|
||||||
LOG_INFO("Battlefield status: queue slot ", queueSlot, " cleared");
|
uint8_t arenaType = 0;
|
||||||
return;
|
if (!classicFormat) {
|
||||||
|
// TBC/WotLK: arenaType(1) + unk(1) before bgTypeId
|
||||||
|
// STATUS_NONE sends only queueSlot + arenaType
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 1) {
|
||||||
|
LOG_INFO("Battlefield status: queue slot ", queueSlot, " cleared");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
arenaType = packet.readUInt8();
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 1) return;
|
||||||
|
packet.readUInt8(); // unk
|
||||||
|
} else {
|
||||||
|
// Classic STATUS_NONE sends only queueSlot + bgTypeId (4 bytes)
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 4) {
|
||||||
|
LOG_INFO("Battlefield status: queue slot ", queueSlot, " cleared");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t arenaType = packet.readUInt8();
|
|
||||||
if (packet.getSize() - packet.getReadPos() < 1) return;
|
|
||||||
|
|
||||||
// Unknown byte
|
|
||||||
packet.readUInt8();
|
|
||||||
if (packet.getSize() - packet.getReadPos() < 4) return;
|
if (packet.getSize() - packet.getReadPos() < 4) return;
|
||||||
uint32_t bgTypeId = packet.readUInt32();
|
uint32_t bgTypeId = packet.readUInt32();
|
||||||
|
|
||||||
|
|
@ -14169,8 +14209,11 @@ void GameHandler::handleAuraUpdate(network::Packet& packet, bool isAll) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::handleLearnedSpell(network::Packet& packet) {
|
void GameHandler::handleLearnedSpell(network::Packet& packet) {
|
||||||
if (packet.getSize() - packet.getReadPos() < 4) return;
|
// Classic 1.12: uint16 spellId; TBC 2.4.3 / WotLK 3.3.5a: uint32 spellId
|
||||||
uint32_t spellId = packet.readUInt32();
|
const bool classicSpellId = isClassicLikeExpansion();
|
||||||
|
const size_t minSz = classicSpellId ? 2u : 4u;
|
||||||
|
if (packet.getSize() - packet.getReadPos() < minSz) return;
|
||||||
|
uint32_t spellId = classicSpellId ? packet.readUInt16() : packet.readUInt32();
|
||||||
knownSpells.insert(spellId);
|
knownSpells.insert(spellId);
|
||||||
LOG_INFO("Learned spell: ", spellId);
|
LOG_INFO("Learned spell: ", spellId);
|
||||||
|
|
||||||
|
|
@ -14198,17 +14241,24 @@ void GameHandler::handleLearnedSpell(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::handleRemovedSpell(network::Packet& packet) {
|
void GameHandler::handleRemovedSpell(network::Packet& packet) {
|
||||||
if (packet.getSize() - packet.getReadPos() < 4) return;
|
// Classic 1.12: uint16 spellId; TBC 2.4.3 / WotLK 3.3.5a: uint32 spellId
|
||||||
uint32_t spellId = packet.readUInt32();
|
const bool classicSpellId = isClassicLikeExpansion();
|
||||||
|
const size_t minSz = classicSpellId ? 2u : 4u;
|
||||||
|
if (packet.getSize() - packet.getReadPos() < minSz) return;
|
||||||
|
uint32_t spellId = classicSpellId ? packet.readUInt16() : packet.readUInt32();
|
||||||
knownSpells.erase(spellId);
|
knownSpells.erase(spellId);
|
||||||
LOG_INFO("Removed spell: ", spellId);
|
LOG_INFO("Removed spell: ", spellId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::handleSupercededSpell(network::Packet& packet) {
|
void GameHandler::handleSupercededSpell(network::Packet& packet) {
|
||||||
// Old spell replaced by new rank (e.g., Fireball Rank 1 -> Fireball Rank 2)
|
// Old spell replaced by new rank (e.g., Fireball Rank 1 -> Fireball Rank 2)
|
||||||
if (packet.getSize() - packet.getReadPos() < 8) return;
|
// Classic 1.12: uint16 oldSpellId + uint16 newSpellId (4 bytes total)
|
||||||
uint32_t oldSpellId = packet.readUInt32();
|
// TBC 2.4.3 / WotLK 3.3.5a: uint32 oldSpellId + uint32 newSpellId (8 bytes total)
|
||||||
uint32_t newSpellId = packet.readUInt32();
|
const bool classicSpellId = isClassicLikeExpansion();
|
||||||
|
const size_t minSz = classicSpellId ? 4u : 8u;
|
||||||
|
if (packet.getSize() - packet.getReadPos() < minSz) return;
|
||||||
|
uint32_t oldSpellId = classicSpellId ? packet.readUInt16() : packet.readUInt32();
|
||||||
|
uint32_t newSpellId = classicSpellId ? packet.readUInt16() : packet.readUInt32();
|
||||||
|
|
||||||
// Remove old spell
|
// Remove old spell
|
||||||
knownSpells.erase(oldSpellId);
|
knownSpells.erase(oldSpellId);
|
||||||
|
|
@ -15784,8 +15834,11 @@ void GameHandler::buyItem(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, u
|
||||||
packet.writeUInt32(itemId); // item entry
|
packet.writeUInt32(itemId); // item entry
|
||||||
packet.writeUInt32(slot); // vendor slot index
|
packet.writeUInt32(slot); // vendor slot index
|
||||||
packet.writeUInt32(count);
|
packet.writeUInt32(count);
|
||||||
// WotLK/AzerothCore expects a trailing byte here.
|
// WotLK/AzerothCore expects a trailing byte; Classic/TBC do not
|
||||||
packet.writeUInt8(0);
|
const bool isWotLk = isActiveExpansion("wotlk");
|
||||||
|
if (isWotLk) {
|
||||||
|
packet.writeUInt8(0);
|
||||||
|
}
|
||||||
socket->send(packet);
|
socket->send(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16159,7 +16212,10 @@ void GameHandler::unstuckHearth() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::handleLootResponse(network::Packet& packet) {
|
void GameHandler::handleLootResponse(network::Packet& packet) {
|
||||||
if (!LootResponseParser::parse(packet, currentLoot)) return;
|
// Classic 1.12 and TBC 2.4.3 use 14 bytes/item (no randomSuffix/randomProp fields);
|
||||||
|
// WotLK 3.3.5a uses 22 bytes/item.
|
||||||
|
const bool wotlkLoot = isActiveExpansion("wotlk");
|
||||||
|
if (!LootResponseParser::parse(packet, currentLoot, wotlkLoot)) return;
|
||||||
lootWindowOpen = true;
|
lootWindowOpen = true;
|
||||||
localLootState_[currentLoot.lootGuid] = LocalLootState{currentLoot, false};
|
localLootState_[currentLoot.lootGuid] = LocalLootState{currentLoot, false};
|
||||||
|
|
||||||
|
|
@ -16852,15 +16908,20 @@ void GameHandler::handleTeleportAck(network::Packet& packet) {
|
||||||
if (packet.getSize() - packet.getReadPos() < 4) return;
|
if (packet.getSize() - packet.getReadPos() < 4) return;
|
||||||
uint32_t counter = packet.readUInt32();
|
uint32_t counter = packet.readUInt32();
|
||||||
|
|
||||||
// Read the movement info embedded in the teleport
|
// Read the movement info embedded in the teleport.
|
||||||
// Format: u32 flags, u16 flags2, u32 time, float x, float y, float z, float o
|
// WotLK: moveFlags(4) + moveFlags2(2) + time(4) + x(4) + y(4) + z(4) + o(4) = 26 bytes
|
||||||
if (packet.getSize() - packet.getReadPos() < 4 + 2 + 4 + 4 * 4) {
|
// Classic 1.12 / TBC 2.4.3: moveFlags(4) + time(4) + x(4) + y(4) + z(4) + o(4) = 24 bytes
|
||||||
|
// (Classic and TBC have no moveFlags2 field in movement packets)
|
||||||
|
const bool taNoFlags2 = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
||||||
|
const size_t minMoveSz = taNoFlags2 ? (4 + 4 + 4 * 4) : (4 + 2 + 4 + 4 * 4);
|
||||||
|
if (packet.getSize() - packet.getReadPos() < minMoveSz) {
|
||||||
LOG_WARNING("MSG_MOVE_TELEPORT_ACK: not enough data for movement info");
|
LOG_WARNING("MSG_MOVE_TELEPORT_ACK: not enough data for movement info");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
packet.readUInt32(); // moveFlags
|
packet.readUInt32(); // moveFlags
|
||||||
packet.readUInt16(); // moveFlags2
|
if (!taNoFlags2)
|
||||||
|
packet.readUInt16(); // moveFlags2 (WotLK only)
|
||||||
uint32_t moveTime = packet.readUInt32();
|
uint32_t moveTime = packet.readUInt32();
|
||||||
float serverX = packet.readFloat();
|
float serverX = packet.readFloat();
|
||||||
float serverY = packet.readFloat();
|
float serverY = packet.readFloat();
|
||||||
|
|
@ -19468,18 +19529,23 @@ void GameHandler::handleTradeStatusExtended(network::Packet& packet) {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
void GameHandler::handleLootRoll(network::Packet& packet) {
|
void GameHandler::handleLootRoll(network::Packet& packet) {
|
||||||
// uint64 objectGuid, uint32 slot, uint64 playerGuid,
|
// WotLK 3.3.5a: uint64 objectGuid, uint32 slot, uint64 playerGuid,
|
||||||
// uint32 itemId, uint32 randomSuffix, uint32 randomPropId,
|
// uint32 itemId, uint32 randomSuffix, uint32 randomPropId, uint8 rollNumber, uint8 rollType (34 bytes)
|
||||||
// uint8 rollNumber, uint8 rollType
|
// Classic/TBC: uint64 objectGuid, uint32 slot, uint64 playerGuid,
|
||||||
|
// uint32 itemId, uint8 rollNumber, uint8 rollType (26 bytes)
|
||||||
|
const bool isWotLK = isActiveExpansion("wotlk");
|
||||||
|
const size_t minSize = isWotLK ? 34u : 26u;
|
||||||
size_t rem = packet.getSize() - packet.getReadPos();
|
size_t rem = packet.getSize() - packet.getReadPos();
|
||||||
if (rem < 26) return; // minimum: 8+4+8+4+4+4+1+1 = 34, be lenient
|
if (rem < minSize) return;
|
||||||
|
|
||||||
uint64_t objectGuid = packet.readUInt64();
|
uint64_t objectGuid = packet.readUInt64();
|
||||||
uint32_t slot = packet.readUInt32();
|
uint32_t slot = packet.readUInt32();
|
||||||
uint64_t rollerGuid = packet.readUInt64();
|
uint64_t rollerGuid = packet.readUInt64();
|
||||||
uint32_t itemId = packet.readUInt32();
|
uint32_t itemId = packet.readUInt32();
|
||||||
/*uint32_t randSuffix =*/ packet.readUInt32();
|
if (isWotLK) {
|
||||||
/*uint32_t randProp =*/ packet.readUInt32();
|
/*uint32_t randSuffix =*/ packet.readUInt32();
|
||||||
|
/*uint32_t randProp =*/ packet.readUInt32();
|
||||||
|
}
|
||||||
uint8_t rollNum = packet.readUInt8();
|
uint8_t rollNum = packet.readUInt8();
|
||||||
uint8_t rollType = packet.readUInt8();
|
uint8_t rollType = packet.readUInt8();
|
||||||
|
|
||||||
|
|
@ -19526,15 +19592,23 @@ void GameHandler::handleLootRoll(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::handleLootRollWon(network::Packet& packet) {
|
void GameHandler::handleLootRollWon(network::Packet& packet) {
|
||||||
|
// WotLK 3.3.5a: uint64 objectGuid, uint32 slot, uint64 winnerGuid,
|
||||||
|
// uint32 itemId, uint32 randomSuffix, uint32 randomPropId, uint8 rollNumber, uint8 rollType (34 bytes)
|
||||||
|
// Classic/TBC: uint64 objectGuid, uint32 slot, uint64 winnerGuid,
|
||||||
|
// uint32 itemId, uint8 rollNumber, uint8 rollType (26 bytes)
|
||||||
|
const bool isWotLK = isActiveExpansion("wotlk");
|
||||||
|
const size_t minSize = isWotLK ? 34u : 26u;
|
||||||
size_t rem = packet.getSize() - packet.getReadPos();
|
size_t rem = packet.getSize() - packet.getReadPos();
|
||||||
if (rem < 26) return;
|
if (rem < minSize) return;
|
||||||
|
|
||||||
/*uint64_t objectGuid =*/ packet.readUInt64();
|
/*uint64_t objectGuid =*/ packet.readUInt64();
|
||||||
/*uint32_t slot =*/ packet.readUInt32();
|
/*uint32_t slot =*/ packet.readUInt32();
|
||||||
uint64_t winnerGuid = packet.readUInt64();
|
uint64_t winnerGuid = packet.readUInt64();
|
||||||
uint32_t itemId = packet.readUInt32();
|
uint32_t itemId = packet.readUInt32();
|
||||||
/*uint32_t randSuffix =*/ packet.readUInt32();
|
if (isWotLK) {
|
||||||
/*uint32_t randProp =*/ packet.readUInt32();
|
/*uint32_t randSuffix =*/ packet.readUInt32();
|
||||||
|
/*uint32_t randProp =*/ packet.readUInt32();
|
||||||
|
}
|
||||||
uint8_t rollNum = packet.readUInt8();
|
uint8_t rollNum = packet.readUInt8();
|
||||||
uint8_t rollType = packet.readUInt8();
|
uint8_t rollType = packet.readUInt8();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -633,6 +633,22 @@ bool ClassicPacketParsers::parseCastFailed(network::Packet& packet, CastFailedDa
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Classic SMSG_CAST_RESULT: same layout as parseCastFailed (spellId + result),
|
||||||
|
// but the result enum starts at 0=AFFECTING_COMBAT (no SUCCESS entry).
|
||||||
|
// Apply the same +1 shift used in parseCastFailed so the result codes
|
||||||
|
// align with WotLK's getSpellCastResultString table.
|
||||||
|
// ============================================================================
|
||||||
|
bool ClassicPacketParsers::parseCastResult(network::Packet& packet, uint32_t& spellId, uint8_t& result) {
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 5) return false;
|
||||||
|
spellId = packet.readUInt32();
|
||||||
|
uint8_t vanillaResult = packet.readUInt8();
|
||||||
|
// Shift +1: Vanilla result 0=AFFECTING_COMBAT maps to WotLK result 1=AFFECTING_COMBAT
|
||||||
|
result = vanillaResult + 1;
|
||||||
|
LOG_DEBUG("[Classic] Cast result: spell=", spellId, " vanillaResult=", (int)vanillaResult);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Classic 1.12.1 parseCharEnum
|
// Classic 1.12.1 parseCharEnum
|
||||||
// Differences from TBC:
|
// Differences from TBC:
|
||||||
|
|
|
||||||
|
|
@ -2901,18 +2901,13 @@ bool XpGainParser::parse(network::Packet& packet, XpGainData& data) {
|
||||||
// Phase 3: Spells, Action Bar, Auras
|
// Phase 3: Spells, Action Bar, Auras
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data) {
|
bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data,
|
||||||
size_t packetSize = packet.getSize();
|
bool vanillaFormat) {
|
||||||
data.talentSpec = packet.readUInt8();
|
data.talentSpec = packet.readUInt8();
|
||||||
uint16_t spellCount = packet.readUInt16();
|
uint16_t spellCount = packet.readUInt16();
|
||||||
|
|
||||||
// Detect vanilla (uint16 spellId) vs WotLK (uint32 spellId) format
|
LOG_DEBUG("SMSG_INITIAL_SPELLS: spellCount=", spellCount,
|
||||||
// Vanilla: 4 bytes/spell (uint16 id + uint16 slot), WotLK: 6 bytes/spell (uint32 id + uint16 unk)
|
vanillaFormat ? " (vanilla uint16 format)" : " (TBC/WotLK uint32 format)");
|
||||||
size_t remainingAfterHeader = packetSize - 3; // subtract talentSpec(1) + spellCount(2)
|
|
||||||
bool vanillaFormat = remainingAfterHeader < static_cast<size_t>(spellCount) * 6 + 2;
|
|
||||||
|
|
||||||
LOG_DEBUG("SMSG_INITIAL_SPELLS: packetSize=", packetSize, " bytes, spellCount=", spellCount,
|
|
||||||
vanillaFormat ? " (vanilla uint16 format)" : " (WotLK uint32 format)");
|
|
||||||
|
|
||||||
data.spellIds.reserve(spellCount);
|
data.spellIds.reserve(spellCount);
|
||||||
for (uint16_t i = 0; i < spellCount; ++i) {
|
for (uint16_t i = 0; i < spellCount; ++i) {
|
||||||
|
|
@ -3320,7 +3315,7 @@ network::Packet LootReleasePacket::build(uint64_t lootGuid) {
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data) {
|
bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data, bool isWotlkFormat) {
|
||||||
data = LootResponseData{};
|
data = LootResponseData{};
|
||||||
if (packet.getSize() - packet.getReadPos() < 14) {
|
if (packet.getSize() - packet.getReadPos() < 14) {
|
||||||
LOG_WARNING("LootResponseParser: packet too short");
|
LOG_WARNING("LootResponseParser: packet too short");
|
||||||
|
|
@ -3332,45 +3327,34 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data)
|
||||||
data.gold = packet.readUInt32();
|
data.gold = packet.readUInt32();
|
||||||
uint8_t itemCount = packet.readUInt8();
|
uint8_t itemCount = packet.readUInt8();
|
||||||
|
|
||||||
|
// Item wire size:
|
||||||
|
// WotLK 3.3.5a: slot(1)+itemId(4)+count(4)+displayInfo(4)+randSuffix(4)+randProp(4)+slotType(1) = 22
|
||||||
|
// Classic/TBC: slot(1)+itemId(4)+count(4)+displayInfo(4)+slotType(1) = 14
|
||||||
|
const size_t kItemSize = isWotlkFormat ? 22u : 14u;
|
||||||
|
|
||||||
auto parseLootItemList = [&](uint8_t listCount, bool markQuestItems) -> bool {
|
auto parseLootItemList = [&](uint8_t listCount, bool markQuestItems) -> bool {
|
||||||
for (uint8_t i = 0; i < listCount; ++i) {
|
for (uint8_t i = 0; i < listCount; ++i) {
|
||||||
size_t remaining = packet.getSize() - packet.getReadPos();
|
size_t remaining = packet.getSize() - packet.getReadPos();
|
||||||
if (remaining < 10) {
|
if (remaining < kItemSize) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefer the richest format when possible:
|
|
||||||
// 22-byte (WotLK/full): slot+id+count+display+randSuffix+randProp+slotType
|
|
||||||
// 14-byte (compact): slot+id+count+display+slotType
|
|
||||||
// 10-byte (minimal): slot+id+count+slotType
|
|
||||||
uint8_t bytesPerItem = 10;
|
|
||||||
if (remaining >= 22) {
|
|
||||||
bytesPerItem = 22;
|
|
||||||
} else if (remaining >= 14) {
|
|
||||||
bytesPerItem = 14;
|
|
||||||
}
|
|
||||||
|
|
||||||
LootItem item;
|
LootItem item;
|
||||||
item.slotIndex = packet.readUInt8();
|
item.slotIndex = packet.readUInt8();
|
||||||
item.itemId = packet.readUInt32();
|
item.itemId = packet.readUInt32();
|
||||||
item.count = packet.readUInt32();
|
item.count = packet.readUInt32();
|
||||||
|
item.displayInfoId = packet.readUInt32();
|
||||||
|
|
||||||
if (bytesPerItem >= 14) {
|
if (isWotlkFormat) {
|
||||||
item.displayInfoId = packet.readUInt32();
|
item.randomSuffix = packet.readUInt32();
|
||||||
} else {
|
|
||||||
item.displayInfoId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bytesPerItem == 22) {
|
|
||||||
item.randomSuffix = packet.readUInt32();
|
|
||||||
item.randomPropertyId = packet.readUInt32();
|
item.randomPropertyId = packet.readUInt32();
|
||||||
} else {
|
} else {
|
||||||
item.randomSuffix = 0;
|
item.randomSuffix = 0;
|
||||||
item.randomPropertyId = 0;
|
item.randomPropertyId = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.lootSlotType = packet.readUInt8();
|
item.lootSlotType = packet.readUInt8();
|
||||||
item.isQuestItem = markQuestItems;
|
item.isQuestItem = markQuestItems;
|
||||||
data.items.push_back(item);
|
data.items.push_back(item);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -3844,7 +3828,9 @@ network::Packet BuyItemPacket::build(uint64_t vendorGuid, uint32_t itemId, uint3
|
||||||
packet.writeUInt32(itemId); // item entry
|
packet.writeUInt32(itemId); // item entry
|
||||||
packet.writeUInt32(slot); // vendor slot index from SMSG_LIST_INVENTORY
|
packet.writeUInt32(slot); // vendor slot index from SMSG_LIST_INVENTORY
|
||||||
packet.writeUInt32(count);
|
packet.writeUInt32(count);
|
||||||
// WotLK/AzerothCore expects a trailing byte on CMSG_BUY_ITEM.
|
// Note: WotLK/AzerothCore expects a trailing byte; Classic/TBC do not.
|
||||||
|
// This static helper always adds it (appropriate for CMaNGOS/AzerothCore).
|
||||||
|
// For Classic/TBC, use the GameHandler::buyItem() path which checks expansion.
|
||||||
packet.writeUInt8(0);
|
packet.writeUInt8(0);
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -377,6 +377,13 @@ void CameraController::update(float deltaTime) {
|
||||||
if (mounted_) sitting = false;
|
if (mounted_) sitting = false;
|
||||||
xKeyWasDown = xDown;
|
xKeyWasDown = xDown;
|
||||||
|
|
||||||
|
// Reset camera with R key (edge-triggered) — only when UI doesn't want keyboard
|
||||||
|
bool rDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_R);
|
||||||
|
if (rDown && !rKeyWasDown) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
rKeyWasDown = rDown;
|
||||||
|
|
||||||
// Stand up on any movement key or jump while sitting (WoW behaviour)
|
// Stand up on any movement key or jump while sitting (WoW behaviour)
|
||||||
if (!uiWantsKeyboard && sitting && !movementSuppressed) {
|
if (!uiWantsKeyboard && sitting && !movementSuppressed) {
|
||||||
bool anyMoveKey =
|
bool anyMoveKey =
|
||||||
|
|
@ -1851,8 +1858,7 @@ void CameraController::update(float deltaTime) {
|
||||||
wasJumping = nowJump;
|
wasJumping = nowJump;
|
||||||
wasFalling = !grounded && verticalVelocity <= 0.0f;
|
wasFalling = !grounded && verticalVelocity <= 0.0f;
|
||||||
|
|
||||||
// R key disabled — was camera reset, conflicts with chat reply
|
// R key is now handled above with chat safeguard (WantTextInput check)
|
||||||
rKeyWasDown = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CameraController::processMouseMotion(const SDL_MouseMotionEvent& event) {
|
void CameraController::processMouseMotion(const SDL_MouseMotionEvent& event) {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
#include "rendering/vk_shader.hpp"
|
#include "rendering/vk_shader.hpp"
|
||||||
#include "rendering/vk_buffer.hpp"
|
#include "rendering/vk_buffer.hpp"
|
||||||
#include "rendering/vk_utils.hpp"
|
#include "rendering/vk_utils.hpp"
|
||||||
|
#include "rendering/vk_frame_data.hpp"
|
||||||
#include "rendering/camera.hpp"
|
#include "rendering/camera.hpp"
|
||||||
#include "pipeline/asset_manager.hpp"
|
#include "pipeline/asset_manager.hpp"
|
||||||
#include "pipeline/blp_loader.hpp"
|
#include "pipeline/blp_loader.hpp"
|
||||||
|
|
@ -45,25 +46,6 @@ namespace wowee {
|
||||||
namespace rendering {
|
namespace rendering {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
size_t envSizeMBOrDefault(const char* name, size_t defMb) {
|
|
||||||
const char* v = std::getenv(name);
|
|
||||||
if (!v || !*v) return defMb;
|
|
||||||
char* end = nullptr;
|
|
||||||
unsigned long long mb = std::strtoull(v, &end, 10);
|
|
||||||
if (end == v || mb == 0) return defMb;
|
|
||||||
if (mb > (std::numeric_limits<size_t>::max() / (1024ull * 1024ull))) return defMb;
|
|
||||||
return static_cast<size_t>(mb);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t envSizeOrDefault(const char* name, size_t defValue) {
|
|
||||||
const char* v = std::getenv(name);
|
|
||||||
if (!v || !*v) return defValue;
|
|
||||||
char* end = nullptr;
|
|
||||||
unsigned long long n = std::strtoull(v, &end, 10);
|
|
||||||
if (end == v || n == 0) return defValue;
|
|
||||||
return static_cast<size_t>(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t approxTextureBytesWithMips(int w, int h) {
|
size_t approxTextureBytesWithMips(int w, int h) {
|
||||||
if (w <= 0 || h <= 0) return 0;
|
if (w <= 0 || h <= 0) return 0;
|
||||||
size_t base = static_cast<size_t>(w) * static_cast<size_t>(h) * 4ull;
|
size_t base = static_cast<size_t>(w) * static_cast<size_t>(h) * 4ull;
|
||||||
|
|
@ -2678,8 +2660,6 @@ void CharacterRenderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& light
|
||||||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipelineLayout_,
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipelineLayout_,
|
||||||
1, 1, &shadowParamsSet_, 0, nullptr);
|
1, 1, &shadowParamsSet_, 0, nullptr);
|
||||||
|
|
||||||
struct ShadowPush { glm::mat4 lightSpaceMatrix; glm::mat4 model; };
|
|
||||||
|
|
||||||
const float shadowRadiusSq = shadowRadius * shadowRadius;
|
const float shadowRadiusSq = shadowRadius * shadowRadius;
|
||||||
for (auto& pair : instances) {
|
for (auto& pair : instances) {
|
||||||
auto& inst = pair.second;
|
auto& inst = pair.second;
|
||||||
|
|
@ -3034,6 +3014,65 @@ bool CharacterRenderer::getInstanceModelName(uint32_t instanceId, std::string& m
|
||||||
return !modelName.empty();
|
return !modelName.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CharacterRenderer::findAttachmentBone(uint32_t modelId, uint32_t attachmentId,
|
||||||
|
uint16_t& outBoneIndex, glm::vec3& outOffset) const {
|
||||||
|
auto modelIt = models.find(modelId);
|
||||||
|
if (modelIt == models.end()) return false;
|
||||||
|
const auto& model = modelIt->second.data;
|
||||||
|
|
||||||
|
outBoneIndex = 0;
|
||||||
|
outOffset = glm::vec3(0.0f);
|
||||||
|
bool found = false;
|
||||||
|
|
||||||
|
// Try attachment lookup first
|
||||||
|
if (attachmentId < model.attachmentLookup.size()) {
|
||||||
|
uint16_t attIdx = model.attachmentLookup[attachmentId];
|
||||||
|
if (attIdx < model.attachments.size()) {
|
||||||
|
outBoneIndex = model.attachments[attIdx].bone;
|
||||||
|
outOffset = model.attachments[attIdx].position;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: scan attachments by id
|
||||||
|
if (!found) {
|
||||||
|
for (const auto& att : model.attachments) {
|
||||||
|
if (att.id == attachmentId) {
|
||||||
|
outBoneIndex = att.bone;
|
||||||
|
outOffset = att.position;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: key-bone lookup for weapon hand attachment IDs (ID 1 = right hand, ID 2 = left hand)
|
||||||
|
if (!found && (attachmentId == 1 || attachmentId == 2)) {
|
||||||
|
int32_t targetKeyBone = (attachmentId == 1) ? 26 : 27;
|
||||||
|
for (size_t i = 0; i < model.bones.size(); i++) {
|
||||||
|
if (model.bones[i].keyBoneId == targetKeyBone) {
|
||||||
|
outBoneIndex = static_cast<uint16_t>(i);
|
||||||
|
outOffset = glm::vec3(0.0f);
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for head attachment (ID 11): use bone 0 if attachment not defined
|
||||||
|
if (!found && attachmentId == 11 && model.bones.size() > 0) {
|
||||||
|
outBoneIndex = 0;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate bone index
|
||||||
|
if (found && outBoneIndex >= model.bones.size()) {
|
||||||
|
found = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
bool CharacterRenderer::attachWeapon(uint32_t charInstanceId, uint32_t attachmentId,
|
bool CharacterRenderer::attachWeapon(uint32_t charInstanceId, uint32_t attachmentId,
|
||||||
const pipeline::M2Model& weaponModel, uint32_t weaponModelId,
|
const pipeline::M2Model& weaponModel, uint32_t weaponModelId,
|
||||||
const std::string& texturePath) {
|
const std::string& texturePath) {
|
||||||
|
|
@ -3045,62 +3084,11 @@ bool CharacterRenderer::attachWeapon(uint32_t charInstanceId, uint32_t attachmen
|
||||||
auto& charInstance = charIt->second;
|
auto& charInstance = charIt->second;
|
||||||
auto charModelIt = models.find(charInstance.modelId);
|
auto charModelIt = models.find(charInstance.modelId);
|
||||||
if (charModelIt == models.end()) return false;
|
if (charModelIt == models.end()) return false;
|
||||||
const auto& charModel = charModelIt->second.data;
|
|
||||||
|
|
||||||
// Find bone index for this attachment point
|
// Find bone index for this attachment point
|
||||||
uint16_t boneIndex = 0;
|
uint16_t boneIndex = 0;
|
||||||
glm::vec3 offset(0.0f);
|
glm::vec3 offset(0.0f);
|
||||||
bool found = false;
|
if (!findAttachmentBone(charInstance.modelId, attachmentId, boneIndex, offset)) {
|
||||||
|
|
||||||
// Try attachment lookup first
|
|
||||||
if (attachmentId < charModel.attachmentLookup.size()) {
|
|
||||||
uint16_t attIdx = charModel.attachmentLookup[attachmentId];
|
|
||||||
if (attIdx < charModel.attachments.size()) {
|
|
||||||
boneIndex = charModel.attachments[attIdx].bone;
|
|
||||||
offset = charModel.attachments[attIdx].position;
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fallback: scan attachments by id
|
|
||||||
if (!found) {
|
|
||||||
for (const auto& att : charModel.attachments) {
|
|
||||||
if (att.id == attachmentId) {
|
|
||||||
boneIndex = att.bone;
|
|
||||||
offset = att.position;
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fallback to key-bone lookup only for weapon hand attachment IDs.
|
|
||||||
if (!found && (attachmentId == 1 || attachmentId == 2)) {
|
|
||||||
int32_t targetKeyBone = (attachmentId == 1) ? 26 : 27;
|
|
||||||
for (size_t i = 0; i < charModel.bones.size(); i++) {
|
|
||||||
if (charModel.bones[i].keyBoneId == targetKeyBone) {
|
|
||||||
boneIndex = static_cast<uint16_t>(i);
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate bone index (bad attachment tables should not silently bind to origin)
|
|
||||||
if (found && boneIndex >= charModel.bones.size()) {
|
|
||||||
found = false;
|
|
||||||
}
|
|
||||||
if (!found && (attachmentId == 1 || attachmentId == 2)) {
|
|
||||||
int32_t targetKeyBone = (attachmentId == 1) ? 26 : 27;
|
|
||||||
for (size_t i = 0; i < charModel.bones.size(); i++) {
|
|
||||||
if (charModel.bones[i].keyBoneId == targetKeyBone) {
|
|
||||||
boneIndex = static_cast<uint16_t>(i);
|
|
||||||
offset = glm::vec3(0.0f);
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
core::Logger::getInstance().warning("attachWeapon: no bone found for attachment ", attachmentId);
|
core::Logger::getInstance().warning("attachWeapon: no bone found for attachment ", attachmentId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -3211,57 +3199,11 @@ bool CharacterRenderer::getAttachmentTransform(uint32_t instanceId, uint32_t att
|
||||||
if (instIt == instances.end()) return false;
|
if (instIt == instances.end()) return false;
|
||||||
const auto& instance = instIt->second;
|
const auto& instance = instIt->second;
|
||||||
|
|
||||||
auto modelIt = models.find(instance.modelId);
|
// Find attachment point using shared lookup logic
|
||||||
if (modelIt == models.end()) return false;
|
|
||||||
const auto& model = modelIt->second.data;
|
|
||||||
|
|
||||||
// Find attachment point
|
|
||||||
uint16_t boneIndex = 0;
|
uint16_t boneIndex = 0;
|
||||||
glm::vec3 offset(0.0f);
|
glm::vec3 offset(0.0f);
|
||||||
bool found = false;
|
if (!findAttachmentBone(instance.modelId, attachmentId, boneIndex, offset)) {
|
||||||
|
return false;
|
||||||
// Try attachment lookup first
|
|
||||||
if (attachmentId < model.attachmentLookup.size()) {
|
|
||||||
uint16_t attIdx = model.attachmentLookup[attachmentId];
|
|
||||||
if (attIdx < model.attachments.size()) {
|
|
||||||
boneIndex = model.attachments[attIdx].bone;
|
|
||||||
offset = model.attachments[attIdx].position;
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: scan attachments by id
|
|
||||||
if (!found) {
|
|
||||||
for (const auto& att : model.attachments) {
|
|
||||||
if (att.id == attachmentId) {
|
|
||||||
boneIndex = att.bone;
|
|
||||||
offset = att.position;
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) return false;
|
|
||||||
|
|
||||||
// Validate bone index; invalid indices bind attachments to origin (looks like weapons at feet).
|
|
||||||
if (boneIndex >= model.bones.size()) {
|
|
||||||
// Fallback: key bones (26/27) only for hand attachments.
|
|
||||||
if (attachmentId == 1 || attachmentId == 2) {
|
|
||||||
int32_t targetKeyBone = (attachmentId == 1) ? 26 : 27;
|
|
||||||
found = false;
|
|
||||||
for (size_t i = 0; i < model.bones.size(); i++) {
|
|
||||||
if (model.bones[i].keyBoneId == targetKeyBone) {
|
|
||||||
boneIndex = static_cast<uint16_t>(i);
|
|
||||||
offset = glm::vec3(0.0f);
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) return false;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get bone matrix
|
// Get bone matrix
|
||||||
|
|
|
||||||
|
|
@ -40,24 +40,6 @@ bool envFlagEnabled(const char* key, bool defaultValue) {
|
||||||
return !(v == "0" || v == "false" || v == "off" || v == "no");
|
return !(v == "0" || v == "false" || v == "off" || v == "no");
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t envSizeMBOrDefault(const char* name, size_t defMb) {
|
|
||||||
const char* raw = std::getenv(name);
|
|
||||||
if (!raw || !*raw) return defMb;
|
|
||||||
char* end = nullptr;
|
|
||||||
unsigned long long mb = std::strtoull(raw, &end, 10);
|
|
||||||
if (end == raw || mb == 0) return defMb;
|
|
||||||
return static_cast<size_t>(mb);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t envSizeOrDefault(const char* name, size_t defValue) {
|
|
||||||
const char* raw = std::getenv(name);
|
|
||||||
if (!raw || !*raw) return defValue;
|
|
||||||
char* end = nullptr;
|
|
||||||
unsigned long long v = std::strtoull(raw, &end, 10);
|
|
||||||
if (end == raw || v == 0) return defValue;
|
|
||||||
return static_cast<size_t>(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
static constexpr uint32_t kParticleFlagRandomized = 0x40;
|
static constexpr uint32_t kParticleFlagRandomized = 0x40;
|
||||||
static constexpr uint32_t kParticleFlagTiled = 0x80;
|
static constexpr uint32_t kParticleFlagTiled = 0x80;
|
||||||
|
|
||||||
|
|
@ -210,22 +192,6 @@ float pointAABBDistanceSq(const glm::vec3& p, const glm::vec3& bmin, const glm::
|
||||||
return glm::dot(d, d);
|
return glm::dot(d, d);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct QueryTimer {
|
|
||||||
double* totalMs = nullptr;
|
|
||||||
uint32_t* callCount = nullptr;
|
|
||||||
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
|
|
||||||
QueryTimer(double* total, uint32_t* calls) : totalMs(total), callCount(calls) {}
|
|
||||||
~QueryTimer() {
|
|
||||||
if (callCount) {
|
|
||||||
(*callCount)++;
|
|
||||||
}
|
|
||||||
if (totalMs) {
|
|
||||||
auto end = std::chrono::steady_clock::now();
|
|
||||||
*totalMs += std::chrono::duration<double, std::milli>(end - start).count();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Möller–Trumbore ray-triangle intersection.
|
// Möller–Trumbore ray-triangle intersection.
|
||||||
// Returns distance along ray if hit, negative if miss.
|
// Returns distance along ray if hit, negative if miss.
|
||||||
float rayTriangleIntersect(const glm::vec3& origin, const glm::vec3& dir,
|
float rayTriangleIntersect(const glm::vec3& origin, const glm::vec3& dir,
|
||||||
|
|
@ -2031,7 +1997,7 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
|
||||||
std::uniform_real_distribution<float> distDrift(-0.2f, 0.2f);
|
std::uniform_real_distribution<float> distDrift(-0.2f, 0.2f);
|
||||||
|
|
||||||
smokeEmitAccum += deltaTime;
|
smokeEmitAccum += deltaTime;
|
||||||
float emitInterval = 1.0f / 8.0f; // 8 particles per second per emitter
|
float emitInterval = 1.0f / 16.0f; // 16 particles per second per emitter
|
||||||
|
|
||||||
if (smokeEmitAccum >= emitInterval &&
|
if (smokeEmitAccum >= emitInterval &&
|
||||||
static_cast<int>(smokeParticles.size()) < MAX_SMOKE_PARTICLES) {
|
static_cast<int>(smokeParticles.size()) < MAX_SMOKE_PARTICLES) {
|
||||||
|
|
@ -2883,16 +2849,6 @@ bool M2Renderer::initializeShadow(VkRenderPass shadowRenderPass) {
|
||||||
if (!vkCtx_ || shadowRenderPass == VK_NULL_HANDLE) return false;
|
if (!vkCtx_ || shadowRenderPass == VK_NULL_HANDLE) return false;
|
||||||
VkDevice device = vkCtx_->getDevice();
|
VkDevice device = vkCtx_->getDevice();
|
||||||
|
|
||||||
// ShadowParams UBO: useBones, useTexture, alphaTest, foliageSway, windTime, foliageMotionDamp
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create ShadowParams UBO
|
// Create ShadowParams UBO
|
||||||
VkBufferCreateInfo bufCI{};
|
VkBufferCreateInfo bufCI{};
|
||||||
bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
||||||
|
|
@ -3070,15 +3026,6 @@ void M2Renderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMa
|
||||||
if (!shadowPipeline_ || !shadowParamsSet_) return;
|
if (!shadowPipeline_ || !shadowParamsSet_) return;
|
||||||
if (instances.empty() || models.empty()) return;
|
if (instances.empty() || models.empty()) return;
|
||||||
|
|
||||||
struct ShadowPush { glm::mat4 lightSpaceMatrix; glm::mat4 model; };
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
const float shadowRadiusSq = shadowRadius * shadowRadius;
|
const float shadowRadiusSq = shadowRadius * shadowRadius;
|
||||||
|
|
||||||
// Reset per-frame texture descriptor pool for foliage alpha-test sets
|
// Reset per-frame texture descriptor pool for foliage alpha-test sets
|
||||||
|
|
|
||||||
|
|
@ -20,17 +20,6 @@
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace rendering {
|
namespace rendering {
|
||||||
|
|
||||||
namespace {
|
|
||||||
size_t envSizeMBOrDefault(const char* name, size_t defMb) {
|
|
||||||
const char* raw = std::getenv(name);
|
|
||||||
if (!raw || !*raw) return defMb;
|
|
||||||
char* end = nullptr;
|
|
||||||
unsigned long long mb = std::strtoull(raw, &end, 10);
|
|
||||||
if (end == raw || mb == 0) return defMb;
|
|
||||||
return static_cast<size_t>(mb);
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
// Matches set 1 binding 7 in terrain.frag.glsl
|
// Matches set 1 binding 7 in terrain.frag.glsl
|
||||||
struct TerrainParamsUBO {
|
struct TerrainParamsUBO {
|
||||||
int32_t layerCount;
|
int32_t layerCount;
|
||||||
|
|
@ -799,15 +788,6 @@ bool TerrainRenderer::initializeShadow(VkRenderPass shadowRenderPass) {
|
||||||
VmaAllocator allocator = vkCtx->getAllocator();
|
VmaAllocator allocator = vkCtx->getAllocator();
|
||||||
|
|
||||||
// ShadowParams UBO — terrain uses no bones, no texture, no alpha test
|
// 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{};
|
VkBufferCreateInfo bufCI{};
|
||||||
bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
||||||
bufCI.size = sizeof(ShadowParamsUBO);
|
bufCI.size = sizeof(ShadowParamsUBO);
|
||||||
|
|
@ -965,7 +945,6 @@ void TerrainRenderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSp
|
||||||
|
|
||||||
// Identity model matrix — terrain vertices are already in world space
|
// Identity model matrix — terrain vertices are already in world space
|
||||||
static const glm::mat4 identity(1.0f);
|
static const glm::mat4 identity(1.0f);
|
||||||
struct ShadowPush { glm::mat4 lightSpaceMatrix; glm::mat4 model; };
|
|
||||||
ShadowPush push{ lightSpaceMatrix, identity };
|
ShadowPush push{ lightSpaceMatrix, identity };
|
||||||
vkCmdPushConstants(cmd, shadowPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT,
|
vkCmdPushConstants(cmd, shadowPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT,
|
||||||
0, 128, &push);
|
0, 128, &push);
|
||||||
|
|
|
||||||
|
|
@ -29,23 +29,6 @@ namespace wowee {
|
||||||
namespace rendering {
|
namespace rendering {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
size_t envSizeMBOrDefault(const char* name, size_t defMb) {
|
|
||||||
const char* raw = std::getenv(name);
|
|
||||||
if (!raw || !*raw) return defMb;
|
|
||||||
char* end = nullptr;
|
|
||||||
unsigned long long mb = std::strtoull(raw, &end, 10);
|
|
||||||
if (end == raw || mb == 0) return defMb;
|
|
||||||
return static_cast<size_t>(mb);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t envSizeOrDefault(const char* name, size_t defValue) {
|
|
||||||
const char* raw = std::getenv(name);
|
|
||||||
if (!raw || !*raw) return defValue;
|
|
||||||
char* end = nullptr;
|
|
||||||
unsigned long long v = std::strtoull(raw, &end, 10);
|
|
||||||
if (end == raw || v == 0) return defValue;
|
|
||||||
return static_cast<size_t>(v);
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// Thread-local scratch buffers for collision queries (allows concurrent getFloorHeight/checkWallCollision calls)
|
// Thread-local scratch buffers for collision queries (allows concurrent getFloorHeight/checkWallCollision calls)
|
||||||
|
|
@ -1545,16 +1528,6 @@ bool WMORenderer::initializeShadow(VkRenderPass shadowRenderPass) {
|
||||||
if (!vkCtx_ || shadowRenderPass == VK_NULL_HANDLE) return false;
|
if (!vkCtx_ || shadowRenderPass == VK_NULL_HANDLE) return false;
|
||||||
VkDevice device = vkCtx_->getDevice();
|
VkDevice device = vkCtx_->getDevice();
|
||||||
|
|
||||||
// ShadowParams UBO: useBones, useTexture, alphaTest, foliageSway, windTime, foliageMotionDamp
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create ShadowParams UBO
|
// Create ShadowParams UBO
|
||||||
VkBufferCreateInfo bufCI{};
|
VkBufferCreateInfo bufCI{};
|
||||||
bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
||||||
|
|
@ -1715,8 +1688,6 @@ void WMORenderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceM
|
||||||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipelineLayout_,
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowPipelineLayout_,
|
||||||
0, 1, &shadowParamsSet_, 0, nullptr);
|
0, 1, &shadowParamsSet_, 0, nullptr);
|
||||||
|
|
||||||
struct ShadowPush { glm::mat4 lightSpaceMatrix; glm::mat4 model; };
|
|
||||||
|
|
||||||
// WMO shadow cull uses the ortho half-extent (shadow map coverage) rather than
|
// WMO shadow cull uses the ortho half-extent (shadow map coverage) rather than
|
||||||
// the proximity radius so that distant buildings whose shadows reach the player
|
// the proximity radius so that distant buildings whose shadows reach the player
|
||||||
// are still rendered into the shadow map.
|
// are still rendered into the shadow map.
|
||||||
|
|
@ -2521,22 +2492,6 @@ static float pointAABBDistanceSq(const glm::vec3& p, const glm::vec3& bmin, cons
|
||||||
return glm::dot(d, d);
|
return glm::dot(d, d);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct QueryTimer {
|
|
||||||
double* totalMs = nullptr;
|
|
||||||
uint32_t* callCount = nullptr;
|
|
||||||
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
|
|
||||||
QueryTimer(double* total, uint32_t* calls) : totalMs(total), callCount(calls) {}
|
|
||||||
~QueryTimer() {
|
|
||||||
if (callCount) {
|
|
||||||
(*callCount)++;
|
|
||||||
}
|
|
||||||
if (totalMs) {
|
|
||||||
auto end = std::chrono::steady_clock::now();
|
|
||||||
*totalMs += std::chrono::duration<double, std::milli>(end - start).count();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Möller–Trumbore ray-triangle intersection
|
// Möller–Trumbore ray-triangle intersection
|
||||||
// Returns distance along ray if hit, or negative if miss
|
// Returns distance along ray if hit, or negative if miss
|
||||||
static float rayTriangleIntersect(const glm::vec3& origin, const glm::vec3& dir,
|
static float rayTriangleIntersect(const glm::vec3& origin, const glm::vec3& dir,
|
||||||
|
|
@ -3628,12 +3583,13 @@ void WMORenderer::recreatePipelines() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Vertex input ---
|
// --- Vertex input ---
|
||||||
|
// WMO vertex: pos3 + normal3 + texCoord2 + color4 + tangent4 = 64 bytes
|
||||||
struct WMOVertexData {
|
struct WMOVertexData {
|
||||||
glm::vec3 position;
|
glm::vec3 position;
|
||||||
glm::vec3 normal;
|
glm::vec3 normal;
|
||||||
glm::vec2 texCoord;
|
glm::vec2 texCoord;
|
||||||
glm::vec4 color;
|
glm::vec4 color;
|
||||||
glm::vec4 tangent;
|
glm::vec4 tangent; // xyz=tangent dir, w=handedness ±1
|
||||||
};
|
};
|
||||||
|
|
||||||
VkVertexInputBindingDescription vertexBinding{};
|
VkVertexInputBindingDescription vertexBinding{};
|
||||||
|
|
|
||||||
|
|
@ -408,7 +408,9 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
renderNameplates(gameHandler); // player names always shown; NPC plates gated by showNameplates_
|
renderNameplates(gameHandler); // player names always shown; NPC plates gated by showNameplates_
|
||||||
renderBattlegroundScore(gameHandler);
|
renderBattlegroundScore(gameHandler);
|
||||||
renderCombatText(gameHandler);
|
renderCombatText(gameHandler);
|
||||||
renderPartyFrames(gameHandler);
|
if (showRaidFrames_) {
|
||||||
|
renderPartyFrames(gameHandler);
|
||||||
|
}
|
||||||
renderBossFrames(gameHandler);
|
renderBossFrames(gameHandler);
|
||||||
renderGroupInvitePopup(gameHandler);
|
renderGroupInvitePopup(gameHandler);
|
||||||
renderDuelRequestPopup(gameHandler);
|
renderDuelRequestPopup(gameHandler);
|
||||||
|
|
@ -440,7 +442,9 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
renderDungeonFinderWindow(gameHandler);
|
renderDungeonFinderWindow(gameHandler);
|
||||||
renderInstanceLockouts(gameHandler);
|
renderInstanceLockouts(gameHandler);
|
||||||
// renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now
|
// renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now
|
||||||
renderMinimapMarkers(gameHandler);
|
if (showMinimap_) {
|
||||||
|
renderMinimapMarkers(gameHandler);
|
||||||
|
}
|
||||||
renderDeathScreen(gameHandler);
|
renderDeathScreen(gameHandler);
|
||||||
renderReclaimCorpseButton(gameHandler);
|
renderReclaimCorpseButton(gameHandler);
|
||||||
renderResurrectDialog(gameHandler);
|
renderResurrectDialog(gameHandler);
|
||||||
|
|
@ -1452,7 +1456,7 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
||||||
gameHandler.tabTarget(movement.x, movement.y, movement.z);
|
gameHandler.tabTarget(movement.x, movement.y, movement.z);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.isKeyJustPressed(SDL_SCANCODE_ESCAPE)) {
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_SETTINGS, true)) {
|
||||||
if (showSettingsWindow) {
|
if (showSettingsWindow) {
|
||||||
// Close settings window if open
|
// Close settings window if open
|
||||||
showSettingsWindow = false;
|
showSettingsWindow = false;
|
||||||
|
|
@ -1470,11 +1474,27 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// V — toggle nameplates (WoW default keybinding)
|
// Toggle nameplates (customizable keybinding, default V)
|
||||||
if (input.isKeyJustPressed(SDL_SCANCODE_V)) {
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_INVENTORY)) {
|
||||||
|
inventoryScreen.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_NAMEPLATES)) {
|
||||||
showNameplates_ = !showNameplates_;
|
showNameplates_ = !showNameplates_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_WORLD_MAP)) {
|
||||||
|
showWorldMap_ = !showWorldMap_;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_MINIMAP)) {
|
||||||
|
showMinimap_ = !showMinimap_;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_RAID_FRAMES)) {
|
||||||
|
showRaidFrames_ = !showRaidFrames_;
|
||||||
|
}
|
||||||
|
|
||||||
// Action bar keys (1-9, 0, -, =)
|
// Action bar keys (1-9, 0, -, =)
|
||||||
static const SDL_Scancode actionBarKeys[] = {
|
static const SDL_Scancode actionBarKeys[] = {
|
||||||
SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4,
|
SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4,
|
||||||
|
|
@ -1506,7 +1526,7 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enter key: focus chat input (empty) — always works unless already typing
|
// Enter key: focus chat input (empty) — always works unless already typing
|
||||||
if (!chatInputActive && input.isKeyJustPressed(SDL_SCANCODE_RETURN)) {
|
if (!chatInputActive && KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHAT, true)) {
|
||||||
refocusChatInput = true;
|
refocusChatInput = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4003,6 +4023,8 @@ void GameScreen::updateCharacterTextures(game::Inventory& inventory) {
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
void GameScreen::renderWorldMap(game::GameHandler& gameHandler) {
|
void GameScreen::renderWorldMap(game::GameHandler& gameHandler) {
|
||||||
|
if (!showWorldMap_) return;
|
||||||
|
|
||||||
auto& app = core::Application::getInstance();
|
auto& app = core::Application::getInstance();
|
||||||
auto* renderer = app.getRenderer();
|
auto* renderer = app.getRenderer();
|
||||||
if (!renderer) return;
|
if (!renderer) return;
|
||||||
|
|
@ -4059,7 +4081,7 @@ VkDescriptorSet GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManage
|
||||||
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
|
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
|
||||||
if (spellDbc && spellDbc->isLoaded()) {
|
if (spellDbc && spellDbc->isLoaded()) {
|
||||||
uint32_t fieldCount = spellDbc->getFieldCount();
|
uint32_t fieldCount = spellDbc->getFieldCount();
|
||||||
// Try expansion layout first
|
// Helper to load icons for a given field layout
|
||||||
auto tryLoadIcons = [&](uint32_t idField, uint32_t iconField) {
|
auto tryLoadIcons = [&](uint32_t idField, uint32_t iconField) {
|
||||||
spellIconIds_.clear();
|
spellIconIds_.clear();
|
||||||
if (iconField >= fieldCount) return;
|
if (iconField >= fieldCount) return;
|
||||||
|
|
@ -4071,16 +4093,16 @@ VkDescriptorSet GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// If the DBC has WotLK-range field count (≥200 fields), it's the binary
|
|
||||||
// WotLK Spell.dbc (CSV fallback). Use WotLK layout regardless of expansion,
|
// Always use expansion-aware layout if available
|
||||||
// since Turtle/Classic CSV files are garbled and fall back to WotLK binary.
|
// Field indices vary by expansion: Classic=117, TBC=124, WotLK=133
|
||||||
if (fieldCount >= 200) {
|
if (spellL) {
|
||||||
tryLoadIcons(0, 133); // WotLK IconID field
|
|
||||||
} else if (spellL) {
|
|
||||||
tryLoadIcons((*spellL)["ID"], (*spellL)["IconID"]);
|
tryLoadIcons((*spellL)["ID"], (*spellL)["IconID"]);
|
||||||
}
|
}
|
||||||
// Fallback to WotLK field 133 if expansion layout yielded nothing
|
|
||||||
if (spellIconIds_.empty() && fieldCount > 133) {
|
// Fallback if expansion layout missing or yielded nothing
|
||||||
|
// Only use WotLK field 133 as last resort if we have no layout
|
||||||
|
if (spellIconIds_.empty() && !spellL && fieldCount > 133) {
|
||||||
tryLoadIcons(0, 133);
|
tryLoadIcons(0, 133);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6402,8 +6424,8 @@ void GameScreen::renderLfgProposalPopup(game::GameHandler& gameHandler) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) {
|
void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) {
|
||||||
// O key toggle (WoW default Social/Guild keybind)
|
// Guild Roster toggle (customizable keybind)
|
||||||
if (!ImGui::GetIO().WantCaptureKeyboard && ImGui::IsKeyPressed(ImGuiKey_O)) {
|
if (!ImGui::GetIO().WantCaptureKeyboard && KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_GUILD_ROSTER)) {
|
||||||
showGuildRoster_ = !showGuildRoster_;
|
showGuildRoster_ = !showGuildRoster_;
|
||||||
if (showGuildRoster_) {
|
if (showGuildRoster_) {
|
||||||
// Open friends tab directly if not in guild
|
// Open friends tab directly if not in guild
|
||||||
|
|
@ -9180,6 +9202,108 @@ void GameScreen::renderSettingsWindow() {
|
||||||
ImGui::EndTabItem();
|
ImGui::EndTabItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// CONTROLS TAB
|
||||||
|
// ============================================================
|
||||||
|
if (ImGui::BeginTabItem("Controls")) {
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
ImGui::Text("Keybindings");
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
auto& km = ui::KeybindingManager::getInstance();
|
||||||
|
int numActions = km.getActionCount();
|
||||||
|
|
||||||
|
for (int i = 0; i < numActions; ++i) {
|
||||||
|
auto action = static_cast<ui::KeybindingManager::Action>(i);
|
||||||
|
const char* actionName = km.getActionName(action);
|
||||||
|
ImGuiKey currentKey = km.getKeyForAction(action);
|
||||||
|
|
||||||
|
// Display current binding
|
||||||
|
ImGui::Text("%s:", actionName);
|
||||||
|
ImGui::SameLine(200);
|
||||||
|
|
||||||
|
// Get human-readable key name (basic implementation)
|
||||||
|
const char* keyName = "Unknown";
|
||||||
|
if (currentKey >= ImGuiKey_A && currentKey <= ImGuiKey_Z) {
|
||||||
|
static char keyBuf[16];
|
||||||
|
snprintf(keyBuf, sizeof(keyBuf), "%c", 'A' + (currentKey - ImGuiKey_A));
|
||||||
|
keyName = keyBuf;
|
||||||
|
} else if (currentKey >= ImGuiKey_0 && currentKey <= ImGuiKey_9) {
|
||||||
|
static char keyBuf[16];
|
||||||
|
snprintf(keyBuf, sizeof(keyBuf), "%c", '0' + (currentKey - ImGuiKey_0));
|
||||||
|
keyName = keyBuf;
|
||||||
|
} else if (currentKey == ImGuiKey_Escape) {
|
||||||
|
keyName = "Escape";
|
||||||
|
} else if (currentKey == ImGuiKey_Enter) {
|
||||||
|
keyName = "Enter";
|
||||||
|
} else if (currentKey == ImGuiKey_Tab) {
|
||||||
|
keyName = "Tab";
|
||||||
|
} else if (currentKey == ImGuiKey_Space) {
|
||||||
|
keyName = "Space";
|
||||||
|
} else if (currentKey >= ImGuiKey_F1 && currentKey <= ImGuiKey_F12) {
|
||||||
|
static char keyBuf[16];
|
||||||
|
snprintf(keyBuf, sizeof(keyBuf), "F%d", 1 + (currentKey - ImGuiKey_F1));
|
||||||
|
keyName = keyBuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Text("[%s]", keyName);
|
||||||
|
|
||||||
|
// Rebind button
|
||||||
|
ImGui::SameLine(350);
|
||||||
|
if (ImGui::Button(awaitingKeyPress && pendingRebindAction == i ? "Waiting..." : "Rebind", ImVec2(100, 0))) {
|
||||||
|
pendingRebindAction = i;
|
||||||
|
awaitingKeyPress = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle key press during rebinding
|
||||||
|
if (awaitingKeyPress && pendingRebindAction >= 0) {
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Text("Press any key to bind to this action (Esc to cancel)...");
|
||||||
|
|
||||||
|
// Check for any key press
|
||||||
|
bool foundKey = false;
|
||||||
|
ImGuiKey newKey = ImGuiKey_None;
|
||||||
|
for (int k = ImGuiKey_NamedKey_BEGIN; k < ImGuiKey_NamedKey_END; ++k) {
|
||||||
|
if (ImGui::IsKeyPressed(static_cast<ImGuiKey>(k), false)) {
|
||||||
|
if (k == ImGuiKey_Escape) {
|
||||||
|
// Cancel rebinding
|
||||||
|
awaitingKeyPress = false;
|
||||||
|
pendingRebindAction = -1;
|
||||||
|
foundKey = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
newKey = static_cast<ImGuiKey>(k);
|
||||||
|
foundKey = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundKey && newKey != ImGuiKey_None) {
|
||||||
|
auto action = static_cast<ui::KeybindingManager::Action>(pendingRebindAction);
|
||||||
|
km.setKeyForAction(action, newKey);
|
||||||
|
awaitingKeyPress = false;
|
||||||
|
pendingRebindAction = -1;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Spacing();
|
||||||
|
|
||||||
|
if (ImGui::Button("Reset to Defaults", ImVec2(-1, 0))) {
|
||||||
|
km.resetToDefaults();
|
||||||
|
awaitingKeyPress = false;
|
||||||
|
pendingRebindAction = -1;
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// CHAT TAB
|
// CHAT TAB
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -10063,6 +10187,11 @@ void GameScreen::saveSettings() {
|
||||||
out << "chat_autojoin_lfg=" << (chatAutoJoinLFG_ ? 1 : 0) << "\n";
|
out << "chat_autojoin_lfg=" << (chatAutoJoinLFG_ ? 1 : 0) << "\n";
|
||||||
out << "chat_autojoin_local=" << (chatAutoJoinLocal_ ? 1 : 0) << "\n";
|
out << "chat_autojoin_local=" << (chatAutoJoinLocal_ ? 1 : 0) << "\n";
|
||||||
|
|
||||||
|
out.close();
|
||||||
|
|
||||||
|
// Save keybindings to the same config file (appends [Keybindings] section)
|
||||||
|
KeybindingManager::getInstance().saveToConfigFile(path);
|
||||||
|
|
||||||
LOG_INFO("Settings saved to ", path);
|
LOG_INFO("Settings saved to ", path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10176,6 +10305,10 @@ void GameScreen::loadSettings() {
|
||||||
else if (key == "chat_autojoin_local") chatAutoJoinLocal_ = (std::stoi(val) != 0);
|
else if (key == "chat_autojoin_local") chatAutoJoinLocal_ = (std::stoi(val) != 0);
|
||||||
} catch (...) {}
|
} catch (...) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load keybindings from the same config file
|
||||||
|
KeybindingManager::getInstance().loadFromConfigFile(path);
|
||||||
|
|
||||||
LOG_INFO("Settings loaded from ", path);
|
LOG_INFO("Settings loaded from ", path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -11551,8 +11684,8 @@ void GameScreen::renderZoneText() {
|
||||||
// Dungeon Finder window (toggle with hotkey or bag-bar button)
|
// Dungeon Finder window (toggle with hotkey or bag-bar button)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) {
|
void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) {
|
||||||
// Toggle on I key when not typing
|
// Toggle Dungeon Finder (customizable keybind)
|
||||||
if (!chatInputActive && ImGui::IsKeyPressed(ImGuiKey_I, false)) {
|
if (!chatInputActive && KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_DUNGEON_FINDER)) {
|
||||||
showDungeonFinder_ = !showDungeonFinder_;
|
showDungeonFinder_ = !showDungeonFinder_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "ui/inventory_screen.hpp"
|
#include "ui/inventory_screen.hpp"
|
||||||
|
#include "ui/keybinding_manager.hpp"
|
||||||
#include "game/game_handler.hpp"
|
#include "game/game_handler.hpp"
|
||||||
#include "core/application.hpp"
|
#include "core/application.hpp"
|
||||||
#include "rendering/vk_context.hpp"
|
#include "rendering/vk_context.hpp"
|
||||||
|
|
@ -709,18 +710,21 @@ bool InventoryScreen::bagHasAnyItems(const game::Inventory& inventory, int bagIn
|
||||||
}
|
}
|
||||||
|
|
||||||
void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) {
|
void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) {
|
||||||
// B key toggle (edge-triggered)
|
// Bags toggle (B key, edge-triggered)
|
||||||
bool wantsTextInput = ImGui::GetIO().WantTextInput;
|
bool bagsDown = KeybindingManager::getInstance().isActionPressed(
|
||||||
bool bDown = !wantsTextInput && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_B);
|
KeybindingManager::Action::TOGGLE_BAGS, false);
|
||||||
bool bToggled = bDown && !bKeyWasDown;
|
bool bToggled = bagsDown && !bKeyWasDown;
|
||||||
bKeyWasDown = bDown;
|
bKeyWasDown = bagsDown;
|
||||||
|
|
||||||
// C key toggle for character screen (edge-triggered)
|
// Character screen toggle (C key, edge-triggered)
|
||||||
bool cDown = !wantsTextInput && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_C);
|
bool characterDown = KeybindingManager::getInstance().isActionPressed(
|
||||||
if (cDown && !cKeyWasDown) {
|
KeybindingManager::Action::TOGGLE_CHARACTER_SCREEN, false);
|
||||||
|
if (characterDown && !cKeyWasDown) {
|
||||||
characterOpen = !characterOpen;
|
characterOpen = !characterOpen;
|
||||||
}
|
}
|
||||||
cKeyWasDown = cDown;
|
cKeyWasDown = characterDown;
|
||||||
|
|
||||||
|
bool wantsTextInput = ImGui::GetIO().WantTextInput;
|
||||||
|
|
||||||
if (separateBags_) {
|
if (separateBags_) {
|
||||||
if (bToggled) {
|
if (bToggled) {
|
||||||
|
|
|
||||||
282
src/ui/keybinding_manager.cpp
Normal file
282
src/ui/keybinding_manager.cpp
Normal file
|
|
@ -0,0 +1,282 @@
|
||||||
|
#include "ui/keybinding_manager.hpp"
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace wowee::ui {
|
||||||
|
|
||||||
|
KeybindingManager& KeybindingManager::getInstance() {
|
||||||
|
static KeybindingManager instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeybindingManager::KeybindingManager() {
|
||||||
|
initializeDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
void KeybindingManager::initializeDefaults() {
|
||||||
|
// Set default keybindings
|
||||||
|
bindings_[static_cast<int>(Action::TOGGLE_CHARACTER_SCREEN)] = ImGuiKey_C;
|
||||||
|
bindings_[static_cast<int>(Action::TOGGLE_INVENTORY)] = ImGuiKey_I;
|
||||||
|
bindings_[static_cast<int>(Action::TOGGLE_BAGS)] = ImGuiKey_B;
|
||||||
|
bindings_[static_cast<int>(Action::TOGGLE_SPELLBOOK)] = ImGuiKey_P; // WoW standard key
|
||||||
|
bindings_[static_cast<int>(Action::TOGGLE_TALENTS)] = ImGuiKey_N; // WoW standard key
|
||||||
|
bindings_[static_cast<int>(Action::TOGGLE_QUESTS)] = ImGuiKey_L;
|
||||||
|
bindings_[static_cast<int>(Action::TOGGLE_MINIMAP)] = ImGuiKey_M;
|
||||||
|
bindings_[static_cast<int>(Action::TOGGLE_SETTINGS)] = ImGuiKey_Escape;
|
||||||
|
bindings_[static_cast<int>(Action::TOGGLE_CHAT)] = ImGuiKey_Enter;
|
||||||
|
bindings_[static_cast<int>(Action::TOGGLE_GUILD_ROSTER)] = ImGuiKey_O;
|
||||||
|
bindings_[static_cast<int>(Action::TOGGLE_DUNGEON_FINDER)] = ImGuiKey_J; // Originally I, reassigned to avoid conflict
|
||||||
|
bindings_[static_cast<int>(Action::TOGGLE_WORLD_MAP)] = ImGuiKey_W;
|
||||||
|
bindings_[static_cast<int>(Action::TOGGLE_NAMEPLATES)] = ImGuiKey_V;
|
||||||
|
bindings_[static_cast<int>(Action::TOGGLE_RAID_FRAMES)] = ImGuiKey_F; // Reassigned from R (now camera reset)
|
||||||
|
bindings_[static_cast<int>(Action::TOGGLE_QUEST_LOG)] = ImGuiKey_Q;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool KeybindingManager::isActionPressed(Action action, bool repeat) {
|
||||||
|
auto it = bindings_.find(static_cast<int>(action));
|
||||||
|
if (it == bindings_.end()) return false;
|
||||||
|
return ImGui::IsKeyPressed(it->second, repeat);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiKey KeybindingManager::getKeyForAction(Action action) const {
|
||||||
|
auto it = bindings_.find(static_cast<int>(action));
|
||||||
|
if (it == bindings_.end()) return ImGuiKey_None;
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
void KeybindingManager::setKeyForAction(Action action, ImGuiKey key) {
|
||||||
|
bindings_[static_cast<int>(action)] = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
void KeybindingManager::resetToDefaults() {
|
||||||
|
bindings_.clear();
|
||||||
|
initializeDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* KeybindingManager::getActionName(Action action) {
|
||||||
|
switch (action) {
|
||||||
|
case Action::TOGGLE_CHARACTER_SCREEN: return "Character Screen";
|
||||||
|
case Action::TOGGLE_INVENTORY: return "Inventory";
|
||||||
|
case Action::TOGGLE_BAGS: return "Bags";
|
||||||
|
case Action::TOGGLE_SPELLBOOK: return "Spellbook";
|
||||||
|
case Action::TOGGLE_TALENTS: return "Talents";
|
||||||
|
case Action::TOGGLE_QUESTS: return "Quests";
|
||||||
|
case Action::TOGGLE_MINIMAP: return "Minimap";
|
||||||
|
case Action::TOGGLE_SETTINGS: return "Settings";
|
||||||
|
case Action::TOGGLE_CHAT: return "Chat";
|
||||||
|
case Action::TOGGLE_GUILD_ROSTER: return "Guild Roster / Social";
|
||||||
|
case Action::TOGGLE_DUNGEON_FINDER: return "Dungeon Finder";
|
||||||
|
case Action::TOGGLE_WORLD_MAP: return "World Map";
|
||||||
|
case Action::TOGGLE_NAMEPLATES: return "Nameplates";
|
||||||
|
case Action::TOGGLE_RAID_FRAMES: return "Raid Frames";
|
||||||
|
case Action::TOGGLE_QUEST_LOG: return "Quest Log";
|
||||||
|
case Action::ACTION_COUNT: break;
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
void KeybindingManager::loadFromConfigFile(const std::string& filePath) {
|
||||||
|
std::ifstream file(filePath);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
std::cerr << "[KeybindingManager] Failed to open config file: " << filePath << std::endl;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
bool inKeybindingsSection = false;
|
||||||
|
|
||||||
|
while (std::getline(file, line)) {
|
||||||
|
// Trim whitespace
|
||||||
|
size_t start = line.find_first_not_of(" \t\r\n");
|
||||||
|
size_t end = line.find_last_not_of(" \t\r\n");
|
||||||
|
if (start == std::string::npos) continue;
|
||||||
|
line = line.substr(start, end - start + 1);
|
||||||
|
|
||||||
|
// Check for section header
|
||||||
|
if (line == "[Keybindings]") {
|
||||||
|
inKeybindingsSection = true;
|
||||||
|
continue;
|
||||||
|
} else if (line[0] == '[') {
|
||||||
|
inKeybindingsSection = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inKeybindingsSection || line.empty() || line[0] == ';' || line[0] == '#') continue;
|
||||||
|
|
||||||
|
// Parse key=value pair
|
||||||
|
size_t eqPos = line.find('=');
|
||||||
|
if (eqPos == std::string::npos) continue;
|
||||||
|
|
||||||
|
std::string action = line.substr(0, eqPos);
|
||||||
|
std::string keyStr = line.substr(eqPos + 1);
|
||||||
|
|
||||||
|
// Trim key string
|
||||||
|
size_t kStart = keyStr.find_first_not_of(" \t");
|
||||||
|
size_t kEnd = keyStr.find_last_not_of(" \t");
|
||||||
|
if (kStart != std::string::npos) {
|
||||||
|
keyStr = keyStr.substr(kStart, kEnd - kStart + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map action name to enum (simplified mapping)
|
||||||
|
int actionIdx = -1;
|
||||||
|
if (action == "toggle_character_screen") actionIdx = static_cast<int>(Action::TOGGLE_CHARACTER_SCREEN);
|
||||||
|
else if (action == "toggle_inventory") actionIdx = static_cast<int>(Action::TOGGLE_INVENTORY);
|
||||||
|
else if (action == "toggle_bags") actionIdx = static_cast<int>(Action::TOGGLE_BAGS);
|
||||||
|
else if (action == "toggle_spellbook") actionIdx = static_cast<int>(Action::TOGGLE_SPELLBOOK);
|
||||||
|
else if (action == "toggle_talents") actionIdx = static_cast<int>(Action::TOGGLE_TALENTS);
|
||||||
|
else if (action == "toggle_quests") actionIdx = static_cast<int>(Action::TOGGLE_QUESTS);
|
||||||
|
else if (action == "toggle_minimap") actionIdx = static_cast<int>(Action::TOGGLE_MINIMAP);
|
||||||
|
else if (action == "toggle_settings") actionIdx = static_cast<int>(Action::TOGGLE_SETTINGS);
|
||||||
|
else if (action == "toggle_chat") actionIdx = static_cast<int>(Action::TOGGLE_CHAT);
|
||||||
|
else if (action == "toggle_guild_roster") actionIdx = static_cast<int>(Action::TOGGLE_GUILD_ROSTER);
|
||||||
|
else if (action == "toggle_dungeon_finder") actionIdx = static_cast<int>(Action::TOGGLE_DUNGEON_FINDER);
|
||||||
|
else if (action == "toggle_world_map") actionIdx = static_cast<int>(Action::TOGGLE_WORLD_MAP);
|
||||||
|
else if (action == "toggle_nameplates") actionIdx = static_cast<int>(Action::TOGGLE_NAMEPLATES);
|
||||||
|
else if (action == "toggle_raid_frames") actionIdx = static_cast<int>(Action::TOGGLE_RAID_FRAMES);
|
||||||
|
else if (action == "toggle_quest_log") actionIdx = static_cast<int>(Action::TOGGLE_QUEST_LOG);
|
||||||
|
|
||||||
|
if (actionIdx < 0) continue;
|
||||||
|
|
||||||
|
// Parse key string to ImGuiKey (simple mapping of common keys)
|
||||||
|
ImGuiKey key = ImGuiKey_None;
|
||||||
|
if (keyStr.length() == 1) {
|
||||||
|
// Single character key (A-Z, 0-9)
|
||||||
|
char c = keyStr[0];
|
||||||
|
if (c >= 'A' && c <= 'Z') {
|
||||||
|
key = static_cast<ImGuiKey>(ImGuiKey_A + (c - 'A'));
|
||||||
|
} else if (c >= '0' && c <= '9') {
|
||||||
|
key = static_cast<ImGuiKey>(ImGuiKey_0 + (c - '0'));
|
||||||
|
}
|
||||||
|
} else if (keyStr == "Escape") {
|
||||||
|
key = ImGuiKey_Escape;
|
||||||
|
} else if (keyStr == "Enter") {
|
||||||
|
key = ImGuiKey_Enter;
|
||||||
|
} else if (keyStr == "Tab") {
|
||||||
|
key = ImGuiKey_Tab;
|
||||||
|
} else if (keyStr == "Backspace") {
|
||||||
|
key = ImGuiKey_Backspace;
|
||||||
|
} else if (keyStr == "Space") {
|
||||||
|
key = ImGuiKey_Space;
|
||||||
|
} else if (keyStr == "Delete") {
|
||||||
|
key = ImGuiKey_Delete;
|
||||||
|
} else if (keyStr == "Home") {
|
||||||
|
key = ImGuiKey_Home;
|
||||||
|
} else if (keyStr == "End") {
|
||||||
|
key = ImGuiKey_End;
|
||||||
|
} else if (keyStr.find("F") == 0 && keyStr.length() <= 3) {
|
||||||
|
// F1-F12 keys
|
||||||
|
int fNum = std::stoi(keyStr.substr(1));
|
||||||
|
if (fNum >= 1 && fNum <= 12) {
|
||||||
|
key = static_cast<ImGuiKey>(ImGuiKey_F1 + (fNum - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key != ImGuiKey_None) {
|
||||||
|
bindings_[actionIdx] = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
std::cout << "[KeybindingManager] Loaded keybindings from " << filePath << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
void KeybindingManager::saveToConfigFile(const std::string& filePath) const {
|
||||||
|
std::ifstream inFile(filePath);
|
||||||
|
std::string content;
|
||||||
|
std::string line;
|
||||||
|
|
||||||
|
// Read existing file, removing [Keybindings] section if it exists
|
||||||
|
bool inKeybindingsSection = false;
|
||||||
|
if (inFile.is_open()) {
|
||||||
|
while (std::getline(inFile, line)) {
|
||||||
|
if (line == "[Keybindings]") {
|
||||||
|
inKeybindingsSection = true;
|
||||||
|
continue;
|
||||||
|
} else if (line[0] == '[') {
|
||||||
|
inKeybindingsSection = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inKeybindingsSection) {
|
||||||
|
content += line + "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inFile.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append new Keybindings section
|
||||||
|
content += "[Keybindings]\n";
|
||||||
|
|
||||||
|
static const struct {
|
||||||
|
Action action;
|
||||||
|
const char* name;
|
||||||
|
} actionMap[] = {
|
||||||
|
{Action::TOGGLE_CHARACTER_SCREEN, "toggle_character_screen"},
|
||||||
|
{Action::TOGGLE_INVENTORY, "toggle_inventory"},
|
||||||
|
{Action::TOGGLE_BAGS, "toggle_bags"},
|
||||||
|
{Action::TOGGLE_SPELLBOOK, "toggle_spellbook"},
|
||||||
|
{Action::TOGGLE_TALENTS, "toggle_talents"},
|
||||||
|
{Action::TOGGLE_QUESTS, "toggle_quests"},
|
||||||
|
{Action::TOGGLE_MINIMAP, "toggle_minimap"},
|
||||||
|
{Action::TOGGLE_SETTINGS, "toggle_settings"},
|
||||||
|
{Action::TOGGLE_CHAT, "toggle_chat"},
|
||||||
|
{Action::TOGGLE_GUILD_ROSTER, "toggle_guild_roster"},
|
||||||
|
{Action::TOGGLE_DUNGEON_FINDER, "toggle_dungeon_finder"},
|
||||||
|
{Action::TOGGLE_WORLD_MAP, "toggle_world_map"},
|
||||||
|
{Action::TOGGLE_NAMEPLATES, "toggle_nameplates"},
|
||||||
|
{Action::TOGGLE_RAID_FRAMES, "toggle_raid_frames"},
|
||||||
|
{Action::TOGGLE_QUEST_LOG, "toggle_quest_log"},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& [action, nameStr] : actionMap) {
|
||||||
|
auto it = bindings_.find(static_cast<int>(action));
|
||||||
|
if (it == bindings_.end()) continue;
|
||||||
|
|
||||||
|
ImGuiKey key = it->second;
|
||||||
|
std::string keyStr;
|
||||||
|
|
||||||
|
// Convert ImGuiKey to string
|
||||||
|
if (key >= ImGuiKey_A && key <= ImGuiKey_Z) {
|
||||||
|
keyStr += static_cast<char>('A' + (key - ImGuiKey_A));
|
||||||
|
} else if (key >= ImGuiKey_0 && key <= ImGuiKey_9) {
|
||||||
|
keyStr += static_cast<char>('0' + (key - ImGuiKey_0));
|
||||||
|
} else if (key == ImGuiKey_Escape) {
|
||||||
|
keyStr = "Escape";
|
||||||
|
} else if (key == ImGuiKey_Enter) {
|
||||||
|
keyStr = "Enter";
|
||||||
|
} else if (key == ImGuiKey_Tab) {
|
||||||
|
keyStr = "Tab";
|
||||||
|
} else if (key == ImGuiKey_Backspace) {
|
||||||
|
keyStr = "Backspace";
|
||||||
|
} else if (key == ImGuiKey_Space) {
|
||||||
|
keyStr = "Space";
|
||||||
|
} else if (key == ImGuiKey_Delete) {
|
||||||
|
keyStr = "Delete";
|
||||||
|
} else if (key == ImGuiKey_Home) {
|
||||||
|
keyStr = "Home";
|
||||||
|
} else if (key == ImGuiKey_End) {
|
||||||
|
keyStr = "End";
|
||||||
|
} else if (key >= ImGuiKey_F1 && key <= ImGuiKey_F12) {
|
||||||
|
keyStr = "F" + std::to_string(1 + (key - ImGuiKey_F1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!keyStr.empty()) {
|
||||||
|
content += nameStr;
|
||||||
|
content += "=";
|
||||||
|
content += keyStr;
|
||||||
|
content += "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write back to file
|
||||||
|
std::ofstream outFile(filePath);
|
||||||
|
if (outFile.is_open()) {
|
||||||
|
outFile << content;
|
||||||
|
outFile.close();
|
||||||
|
std::cout << "[KeybindingManager] Saved keybindings to " << filePath << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cerr << "[KeybindingManager] Failed to write config file: " << filePath << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace wowee::ui
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "ui/quest_log_screen.hpp"
|
#include "ui/quest_log_screen.hpp"
|
||||||
|
#include "ui/keybinding_manager.hpp"
|
||||||
#include "core/application.hpp"
|
#include "core/application.hpp"
|
||||||
#include "core/input.hpp"
|
#include "core/input.hpp"
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
|
@ -206,13 +207,14 @@ std::string cleanQuestTitleForUi(const std::string& raw, uint32_t questId) {
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|
||||||
void QuestLogScreen::render(game::GameHandler& gameHandler) {
|
void QuestLogScreen::render(game::GameHandler& gameHandler) {
|
||||||
// L key toggle (edge-triggered)
|
// Quests toggle via keybinding (edge-triggered)
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
// Customizable key (default: L) from KeybindingManager
|
||||||
bool lDown = !io.WantTextInput && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_L);
|
bool questsDown = KeybindingManager::getInstance().isActionPressed(
|
||||||
if (lDown && !lKeyWasDown) {
|
KeybindingManager::Action::TOGGLE_QUESTS, false);
|
||||||
|
if (questsDown && !lKeyWasDown) {
|
||||||
open = !open;
|
open = !open;
|
||||||
}
|
}
|
||||||
lKeyWasDown = lDown;
|
lKeyWasDown = questsDown;
|
||||||
|
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "ui/spellbook_screen.hpp"
|
#include "ui/spellbook_screen.hpp"
|
||||||
|
#include "ui/keybinding_manager.hpp"
|
||||||
#include "core/input.hpp"
|
#include "core/input.hpp"
|
||||||
#include "core/application.hpp"
|
#include "core/application.hpp"
|
||||||
#include "rendering/vk_context.hpp"
|
#include "rendering/vk_context.hpp"
|
||||||
|
|
@ -563,13 +564,14 @@ void SpellbookScreen::renderSpellTooltip(const SpellInfo* info, game::GameHandle
|
||||||
}
|
}
|
||||||
|
|
||||||
void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetManager* assetManager) {
|
void SpellbookScreen::render(game::GameHandler& gameHandler, pipeline::AssetManager* assetManager) {
|
||||||
// P key toggle (edge-triggered)
|
// Spellbook toggle via keybinding (edge-triggered)
|
||||||
bool wantsTextInput = ImGui::GetIO().WantTextInput;
|
// Customizable key (default: P) from KeybindingManager
|
||||||
bool pDown = !wantsTextInput && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_P);
|
bool spellbookDown = KeybindingManager::getInstance().isActionPressed(
|
||||||
if (pDown && !pKeyWasDown) {
|
KeybindingManager::Action::TOGGLE_SPELLBOOK, false);
|
||||||
|
if (spellbookDown && !pKeyWasDown) {
|
||||||
open = !open;
|
open = !open;
|
||||||
}
|
}
|
||||||
pKeyWasDown = pDown;
|
pKeyWasDown = spellbookDown;
|
||||||
|
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "ui/talent_screen.hpp"
|
#include "ui/talent_screen.hpp"
|
||||||
|
#include "ui/keybinding_manager.hpp"
|
||||||
#include "core/input.hpp"
|
#include "core/input.hpp"
|
||||||
#include "core/application.hpp"
|
#include "core/application.hpp"
|
||||||
#include "core/logger.hpp"
|
#include "core/logger.hpp"
|
||||||
|
|
@ -22,13 +23,14 @@ static const char* getClassName(uint8_t classId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void TalentScreen::render(game::GameHandler& gameHandler) {
|
void TalentScreen::render(game::GameHandler& gameHandler) {
|
||||||
// N key toggle (edge-triggered)
|
// Talents toggle via keybinding (edge-triggered)
|
||||||
bool wantsTextInput = ImGui::GetIO().WantTextInput;
|
// Customizable key (default: N) from KeybindingManager
|
||||||
bool nDown = !wantsTextInput && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_N);
|
bool talentsDown = KeybindingManager::getInstance().isActionPressed(
|
||||||
if (nDown && !nKeyWasDown) {
|
KeybindingManager::Action::TOGGLE_TALENTS, false);
|
||||||
|
if (talentsDown && !nKeyWasDown) {
|
||||||
open = !open;
|
open = !open;
|
||||||
}
|
}
|
||||||
nKeyWasDown = nDown;
|
nKeyWasDown = talentsDown;
|
||||||
|
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue