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/spellbook_screen.cpp
|
||||
src/ui/talent_screen.cpp
|
||||
src/ui/keybinding_manager.cpp
|
||||
|
||||
# Main
|
||||
src/main.cpp
|
||||
|
|
@ -653,6 +654,7 @@ set(WOWEE_HEADERS
|
|||
include/ui/inventory_screen.hpp
|
||||
include/ui/spellbook_screen.hpp
|
||||
include/ui/talent_screen.hpp
|
||||
include/ui/keybinding_manager.hpp
|
||||
)
|
||||
|
||||
set(WOWEE_PLATFORM_SOURCES)
|
||||
|
|
|
|||
|
|
@ -389,6 +389,7 @@ public:
|
|||
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;
|
||||
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 parseGameObjectQueryResponse(network::Packet& packet, GameObjectQueryResponseData& data) override;
|
||||
// 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 {
|
||||
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)
|
||||
bool parseSpellStart(network::Packet& packet, SpellStartData& data) override;
|
||||
bool parseSpellGo(network::Packet& packet, SpellGoData& data) override;
|
||||
|
|
|
|||
|
|
@ -1758,7 +1758,10 @@ struct InitialSpellsData {
|
|||
|
||||
class InitialSpellsParser {
|
||||
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 */
|
||||
|
|
@ -2015,7 +2018,9 @@ public:
|
|||
/** SMSG_LOOT_RESPONSE parser */
|
||||
class LootResponseParser {
|
||||
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,
|
||||
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:
|
||||
/**
|
||||
* Build a composited character skin texture by alpha-blending overlay
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <chrono>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
|
@ -25,5 +26,38 @@ struct GPUPushConstants {
|
|||
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 wowee
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
#include <vulkan/vulkan.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
|
@ -56,5 +58,25 @@ inline bool vkCheck(VkResult result, [[maybe_unused]] const char* msg) {
|
|||
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 wowee
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include "ui/quest_log_screen.hpp"
|
||||
#include "ui/spellbook_screen.hpp"
|
||||
#include "ui/talent_screen.hpp"
|
||||
#include "ui/keybinding_manager.hpp"
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <imgui.h>
|
||||
#include <string>
|
||||
|
|
@ -62,10 +63,13 @@ private:
|
|||
// UI state
|
||||
bool showEntityWindow = false;
|
||||
bool showChatWindow = true;
|
||||
bool showMinimap_ = true; // M key toggles minimap
|
||||
bool showNameplates_ = true; // V key toggles nameplates
|
||||
bool showPlayerInfo = false;
|
||||
bool showSocialFrame_ = false; // O key toggles social/friends list
|
||||
bool showGuildRoster_ = false;
|
||||
bool showRaidFrames_ = true; // F key toggles raid/party frames
|
||||
bool showWorldMap_ = false; // W key toggles world map
|
||||
std::string selectedGuildMember_;
|
||||
bool showGuildNoteEdit_ = false;
|
||||
bool editingOfficerNote_ = false;
|
||||
|
|
@ -111,6 +115,10 @@ private:
|
|||
bool pendingMinimapNpcDots = false;
|
||||
bool pendingSeparateBags = true;
|
||||
bool pendingAutoLoot = false;
|
||||
|
||||
// Keybinding customization
|
||||
int pendingRebindAction = -1; // -1 = not rebinding, otherwise action index
|
||||
bool awaitingKeyPress = false;
|
||||
bool pendingUseOriginalSoundtrack = true;
|
||||
bool pendingShowActionBar2 = true; // Show second action bar above main bar
|
||||
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 geosetPants = pickGeoset(1301, 13); // Bare legs (group 13)
|
||||
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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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
|
||||
if (extra.equipDisplayId[10] != 0) {
|
||||
|
|
@ -6138,9 +6142,10 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
" sleeves=", geosetSleeves, " pants=", geosetPants,
|
||||
" boots=", geosetBoots, " gloves=", geosetGloves);
|
||||
|
||||
// TODO(#helmet-attach): NPC helmet attachment anchors are currently unreliable
|
||||
// on some humanoid models (floating/incorrect bone bind). Keep hidden for now.
|
||||
static constexpr bool kEnableNpcHelmetAttachmentsMainPath = false;
|
||||
// NOTE: NPC helmet attachment with fallback logic to use bone 0 if attachment
|
||||
// point 11 is missing. This improves compatibility with models that don't have
|
||||
// attachment 11 explicitly defined.
|
||||
static constexpr bool kEnableNpcHelmetAttachmentsMainPath = true;
|
||||
// Load and attach helmet model if equipped
|
||||
if (kEnableNpcHelmetAttachmentsMainPath && extra.equipDisplayId[0] != 0 && itemDisplayDbc) {
|
||||
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,
|
||||
// IN_GAME retry loop will attempt again shortly.
|
||||
bool weaponsAttachedNow = tryAttachCreatureVirtualWeapons(guid, instanceId);
|
||||
|
|
|
|||
|
|
@ -1963,15 +1963,21 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
|
||||
// ---- Loot start roll (Need/Greed popup trigger) ----
|
||||
case Opcode::SMSG_LOOT_START_ROLL: {
|
||||
// uint64 objectGuid + uint32 mapId + uint32 lootSlot + uint32 itemId
|
||||
// + uint32 randomSuffix + uint32 randomPropId + uint32 countdown + uint8 voteMask
|
||||
if (packet.getSize() - packet.getReadPos() < 33) break;
|
||||
// WotLK 3.3.5a: uint64 objectGuid + uint32 mapId + uint32 lootSlot + uint32 itemId
|
||||
// + uint32 randomSuffix + uint32 randomPropId + uint32 countdown + uint8 voteMask (33 bytes)
|
||||
// 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();
|
||||
/*uint32_t mapId =*/ packet.readUInt32();
|
||||
uint32_t slot = packet.readUInt32();
|
||||
uint32_t itemId = packet.readUInt32();
|
||||
if (isWotLK) {
|
||||
/*uint32_t randSuffix =*/ packet.readUInt32();
|
||||
/*uint32_t randProp =*/ packet.readUInt32();
|
||||
}
|
||||
/*uint32_t countdown =*/ packet.readUInt32();
|
||||
/*uint8_t voteMask =*/ packet.readUInt8();
|
||||
// Trigger the roll popup for local player
|
||||
|
|
@ -2344,11 +2350,18 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
|
||||
// ---- Loot notifications ----
|
||||
case Opcode::SMSG_LOOT_ALL_PASSED: {
|
||||
// uint64 objectGuid + uint32 slot + uint32 itemId + uint32 randSuffix + uint32 randPropId
|
||||
if (packet.getSize() - packet.getReadPos() < 24) break;
|
||||
// WotLK 3.3.5a: uint64 objectGuid + uint32 slot + uint32 itemId + uint32 randSuffix + uint32 randPropId (24 bytes)
|
||||
// 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();
|
||||
/*uint32_t slot =*/ packet.readUInt32();
|
||||
uint32_t itemId = packet.readUInt32();
|
||||
if (isWotLK) {
|
||||
/*uint32_t randSuffix =*/ packet.readUInt32();
|
||||
/*uint32_t randProp =*/ packet.readUInt32();
|
||||
}
|
||||
auto* info = getItemInfo(itemId);
|
||||
char buf[256];
|
||||
std::snprintf(buf, sizeof(buf), "Everyone passed on [%s].",
|
||||
|
|
@ -2747,16 +2760,21 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
break;
|
||||
case Opcode::SMSG_SPELL_FAILURE: {
|
||||
// WotLK: packed_guid + uint8 castCount + uint32 spellId + uint8 failReason
|
||||
// TBC/Classic: full uint64 + uint8 castCount + uint32 spellId + uint8 failReason
|
||||
const bool tbcOrClassic = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
||||
uint64_t failGuid = tbcOrClassic
|
||||
// TBC: full uint64 + uint8 castCount + uint32 spellId + uint8 failReason
|
||||
// Classic: full uint64 + uint32 spellId + uint8 failReason (NO castCount)
|
||||
const bool isClassic = isClassicLikeExpansion();
|
||||
const bool isTbc = isActiveExpansion("tbc");
|
||||
uint64_t failGuid = (isClassic || isTbc)
|
||||
? (packet.getSize() - packet.getReadPos() >= 8 ? packet.readUInt64() : 0)
|
||||
: UpdateObjectParser::readPackedGuid(packet);
|
||||
// Read castCount + spellId + failReason
|
||||
if (packet.getSize() - packet.getReadPos() >= 6) {
|
||||
/*uint8_t castCount =*/ packet.readUInt8();
|
||||
// Classic omits the castCount byte; TBC and WotLK include it
|
||||
const size_t remainingFields = isClassic ? 5u : 6u; // spellId(4)+reason(1) [+castCount(1)]
|
||||
if (packet.getSize() - packet.getReadPos() >= remainingFields) {
|
||||
if (!isClassic) /*uint8_t castCount =*/ packet.readUInt8();
|
||||
/*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) {
|
||||
// Show interruption/failure reason in chat for player
|
||||
int pt = -1;
|
||||
|
|
@ -4073,10 +4091,12 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
break;
|
||||
}
|
||||
case Opcode::SMSG_WEATHER: {
|
||||
// Format: uint32 weatherType, float intensity, uint8 isAbrupt
|
||||
if (packet.getSize() - packet.getReadPos() >= 9) {
|
||||
// Classic 1.12: uint32 weatherType + float intensity (8 bytes, no isAbrupt)
|
||||
// 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();
|
||||
float wIntensity = packet.readFloat();
|
||||
if (packet.getSize() - packet.getReadPos() >= 1)
|
||||
/*uint8_t isAbrupt =*/ packet.readUInt8();
|
||||
weatherType_ = wType;
|
||||
weatherIntensity_ = wIntensity;
|
||||
|
|
@ -12237,20 +12257,40 @@ void GameHandler::handleMoveKnockBack(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;
|
||||
uint32_t queueSlot = packet.readUInt32();
|
||||
|
||||
// Minimal packet = just queueSlot + arenaType(1) when status is NONE
|
||||
const bool classicFormat = isClassicLikeExpansion();
|
||||
|
||||
uint8_t arenaType = 0;
|
||||
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;
|
||||
}
|
||||
|
||||
uint8_t arenaType = packet.readUInt8();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown byte
|
||||
packet.readUInt8();
|
||||
if (packet.getSize() - packet.getReadPos() < 4) return;
|
||||
uint32_t bgTypeId = packet.readUInt32();
|
||||
|
||||
|
|
@ -14169,8 +14209,11 @@ void GameHandler::handleAuraUpdate(network::Packet& packet, bool isAll) {
|
|||
}
|
||||
|
||||
void GameHandler::handleLearnedSpell(network::Packet& packet) {
|
||||
if (packet.getSize() - packet.getReadPos() < 4) return;
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
// Classic 1.12: uint16 spellId; TBC 2.4.3 / WotLK 3.3.5a: uint32 spellId
|
||||
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);
|
||||
LOG_INFO("Learned spell: ", spellId);
|
||||
|
||||
|
|
@ -14198,17 +14241,24 @@ void GameHandler::handleLearnedSpell(network::Packet& packet) {
|
|||
}
|
||||
|
||||
void GameHandler::handleRemovedSpell(network::Packet& packet) {
|
||||
if (packet.getSize() - packet.getReadPos() < 4) return;
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
// Classic 1.12: uint16 spellId; TBC 2.4.3 / WotLK 3.3.5a: uint32 spellId
|
||||
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);
|
||||
LOG_INFO("Removed spell: ", spellId);
|
||||
}
|
||||
|
||||
void GameHandler::handleSupercededSpell(network::Packet& packet) {
|
||||
// Old spell replaced by new rank (e.g., Fireball Rank 1 -> Fireball Rank 2)
|
||||
if (packet.getSize() - packet.getReadPos() < 8) return;
|
||||
uint32_t oldSpellId = packet.readUInt32();
|
||||
uint32_t newSpellId = packet.readUInt32();
|
||||
// Classic 1.12: uint16 oldSpellId + uint16 newSpellId (4 bytes total)
|
||||
// TBC 2.4.3 / WotLK 3.3.5a: uint32 oldSpellId + uint32 newSpellId (8 bytes total)
|
||||
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
|
||||
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(slot); // vendor slot index
|
||||
packet.writeUInt32(count);
|
||||
// WotLK/AzerothCore expects a trailing byte here.
|
||||
// WotLK/AzerothCore expects a trailing byte; Classic/TBC do not
|
||||
const bool isWotLk = isActiveExpansion("wotlk");
|
||||
if (isWotLk) {
|
||||
packet.writeUInt8(0);
|
||||
}
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
|
|
@ -16159,7 +16212,10 @@ void GameHandler::unstuckHearth() {
|
|||
}
|
||||
|
||||
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;
|
||||
localLootState_[currentLoot.lootGuid] = LocalLootState{currentLoot, false};
|
||||
|
||||
|
|
@ -16852,15 +16908,20 @@ void GameHandler::handleTeleportAck(network::Packet& packet) {
|
|||
if (packet.getSize() - packet.getReadPos() < 4) return;
|
||||
uint32_t counter = packet.readUInt32();
|
||||
|
||||
// Read the movement info embedded in the teleport
|
||||
// Format: u32 flags, u16 flags2, u32 time, float x, float y, float z, float o
|
||||
if (packet.getSize() - packet.getReadPos() < 4 + 2 + 4 + 4 * 4) {
|
||||
// Read the movement info embedded in the teleport.
|
||||
// WotLK: moveFlags(4) + moveFlags2(2) + time(4) + x(4) + y(4) + z(4) + o(4) = 26 bytes
|
||||
// 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");
|
||||
return;
|
||||
}
|
||||
|
||||
packet.readUInt32(); // moveFlags
|
||||
packet.readUInt16(); // moveFlags2
|
||||
if (!taNoFlags2)
|
||||
packet.readUInt16(); // moveFlags2 (WotLK only)
|
||||
uint32_t moveTime = packet.readUInt32();
|
||||
float serverX = packet.readFloat();
|
||||
float serverY = packet.readFloat();
|
||||
|
|
@ -19468,18 +19529,23 @@ void GameHandler::handleTradeStatusExtended(network::Packet& packet) {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
void GameHandler::handleLootRoll(network::Packet& packet) {
|
||||
// uint64 objectGuid, uint32 slot, uint64 playerGuid,
|
||||
// uint32 itemId, uint32 randomSuffix, uint32 randomPropId,
|
||||
// uint8 rollNumber, uint8 rollType
|
||||
// WotLK 3.3.5a: uint64 objectGuid, uint32 slot, uint64 playerGuid,
|
||||
// uint32 itemId, uint32 randomSuffix, uint32 randomPropId, uint8 rollNumber, uint8 rollType (34 bytes)
|
||||
// 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();
|
||||
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();
|
||||
uint32_t slot = packet.readUInt32();
|
||||
uint64_t rollerGuid = packet.readUInt64();
|
||||
uint32_t itemId = packet.readUInt32();
|
||||
if (isWotLK) {
|
||||
/*uint32_t randSuffix =*/ packet.readUInt32();
|
||||
/*uint32_t randProp =*/ packet.readUInt32();
|
||||
}
|
||||
uint8_t rollNum = packet.readUInt8();
|
||||
uint8_t rollType = packet.readUInt8();
|
||||
|
||||
|
|
@ -19526,15 +19592,23 @@ void GameHandler::handleLootRoll(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();
|
||||
if (rem < 26) return;
|
||||
if (rem < minSize) return;
|
||||
|
||||
/*uint64_t objectGuid =*/ packet.readUInt64();
|
||||
/*uint32_t slot =*/ packet.readUInt32();
|
||||
uint64_t winnerGuid = packet.readUInt64();
|
||||
uint32_t itemId = packet.readUInt32();
|
||||
if (isWotLK) {
|
||||
/*uint32_t randSuffix =*/ packet.readUInt32();
|
||||
/*uint32_t randProp =*/ packet.readUInt32();
|
||||
}
|
||||
uint8_t rollNum = packet.readUInt8();
|
||||
uint8_t rollType = packet.readUInt8();
|
||||
|
||||
|
|
|
|||
|
|
@ -633,6 +633,22 @@ bool ClassicPacketParsers::parseCastFailed(network::Packet& packet, CastFailedDa
|
|||
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
|
||||
// Differences from TBC:
|
||||
|
|
|
|||
|
|
@ -2901,18 +2901,13 @@ bool XpGainParser::parse(network::Packet& packet, XpGainData& data) {
|
|||
// Phase 3: Spells, Action Bar, Auras
|
||||
// ============================================================
|
||||
|
||||
bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data) {
|
||||
size_t packetSize = packet.getSize();
|
||||
bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data,
|
||||
bool vanillaFormat) {
|
||||
data.talentSpec = packet.readUInt8();
|
||||
uint16_t spellCount = packet.readUInt16();
|
||||
|
||||
// Detect vanilla (uint16 spellId) vs WotLK (uint32 spellId) format
|
||||
// Vanilla: 4 bytes/spell (uint16 id + uint16 slot), WotLK: 6 bytes/spell (uint32 id + uint16 unk)
|
||||
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)");
|
||||
LOG_DEBUG("SMSG_INITIAL_SPELLS: spellCount=", spellCount,
|
||||
vanillaFormat ? " (vanilla uint16 format)" : " (TBC/WotLK uint32 format)");
|
||||
|
||||
data.spellIds.reserve(spellCount);
|
||||
for (uint16_t i = 0; i < spellCount; ++i) {
|
||||
|
|
@ -3320,7 +3315,7 @@ network::Packet LootReleasePacket::build(uint64_t lootGuid) {
|
|||
return packet;
|
||||
}
|
||||
|
||||
bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data) {
|
||||
bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data, bool isWotlkFormat) {
|
||||
data = LootResponseData{};
|
||||
if (packet.getSize() - packet.getReadPos() < 14) {
|
||||
LOG_WARNING("LootResponseParser: packet too short");
|
||||
|
|
@ -3332,36 +3327,25 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data)
|
|||
data.gold = packet.readUInt32();
|
||||
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 {
|
||||
for (uint8_t i = 0; i < listCount; ++i) {
|
||||
size_t remaining = packet.getSize() - packet.getReadPos();
|
||||
if (remaining < 10) {
|
||||
if (remaining < kItemSize) {
|
||||
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;
|
||||
item.slotIndex = packet.readUInt8();
|
||||
item.itemId = packet.readUInt32();
|
||||
item.count = packet.readUInt32();
|
||||
|
||||
if (bytesPerItem >= 14) {
|
||||
item.displayInfoId = packet.readUInt32();
|
||||
} else {
|
||||
item.displayInfoId = 0;
|
||||
}
|
||||
|
||||
if (bytesPerItem == 22) {
|
||||
if (isWotlkFormat) {
|
||||
item.randomSuffix = packet.readUInt32();
|
||||
item.randomPropertyId = packet.readUInt32();
|
||||
} else {
|
||||
|
|
@ -3844,7 +3828,9 @@ network::Packet BuyItemPacket::build(uint64_t vendorGuid, uint32_t itemId, uint3
|
|||
packet.writeUInt32(itemId); // item entry
|
||||
packet.writeUInt32(slot); // vendor slot index from SMSG_LIST_INVENTORY
|
||||
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);
|
||||
return packet;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -377,6 +377,13 @@ void CameraController::update(float deltaTime) {
|
|||
if (mounted_) sitting = false;
|
||||
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)
|
||||
if (!uiWantsKeyboard && sitting && !movementSuppressed) {
|
||||
bool anyMoveKey =
|
||||
|
|
@ -1851,8 +1858,7 @@ void CameraController::update(float deltaTime) {
|
|||
wasJumping = nowJump;
|
||||
wasFalling = !grounded && verticalVelocity <= 0.0f;
|
||||
|
||||
// R key disabled — was camera reset, conflicts with chat reply
|
||||
rKeyWasDown = false;
|
||||
// R key is now handled above with chat safeguard (WantTextInput check)
|
||||
}
|
||||
|
||||
void CameraController::processMouseMotion(const SDL_MouseMotionEvent& event) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include "rendering/vk_shader.hpp"
|
||||
#include "rendering/vk_buffer.hpp"
|
||||
#include "rendering/vk_utils.hpp"
|
||||
#include "rendering/vk_frame_data.hpp"
|
||||
#include "rendering/camera.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/blp_loader.hpp"
|
||||
|
|
@ -45,25 +46,6 @@ namespace wowee {
|
|||
namespace rendering {
|
||||
|
||||
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) {
|
||||
if (w <= 0 || h <= 0) return 0;
|
||||
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_,
|
||||
1, 1, &shadowParamsSet_, 0, nullptr);
|
||||
|
||||
struct ShadowPush { glm::mat4 lightSpaceMatrix; glm::mat4 model; };
|
||||
|
||||
const float shadowRadiusSq = shadowRadius * shadowRadius;
|
||||
for (auto& pair : instances) {
|
||||
auto& inst = pair.second;
|
||||
|
|
@ -3034,6 +3014,65 @@ bool CharacterRenderer::getInstanceModelName(uint32_t instanceId, std::string& m
|
|||
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,
|
||||
const pipeline::M2Model& weaponModel, uint32_t weaponModelId,
|
||||
const std::string& texturePath) {
|
||||
|
|
@ -3045,62 +3084,11 @@ bool CharacterRenderer::attachWeapon(uint32_t charInstanceId, uint32_t attachmen
|
|||
auto& charInstance = charIt->second;
|
||||
auto charModelIt = models.find(charInstance.modelId);
|
||||
if (charModelIt == models.end()) return false;
|
||||
const auto& charModel = charModelIt->second.data;
|
||||
|
||||
// Find bone index for this attachment point
|
||||
uint16_t boneIndex = 0;
|
||||
glm::vec3 offset(0.0f);
|
||||
bool found = false;
|
||||
|
||||
// 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) {
|
||||
if (!findAttachmentBone(charInstance.modelId, attachmentId, boneIndex, offset)) {
|
||||
core::Logger::getInstance().warning("attachWeapon: no bone found for attachment ", attachmentId);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -3211,58 +3199,12 @@ bool CharacterRenderer::getAttachmentTransform(uint32_t instanceId, uint32_t att
|
|||
if (instIt == instances.end()) return false;
|
||||
const auto& instance = instIt->second;
|
||||
|
||||
auto modelIt = models.find(instance.modelId);
|
||||
if (modelIt == models.end()) return false;
|
||||
const auto& model = modelIt->second.data;
|
||||
|
||||
// Find attachment point
|
||||
// Find attachment point using shared lookup logic
|
||||
uint16_t boneIndex = 0;
|
||||
glm::vec3 offset(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()) {
|
||||
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 {
|
||||
if (!findAttachmentBone(instance.modelId, attachmentId, boneIndex, offset)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get bone matrix
|
||||
glm::mat4 boneMat(1.0f);
|
||||
|
|
|
|||
|
|
@ -40,24 +40,6 @@ bool envFlagEnabled(const char* key, bool defaultValue) {
|
|||
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 kParticleFlagTiled = 0x80;
|
||||
|
||||
|
|
@ -210,22 +192,6 @@ float pointAABBDistanceSq(const glm::vec3& p, const glm::vec3& bmin, const glm::
|
|||
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.
|
||||
// Returns distance along ray if hit, negative if miss.
|
||||
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);
|
||||
|
||||
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 &&
|
||||
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;
|
||||
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
|
||||
VkBufferCreateInfo bufCI{};
|
||||
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 (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;
|
||||
|
||||
// Reset per-frame texture descriptor pool for foliage alpha-test sets
|
||||
|
|
|
|||
|
|
@ -20,17 +20,6 @@
|
|||
namespace wowee {
|
||||
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
|
||||
struct TerrainParamsUBO {
|
||||
int32_t layerCount;
|
||||
|
|
@ -799,15 +788,6 @@ bool TerrainRenderer::initializeShadow(VkRenderPass shadowRenderPass) {
|
|||
VmaAllocator allocator = vkCtx->getAllocator();
|
||||
|
||||
// ShadowParams UBO — terrain uses no bones, no texture, no alpha test
|
||||
struct ShadowParamsUBO {
|
||||
int32_t useBones = 0;
|
||||
int32_t useTexture = 0;
|
||||
int32_t alphaTest = 0;
|
||||
int32_t foliageSway = 0;
|
||||
float windTime = 0.0f;
|
||||
float foliageMotionDamp = 1.0f;
|
||||
};
|
||||
|
||||
VkBufferCreateInfo bufCI{};
|
||||
bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
||||
bufCI.size = sizeof(ShadowParamsUBO);
|
||||
|
|
@ -965,7 +945,6 @@ void TerrainRenderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSp
|
|||
|
||||
// Identity model matrix — terrain vertices are already in world space
|
||||
static const glm::mat4 identity(1.0f);
|
||||
struct ShadowPush { glm::mat4 lightSpaceMatrix; glm::mat4 model; };
|
||||
ShadowPush push{ lightSpaceMatrix, identity };
|
||||
vkCmdPushConstants(cmd, shadowPipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT,
|
||||
0, 128, &push);
|
||||
|
|
|
|||
|
|
@ -29,23 +29,6 @@ namespace wowee {
|
|||
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);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// 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;
|
||||
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
|
||||
VkBufferCreateInfo bufCI{};
|
||||
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_,
|
||||
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
|
||||
// the proximity radius so that distant buildings whose shadows reach the player
|
||||
// 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);
|
||||
}
|
||||
|
||||
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
|
||||
// Returns distance along ray if hit, or negative if miss
|
||||
static float rayTriangleIntersect(const glm::vec3& origin, const glm::vec3& dir,
|
||||
|
|
@ -3628,12 +3583,13 @@ void WMORenderer::recreatePipelines() {
|
|||
}
|
||||
|
||||
// --- Vertex input ---
|
||||
// WMO vertex: pos3 + normal3 + texCoord2 + color4 + tangent4 = 64 bytes
|
||||
struct WMOVertexData {
|
||||
glm::vec3 position;
|
||||
glm::vec3 normal;
|
||||
glm::vec2 texCoord;
|
||||
glm::vec4 color;
|
||||
glm::vec4 tangent;
|
||||
glm::vec4 tangent; // xyz=tangent dir, w=handedness ±1
|
||||
};
|
||||
|
||||
VkVertexInputBindingDescription vertexBinding{};
|
||||
|
|
|
|||
|
|
@ -408,7 +408,9 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
renderNameplates(gameHandler); // player names always shown; NPC plates gated by showNameplates_
|
||||
renderBattlegroundScore(gameHandler);
|
||||
renderCombatText(gameHandler);
|
||||
if (showRaidFrames_) {
|
||||
renderPartyFrames(gameHandler);
|
||||
}
|
||||
renderBossFrames(gameHandler);
|
||||
renderGroupInvitePopup(gameHandler);
|
||||
renderDuelRequestPopup(gameHandler);
|
||||
|
|
@ -440,7 +442,9 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
renderDungeonFinderWindow(gameHandler);
|
||||
renderInstanceLockouts(gameHandler);
|
||||
// renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now
|
||||
if (showMinimap_) {
|
||||
renderMinimapMarkers(gameHandler);
|
||||
}
|
||||
renderDeathScreen(gameHandler);
|
||||
renderReclaimCorpseButton(gameHandler);
|
||||
renderResurrectDialog(gameHandler);
|
||||
|
|
@ -1452,7 +1456,7 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
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) {
|
||||
// Close settings window if open
|
||||
showSettingsWindow = false;
|
||||
|
|
@ -1470,11 +1474,27 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
}
|
||||
}
|
||||
|
||||
// V — toggle nameplates (WoW default keybinding)
|
||||
if (input.isKeyJustPressed(SDL_SCANCODE_V)) {
|
||||
// Toggle nameplates (customizable keybinding, default V)
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_INVENTORY)) {
|
||||
inventoryScreen.toggle();
|
||||
}
|
||||
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_NAMEPLATES)) {
|
||||
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, -, =)
|
||||
static const SDL_Scancode actionBarKeys[] = {
|
||||
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
|
||||
if (!chatInputActive && input.isKeyJustPressed(SDL_SCANCODE_RETURN)) {
|
||||
if (!chatInputActive && KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHAT, true)) {
|
||||
refocusChatInput = true;
|
||||
}
|
||||
|
||||
|
|
@ -4003,6 +4023,8 @@ void GameScreen::updateCharacterTextures(game::Inventory& inventory) {
|
|||
// ============================================================
|
||||
|
||||
void GameScreen::renderWorldMap(game::GameHandler& gameHandler) {
|
||||
if (!showWorldMap_) return;
|
||||
|
||||
auto& app = core::Application::getInstance();
|
||||
auto* renderer = app.getRenderer();
|
||||
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;
|
||||
if (spellDbc && spellDbc->isLoaded()) {
|
||||
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) {
|
||||
spellIconIds_.clear();
|
||||
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,
|
||||
// since Turtle/Classic CSV files are garbled and fall back to WotLK binary.
|
||||
if (fieldCount >= 200) {
|
||||
tryLoadIcons(0, 133); // WotLK IconID field
|
||||
} else if (spellL) {
|
||||
|
||||
// Always use expansion-aware layout if available
|
||||
// Field indices vary by expansion: Classic=117, TBC=124, WotLK=133
|
||||
if (spellL) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -6402,8 +6424,8 @@ void GameScreen::renderLfgProposalPopup(game::GameHandler& gameHandler) {
|
|||
}
|
||||
|
||||
void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) {
|
||||
// O key toggle (WoW default Social/Guild keybind)
|
||||
if (!ImGui::GetIO().WantCaptureKeyboard && ImGui::IsKeyPressed(ImGuiKey_O)) {
|
||||
// Guild Roster toggle (customizable keybind)
|
||||
if (!ImGui::GetIO().WantCaptureKeyboard && KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_GUILD_ROSTER)) {
|
||||
showGuildRoster_ = !showGuildRoster_;
|
||||
if (showGuildRoster_) {
|
||||
// Open friends tab directly if not in guild
|
||||
|
|
@ -9180,6 +9202,108 @@ void GameScreen::renderSettingsWindow() {
|
|||
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
|
||||
// ============================================================
|
||||
|
|
@ -10063,6 +10187,11 @@ void GameScreen::saveSettings() {
|
|||
out << "chat_autojoin_lfg=" << (chatAutoJoinLFG_ ? 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);
|
||||
}
|
||||
|
||||
|
|
@ -10176,6 +10305,10 @@ void GameScreen::loadSettings() {
|
|||
else if (key == "chat_autojoin_local") chatAutoJoinLocal_ = (std::stoi(val) != 0);
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
// Load keybindings from the same config file
|
||||
KeybindingManager::getInstance().loadFromConfigFile(path);
|
||||
|
||||
LOG_INFO("Settings loaded from ", path);
|
||||
}
|
||||
|
||||
|
|
@ -11551,8 +11684,8 @@ void GameScreen::renderZoneText() {
|
|||
// Dungeon Finder window (toggle with hotkey or bag-bar button)
|
||||
// ---------------------------------------------------------------------------
|
||||
void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) {
|
||||
// Toggle on I key when not typing
|
||||
if (!chatInputActive && ImGui::IsKeyPressed(ImGuiKey_I, false)) {
|
||||
// Toggle Dungeon Finder (customizable keybind)
|
||||
if (!chatInputActive && KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_DUNGEON_FINDER)) {
|
||||
showDungeonFinder_ = !showDungeonFinder_;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "ui/inventory_screen.hpp"
|
||||
#include "ui/keybinding_manager.hpp"
|
||||
#include "game/game_handler.hpp"
|
||||
#include "core/application.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) {
|
||||
// B key toggle (edge-triggered)
|
||||
bool wantsTextInput = ImGui::GetIO().WantTextInput;
|
||||
bool bDown = !wantsTextInput && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_B);
|
||||
bool bToggled = bDown && !bKeyWasDown;
|
||||
bKeyWasDown = bDown;
|
||||
// Bags toggle (B key, edge-triggered)
|
||||
bool bagsDown = KeybindingManager::getInstance().isActionPressed(
|
||||
KeybindingManager::Action::TOGGLE_BAGS, false);
|
||||
bool bToggled = bagsDown && !bKeyWasDown;
|
||||
bKeyWasDown = bagsDown;
|
||||
|
||||
// C key toggle for character screen (edge-triggered)
|
||||
bool cDown = !wantsTextInput && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_C);
|
||||
if (cDown && !cKeyWasDown) {
|
||||
// Character screen toggle (C key, edge-triggered)
|
||||
bool characterDown = KeybindingManager::getInstance().isActionPressed(
|
||||
KeybindingManager::Action::TOGGLE_CHARACTER_SCREEN, false);
|
||||
if (characterDown && !cKeyWasDown) {
|
||||
characterOpen = !characterOpen;
|
||||
}
|
||||
cKeyWasDown = cDown;
|
||||
cKeyWasDown = characterDown;
|
||||
|
||||
bool wantsTextInput = ImGui::GetIO().WantTextInput;
|
||||
|
||||
if (separateBags_) {
|
||||
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/keybinding_manager.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/input.hpp"
|
||||
#include <imgui.h>
|
||||
|
|
@ -206,13 +207,14 @@ std::string cleanQuestTitleForUi(const std::string& raw, uint32_t questId) {
|
|||
} // anonymous namespace
|
||||
|
||||
void QuestLogScreen::render(game::GameHandler& gameHandler) {
|
||||
// L key toggle (edge-triggered)
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
bool lDown = !io.WantTextInput && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_L);
|
||||
if (lDown && !lKeyWasDown) {
|
||||
// Quests toggle via keybinding (edge-triggered)
|
||||
// Customizable key (default: L) from KeybindingManager
|
||||
bool questsDown = KeybindingManager::getInstance().isActionPressed(
|
||||
KeybindingManager::Action::TOGGLE_QUESTS, false);
|
||||
if (questsDown && !lKeyWasDown) {
|
||||
open = !open;
|
||||
}
|
||||
lKeyWasDown = lDown;
|
||||
lKeyWasDown = questsDown;
|
||||
|
||||
if (!open) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "ui/spellbook_screen.hpp"
|
||||
#include "ui/keybinding_manager.hpp"
|
||||
#include "core/input.hpp"
|
||||
#include "core/application.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) {
|
||||
// P key toggle (edge-triggered)
|
||||
bool wantsTextInput = ImGui::GetIO().WantTextInput;
|
||||
bool pDown = !wantsTextInput && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_P);
|
||||
if (pDown && !pKeyWasDown) {
|
||||
// Spellbook toggle via keybinding (edge-triggered)
|
||||
// Customizable key (default: P) from KeybindingManager
|
||||
bool spellbookDown = KeybindingManager::getInstance().isActionPressed(
|
||||
KeybindingManager::Action::TOGGLE_SPELLBOOK, false);
|
||||
if (spellbookDown && !pKeyWasDown) {
|
||||
open = !open;
|
||||
}
|
||||
pKeyWasDown = pDown;
|
||||
pKeyWasDown = spellbookDown;
|
||||
|
||||
if (!open) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "ui/talent_screen.hpp"
|
||||
#include "ui/keybinding_manager.hpp"
|
||||
#include "core/input.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/logger.hpp"
|
||||
|
|
@ -22,13 +23,14 @@ static const char* getClassName(uint8_t classId) {
|
|||
}
|
||||
|
||||
void TalentScreen::render(game::GameHandler& gameHandler) {
|
||||
// N key toggle (edge-triggered)
|
||||
bool wantsTextInput = ImGui::GetIO().WantTextInput;
|
||||
bool nDown = !wantsTextInput && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_N);
|
||||
if (nDown && !nKeyWasDown) {
|
||||
// Talents toggle via keybinding (edge-triggered)
|
||||
// Customizable key (default: N) from KeybindingManager
|
||||
bool talentsDown = KeybindingManager::getInstance().isActionPressed(
|
||||
KeybindingManager::Action::TOGGLE_TALENTS, false);
|
||||
if (talentsDown && !nKeyWasDown) {
|
||||
open = !open;
|
||||
}
|
||||
nKeyWasDown = nDown;
|
||||
nKeyWasDown = talentsDown;
|
||||
|
||||
if (!open) return;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue